Add idiomatic documentation to Go, Java, Python, and Rust clients
This commit is contained in:
@@ -1,3 +1,8 @@
|
|||||||
|
// Command mxgw-go is the reference Go CLI for the MXAccess Gateway.
|
||||||
|
//
|
||||||
|
// It exposes versioning, session lifecycle, command invocation, event
|
||||||
|
// streaming, a smoke-test workflow, and Galaxy Repository browse subcommands
|
||||||
|
// that exercise the same gRPC contract used by the mxgateway library.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
// Package mxgateway is the Go client for the MXAccess Gateway gRPC service.
|
||||||
|
//
|
||||||
|
// The package wraps the generated gRPC contract with session-oriented helpers
|
||||||
|
// for invoking MXAccess commands, streaming events, and browsing the Galaxy
|
||||||
|
// Repository. Authentication uses an API-key bearer token attached as gRPC
|
||||||
|
// metadata on every call.
|
||||||
|
//
|
||||||
|
// Typical use opens a Client with Dial, opens a Session, invokes commands such
|
||||||
|
// as Register, AddItem, Advise, and Write, and consumes events via
|
||||||
|
// SubscribeEvents. Galaxy Repository browse RPCs are exposed through
|
||||||
|
// GalaxyClient.
|
||||||
package mxgateway
|
package mxgateway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -219,10 +230,15 @@ func resolveTransportCredentials(opts Options) (credentials.TransportCredentials
|
|||||||
|
|
||||||
// OpenSessionOptions describes fields used to create an OpenSessionRequest.
|
// OpenSessionOptions describes fields used to create an OpenSessionRequest.
|
||||||
type OpenSessionOptions struct {
|
type OpenSessionOptions struct {
|
||||||
RequestedBackend string
|
// RequestedBackend selects the gateway worker backend (empty for default).
|
||||||
ClientSessionName string
|
RequestedBackend string
|
||||||
|
// ClientSessionName is a human-readable name recorded on the session.
|
||||||
|
ClientSessionName string
|
||||||
|
// ClientCorrelationID echoes through gateway logs and replies for tracing.
|
||||||
ClientCorrelationID string
|
ClientCorrelationID string
|
||||||
CommandTimeout time.Duration
|
// CommandTimeout sets the per-command timeout the gateway forwards to the
|
||||||
|
// worker; zero leaves the gateway default in place.
|
||||||
|
CommandTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request returns the raw protobuf OpenSessionRequest for these options.
|
// Request returns the raw protobuf OpenSessionRequest for these options.
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ import (
|
|||||||
|
|
||||||
// GatewayError wraps transport-level gRPC failures.
|
// GatewayError wraps transport-level gRPC failures.
|
||||||
type GatewayError struct {
|
type GatewayError struct {
|
||||||
Op string
|
// Op names the operation that failed (for example "dial" or "invoke").
|
||||||
|
Op string
|
||||||
|
// Err is the underlying gRPC or transport error.
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error returns the formatted gateway error message.
|
||||||
func (e *GatewayError) Error() string {
|
func (e *GatewayError) Error() string {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -22,6 +25,7 @@ func (e *GatewayError) Error() string {
|
|||||||
return fmt.Sprintf("mxgateway: %s failed: %v", e.Op, e.Err)
|
return fmt.Sprintf("mxgateway: %s failed: %v", e.Op, e.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the wrapped transport error.
|
||||||
func (e *GatewayError) Unwrap() error {
|
func (e *GatewayError) Unwrap() error {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -32,11 +36,15 @@ func (e *GatewayError) Unwrap() error {
|
|||||||
// CommandError reports a non-OK gateway protocol status and keeps the raw
|
// CommandError reports a non-OK gateway protocol status and keeps the raw
|
||||||
// command reply when one exists.
|
// command reply when one exists.
|
||||||
type CommandError struct {
|
type CommandError struct {
|
||||||
Op string
|
// Op names the gateway operation that produced the non-OK status.
|
||||||
|
Op string
|
||||||
|
// Status carries the gateway-reported protocol status.
|
||||||
Status *ProtocolStatus
|
Status *ProtocolStatus
|
||||||
Reply *MxCommandReply
|
// Reply is the raw command reply, when one was returned alongside the status.
|
||||||
|
Reply *MxCommandReply
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error returns the formatted command error message.
|
||||||
func (e *CommandError) Error() string {
|
func (e *CommandError) Error() string {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -53,10 +61,13 @@ func (e *CommandError) Error() string {
|
|||||||
|
|
||||||
// MxAccessError reports HRESULT or MXSTATUS_PROXY failures returned by MXAccess.
|
// MxAccessError reports HRESULT or MXSTATUS_PROXY failures returned by MXAccess.
|
||||||
type MxAccessError struct {
|
type MxAccessError struct {
|
||||||
|
// Command is the wrapped CommandError when the protocol status carried one.
|
||||||
Command *CommandError
|
Command *CommandError
|
||||||
Reply *MxCommandReply
|
// Reply is the raw MXAccess command reply that surfaced the failure.
|
||||||
|
Reply *MxCommandReply
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error returns the formatted MXAccess error message.
|
||||||
func (e *MxAccessError) Error() string {
|
func (e *MxAccessError) Error() string {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -73,6 +84,7 @@ func (e *MxAccessError) Error() string {
|
|||||||
return "mxgateway: MXAccess command failed"
|
return "mxgateway: MXAccess command failed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the wrapped CommandError, when one is present.
|
||||||
func (e *MxAccessError) Unwrap() error {
|
func (e *MxAccessError) Unwrap() error {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -20,16 +20,26 @@ type RawGalaxyRepositoryClient = pb.GalaxyRepositoryClient
|
|||||||
|
|
||||||
// Generated protobuf aliases for Galaxy Repository messages.
|
// Generated protobuf aliases for Galaxy Repository messages.
|
||||||
type (
|
type (
|
||||||
TestConnectionRequest = pb.TestConnectionRequest
|
// TestConnectionRequest is the request for Galaxy Repository TestConnection.
|
||||||
TestConnectionReply = pb.TestConnectionReply
|
TestConnectionRequest = pb.TestConnectionRequest
|
||||||
GetLastDeployTimeRequest = pb.GetLastDeployTimeRequest
|
// TestConnectionReply is the reply for Galaxy Repository TestConnection.
|
||||||
GetLastDeployTimeReply = pb.GetLastDeployTimeReply
|
TestConnectionReply = pb.TestConnectionReply
|
||||||
DiscoverHierarchyRequest = pb.DiscoverHierarchyRequest
|
// GetLastDeployTimeRequest is the request for GetLastDeployTime.
|
||||||
DiscoverHierarchyReply = pb.DiscoverHierarchyReply
|
GetLastDeployTimeRequest = pb.GetLastDeployTimeRequest
|
||||||
GalaxyObject = pb.GalaxyObject
|
// GetLastDeployTimeReply is the reply for GetLastDeployTime.
|
||||||
GalaxyAttribute = pb.GalaxyAttribute
|
GetLastDeployTimeReply = pb.GetLastDeployTimeReply
|
||||||
WatchDeployEventsRequest = pb.WatchDeployEventsRequest
|
// DiscoverHierarchyRequest is the request for DiscoverHierarchy.
|
||||||
DeployEvent = pb.DeployEvent
|
DiscoverHierarchyRequest = pb.DiscoverHierarchyRequest
|
||||||
|
// DiscoverHierarchyReply is the reply for DiscoverHierarchy.
|
||||||
|
DiscoverHierarchyReply = pb.DiscoverHierarchyReply
|
||||||
|
// GalaxyObject describes one Galaxy object with its dynamic attributes.
|
||||||
|
GalaxyObject = pb.GalaxyObject
|
||||||
|
// GalaxyAttribute describes one dynamic attribute on a GalaxyObject.
|
||||||
|
GalaxyAttribute = pb.GalaxyAttribute
|
||||||
|
// WatchDeployEventsRequest is the request for WatchDeployEvents.
|
||||||
|
WatchDeployEventsRequest = pb.WatchDeployEventsRequest
|
||||||
|
// DeployEvent is one Galaxy Repository deploy event.
|
||||||
|
DeployEvent = pb.DeployEvent
|
||||||
)
|
)
|
||||||
|
|
||||||
// RawDeployEventStream is the generated WatchDeployEvents client stream.
|
// RawDeployEventStream is the generated WatchDeployEvents client stream.
|
||||||
|
|||||||
@@ -11,16 +11,29 @@ import (
|
|||||||
|
|
||||||
// Options configures gateway connections.
|
// Options configures gateway connections.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Endpoint string
|
// Endpoint is the gateway host:port address to dial.
|
||||||
APIKey string
|
Endpoint string
|
||||||
Plaintext bool
|
// APIKey is the bearer token attached to outgoing gRPC metadata.
|
||||||
CACertFile string
|
APIKey string
|
||||||
ServerNameOverride string
|
// Plaintext disables TLS and uses insecure credentials when true.
|
||||||
DialTimeout time.Duration
|
Plaintext bool
|
||||||
CallTimeout time.Duration
|
// CACertFile points to a PEM file used to verify the gateway certificate.
|
||||||
TLSConfig *tls.Config
|
CACertFile string
|
||||||
|
// ServerNameOverride overrides the TLS SNI/SAN name presented to the gateway.
|
||||||
|
ServerNameOverride string
|
||||||
|
// DialTimeout bounds the blocking Dial; zero applies a built-in default.
|
||||||
|
DialTimeout time.Duration
|
||||||
|
// CallTimeout bounds each unary RPC; zero applies a built-in default and
|
||||||
|
// negative disables the bound entirely.
|
||||||
|
CallTimeout time.Duration
|
||||||
|
// TLSConfig supplies a custom TLS configuration; takes precedence over
|
||||||
|
// CACertFile when TransportCredentials is unset.
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
// TransportCredentials, when non-nil, overrides every other transport-level
|
||||||
|
// option and is used as-is.
|
||||||
TransportCredentials credentials.TransportCredentials
|
TransportCredentials credentials.TransportCredentials
|
||||||
DialOptions []grpc.DialOption
|
// DialOptions are appended to the gRPC dial options after the defaults.
|
||||||
|
DialOptions []grpc.DialOption
|
||||||
}
|
}
|
||||||
|
|
||||||
// RedactedAPIKey returns a display-safe representation of the configured API
|
// RedactedAPIKey returns a display-safe representation of the configured API
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ const maxBulkItems = 1000
|
|||||||
|
|
||||||
// EventResult carries either the next ordered event or a terminal stream error.
|
// EventResult carries either the next ordered event or a terminal stream error.
|
||||||
type EventResult struct {
|
type EventResult struct {
|
||||||
|
// Event is the next event from the stream when Err is nil.
|
||||||
Event *MxEvent
|
Event *MxEvent
|
||||||
Err error
|
// Err is the terminal stream error; when non-nil no further results follow.
|
||||||
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventSubscription owns a running gateway event stream.
|
// EventSubscription owns a running gateway event stream.
|
||||||
|
|||||||
+126
-58
@@ -12,77 +12,145 @@ type RawEventStream = pb.MxAccessGateway_StreamEventsClient
|
|||||||
// Generated protobuf aliases keep raw contract access available from the public
|
// Generated protobuf aliases keep raw contract access available from the public
|
||||||
// mxgateway package while generated code remains under internal/generated.
|
// mxgateway package while generated code remains under internal/generated.
|
||||||
type (
|
type (
|
||||||
OpenSessionRequest = pb.OpenSessionRequest
|
// OpenSessionRequest is the gateway OpenSession request message.
|
||||||
OpenSessionReply = pb.OpenSessionReply
|
OpenSessionRequest = pb.OpenSessionRequest
|
||||||
CloseSessionRequest = pb.CloseSessionRequest
|
// OpenSessionReply is the gateway OpenSession reply message.
|
||||||
CloseSessionReply = pb.CloseSessionReply
|
OpenSessionReply = pb.OpenSessionReply
|
||||||
StreamEventsRequest = pb.StreamEventsRequest
|
// CloseSessionRequest is the gateway CloseSession request message.
|
||||||
MxCommandRequest = pb.MxCommandRequest
|
CloseSessionRequest = pb.CloseSessionRequest
|
||||||
MxCommandReply = pb.MxCommandReply
|
// CloseSessionReply is the gateway CloseSession reply message.
|
||||||
MxCommand = pb.MxCommand
|
CloseSessionReply = pb.CloseSessionReply
|
||||||
MxEvent = pb.MxEvent
|
// StreamEventsRequest is the gateway StreamEvents request message.
|
||||||
MxValue = pb.MxValue
|
StreamEventsRequest = pb.StreamEventsRequest
|
||||||
Value = pb.MxValue
|
// MxCommandRequest carries one MXAccess command for Invoke.
|
||||||
MxArray = pb.MxArray
|
MxCommandRequest = pb.MxCommandRequest
|
||||||
MxStatusProxy = pb.MxStatusProxy
|
// MxCommandReply is the reply to an MXAccess command Invoke.
|
||||||
ProtocolStatus = pb.ProtocolStatus
|
MxCommandReply = pb.MxCommandReply
|
||||||
RegisterCommand = pb.RegisterCommand
|
// MxCommand is the discriminated union of MXAccess command payloads.
|
||||||
UnregisterCommand = pb.UnregisterCommand
|
MxCommand = pb.MxCommand
|
||||||
AddItemCommand = pb.AddItemCommand
|
// MxEvent is one ordered event delivered on a session event stream.
|
||||||
AddItem2Command = pb.AddItem2Command
|
MxEvent = pb.MxEvent
|
||||||
RemoveItemCommand = pb.RemoveItemCommand
|
// MxValue is the protobuf representation of an MXAccess value.
|
||||||
AdviseCommand = pb.AdviseCommand
|
MxValue = pb.MxValue
|
||||||
UnAdviseCommand = pb.UnAdviseCommand
|
// Value is an alias for MxValue retained for symmetry with other clients.
|
||||||
AddItemBulkCommand = pb.AddItemBulkCommand
|
Value = pb.MxValue
|
||||||
AdviseItemBulkCommand = pb.AdviseItemBulkCommand
|
// MxArray is the protobuf representation of an MXAccess array value.
|
||||||
RemoveItemBulkCommand = pb.RemoveItemBulkCommand
|
MxArray = pb.MxArray
|
||||||
|
// MxStatusProxy mirrors the MXAccess MXSTATUS_PROXY structure.
|
||||||
|
MxStatusProxy = pb.MxStatusProxy
|
||||||
|
// ProtocolStatus is the gateway-level status carried on every reply.
|
||||||
|
ProtocolStatus = pb.ProtocolStatus
|
||||||
|
// RegisterCommand is the payload of an MXAccess Register command.
|
||||||
|
RegisterCommand = pb.RegisterCommand
|
||||||
|
// UnregisterCommand is the payload of an MXAccess Unregister command.
|
||||||
|
UnregisterCommand = pb.UnregisterCommand
|
||||||
|
// AddItemCommand is the payload of an MXAccess AddItem command.
|
||||||
|
AddItemCommand = pb.AddItemCommand
|
||||||
|
// AddItem2Command is the payload of an MXAccess AddItem2 command.
|
||||||
|
AddItem2Command = pb.AddItem2Command
|
||||||
|
// RemoveItemCommand is the payload of an MXAccess RemoveItem command.
|
||||||
|
RemoveItemCommand = pb.RemoveItemCommand
|
||||||
|
// AdviseCommand is the payload of an MXAccess Advise command.
|
||||||
|
AdviseCommand = pb.AdviseCommand
|
||||||
|
// UnAdviseCommand is the payload of an MXAccess UnAdvise command.
|
||||||
|
UnAdviseCommand = pb.UnAdviseCommand
|
||||||
|
// AddItemBulkCommand is the payload of an AddItem bulk command.
|
||||||
|
AddItemBulkCommand = pb.AddItemBulkCommand
|
||||||
|
// AdviseItemBulkCommand is the payload of an Advise bulk command.
|
||||||
|
AdviseItemBulkCommand = pb.AdviseItemBulkCommand
|
||||||
|
// RemoveItemBulkCommand is the payload of a RemoveItem bulk command.
|
||||||
|
RemoveItemBulkCommand = pb.RemoveItemBulkCommand
|
||||||
|
// UnAdviseItemBulkCommand is the payload of an UnAdvise bulk command.
|
||||||
UnAdviseItemBulkCommand = pb.UnAdviseItemBulkCommand
|
UnAdviseItemBulkCommand = pb.UnAdviseItemBulkCommand
|
||||||
SubscribeBulkCommand = pb.SubscribeBulkCommand
|
// SubscribeBulkCommand combines AddItem and Advise for a list of tags.
|
||||||
UnsubscribeBulkCommand = pb.UnsubscribeBulkCommand
|
SubscribeBulkCommand = pb.SubscribeBulkCommand
|
||||||
WriteCommand = pb.WriteCommand
|
// UnsubscribeBulkCommand combines UnAdvise and RemoveItem for a list of items.
|
||||||
Write2Command = pb.Write2Command
|
UnsubscribeBulkCommand = pb.UnsubscribeBulkCommand
|
||||||
RegisterReply = pb.RegisterReply
|
// WriteCommand is the payload of an MXAccess Write command.
|
||||||
AddItemReply = pb.AddItemReply
|
WriteCommand = pb.WriteCommand
|
||||||
AddItem2Reply = pb.AddItem2Reply
|
// Write2Command is the payload of an MXAccess Write2 command.
|
||||||
SubscribeResult = pb.SubscribeResult
|
Write2Command = pb.Write2Command
|
||||||
BulkSubscribeReply = pb.BulkSubscribeReply
|
// RegisterReply carries the ServerHandle returned by Register.
|
||||||
|
RegisterReply = pb.RegisterReply
|
||||||
|
// AddItemReply carries the ItemHandle returned by AddItem.
|
||||||
|
AddItemReply = pb.AddItemReply
|
||||||
|
// AddItem2Reply carries the ItemHandle returned by AddItem2.
|
||||||
|
AddItem2Reply = pb.AddItem2Reply
|
||||||
|
// SubscribeResult is one entry in a bulk command result list.
|
||||||
|
SubscribeResult = pb.SubscribeResult
|
||||||
|
// BulkSubscribeReply aggregates SubscribeResult entries for a bulk command.
|
||||||
|
BulkSubscribeReply = pb.BulkSubscribeReply
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Enumerations from the generated contract re-exported for client callers.
|
||||||
type (
|
type (
|
||||||
MxCommandKind = pb.MxCommandKind
|
// MxCommandKind discriminates which MXAccess command an MxCommand carries.
|
||||||
MxDataType = pb.MxDataType
|
MxCommandKind = pb.MxCommandKind
|
||||||
MxEventFamily = pb.MxEventFamily
|
// MxDataType is the MXAccess data type tag on values and arrays.
|
||||||
MxStatusCategory = pb.MxStatusCategory
|
MxDataType = pb.MxDataType
|
||||||
MxStatusSource = pb.MxStatusSource
|
// MxEventFamily groups MXAccess events by source category.
|
||||||
|
MxEventFamily = pb.MxEventFamily
|
||||||
|
// MxStatusCategory classifies MXSTATUS_PROXY entries.
|
||||||
|
MxStatusCategory = pb.MxStatusCategory
|
||||||
|
// MxStatusSource identifies the originator of a status entry.
|
||||||
|
MxStatusSource = pb.MxStatusSource
|
||||||
|
// ProtocolStatusCode enumerates gateway-level status codes.
|
||||||
ProtocolStatusCode = pb.ProtocolStatusCode
|
ProtocolStatusCode = pb.ProtocolStatusCode
|
||||||
SessionState = pb.SessionState
|
// SessionState enumerates gateway session lifecycle states.
|
||||||
|
SessionState = pb.SessionState
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MXAccess command kind, data type, and protocol status constants surfaced
|
||||||
|
// from the generated contract.
|
||||||
const (
|
const (
|
||||||
CommandKindRegister = pb.MxCommandKind_MX_COMMAND_KIND_REGISTER
|
// CommandKindRegister selects the MXAccess Register command.
|
||||||
CommandKindUnregister = pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER
|
CommandKindRegister = pb.MxCommandKind_MX_COMMAND_KIND_REGISTER
|
||||||
CommandKindAddItem = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM
|
// CommandKindUnregister selects the MXAccess Unregister command.
|
||||||
CommandKindAddItem2 = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2
|
CommandKindUnregister = pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER
|
||||||
CommandKindRemoveItem = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM
|
// CommandKindAddItem selects the MXAccess AddItem command.
|
||||||
CommandKindAdvise = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE
|
CommandKindAddItem = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM
|
||||||
CommandKindUnAdvise = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE
|
// CommandKindAddItem2 selects the MXAccess AddItem2 command.
|
||||||
CommandKindAddItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM_BULK
|
CommandKindAddItem2 = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2
|
||||||
CommandKindAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_ITEM_BULK
|
// CommandKindRemoveItem selects the MXAccess RemoveItem command.
|
||||||
CommandKindRemoveItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM_BULK
|
CommandKindRemoveItem = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM
|
||||||
|
// CommandKindAdvise selects the MXAccess Advise command.
|
||||||
|
CommandKindAdvise = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE
|
||||||
|
// CommandKindUnAdvise selects the MXAccess UnAdvise command.
|
||||||
|
CommandKindUnAdvise = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE
|
||||||
|
// CommandKindAddItemBulk selects the AddItem bulk command.
|
||||||
|
CommandKindAddItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM_BULK
|
||||||
|
// CommandKindAdviseItemBulk selects the Advise bulk command.
|
||||||
|
CommandKindAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_ITEM_BULK
|
||||||
|
// CommandKindRemoveItemBulk selects the RemoveItem bulk command.
|
||||||
|
CommandKindRemoveItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM_BULK
|
||||||
|
// CommandKindUnAdviseItemBulk selects the UnAdvise bulk command.
|
||||||
CommandKindUnAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK
|
CommandKindUnAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE_ITEM_BULK
|
||||||
CommandKindSubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK
|
// CommandKindSubscribeBulk selects the AddItem+Advise combined bulk command.
|
||||||
CommandKindUnsubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_UNSUBSCRIBE_BULK
|
CommandKindSubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK
|
||||||
CommandKindWrite = pb.MxCommandKind_MX_COMMAND_KIND_WRITE
|
// CommandKindUnsubscribeBulk selects the UnAdvise+RemoveItem combined bulk command.
|
||||||
CommandKindWrite2 = pb.MxCommandKind_MX_COMMAND_KIND_WRITE2
|
CommandKindUnsubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_UNSUBSCRIBE_BULK
|
||||||
|
// CommandKindWrite selects the MXAccess Write command.
|
||||||
|
CommandKindWrite = pb.MxCommandKind_MX_COMMAND_KIND_WRITE
|
||||||
|
// CommandKindWrite2 selects the MXAccess Write2 command.
|
||||||
|
CommandKindWrite2 = pb.MxCommandKind_MX_COMMAND_KIND_WRITE2
|
||||||
|
|
||||||
|
// DataTypeUnknown denotes an unrecognized MXAccess data type.
|
||||||
DataTypeUnknown = pb.MxDataType_MX_DATA_TYPE_UNKNOWN
|
DataTypeUnknown = pb.MxDataType_MX_DATA_TYPE_UNKNOWN
|
||||||
|
// DataTypeBoolean denotes an MXAccess Boolean value.
|
||||||
DataTypeBoolean = pb.MxDataType_MX_DATA_TYPE_BOOLEAN
|
DataTypeBoolean = pb.MxDataType_MX_DATA_TYPE_BOOLEAN
|
||||||
|
// DataTypeInteger denotes an MXAccess Integer value.
|
||||||
DataTypeInteger = pb.MxDataType_MX_DATA_TYPE_INTEGER
|
DataTypeInteger = pb.MxDataType_MX_DATA_TYPE_INTEGER
|
||||||
DataTypeFloat = pb.MxDataType_MX_DATA_TYPE_FLOAT
|
// DataTypeFloat denotes an MXAccess Float (single precision) value.
|
||||||
DataTypeDouble = pb.MxDataType_MX_DATA_TYPE_DOUBLE
|
DataTypeFloat = pb.MxDataType_MX_DATA_TYPE_FLOAT
|
||||||
DataTypeString = pb.MxDataType_MX_DATA_TYPE_STRING
|
// DataTypeDouble denotes an MXAccess Double (double precision) value.
|
||||||
DataTypeTime = pb.MxDataType_MX_DATA_TYPE_TIME
|
DataTypeDouble = pb.MxDataType_MX_DATA_TYPE_DOUBLE
|
||||||
|
// DataTypeString denotes an MXAccess String value.
|
||||||
|
DataTypeString = pb.MxDataType_MX_DATA_TYPE_STRING
|
||||||
|
// DataTypeTime denotes an MXAccess timestamp value.
|
||||||
|
DataTypeTime = pb.MxDataType_MX_DATA_TYPE_TIME
|
||||||
|
|
||||||
ProtocolStatusOK = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK
|
// ProtocolStatusOK indicates the gateway processed the request successfully.
|
||||||
|
ProtocolStatusOK = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK
|
||||||
|
// ProtocolStatusMxAccessFailure indicates the worker reported an MXAccess failure.
|
||||||
ProtocolStatusMxAccessFailure = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE
|
ProtocolStatusMxAccessFailure = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE
|
||||||
)
|
)
|
||||||
|
|||||||
+24
@@ -38,6 +38,10 @@ import picocli.CommandLine.Model.CommandSpec;
|
|||||||
import picocli.CommandLine.Option;
|
import picocli.CommandLine.Option;
|
||||||
import picocli.CommandLine.Spec;
|
import picocli.CommandLine.Spec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picocli entry point for the {@code mxgw-java} test CLI used by the
|
||||||
|
* cross-language smoke matrix.
|
||||||
|
*/
|
||||||
@Command(
|
@Command(
|
||||||
name = "mxgw-java",
|
name = "mxgw-java",
|
||||||
mixinStandardHelpOptions = true,
|
mixinStandardHelpOptions = true,
|
||||||
@@ -48,6 +52,9 @@ public final class MxGatewayCli implements Callable<Integer> {
|
|||||||
@Spec
|
@Spec
|
||||||
private CommandSpec spec;
|
private CommandSpec spec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a CLI bound to the default gRPC client factory.
|
||||||
|
*/
|
||||||
public MxGatewayCli() {
|
public MxGatewayCli() {
|
||||||
this(new GrpcMxGatewayCliClientFactory());
|
this(new GrpcMxGatewayCliClientFactory());
|
||||||
}
|
}
|
||||||
@@ -56,11 +63,25 @@ public final class MxGatewayCli implements Callable<Integer> {
|
|||||||
this.clientFactory = clientFactory;
|
this.clientFactory = clientFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process entry point.
|
||||||
|
*
|
||||||
|
* @param args command-line arguments
|
||||||
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
int exitCode = commandLine(new GrpcMxGatewayCliClientFactory()).execute(args);
|
int exitCode = commandLine(new GrpcMxGatewayCliClientFactory()).execute(args);
|
||||||
System.exit(exitCode);
|
System.exit(exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test-friendly entry point that runs the CLI against the supplied
|
||||||
|
* {@link PrintWriter} pair instead of the system streams.
|
||||||
|
*
|
||||||
|
* @param out writer that receives standard output
|
||||||
|
* @param err writer that receives standard error
|
||||||
|
* @param args command-line arguments
|
||||||
|
* @return the picocli exit code
|
||||||
|
*/
|
||||||
public static int execute(PrintWriter out, PrintWriter err, String... args) {
|
public static int execute(PrintWriter out, PrintWriter err, String... args) {
|
||||||
return execute(new GrpcMxGatewayCliClientFactory(), out, err, args);
|
return execute(new GrpcMxGatewayCliClientFactory(), out, err, args);
|
||||||
}
|
}
|
||||||
@@ -280,6 +301,9 @@ public final class MxGatewayCli implements Callable<Integer> {
|
|||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picocli subcommand that prints the client and protocol version numbers.
|
||||||
|
*/
|
||||||
@Command(name = "version", description = "Prints the Java client version.")
|
@Command(name = "version", description = "Prints the Java client version.")
|
||||||
public static final class VersionCommand implements Callable<Integer> {
|
public static final class VersionCommand implements Callable<Integer> {
|
||||||
@Spec
|
@Spec
|
||||||
|
|||||||
+5
@@ -45,6 +45,11 @@ public final class DeployEventSubscription implements AutoCloseable {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the underlying gRPC call. Safe to invoke before the call has
|
||||||
|
* started; cancellation is recorded and applied as soon as the stream
|
||||||
|
* attaches.
|
||||||
|
*/
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
cancelled.set(true);
|
cancelled.set(true);
|
||||||
ClientCallStreamObserver<WatchDeployEventsRequest> stream = requestStream.get();
|
ClientCallStreamObserver<WatchDeployEventsRequest> stream = requestStream.get();
|
||||||
|
|||||||
+79
-10
@@ -52,8 +52,12 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a client over a caller-managed {@link Channel}. The caller owns
|
* Constructs a client over a caller-managed {@link Channel}. The caller owns
|
||||||
* channel lifecycle; {@link #close()} is a no-op for this constructor.
|
* channel lifecycle; {@link #close()} is a no-op for this constructor.
|
||||||
|
*
|
||||||
|
* @param channel the gRPC channel to use for outbound calls
|
||||||
|
* @param options the client options carrying the API key and timeouts
|
||||||
|
* @throws NullPointerException if {@code options} is {@code null}
|
||||||
*/
|
*/
|
||||||
public GalaxyRepositoryClient(Channel channel, MxGatewayClientOptions options) {
|
public GalaxyRepositoryClient(Channel channel, MxGatewayClientOptions options) {
|
||||||
this.ownedChannel = null;
|
this.ownedChannel = null;
|
||||||
@@ -64,25 +68,49 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
asyncStub = GalaxyRepositoryGrpc.newStub(intercepted);
|
asyncStub = GalaxyRepositoryGrpc.newStub(intercepted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build a new client and own its channel; close shuts the channel down. */
|
/**
|
||||||
|
* Builds a new client and owns its channel; {@link #close()} shuts the
|
||||||
|
* channel down.
|
||||||
|
*
|
||||||
|
* @param options the client options carrying the endpoint and credentials
|
||||||
|
* @return a connected client
|
||||||
|
*/
|
||||||
public static GalaxyRepositoryClient connect(MxGatewayClientOptions options) {
|
public static GalaxyRepositoryClient connect(MxGatewayClientOptions options) {
|
||||||
return new GalaxyRepositoryClient(createChannel(options), options);
|
return new GalaxyRepositoryClient(createChannel(options), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying blocking stub with the per-call deadline applied.
|
||||||
|
*
|
||||||
|
* @return the blocking stub
|
||||||
|
*/
|
||||||
public GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub rawBlockingStub() {
|
public GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub rawBlockingStub() {
|
||||||
return withDeadline(blockingStub);
|
return withDeadline(blockingStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying future stub with the per-call deadline applied.
|
||||||
|
*
|
||||||
|
* @return the future stub
|
||||||
|
*/
|
||||||
public GalaxyRepositoryGrpc.GalaxyRepositoryFutureStub rawFutureStub() {
|
public GalaxyRepositoryGrpc.GalaxyRepositoryFutureStub rawFutureStub() {
|
||||||
return withDeadline(futureStub);
|
return withDeadline(futureStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying async stub. Stream deadlines are applied per call.
|
||||||
|
*
|
||||||
|
* @return the async stub
|
||||||
|
*/
|
||||||
public GalaxyRepositoryGrpc.GalaxyRepositoryStub rawAsyncStub() {
|
public GalaxyRepositoryGrpc.GalaxyRepositoryStub rawAsyncStub() {
|
||||||
return asyncStub;
|
return asyncStub;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke the {@code TestConnection} RPC and return the {@code ok} flag.
|
* Invokes the {@code TestConnection} RPC and returns the {@code ok} flag.
|
||||||
|
*
|
||||||
|
* @return {@code true} when the gateway reached the Galaxy Repository database
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
*/
|
*/
|
||||||
public boolean testConnection() {
|
public boolean testConnection() {
|
||||||
try {
|
try {
|
||||||
@@ -96,14 +124,23 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes {@code TestConnection} asynchronously.
|
||||||
|
*
|
||||||
|
* @return a future completed with the {@code ok} flag, or completed
|
||||||
|
* exceptionally with {@link MxGatewayException} on failure
|
||||||
|
*/
|
||||||
public CompletableFuture<Boolean> testConnectionAsync() {
|
public CompletableFuture<Boolean> testConnectionAsync() {
|
||||||
return toCompletable(rawFutureStub().testConnection(TestConnectionRequest.getDefaultInstance()))
|
return toCompletable(rawFutureStub().testConnection(TestConnectionRequest.getDefaultInstance()))
|
||||||
.thenApply(TestConnectionReply::getOk);
|
.thenApply(TestConnectionReply::getOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke the {@code GetLastDeployTime} RPC. Returns {@link Optional#empty()}
|
* Invokes the {@code GetLastDeployTime} RPC.
|
||||||
* when the server reports {@code present=false}.
|
*
|
||||||
|
* @return the time of the last deploy, or {@link Optional#empty()} when the
|
||||||
|
* server reports {@code present=false}
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
*/
|
*/
|
||||||
public Optional<Instant> getLastDeployTime() {
|
public Optional<Instant> getLastDeployTime() {
|
||||||
try {
|
try {
|
||||||
@@ -118,15 +155,25 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes {@code GetLastDeployTime} asynchronously.
|
||||||
|
*
|
||||||
|
* @return a future completed with the time of the last deploy, or
|
||||||
|
* {@link Optional#empty()} when the server reports {@code present=false};
|
||||||
|
* completed exceptionally with {@link MxGatewayException} on failure
|
||||||
|
*/
|
||||||
public CompletableFuture<Optional<Instant>> getLastDeployTimeAsync() {
|
public CompletableFuture<Optional<Instant>> getLastDeployTimeAsync() {
|
||||||
return toCompletable(rawFutureStub().getLastDeployTime(GetLastDeployTimeRequest.getDefaultInstance()))
|
return toCompletable(rawFutureStub().getLastDeployTime(GetLastDeployTimeRequest.getDefaultInstance()))
|
||||||
.thenApply(GalaxyRepositoryClient::mapDeployTime);
|
.thenApply(GalaxyRepositoryClient::mapDeployTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke the {@code DiscoverHierarchy} RPC and return the generated
|
* Invokes the {@code DiscoverHierarchy} RPC and returns the generated
|
||||||
* {@link GalaxyObject} messages directly. Callers can read every field of
|
* {@link GalaxyObject} messages directly. Callers can read every field of
|
||||||
* the proto message without an extra DTO layer.
|
* the proto message without an extra DTO layer.
|
||||||
|
*
|
||||||
|
* @return the Galaxy object hierarchy
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
*/
|
*/
|
||||||
public List<GalaxyObject> discoverHierarchy() {
|
public List<GalaxyObject> discoverHierarchy() {
|
||||||
try {
|
try {
|
||||||
@@ -141,18 +188,25 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes {@code DiscoverHierarchy} asynchronously.
|
||||||
|
*
|
||||||
|
* @return a future completed with the Galaxy object hierarchy, or completed
|
||||||
|
* exceptionally with {@link MxGatewayException} on failure
|
||||||
|
*/
|
||||||
public CompletableFuture<List<GalaxyObject>> discoverHierarchyAsync() {
|
public CompletableFuture<List<GalaxyObject>> discoverHierarchyAsync() {
|
||||||
return toCompletable(rawFutureStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance()))
|
return toCompletable(rawFutureStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance()))
|
||||||
.thenApply(DiscoverHierarchyReply::getObjectsList);
|
.thenApply(DiscoverHierarchyReply::getObjectsList);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to {@code WatchDeployEvents} via the async stub and consume
|
* Subscribes to {@code WatchDeployEvents} via the async stub and consumes
|
||||||
* results through a blocking iterator. Closing the returned stream cancels
|
* results through a blocking iterator. Closing the returned stream cancels
|
||||||
* the underlying gRPC call.
|
* the underlying gRPC call.
|
||||||
*
|
*
|
||||||
* @param lastSeenDeployTime optional. When non-null, the bootstrap event is
|
* @param lastSeenDeployTime optional. When non-{@code null}, the bootstrap
|
||||||
* suppressed if the cached deploy time matches.
|
* event is suppressed if the cached deploy time matches.
|
||||||
|
* @return an iterator-style stream of deploy events
|
||||||
*/
|
*/
|
||||||
public DeployEventStream watchDeployEvents(Instant lastSeenDeployTime) {
|
public DeployEventStream watchDeployEvents(Instant lastSeenDeployTime) {
|
||||||
DeployEventStream stream = new DeployEventStream(16);
|
DeployEventStream stream = new DeployEventStream(16);
|
||||||
@@ -163,15 +217,23 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
/**
|
/**
|
||||||
* Iterator-style alias for {@link #watchDeployEvents(Instant)} matching the
|
* Iterator-style alias for {@link #watchDeployEvents(Instant)} matching the
|
||||||
* task-spec signature.
|
* task-spec signature.
|
||||||
|
*
|
||||||
|
* @param lastSeenDeployTime optional cached deploy time for bootstrap suppression
|
||||||
|
* @return an iterator over deploy events
|
||||||
*/
|
*/
|
||||||
public Iterator<DeployEvent> watchDeployEventsIterator(Instant lastSeenDeployTime) {
|
public Iterator<DeployEvent> watchDeployEventsIterator(Instant lastSeenDeployTime) {
|
||||||
return watchDeployEvents(lastSeenDeployTime);
|
return watchDeployEvents(lastSeenDeployTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to {@code WatchDeployEvents} via the async stub, dispatching
|
* Subscribes to {@code WatchDeployEvents} via the async stub, dispatching
|
||||||
* each event to {@code observer}. The returned subscription is cancellable
|
* each event to {@code observer}. The returned subscription is cancellable
|
||||||
* and {@link AutoCloseable}.
|
* and {@link AutoCloseable}.
|
||||||
|
*
|
||||||
|
* @param lastSeenDeployTime optional cached deploy time for bootstrap suppression
|
||||||
|
* @param observer caller-supplied observer that receives events and completion
|
||||||
|
* @return a cancellable subscription handle
|
||||||
|
* @throws NullPointerException if {@code observer} is {@code null}
|
||||||
*/
|
*/
|
||||||
public DeployEventSubscription watchDeployEventsAsync(
|
public DeployEventSubscription watchDeployEventsAsync(
|
||||||
Instant lastSeenDeployTime, StreamObserver<DeployEvent> observer) {
|
Instant lastSeenDeployTime, StreamObserver<DeployEvent> observer) {
|
||||||
@@ -207,6 +269,13 @@ public final class GalaxyRepositoryClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuts the owned channel down and waits up to the configured connect
|
||||||
|
* timeout for termination, forcibly shutting it down on timeout. No-op
|
||||||
|
* for clients that do not own their channel.
|
||||||
|
*
|
||||||
|
* @throws InterruptedException if the calling thread is interrupted while waiting
|
||||||
|
*/
|
||||||
public void closeAndAwaitTermination() throws InterruptedException {
|
public void closeAndAwaitTermination() throws InterruptedException {
|
||||||
if (ownedChannel != null) {
|
if (ownedChannel != null) {
|
||||||
ownedChannel.shutdown();
|
ownedChannel.shutdown();
|
||||||
|
|||||||
+18
@@ -3,11 +3,29 @@ package com.dohertylan.mxgateway.client;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when the worker reports an MXAccess COM-side failure. Distinguishes
|
||||||
|
* MXAccess errors (non-zero {@code HResult} or unsuccessful {@code MxStatusProxy})
|
||||||
|
* from other gateway protocol failures.
|
||||||
|
*/
|
||||||
public final class MxAccessException extends MxGatewayCommandException {
|
public final class MxAccessException extends MxGatewayCommandException {
|
||||||
|
/**
|
||||||
|
* Creates a new MXAccess exception with an explicit protocol status.
|
||||||
|
*
|
||||||
|
* @param operation human-readable name of the failing operation
|
||||||
|
* @param protocolStatus protocol status reported by the gateway
|
||||||
|
* @param reply raw command reply containing the MXAccess failure detail
|
||||||
|
*/
|
||||||
public MxAccessException(String operation, ProtocolStatus protocolStatus, MxCommandReply reply) {
|
public MxAccessException(String operation, ProtocolStatus protocolStatus, MxCommandReply reply) {
|
||||||
super(operation, protocolStatus, reply);
|
super(operation, protocolStatus, reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new MXAccess exception derived from a command reply.
|
||||||
|
*
|
||||||
|
* @param operation human-readable name of the failing operation
|
||||||
|
* @param reply raw command reply; the protocol status is taken from this reply when present
|
||||||
|
*/
|
||||||
public MxAccessException(String operation, MxCommandReply reply) {
|
public MxAccessException(String operation, MxCommandReply reply) {
|
||||||
super(operation, reply == null ? null : reply.getProtocolStatus(), reply);
|
super(operation, reply == null ? null : reply.getProtocolStatus(), reply);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -12,6 +12,16 @@ import java.util.concurrent.BlockingQueue;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator-style adaptor over the gateway {@code StreamEvents} server-streaming
|
||||||
|
* RPC.
|
||||||
|
*
|
||||||
|
* <p>Events arrive on a background gRPC thread and are buffered in a bounded
|
||||||
|
* blocking queue; the iterator drains them on the calling thread. Closing the
|
||||||
|
* stream cancels the underlying gRPC call. If the queue overflows the call is
|
||||||
|
* cancelled and a follow-up call to {@link #next()} throws
|
||||||
|
* {@link MxGatewayException}.
|
||||||
|
*/
|
||||||
public final class MxEventStream implements Iterator<MxEvent>, AutoCloseable {
|
public final class MxEventStream implements Iterator<MxEvent>, AutoCloseable {
|
||||||
private static final Object END = new Object();
|
private static final Object END = new Object();
|
||||||
|
|
||||||
|
|||||||
+10
@@ -8,12 +8,22 @@ import io.grpc.ForwardingClientCall;
|
|||||||
import io.grpc.Metadata;
|
import io.grpc.Metadata;
|
||||||
import io.grpc.MethodDescriptor;
|
import io.grpc.MethodDescriptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gRPC client interceptor that attaches the {@code authorization: Bearer ...}
|
||||||
|
* header carrying the gateway API key. A blank or {@code null} key disables
|
||||||
|
* the interceptor so unauthenticated calls pass through unchanged.
|
||||||
|
*/
|
||||||
public final class MxGatewayAuthInterceptor implements ClientInterceptor {
|
public final class MxGatewayAuthInterceptor implements ClientInterceptor {
|
||||||
static final Metadata.Key<String> AUTHORIZATION_HEADER =
|
static final Metadata.Key<String> AUTHORIZATION_HEADER =
|
||||||
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
|
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
|
||||||
|
|
||||||
private final String apiKey;
|
private final String apiKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new interceptor using the supplied API key.
|
||||||
|
*
|
||||||
|
* @param apiKey gateway API key; {@code null} or blank disables the interceptor
|
||||||
|
*/
|
||||||
public MxGatewayAuthInterceptor(String apiKey) {
|
public MxGatewayAuthInterceptor(String apiKey) {
|
||||||
this.apiKey = apiKey == null ? "" : apiKey;
|
this.apiKey = apiKey == null ? "" : apiKey;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -1,6 +1,16 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
package com.dohertylan.mxgateway.client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when the gateway rejects a call because the supplied API key is
|
||||||
|
* missing, malformed, or unrecognised (gRPC {@code UNAUTHENTICATED}).
|
||||||
|
*/
|
||||||
public final class MxGatewayAuthenticationException extends MxGatewayException {
|
public final class MxGatewayAuthenticationException extends MxGatewayException {
|
||||||
|
/**
|
||||||
|
* Creates a new authentication exception.
|
||||||
|
*
|
||||||
|
* @param message human-readable description of the failure
|
||||||
|
* @param cause underlying gRPC error reported by the transport
|
||||||
|
*/
|
||||||
public MxGatewayAuthenticationException(String message, Throwable cause) {
|
public MxGatewayAuthenticationException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -1,6 +1,16 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
package com.dohertylan.mxgateway.client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when the gateway accepts an API key but rejects a call because the
|
||||||
|
* key lacks the required scope (gRPC {@code PERMISSION_DENIED}).
|
||||||
|
*/
|
||||||
public final class MxGatewayAuthorizationException extends MxGatewayException {
|
public final class MxGatewayAuthorizationException extends MxGatewayException {
|
||||||
|
/**
|
||||||
|
* Creates a new authorization exception.
|
||||||
|
*
|
||||||
|
* @param message human-readable description of the failure
|
||||||
|
* @param cause underlying gRPC error reported by the transport
|
||||||
|
*/
|
||||||
public MxGatewayAuthorizationException(String message, Throwable cause) {
|
public MxGatewayAuthorizationException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|||||||
+115
@@ -25,6 +25,15 @@ import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Idiomatic Java wrapper around the generated {@code MxAccessGateway} gRPC
|
||||||
|
* stubs.
|
||||||
|
*
|
||||||
|
* <p>Owns or borrows a {@link ManagedChannel}, attaches a
|
||||||
|
* {@link MxGatewayAuthInterceptor} carrying the configured API key, and
|
||||||
|
* exposes blocking, future, and async stub variants. Translates protocol
|
||||||
|
* status failures into typed {@link MxGatewayException} subclasses.
|
||||||
|
*/
|
||||||
public final class MxGatewayClient implements AutoCloseable {
|
public final class MxGatewayClient implements AutoCloseable {
|
||||||
private final ManagedChannel ownedChannel;
|
private final ManagedChannel ownedChannel;
|
||||||
private final MxGatewayClientOptions options;
|
private final MxGatewayClientOptions options;
|
||||||
@@ -41,6 +50,14 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
|
asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a client over a caller-managed {@link Channel}. The caller
|
||||||
|
* owns channel lifecycle; {@link #close()} is a no-op for this constructor.
|
||||||
|
*
|
||||||
|
* @param channel the gRPC channel to use for outbound calls
|
||||||
|
* @param options the client options carrying the API key and timeouts
|
||||||
|
* @throws NullPointerException if {@code options} is {@code null}
|
||||||
|
*/
|
||||||
public MxGatewayClient(Channel channel, MxGatewayClientOptions options) {
|
public MxGatewayClient(Channel channel, MxGatewayClientOptions options) {
|
||||||
this.ownedChannel = null;
|
this.ownedChannel = null;
|
||||||
this.options = Objects.requireNonNull(options, "options");
|
this.options = Objects.requireNonNull(options, "options");
|
||||||
@@ -50,27 +67,64 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
|
asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a new client and owns its channel; {@link #close()} shuts the
|
||||||
|
* channel down.
|
||||||
|
*
|
||||||
|
* @param options the client options carrying the endpoint and credentials
|
||||||
|
* @return a connected client
|
||||||
|
*/
|
||||||
public static MxGatewayClient connect(MxGatewayClientOptions options) {
|
public static MxGatewayClient connect(MxGatewayClientOptions options) {
|
||||||
return new MxGatewayClient(createChannel(options), options);
|
return new MxGatewayClient(createChannel(options), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying blocking stub with the per-call deadline applied.
|
||||||
|
*
|
||||||
|
* @return the blocking stub
|
||||||
|
*/
|
||||||
public MxAccessGatewayGrpc.MxAccessGatewayBlockingStub rawBlockingStub() {
|
public MxAccessGatewayGrpc.MxAccessGatewayBlockingStub rawBlockingStub() {
|
||||||
return withDeadline(blockingStub);
|
return withDeadline(blockingStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying future stub with the per-call deadline applied.
|
||||||
|
*
|
||||||
|
* @return the future stub
|
||||||
|
*/
|
||||||
public MxAccessGatewayGrpc.MxAccessGatewayFutureStub rawFutureStub() {
|
public MxAccessGatewayGrpc.MxAccessGatewayFutureStub rawFutureStub() {
|
||||||
return withDeadline(futureStub);
|
return withDeadline(futureStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying async stub. Stream deadlines are applied per call.
|
||||||
|
*
|
||||||
|
* @return the async stub
|
||||||
|
*/
|
||||||
public MxAccessGatewayGrpc.MxAccessGatewayStub rawAsyncStub() {
|
public MxAccessGatewayGrpc.MxAccessGatewayStub rawAsyncStub() {
|
||||||
return asyncStub;
|
return asyncStub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a gateway session and returns a typed handle for further commands.
|
||||||
|
*
|
||||||
|
* @param request the {@code OpenSessionRequest} to send
|
||||||
|
* @return a session bound to the resulting {@code OpenSessionReply}
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public MxGatewaySession openSession(OpenSessionRequest request) {
|
public MxGatewaySession openSession(OpenSessionRequest request) {
|
||||||
OpenSessionReply reply = openSessionRaw(request);
|
OpenSessionReply reply = openSessionRaw(request);
|
||||||
return new MxGatewaySession(this, reply);
|
return new MxGatewaySession(this, reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a gateway session using the configured call timeout for the
|
||||||
|
* worker command timeout and a caller-supplied client session name.
|
||||||
|
*
|
||||||
|
* @param clientSessionName the human-readable session name reported by the gateway
|
||||||
|
* @return a session bound to the resulting {@code OpenSessionReply}
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public MxGatewaySession openSession(String clientSessionName) {
|
public MxGatewaySession openSession(String clientSessionName) {
|
||||||
return openSession(OpenSessionRequest.newBuilder()
|
return openSession(OpenSessionRequest.newBuilder()
|
||||||
.setClientSessionName(clientSessionName)
|
.setClientSessionName(clientSessionName)
|
||||||
@@ -81,6 +135,13 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes {@code OpenSession} and returns the raw reply.
|
||||||
|
*
|
||||||
|
* @param request the {@code OpenSessionRequest} to send
|
||||||
|
* @return the raw {@code OpenSessionReply}
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public OpenSessionReply openSessionRaw(OpenSessionRequest request) {
|
public OpenSessionReply openSessionRaw(OpenSessionRequest request) {
|
||||||
try {
|
try {
|
||||||
OpenSessionReply reply = rawBlockingStub().openSession(request);
|
OpenSessionReply reply = rawBlockingStub().openSession(request);
|
||||||
@@ -94,6 +155,13 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes {@code OpenSession} asynchronously.
|
||||||
|
*
|
||||||
|
* @param request the {@code OpenSessionRequest} to send
|
||||||
|
* @return a future completed with the raw reply, or completed exceptionally
|
||||||
|
* with {@link MxGatewayException} on failure
|
||||||
|
*/
|
||||||
public CompletableFuture<OpenSessionReply> openSessionAsync(OpenSessionRequest request) {
|
public CompletableFuture<OpenSessionReply> openSessionAsync(OpenSessionRequest request) {
|
||||||
CompletableFuture<OpenSessionReply> future = toCompletable(rawFutureStub().openSession(request));
|
CompletableFuture<OpenSessionReply> future = toCompletable(rawFutureStub().openSession(request));
|
||||||
return future.thenApply(reply -> {
|
return future.thenApply(reply -> {
|
||||||
@@ -102,6 +170,15 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the {@code Invoke} unary RPC and validates both the protocol
|
||||||
|
* status and any MXAccess-side failure carried in the reply.
|
||||||
|
*
|
||||||
|
* @param request the {@code MxCommandRequest} to send
|
||||||
|
* @return the raw command reply
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
* @throws MxAccessException when the worker reports an MXAccess COM-side failure
|
||||||
|
*/
|
||||||
public MxCommandReply invoke(MxCommandRequest request) {
|
public MxCommandReply invoke(MxCommandRequest request) {
|
||||||
try {
|
try {
|
||||||
MxCommandReply reply = rawBlockingStub().invoke(request);
|
MxCommandReply reply = rawBlockingStub().invoke(request);
|
||||||
@@ -116,6 +193,14 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the {@code Invoke} RPC asynchronously.
|
||||||
|
*
|
||||||
|
* @param request the {@code MxCommandRequest} to send
|
||||||
|
* @return a future completed with the raw reply, or completed exceptionally
|
||||||
|
* with {@link MxGatewayException} (including {@link MxAccessException})
|
||||||
|
* on failure
|
||||||
|
*/
|
||||||
public CompletableFuture<MxCommandReply> invokeAsync(MxCommandRequest request) {
|
public CompletableFuture<MxCommandReply> invokeAsync(MxCommandRequest request) {
|
||||||
CompletableFuture<MxCommandReply> future = toCompletable(rawFutureStub().invoke(request));
|
CompletableFuture<MxCommandReply> future = toCompletable(rawFutureStub().invoke(request));
|
||||||
return future.thenApply(reply -> {
|
return future.thenApply(reply -> {
|
||||||
@@ -125,6 +210,13 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the {@code CloseSession} unary RPC.
|
||||||
|
*
|
||||||
|
* @param request the {@code CloseSessionRequest} to send
|
||||||
|
* @return the raw reply
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public CloseSessionReply closeSessionRaw(CloseSessionRequest request) {
|
public CloseSessionReply closeSessionRaw(CloseSessionRequest request) {
|
||||||
try {
|
try {
|
||||||
CloseSessionReply reply = rawBlockingStub().closeSession(request);
|
CloseSessionReply reply = rawBlockingStub().closeSession(request);
|
||||||
@@ -138,12 +230,28 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to the {@code StreamEvents} server-streaming RPC and exposes
|
||||||
|
* results as a blocking iterator. Closing the returned stream cancels the
|
||||||
|
* underlying gRPC call.
|
||||||
|
*
|
||||||
|
* @param request the {@code StreamEventsRequest} carrying the session id and resume cursor
|
||||||
|
* @return an iterator-style stream of events
|
||||||
|
*/
|
||||||
public MxEventStream streamEvents(StreamEventsRequest request) {
|
public MxEventStream streamEvents(StreamEventsRequest request) {
|
||||||
MxEventStream stream = new MxEventStream(16);
|
MxEventStream stream = new MxEventStream(16);
|
||||||
withStreamDeadline(rawAsyncStub()).streamEvents(request, stream.observer());
|
withStreamDeadline(rawAsyncStub()).streamEvents(request, stream.observer());
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to {@code StreamEvents} and dispatches each event to the
|
||||||
|
* supplied observer. The returned subscription is cancellable.
|
||||||
|
*
|
||||||
|
* @param request the {@code StreamEventsRequest} to send
|
||||||
|
* @param observer caller-supplied observer that receives events and completion
|
||||||
|
* @return a cancellable subscription handle
|
||||||
|
*/
|
||||||
public MxGatewayEventSubscription streamEventsAsync(
|
public MxGatewayEventSubscription streamEventsAsync(
|
||||||
StreamEventsRequest request, StreamObserver<MxEvent> observer) {
|
StreamEventsRequest request, StreamObserver<MxEvent> observer) {
|
||||||
MxGatewayEventSubscription subscription = new MxGatewayEventSubscription();
|
MxGatewayEventSubscription subscription = new MxGatewayEventSubscription();
|
||||||
@@ -158,6 +266,13 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuts the owned channel down and waits up to the configured connect
|
||||||
|
* timeout for termination, forcibly shutting it down on timeout. No-op
|
||||||
|
* for clients that do not own their channel.
|
||||||
|
*
|
||||||
|
* @throws InterruptedException if the calling thread is interrupted while waiting
|
||||||
|
*/
|
||||||
public void closeAndAwaitTermination() throws InterruptedException {
|
public void closeAndAwaitTermination() throws InterruptedException {
|
||||||
if (ownedChannel != null) {
|
if (ownedChannel != null) {
|
||||||
ownedChannel.shutdown();
|
ownedChannel.shutdown();
|
||||||
|
|||||||
+118
@@ -4,6 +4,13 @@ import java.nio.file.Path;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable configuration for {@link MxGatewayClient} and
|
||||||
|
* {@link GalaxyRepositoryClient}.
|
||||||
|
*
|
||||||
|
* <p>Captures the gateway endpoint, API key, transport security selection and
|
||||||
|
* call/stream timeouts. Instances are constructed via {@link #builder()}.
|
||||||
|
*/
|
||||||
public final class MxGatewayClientOptions {
|
public final class MxGatewayClientOptions {
|
||||||
private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
|
private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
|
||||||
private static final Duration DEFAULT_CALL_TIMEOUT = Duration.ofSeconds(30);
|
private static final Duration DEFAULT_CALL_TIMEOUT = Duration.ofSeconds(30);
|
||||||
@@ -28,42 +35,93 @@ public final class MxGatewayClientOptions {
|
|||||||
streamTimeout = builder.streamTimeout;
|
streamTimeout = builder.streamTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a fresh builder with default timeouts and no endpoint set.
|
||||||
|
*
|
||||||
|
* @return a new {@link Builder}
|
||||||
|
*/
|
||||||
public static Builder builder() {
|
public static Builder builder() {
|
||||||
return new Builder();
|
return new Builder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the configured gRPC target endpoint.
|
||||||
|
*
|
||||||
|
* @return the endpoint string in {@code host:port} or DNS-target form
|
||||||
|
*/
|
||||||
public String endpoint() {
|
public String endpoint() {
|
||||||
return endpoint;
|
return endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the configured API key, or an empty string if none was supplied.
|
||||||
|
*
|
||||||
|
* @return the raw API key
|
||||||
|
*/
|
||||||
public String apiKey() {
|
public String apiKey() {
|
||||||
return apiKey;
|
return apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the API key with the body redacted, safe to write to logs.
|
||||||
|
*
|
||||||
|
* @return the redacted form produced by {@link MxGatewaySecrets#redactApiKey(String)}
|
||||||
|
*/
|
||||||
public String redactedApiKey() {
|
public String redactedApiKey() {
|
||||||
return MxGatewaySecrets.redactApiKey(apiKey);
|
return MxGatewaySecrets.redactApiKey(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the client is configured to use plaintext transport.
|
||||||
|
*
|
||||||
|
* @return {@code true} for plaintext, {@code false} for TLS
|
||||||
|
*/
|
||||||
public boolean plaintext() {
|
public boolean plaintext() {
|
||||||
return plaintext;
|
return plaintext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the configured CA certificate file used to verify the gateway,
|
||||||
|
* or {@code null} when the platform trust store is used.
|
||||||
|
*
|
||||||
|
* @return the CA certificate path, or {@code null}
|
||||||
|
*/
|
||||||
public Path caCertificatePath() {
|
public Path caCertificatePath() {
|
||||||
return caCertificatePath;
|
return caCertificatePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the TLS server-name override, or an empty string when none was supplied.
|
||||||
|
*
|
||||||
|
* @return the server-name override
|
||||||
|
*/
|
||||||
public String serverNameOverride() {
|
public String serverNameOverride() {
|
||||||
return serverNameOverride;
|
return serverNameOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the channel connect timeout.
|
||||||
|
*
|
||||||
|
* @return the connect timeout duration
|
||||||
|
*/
|
||||||
public Duration connectTimeout() {
|
public Duration connectTimeout() {
|
||||||
return connectTimeout;
|
return connectTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the per-call deadline applied to unary RPCs.
|
||||||
|
*
|
||||||
|
* @return the call timeout duration
|
||||||
|
*/
|
||||||
public Duration callTimeout() {
|
public Duration callTimeout() {
|
||||||
return callTimeout;
|
return callTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the deadline applied to server-streaming RPCs, or {@code null} when none is set.
|
||||||
|
*
|
||||||
|
* @return the stream timeout duration, or {@code null}
|
||||||
|
*/
|
||||||
public Duration streamTimeout() {
|
public Duration streamTimeout() {
|
||||||
return streamTimeout;
|
return streamTimeout;
|
||||||
}
|
}
|
||||||
@@ -100,6 +158,9 @@ public final class MxGatewayClientOptions {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutable builder for {@link MxGatewayClientOptions}.
|
||||||
|
*/
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
private String endpoint;
|
private String endpoint;
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
@@ -113,46 +174,103 @@ public final class MxGatewayClientOptions {
|
|||||||
private Builder() {
|
private Builder() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the gRPC target endpoint.
|
||||||
|
*
|
||||||
|
* @param value endpoint in {@code host:port} or DNS-target form; required
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
public Builder endpoint(String value) {
|
public Builder endpoint(String value) {
|
||||||
endpoint = value;
|
endpoint = value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the API key sent in the {@code authorization} header.
|
||||||
|
*
|
||||||
|
* @param value the API key, or {@code null}/blank to disable authentication
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
public Builder apiKey(String value) {
|
public Builder apiKey(String value) {
|
||||||
apiKey = value;
|
apiKey = value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects plaintext transport instead of TLS.
|
||||||
|
*
|
||||||
|
* @param value {@code true} for plaintext, {@code false} for TLS
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
public Builder plaintext(boolean value) {
|
public Builder plaintext(boolean value) {
|
||||||
plaintext = value;
|
plaintext = value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the CA certificate used to verify the gateway server.
|
||||||
|
*
|
||||||
|
* @param value path to a PEM-encoded CA certificate, or {@code null} to use the platform trust store
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
public Builder caCertificatePath(Path value) {
|
public Builder caCertificatePath(Path value) {
|
||||||
caCertificatePath = value;
|
caCertificatePath = value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the TLS server name used during the handshake.
|
||||||
|
*
|
||||||
|
* @param value the override host name, or empty/{@code null} for none
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
public Builder serverNameOverride(String value) {
|
public Builder serverNameOverride(String value) {
|
||||||
serverNameOverride = value;
|
serverNameOverride = value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the channel connect timeout.
|
||||||
|
*
|
||||||
|
* @param value the connect timeout, must be non-{@code null}
|
||||||
|
* @return this builder
|
||||||
|
* @throws NullPointerException if {@code value} is {@code null}
|
||||||
|
*/
|
||||||
public Builder connectTimeout(Duration value) {
|
public Builder connectTimeout(Duration value) {
|
||||||
connectTimeout = Objects.requireNonNull(value, "connectTimeout");
|
connectTimeout = Objects.requireNonNull(value, "connectTimeout");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the per-call deadline applied to unary RPCs.
|
||||||
|
*
|
||||||
|
* @param value the call timeout, must be non-{@code null}
|
||||||
|
* @return this builder
|
||||||
|
* @throws NullPointerException if {@code value} is {@code null}
|
||||||
|
*/
|
||||||
public Builder callTimeout(Duration value) {
|
public Builder callTimeout(Duration value) {
|
||||||
callTimeout = Objects.requireNonNull(value, "callTimeout");
|
callTimeout = Objects.requireNonNull(value, "callTimeout");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the deadline applied to server-streaming RPCs.
|
||||||
|
*
|
||||||
|
* @param value the stream timeout, must be non-{@code null}
|
||||||
|
* @return this builder
|
||||||
|
* @throws NullPointerException if {@code value} is {@code null}
|
||||||
|
*/
|
||||||
public Builder streamTimeout(Duration value) {
|
public Builder streamTimeout(Duration value) {
|
||||||
streamTimeout = Objects.requireNonNull(value, "streamTimeout");
|
streamTimeout = Objects.requireNonNull(value, "streamTimeout");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an immutable {@link MxGatewayClientOptions} from the current state.
|
||||||
|
*
|
||||||
|
* @return a new options instance
|
||||||
|
* @throws IllegalArgumentException if {@code endpoint} was not set or is blank
|
||||||
|
*/
|
||||||
public MxGatewayClientOptions build() {
|
public MxGatewayClientOptions build() {
|
||||||
return new MxGatewayClientOptions(this);
|
return new MxGatewayClientOptions(this);
|
||||||
}
|
}
|
||||||
|
|||||||
+21
@@ -1,5 +1,11 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
package com.dohertylan.mxgateway.client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reports the client and protocol version numbers compiled into this build.
|
||||||
|
*
|
||||||
|
* <p>Used by smoke-test tooling and the CLI to confirm that a gateway and
|
||||||
|
* worker speak the same protocol version as the client.
|
||||||
|
*/
|
||||||
public final class MxGatewayClientVersion {
|
public final class MxGatewayClientVersion {
|
||||||
private static final int GATEWAY_PROTOCOL_VERSION = 1;
|
private static final int GATEWAY_PROTOCOL_VERSION = 1;
|
||||||
private static final int WORKER_PROTOCOL_VERSION = 1;
|
private static final int WORKER_PROTOCOL_VERSION = 1;
|
||||||
@@ -8,14 +14,29 @@ public final class MxGatewayClientVersion {
|
|||||||
private MxGatewayClientVersion() {
|
private MxGatewayClientVersion() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the human-readable client release version.
|
||||||
|
*
|
||||||
|
* @return the client version string
|
||||||
|
*/
|
||||||
public static String clientVersion() {
|
public static String clientVersion() {
|
||||||
return CLIENT_VERSION;
|
return CLIENT_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the gRPC gateway protocol version this client targets.
|
||||||
|
*
|
||||||
|
* @return the gateway protocol version
|
||||||
|
*/
|
||||||
public static int gatewayProtocolVersion() {
|
public static int gatewayProtocolVersion() {
|
||||||
return GATEWAY_PROTOCOL_VERSION;
|
return GATEWAY_PROTOCOL_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the worker IPC protocol version this client targets.
|
||||||
|
*
|
||||||
|
* @return the worker protocol version
|
||||||
|
*/
|
||||||
public static int workerProtocolVersion() {
|
public static int workerProtocolVersion() {
|
||||||
return WORKER_PROTOCOL_VERSION;
|
return WORKER_PROTOCOL_VERSION;
|
||||||
}
|
}
|
||||||
|
|||||||
+22
@@ -3,20 +3,42 @@ package com.dohertylan.mxgateway.client;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when the gateway accepts an MXAccess command but the command itself
|
||||||
|
* fails at the protocol layer. Carries the original {@code MxCommandReply} and
|
||||||
|
* {@code ProtocolStatus} so callers can inspect the failure detail.
|
||||||
|
*/
|
||||||
public class MxGatewayCommandException extends MxGatewayException {
|
public class MxGatewayCommandException extends MxGatewayException {
|
||||||
private final ProtocolStatus protocolStatus;
|
private final ProtocolStatus protocolStatus;
|
||||||
private final MxCommandReply reply;
|
private final MxCommandReply reply;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new command exception.
|
||||||
|
*
|
||||||
|
* @param operation human-readable name of the failing operation
|
||||||
|
* @param protocolStatus protocol status returned by the gateway
|
||||||
|
* @param reply raw command reply, or {@code null} when the call failed before a reply was produced
|
||||||
|
*/
|
||||||
public MxGatewayCommandException(String operation, ProtocolStatus protocolStatus, MxCommandReply reply) {
|
public MxGatewayCommandException(String operation, ProtocolStatus protocolStatus, MxCommandReply reply) {
|
||||||
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
||||||
this.protocolStatus = protocolStatus;
|
this.protocolStatus = protocolStatus;
|
||||||
this.reply = reply;
|
this.reply = reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the gateway protocol status that triggered this exception.
|
||||||
|
*
|
||||||
|
* @return the protocol status, or {@code null} if none was supplied
|
||||||
|
*/
|
||||||
public ProtocolStatus protocolStatus() {
|
public ProtocolStatus protocolStatus() {
|
||||||
return protocolStatus;
|
return protocolStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw command reply associated with the failure.
|
||||||
|
*
|
||||||
|
* @return the command reply, or {@code null} if no reply was available
|
||||||
|
*/
|
||||||
public MxCommandReply reply() {
|
public MxCommandReply reply() {
|
||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -8,6 +8,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancellable handle returned by the async {@code streamEvents} variant.
|
||||||
|
*
|
||||||
|
* <p>Wraps a caller-supplied {@link StreamObserver} and exposes a
|
||||||
|
* {@link #cancel()} entry point that aborts the underlying gRPC call. The
|
||||||
|
* subscription also implements {@link AutoCloseable} so it can participate in
|
||||||
|
* try-with-resources blocks.
|
||||||
|
*/
|
||||||
public final class MxGatewayEventSubscription implements AutoCloseable {
|
public final class MxGatewayEventSubscription implements AutoCloseable {
|
||||||
private final AtomicReference<ClientCallStreamObserver<StreamEventsRequest>> requestStream = new AtomicReference<>();
|
private final AtomicReference<ClientCallStreamObserver<StreamEventsRequest>> requestStream = new AtomicReference<>();
|
||||||
private final AtomicBoolean cancelled = new AtomicBoolean();
|
private final AtomicBoolean cancelled = new AtomicBoolean();
|
||||||
@@ -39,6 +47,11 @@ public final class MxGatewayEventSubscription implements AutoCloseable {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the underlying gRPC call. Safe to invoke before the call has
|
||||||
|
* started; cancellation is recorded and applied as soon as the stream
|
||||||
|
* attaches.
|
||||||
|
*/
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
cancelled.set(true);
|
cancelled.set(true);
|
||||||
ClientCallStreamObserver<StreamEventsRequest> stream = requestStream.get();
|
ClientCallStreamObserver<StreamEventsRequest> stream = requestStream.get();
|
||||||
|
|||||||
+18
@@ -1,10 +1,28 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
package com.dohertylan.mxgateway.client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base unchecked exception thrown by the MXAccess Gateway Java client.
|
||||||
|
*
|
||||||
|
* <p>All gateway-specific failures derive from this type so callers can catch a
|
||||||
|
* single supertype regardless of whether the cause was a transport error,
|
||||||
|
* protocol-level failure, or MXAccess-side problem.
|
||||||
|
*/
|
||||||
public class MxGatewayException extends RuntimeException {
|
public class MxGatewayException extends RuntimeException {
|
||||||
|
/**
|
||||||
|
* Creates a new exception with the supplied message.
|
||||||
|
*
|
||||||
|
* @param message human-readable description of the failure
|
||||||
|
*/
|
||||||
public MxGatewayException(String message) {
|
public MxGatewayException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new exception with the supplied message and underlying cause.
|
||||||
|
*
|
||||||
|
* @param message human-readable description of the failure
|
||||||
|
* @param cause underlying error that triggered the failure
|
||||||
|
*/
|
||||||
public MxGatewayException(String message, Throwable cause) {
|
public MxGatewayException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|||||||
+24
@@ -1,9 +1,24 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
package com.dohertylan.mxgateway.client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers for redacting secrets such as gateway API keys from log output.
|
||||||
|
*
|
||||||
|
* <p>API keys must never reach logs in plaintext. The methods on this class
|
||||||
|
* produce shortened, masked forms safe for diagnostic messages.
|
||||||
|
*/
|
||||||
public final class MxGatewaySecrets {
|
public final class MxGatewaySecrets {
|
||||||
private MxGatewaySecrets() {
|
private MxGatewaySecrets() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redacts the body of an API key, leaving only short prefix and suffix
|
||||||
|
* windows so it remains comparable in logs.
|
||||||
|
*
|
||||||
|
* @param apiKey the API key to redact, may be {@code null} or empty
|
||||||
|
* @return an empty string for {@code null}/empty input, {@code "<redacted>"}
|
||||||
|
* for keys eight characters or shorter, or a masked form preserving
|
||||||
|
* the leading and trailing four characters
|
||||||
|
*/
|
||||||
public static String redactApiKey(String apiKey) {
|
public static String redactApiKey(String apiKey) {
|
||||||
if (apiKey == null || apiKey.isEmpty()) {
|
if (apiKey == null || apiKey.isEmpty()) {
|
||||||
return "";
|
return "";
|
||||||
@@ -17,6 +32,15 @@ public final class MxGatewaySecrets {
|
|||||||
+ apiKey.substring(apiKey.length() - 4);
|
+ apiKey.substring(apiKey.length() - 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces gateway-style credential tokens (the {@code mxgw_} prefix and
|
||||||
|
* any {@code Bearer} marker) inside a free-form string with a redaction
|
||||||
|
* placeholder.
|
||||||
|
*
|
||||||
|
* @param value the string to scrub, may be {@code null}
|
||||||
|
* @return an empty string for {@code null}, the original value when blank,
|
||||||
|
* or the value with credential tokens replaced by {@code "<redacted>"}
|
||||||
|
*/
|
||||||
public static String redactCredentials(String value) {
|
public static String redactCredentials(String value) {
|
||||||
if (value == null || value.isBlank()) {
|
if (value == null || value.isBlank()) {
|
||||||
return value == null ? "" : value;
|
return value == null ? "" : value;
|
||||||
|
|||||||
+240
@@ -30,6 +30,14 @@ import mxaccess_gateway.v1.MxaccessGateway.UnregisterCommand;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.Write2Command;
|
import mxaccess_gateway.v1.MxaccessGateway.Write2Command;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.WriteCommand;
|
import mxaccess_gateway.v1.MxaccessGateway.WriteCommand;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typed handle for a single MXAccess gateway session.
|
||||||
|
*
|
||||||
|
* <p>Wraps an {@link OpenSessionReply} together with the {@link MxGatewayClient}
|
||||||
|
* that opened it and exposes the MXAccess command surface (Register, AddItem,
|
||||||
|
* Advise, bulk subscribe variants, Write, event streaming, and close). Each
|
||||||
|
* command request carries a freshly generated client correlation id.
|
||||||
|
*/
|
||||||
public final class MxGatewaySession implements AutoCloseable {
|
public final class MxGatewaySession implements AutoCloseable {
|
||||||
private static final SecureRandom RANDOM = new SecureRandom();
|
private static final SecureRandom RANDOM = new SecureRandom();
|
||||||
|
|
||||||
@@ -42,19 +50,45 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
this.openReply = Objects.requireNonNull(openReply, "openReply");
|
this.openReply = Objects.requireNonNull(openReply, "openReply");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a session handle for an existing gateway session id without
|
||||||
|
* issuing an {@code OpenSession} call. Useful for CLI tools that operate
|
||||||
|
* against a session opened in a separate invocation.
|
||||||
|
*
|
||||||
|
* @param client the gateway client used for further commands
|
||||||
|
* @param sessionId the existing gateway session id
|
||||||
|
* @return a session handle bound to the supplied id
|
||||||
|
*/
|
||||||
public static MxGatewaySession forSessionId(MxGatewayClient client, String sessionId) {
|
public static MxGatewaySession forSessionId(MxGatewayClient client, String sessionId) {
|
||||||
return new MxGatewaySession(
|
return new MxGatewaySession(
|
||||||
client, OpenSessionReply.newBuilder().setSessionId(sessionId).build());
|
client, OpenSessionReply.newBuilder().setSessionId(sessionId).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the gateway-assigned session id.
|
||||||
|
*
|
||||||
|
* @return the session id
|
||||||
|
*/
|
||||||
public String sessionId() {
|
public String sessionId() {
|
||||||
return openReply.getSessionId();
|
return openReply.getSessionId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the original {@link OpenSessionReply} that this session was opened with.
|
||||||
|
*
|
||||||
|
* @return the open-session reply
|
||||||
|
*/
|
||||||
public OpenSessionReply openReply() {
|
public OpenSessionReply openReply() {
|
||||||
return openReply;
|
return openReply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a {@code CloseSession} RPC and caches the reply so subsequent calls
|
||||||
|
* are idempotent.
|
||||||
|
*
|
||||||
|
* @return the raw close-session reply
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public synchronized CloseSessionReply closeRaw() {
|
public synchronized CloseSessionReply closeRaw() {
|
||||||
if (closeReply == null) {
|
if (closeReply == null) {
|
||||||
closeReply = client.closeSessionRaw(CloseSessionRequest.newBuilder()
|
closeReply = client.closeSessionRaw(CloseSessionRequest.newBuilder()
|
||||||
@@ -70,6 +104,13 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
closeRaw();
|
closeRaw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code Register} and returns the server handle.
|
||||||
|
*
|
||||||
|
* @param clientName the MXAccess client name to register
|
||||||
|
* @return the {@code ServerHandle} returned by MXAccess
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public int register(String clientName) {
|
public int register(String clientName) {
|
||||||
MxCommandReply reply = registerRaw(clientName);
|
MxCommandReply reply = registerRaw(clientName);
|
||||||
if (reply.hasRegister()) {
|
if (reply.hasRegister()) {
|
||||||
@@ -78,6 +119,13 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getReturnValue().getInt32Value();
|
return reply.getReturnValue().getInt32Value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code Register} and returns the raw reply.
|
||||||
|
*
|
||||||
|
* @param clientName the MXAccess client name to register
|
||||||
|
* @return the raw command reply
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public MxCommandReply registerRaw(String clientName) {
|
public MxCommandReply registerRaw(String clientName) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_REGISTER)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_REGISTER)
|
||||||
@@ -85,6 +133,12 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code Unregister}.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} returned by {@link #register(String)}
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public void unregister(int serverHandle) {
|
public void unregister(int serverHandle) {
|
||||||
invokeCommand(MxCommand.newBuilder()
|
invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_UNREGISTER)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_UNREGISTER)
|
||||||
@@ -92,6 +146,14 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code AddItem} and returns the new item handle.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the item
|
||||||
|
* @param itemDefinition the MXAccess item definition (tag reference)
|
||||||
|
* @return the {@code ItemHandle} assigned by MXAccess
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public int addItem(int serverHandle, String itemDefinition) {
|
public int addItem(int serverHandle, String itemDefinition) {
|
||||||
MxCommandReply reply = addItemRaw(serverHandle, itemDefinition);
|
MxCommandReply reply = addItemRaw(serverHandle, itemDefinition);
|
||||||
if (reply.hasAddItem()) {
|
if (reply.hasAddItem()) {
|
||||||
@@ -100,6 +162,14 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getReturnValue().getInt32Value();
|
return reply.getReturnValue().getInt32Value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code AddItem} and returns the raw reply.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the item
|
||||||
|
* @param itemDefinition the MXAccess item definition
|
||||||
|
* @return the raw command reply
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public MxCommandReply addItemRaw(int serverHandle, String itemDefinition) {
|
public MxCommandReply addItemRaw(int serverHandle, String itemDefinition) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM)
|
||||||
@@ -109,6 +179,15 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code AddItem2} and returns the new item handle.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the item
|
||||||
|
* @param itemDefinition the MXAccess item definition
|
||||||
|
* @param itemContext the MXAccess item context (e.g. galaxy/object scope)
|
||||||
|
* @return the {@code ItemHandle} assigned by MXAccess
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public int addItem2(int serverHandle, String itemDefinition, String itemContext) {
|
public int addItem2(int serverHandle, String itemDefinition, String itemContext) {
|
||||||
MxCommandReply reply = addItem2Raw(serverHandle, itemDefinition, itemContext);
|
MxCommandReply reply = addItem2Raw(serverHandle, itemDefinition, itemContext);
|
||||||
if (reply.hasAddItem2()) {
|
if (reply.hasAddItem2()) {
|
||||||
@@ -117,6 +196,15 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getReturnValue().getInt32Value();
|
return reply.getReturnValue().getInt32Value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code AddItem2} and returns the raw reply.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the item
|
||||||
|
* @param itemDefinition the MXAccess item definition
|
||||||
|
* @param itemContext the MXAccess item context
|
||||||
|
* @return the raw command reply
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public MxCommandReply addItem2Raw(int serverHandle, String itemDefinition, String itemContext) {
|
public MxCommandReply addItem2Raw(int serverHandle, String itemDefinition, String itemContext) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM2)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM2)
|
||||||
@@ -127,10 +215,25 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code RemoveItem}.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the item
|
||||||
|
* @param itemHandle the {@code ItemHandle} to remove
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public void removeItem(int serverHandle, int itemHandle) {
|
public void removeItem(int serverHandle, int itemHandle) {
|
||||||
removeItemRaw(serverHandle, itemHandle);
|
removeItemRaw(serverHandle, itemHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code RemoveItem} and returns the raw reply.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the item
|
||||||
|
* @param itemHandle the {@code ItemHandle} to remove
|
||||||
|
* @return the raw command reply
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public MxCommandReply removeItemRaw(int serverHandle, int itemHandle) {
|
public MxCommandReply removeItemRaw(int serverHandle, int itemHandle) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_REMOVE_ITEM)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_REMOVE_ITEM)
|
||||||
@@ -140,10 +243,25 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code Advise} so the item starts emitting data-change events.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the item
|
||||||
|
* @param itemHandle the {@code ItemHandle} to advise
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public void advise(int serverHandle, int itemHandle) {
|
public void advise(int serverHandle, int itemHandle) {
|
||||||
adviseRaw(serverHandle, itemHandle);
|
adviseRaw(serverHandle, itemHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code Advise} and returns the raw reply.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the item
|
||||||
|
* @param itemHandle the {@code ItemHandle} to advise
|
||||||
|
* @return the raw command reply
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public MxCommandReply adviseRaw(int serverHandle, int itemHandle) {
|
public MxCommandReply adviseRaw(int serverHandle, int itemHandle) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE)
|
||||||
@@ -153,10 +271,25 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code UnAdvise} so the item stops emitting data-change events.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the item
|
||||||
|
* @param itemHandle the {@code ItemHandle} to un-advise
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public void unAdvise(int serverHandle, int itemHandle) {
|
public void unAdvise(int serverHandle, int itemHandle) {
|
||||||
unAdviseRaw(serverHandle, itemHandle);
|
unAdviseRaw(serverHandle, itemHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code UnAdvise} and returns the raw reply.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the item
|
||||||
|
* @param itemHandle the {@code ItemHandle} to un-advise
|
||||||
|
* @return the raw command reply
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public MxCommandReply unAdviseRaw(int serverHandle, int itemHandle) {
|
public MxCommandReply unAdviseRaw(int serverHandle, int itemHandle) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_UN_ADVISE)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_UN_ADVISE)
|
||||||
@@ -166,6 +299,15 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the bulk {@code AddItem} variant.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the items
|
||||||
|
* @param tagAddresses the MXAccess tag addresses to add
|
||||||
|
* @return a per-tag {@link SubscribeResult} list
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
* @throws NullPointerException if {@code tagAddresses} is {@code null}
|
||||||
|
*/
|
||||||
public List<SubscribeResult> addItemBulk(int serverHandle, List<String> tagAddresses) {
|
public List<SubscribeResult> addItemBulk(int serverHandle, List<String> tagAddresses) {
|
||||||
Objects.requireNonNull(tagAddresses, "tagAddresses");
|
Objects.requireNonNull(tagAddresses, "tagAddresses");
|
||||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||||
@@ -177,6 +319,15 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getAddItemBulk().getResultsList();
|
return reply.getAddItemBulk().getResultsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the bulk {@code Advise} variant.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the items
|
||||||
|
* @param itemHandles the {@code ItemHandle} list to advise
|
||||||
|
* @return a per-item {@link SubscribeResult} list
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
* @throws NullPointerException if {@code itemHandles} is {@code null}
|
||||||
|
*/
|
||||||
public List<SubscribeResult> adviseItemBulk(int serverHandle, List<Integer> itemHandles) {
|
public List<SubscribeResult> adviseItemBulk(int serverHandle, List<Integer> itemHandles) {
|
||||||
Objects.requireNonNull(itemHandles, "itemHandles");
|
Objects.requireNonNull(itemHandles, "itemHandles");
|
||||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||||
@@ -188,6 +339,15 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getAdviseItemBulk().getResultsList();
|
return reply.getAdviseItemBulk().getResultsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the bulk {@code RemoveItem} variant.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the items
|
||||||
|
* @param itemHandles the {@code ItemHandle} list to remove
|
||||||
|
* @return a per-item {@link SubscribeResult} list
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
* @throws NullPointerException if {@code itemHandles} is {@code null}
|
||||||
|
*/
|
||||||
public List<SubscribeResult> removeItemBulk(int serverHandle, List<Integer> itemHandles) {
|
public List<SubscribeResult> removeItemBulk(int serverHandle, List<Integer> itemHandles) {
|
||||||
Objects.requireNonNull(itemHandles, "itemHandles");
|
Objects.requireNonNull(itemHandles, "itemHandles");
|
||||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||||
@@ -199,6 +359,15 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getRemoveItemBulk().getResultsList();
|
return reply.getRemoveItemBulk().getResultsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the bulk {@code UnAdvise} variant.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the items
|
||||||
|
* @param itemHandles the {@code ItemHandle} list to un-advise
|
||||||
|
* @return a per-item {@link SubscribeResult} list
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
* @throws NullPointerException if {@code itemHandles} is {@code null}
|
||||||
|
*/
|
||||||
public List<SubscribeResult> unAdviseItemBulk(int serverHandle, List<Integer> itemHandles) {
|
public List<SubscribeResult> unAdviseItemBulk(int serverHandle, List<Integer> itemHandles) {
|
||||||
Objects.requireNonNull(itemHandles, "itemHandles");
|
Objects.requireNonNull(itemHandles, "itemHandles");
|
||||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||||
@@ -210,6 +379,16 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getUnAdviseItemBulk().getResultsList();
|
return reply.getUnAdviseItemBulk().getResultsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the gateway {@code SubscribeBulk} convenience that combines
|
||||||
|
* AddItem and Advise for the supplied tag addresses.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the items
|
||||||
|
* @param tagAddresses the MXAccess tag addresses to subscribe
|
||||||
|
* @return a per-tag {@link SubscribeResult} list
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
* @throws NullPointerException if {@code tagAddresses} is {@code null}
|
||||||
|
*/
|
||||||
public List<SubscribeResult> subscribeBulk(int serverHandle, List<String> tagAddresses) {
|
public List<SubscribeResult> subscribeBulk(int serverHandle, List<String> tagAddresses) {
|
||||||
Objects.requireNonNull(tagAddresses, "tagAddresses");
|
Objects.requireNonNull(tagAddresses, "tagAddresses");
|
||||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||||
@@ -221,6 +400,16 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getSubscribeBulk().getResultsList();
|
return reply.getSubscribeBulk().getResultsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the gateway {@code UnsubscribeBulk} convenience that combines
|
||||||
|
* UnAdvise and RemoveItem for the supplied item handles.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the items
|
||||||
|
* @param itemHandles the {@code ItemHandle} list to unsubscribe
|
||||||
|
* @return a per-item {@link SubscribeResult} list
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
* @throws NullPointerException if {@code itemHandles} is {@code null}
|
||||||
|
*/
|
||||||
public List<SubscribeResult> unsubscribeBulk(int serverHandle, List<Integer> itemHandles) {
|
public List<SubscribeResult> unsubscribeBulk(int serverHandle, List<Integer> itemHandles) {
|
||||||
Objects.requireNonNull(itemHandles, "itemHandles");
|
Objects.requireNonNull(itemHandles, "itemHandles");
|
||||||
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
MxCommandReply reply = invokeCommand(MxCommand.newBuilder()
|
||||||
@@ -232,10 +421,29 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
return reply.getUnsubscribeBulk().getResultsList();
|
return reply.getUnsubscribeBulk().getResultsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code Write}.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the item
|
||||||
|
* @param itemHandle the {@code ItemHandle} to write
|
||||||
|
* @param value the value to write
|
||||||
|
* @param userId the MXAccess user id used for security checks
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public void write(int serverHandle, int itemHandle, MxValue value, int userId) {
|
public void write(int serverHandle, int itemHandle, MxValue value, int userId) {
|
||||||
writeRaw(serverHandle, itemHandle, value, userId);
|
writeRaw(serverHandle, itemHandle, value, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code Write} and returns the raw reply.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the item
|
||||||
|
* @param itemHandle the {@code ItemHandle} to write
|
||||||
|
* @param value the value to write
|
||||||
|
* @param userId the MXAccess user id used for security checks
|
||||||
|
* @return the raw command reply
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
|
public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
return invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE)
|
||||||
@@ -247,6 +455,16 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes MXAccess {@code Write2}, which carries an explicit timestamp.
|
||||||
|
*
|
||||||
|
* @param serverHandle the {@code ServerHandle} owning the item
|
||||||
|
* @param itemHandle the {@code ItemHandle} to write
|
||||||
|
* @param value the value to write
|
||||||
|
* @param timestampValue the timestamp value to associate with the write
|
||||||
|
* @param userId the MXAccess user id used for security checks
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public void write2(int serverHandle, int itemHandle, MxValue value, MxValue timestampValue, int userId) {
|
public void write2(int serverHandle, int itemHandle, MxValue value, MxValue timestampValue, int userId) {
|
||||||
invokeCommand(MxCommand.newBuilder()
|
invokeCommand(MxCommand.newBuilder()
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE2)
|
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE2)
|
||||||
@@ -259,10 +477,24 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to gateway events for this session starting from the
|
||||||
|
* beginning of the worker event log.
|
||||||
|
*
|
||||||
|
* @return an iterator-style stream of events
|
||||||
|
*/
|
||||||
public MxEventStream streamEvents() {
|
public MxEventStream streamEvents() {
|
||||||
return streamEventsAfter(0);
|
return streamEventsAfter(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to gateway events for this session starting after the
|
||||||
|
* supplied worker sequence number.
|
||||||
|
*
|
||||||
|
* @param afterWorkerSequence the resume cursor; events with worker sequence
|
||||||
|
* greater than this value are delivered
|
||||||
|
* @return an iterator-style stream of events
|
||||||
|
*/
|
||||||
public MxEventStream streamEventsAfter(long afterWorkerSequence) {
|
public MxEventStream streamEventsAfter(long afterWorkerSequence) {
|
||||||
return client.streamEvents(StreamEventsRequest.newBuilder()
|
return client.streamEvents(StreamEventsRequest.newBuilder()
|
||||||
.setSessionId(sessionId())
|
.setSessionId(sessionId())
|
||||||
@@ -270,6 +502,14 @@ public final class MxGatewaySession implements AutoCloseable {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a pre-built {@link MxCommand} for this session and returns the raw
|
||||||
|
* reply, attaching a freshly generated client correlation id.
|
||||||
|
*
|
||||||
|
* @param command the command to send
|
||||||
|
* @return the raw command reply
|
||||||
|
* @throws MxGatewayException on transport or protocol failure
|
||||||
|
*/
|
||||||
public MxCommandReply invokeCommand(MxCommand command) {
|
public MxCommandReply invokeCommand(MxCommand command) {
|
||||||
return client.invoke(MxCommandRequest.newBuilder()
|
return client.invoke(MxCommandRequest.newBuilder()
|
||||||
.setSessionId(sessionId())
|
.setSessionId(sessionId())
|
||||||
|
|||||||
+15
@@ -2,14 +2,29 @@ package com.dohertylan.mxgateway.client;
|
|||||||
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when the gateway reports a session-related protocol failure such as
|
||||||
|
* {@code SESSION_NOT_FOUND} or {@code SESSION_NOT_READY}.
|
||||||
|
*/
|
||||||
public final class MxGatewaySessionException extends MxGatewayException {
|
public final class MxGatewaySessionException extends MxGatewayException {
|
||||||
private final ProtocolStatus protocolStatus;
|
private final ProtocolStatus protocolStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new session exception from a protocol status.
|
||||||
|
*
|
||||||
|
* @param operation human-readable name of the failing operation
|
||||||
|
* @param protocolStatus protocol status returned by the gateway
|
||||||
|
*/
|
||||||
public MxGatewaySessionException(String operation, ProtocolStatus protocolStatus) {
|
public MxGatewaySessionException(String operation, ProtocolStatus protocolStatus) {
|
||||||
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
||||||
this.protocolStatus = protocolStatus;
|
this.protocolStatus = protocolStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the gateway protocol status that triggered this exception.
|
||||||
|
*
|
||||||
|
* @return the protocol status, or {@code null} if none was supplied
|
||||||
|
*/
|
||||||
public ProtocolStatus protocolStatus() {
|
public ProtocolStatus protocolStatus() {
|
||||||
return protocolStatus;
|
return protocolStatus;
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -2,14 +2,29 @@ package com.dohertylan.mxgateway.client;
|
|||||||
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when the gateway reports a worker-side protocol failure such as
|
||||||
|
* {@code WORKER_UNAVAILABLE} or {@code PROTOCOL_VIOLATION}.
|
||||||
|
*/
|
||||||
public final class MxGatewayWorkerException extends MxGatewayException {
|
public final class MxGatewayWorkerException extends MxGatewayException {
|
||||||
private final ProtocolStatus protocolStatus;
|
private final ProtocolStatus protocolStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new worker exception from a protocol status.
|
||||||
|
*
|
||||||
|
* @param operation human-readable name of the failing operation
|
||||||
|
* @param protocolStatus protocol status returned by the gateway
|
||||||
|
*/
|
||||||
public MxGatewayWorkerException(String operation, ProtocolStatus protocolStatus) {
|
public MxGatewayWorkerException(String operation, ProtocolStatus protocolStatus) {
|
||||||
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
||||||
this.protocolStatus = protocolStatus;
|
this.protocolStatus = protocolStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the gateway protocol status that triggered this exception.
|
||||||
|
*
|
||||||
|
* @return the protocol status, or {@code null} if none was supplied
|
||||||
|
*/
|
||||||
public ProtocolStatus protocolStatus() {
|
public ProtocolStatus protocolStatus() {
|
||||||
return protocolStatus;
|
return protocolStatus;
|
||||||
}
|
}
|
||||||
|
|||||||
+61
@@ -4,43 +4,104 @@ import mxaccess_gateway.v1.MxaccessGateway.MxStatusCategory;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy;
|
import mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusSource;
|
import mxaccess_gateway.v1.MxaccessGateway.MxStatusSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers for inspecting {@link MxStatusProxy} values returned by the gateway.
|
||||||
|
*
|
||||||
|
* <p>An {@code MxStatusProxy} mirrors the MXAccess COM {@code MXSTATUS_PROXY}
|
||||||
|
* struct. The success flag uses the MXAccess convention where any non-zero
|
||||||
|
* value indicates success.
|
||||||
|
*/
|
||||||
public final class MxStatuses {
|
public final class MxStatuses {
|
||||||
private MxStatuses() {
|
private MxStatuses() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the supplied status proxy reports success.
|
||||||
|
*
|
||||||
|
* @param status the status proxy, may be {@code null}
|
||||||
|
* @return {@code true} if {@code status} is {@code null} or its success
|
||||||
|
* flag is non-zero, {@code false} otherwise
|
||||||
|
*/
|
||||||
public static boolean succeeded(MxStatusProxy status) {
|
public static boolean succeeded(MxStatusProxy status) {
|
||||||
return status == null || status.getSuccess() != 0;
|
return status == null || status.getSuccess() != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a raw {@link MxStatusProxy} in an accessor view that exposes its
|
||||||
|
* fields with idiomatic Java getters.
|
||||||
|
*
|
||||||
|
* @param status the raw status proxy
|
||||||
|
* @return a view backed by {@code status}
|
||||||
|
*/
|
||||||
public static MxStatusView view(MxStatusProxy status) {
|
public static MxStatusView view(MxStatusProxy status) {
|
||||||
return new MxStatusView(status);
|
return new MxStatusView(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Idiomatic-Java accessor view over a raw {@link MxStatusProxy}.
|
||||||
|
*
|
||||||
|
* @param raw the underlying status proxy this view delegates to
|
||||||
|
*/
|
||||||
public record MxStatusView(MxStatusProxy raw) {
|
public record MxStatusView(MxStatusProxy raw) {
|
||||||
|
/**
|
||||||
|
* Returns the raw success flag (non-zero indicates success).
|
||||||
|
*
|
||||||
|
* @return the success flag value
|
||||||
|
*/
|
||||||
public int success() {
|
public int success() {
|
||||||
return raw.getSuccess();
|
return raw.getSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the high-level status category.
|
||||||
|
*
|
||||||
|
* @return the status category enum value
|
||||||
|
*/
|
||||||
public MxStatusCategory category() {
|
public MxStatusCategory category() {
|
||||||
return raw.getCategory();
|
return raw.getCategory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns which subsystem detected the status.
|
||||||
|
*
|
||||||
|
* @return the detection source enum value
|
||||||
|
*/
|
||||||
public MxStatusSource detectedBy() {
|
public MxStatusSource detectedBy() {
|
||||||
return raw.getDetectedBy();
|
return raw.getDetectedBy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the detail code accompanying the status.
|
||||||
|
*
|
||||||
|
* @return the raw detail code
|
||||||
|
*/
|
||||||
public int detail() {
|
public int detail() {
|
||||||
return raw.getDetail();
|
return raw.getDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw, unmapped category code from MXAccess.
|
||||||
|
*
|
||||||
|
* @return the raw category integer
|
||||||
|
*/
|
||||||
public int rawCategory() {
|
public int rawCategory() {
|
||||||
return raw.getRawCategory();
|
return raw.getRawCategory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw, unmapped detection-source code from MXAccess.
|
||||||
|
*
|
||||||
|
* @return the raw detection-source integer
|
||||||
|
*/
|
||||||
public int rawDetectedBy() {
|
public int rawDetectedBy() {
|
||||||
return raw.getRawDetectedBy();
|
return raw.getRawDetectedBy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the diagnostic text supplied by MXAccess, if any.
|
||||||
|
*
|
||||||
|
* @return the diagnostic message, possibly empty
|
||||||
|
*/
|
||||||
public String diagnosticText() {
|
public String diagnosticText() {
|
||||||
return raw.getDiagnosticText();
|
return raw.getDiagnosticText();
|
||||||
}
|
}
|
||||||
|
|||||||
+84
@@ -17,10 +17,24 @@ import mxaccess_gateway.v1.MxaccessGateway.RawArray;
|
|||||||
import mxaccess_gateway.v1.MxaccessGateway.StringArray;
|
import mxaccess_gateway.v1.MxaccessGateway.StringArray;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.TimestampArray;
|
import mxaccess_gateway.v1.MxaccessGateway.TimestampArray;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory helpers for building {@link MxValue} and {@link MxArray} protobuf
|
||||||
|
* messages and for converting them back into native Java types.
|
||||||
|
*
|
||||||
|
* <p>Each {@code *Value} factory sets the matching {@code MxDataType} and
|
||||||
|
* COM {@code variant} type string so the worker can round-trip the value
|
||||||
|
* through MXAccess without further coercion.
|
||||||
|
*/
|
||||||
public final class MxValues {
|
public final class MxValues {
|
||||||
private MxValues() {
|
private MxValues() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a boolean {@link MxValue}.
|
||||||
|
*
|
||||||
|
* @param value the boolean payload
|
||||||
|
* @return a populated {@code MxValue}
|
||||||
|
*/
|
||||||
public static MxValue boolValue(boolean value) {
|
public static MxValue boolValue(boolean value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_BOOLEAN)
|
.setDataType(MxDataType.MX_DATA_TYPE_BOOLEAN)
|
||||||
@@ -29,6 +43,12 @@ public final class MxValues {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a 32-bit integer {@link MxValue}.
|
||||||
|
*
|
||||||
|
* @param value the int32 payload
|
||||||
|
* @return a populated {@code MxValue}
|
||||||
|
*/
|
||||||
public static MxValue int32Value(int value) {
|
public static MxValue int32Value(int value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
||||||
@@ -37,6 +57,12 @@ public final class MxValues {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a 64-bit integer {@link MxValue}.
|
||||||
|
*
|
||||||
|
* @param value the int64 payload
|
||||||
|
* @return a populated {@code MxValue}
|
||||||
|
*/
|
||||||
public static MxValue int64Value(long value) {
|
public static MxValue int64Value(long value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
||||||
@@ -45,6 +71,12 @@ public final class MxValues {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a 32-bit floating-point {@link MxValue}.
|
||||||
|
*
|
||||||
|
* @param value the float payload
|
||||||
|
* @return a populated {@code MxValue}
|
||||||
|
*/
|
||||||
public static MxValue floatValue(float value) {
|
public static MxValue floatValue(float value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_FLOAT)
|
.setDataType(MxDataType.MX_DATA_TYPE_FLOAT)
|
||||||
@@ -53,6 +85,12 @@ public final class MxValues {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a 64-bit floating-point {@link MxValue}.
|
||||||
|
*
|
||||||
|
* @param value the double payload
|
||||||
|
* @return a populated {@code MxValue}
|
||||||
|
*/
|
||||||
public static MxValue doubleValue(double value) {
|
public static MxValue doubleValue(double value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_DOUBLE)
|
.setDataType(MxDataType.MX_DATA_TYPE_DOUBLE)
|
||||||
@@ -61,6 +99,12 @@ public final class MxValues {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a string {@link MxValue}.
|
||||||
|
*
|
||||||
|
* @param value the string payload
|
||||||
|
* @return a populated {@code MxValue}
|
||||||
|
*/
|
||||||
public static MxValue stringValue(String value) {
|
public static MxValue stringValue(String value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_STRING)
|
.setDataType(MxDataType.MX_DATA_TYPE_STRING)
|
||||||
@@ -69,6 +113,12 @@ public final class MxValues {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a timestamp {@link MxValue} from an {@link Instant}.
|
||||||
|
*
|
||||||
|
* @param value the instant to encode as MXAccess time
|
||||||
|
* @return a populated {@code MxValue}
|
||||||
|
*/
|
||||||
public static MxValue timestampValue(Instant value) {
|
public static MxValue timestampValue(Instant value) {
|
||||||
return MxValue.newBuilder()
|
return MxValue.newBuilder()
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_TIME)
|
.setDataType(MxDataType.MX_DATA_TYPE_TIME)
|
||||||
@@ -80,6 +130,14 @@ public final class MxValues {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an {@link MxValue} back into a native Java value.
|
||||||
|
*
|
||||||
|
* @param value the MXAccess value, may be {@code null} or marked as null
|
||||||
|
* @return the boxed primitive, {@code String}, {@link Instant}, byte array,
|
||||||
|
* {@code List} of array elements, or {@code null} when the value
|
||||||
|
* carries no payload
|
||||||
|
*/
|
||||||
public static Object nativeValue(MxValue value) {
|
public static Object nativeValue(MxValue value) {
|
||||||
if (value == null || value.getIsNull()) {
|
if (value == null || value.getIsNull()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -99,6 +157,14 @@ public final class MxValues {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an {@link MxArray} into a native Java {@link List}.
|
||||||
|
*
|
||||||
|
* @param array the MXAccess array, may be {@code null}
|
||||||
|
* @return a list of boxed primitives, strings, instants, or byte arrays;
|
||||||
|
* an empty list when the array carries no elements;
|
||||||
|
* {@code null} when {@code array} is {@code null}
|
||||||
|
*/
|
||||||
public static Object nativeArray(MxArray array) {
|
public static Object nativeArray(MxArray array) {
|
||||||
if (array == null) {
|
if (array == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -117,6 +183,12 @@ public final class MxValues {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an {@link MxArray} of strings.
|
||||||
|
*
|
||||||
|
* @param values the string elements; the resulting array carries the size as a single dimension
|
||||||
|
* @return a populated {@code MxArray}
|
||||||
|
*/
|
||||||
public static MxArray stringArray(List<String> values) {
|
public static MxArray stringArray(List<String> values) {
|
||||||
return MxArray.newBuilder()
|
return MxArray.newBuilder()
|
||||||
.setElementDataType(MxDataType.MX_DATA_TYPE_STRING)
|
.setElementDataType(MxDataType.MX_DATA_TYPE_STRING)
|
||||||
@@ -126,6 +198,12 @@ public final class MxValues {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an {@link MxArray} of 32-bit integers.
|
||||||
|
*
|
||||||
|
* @param values the int32 elements; the resulting array carries the size as a single dimension
|
||||||
|
* @return a populated {@code MxArray}
|
||||||
|
*/
|
||||||
public static MxArray int32Array(List<Integer> values) {
|
public static MxArray int32Array(List<Integer> values) {
|
||||||
return MxArray.newBuilder()
|
return MxArray.newBuilder()
|
||||||
.setElementDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
.setElementDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
||||||
@@ -135,6 +213,12 @@ public final class MxValues {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a stable name for the {@link MxValue} kind, useful for logs.
|
||||||
|
*
|
||||||
|
* @param value the MXAccess value, may be {@code null}
|
||||||
|
* @return the {@code KindCase} name, or {@code "KIND_NOT_SET"} when {@code value} is {@code null}
|
||||||
|
*/
|
||||||
public static String kindName(MxValue value) {
|
public static String kindName(MxValue value) {
|
||||||
return value == null ? "KIND_NOT_SET" : value.getKindCase().name();
|
return value == null ? "KIND_NOT_SET" : value.getKindCase().name();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,16 @@ class ApiKey:
|
|||||||
value: str
|
value: str
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
|
"""Validate that the API key value is non-empty."""
|
||||||
if not self.value:
|
if not self.value:
|
||||||
raise ValueError("api_key must not be empty")
|
raise ValueError("api_key must not be empty")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
"""Return a repr that redacts the secret value."""
|
||||||
return f"{type(self).__name__}({REDACTED!r})"
|
return f"{type(self).__name__}({REDACTED!r})"
|
||||||
|
|
||||||
def bearer_value(self) -> str:
|
def bearer_value(self) -> str:
|
||||||
|
"""Return the value formatted as an HTTP `Bearer` token."""
|
||||||
return f"Bearer {self.value}"
|
return f"Bearer {self.value}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class GatewayClient:
|
|||||||
stub: Any,
|
stub: Any,
|
||||||
channel: grpc.aio.Channel | None = None,
|
channel: grpc.aio.Channel | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Initialize the client with resolved options and a gRPC stub."""
|
||||||
self.options = options
|
self.options = options
|
||||||
self.raw_stub = stub
|
self.raw_stub = stub
|
||||||
self._channel = channel
|
self._channel = channel
|
||||||
@@ -63,9 +64,11 @@ class GatewayClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def __aenter__(self) -> "GatewayClient":
|
async def __aenter__(self) -> "GatewayClient":
|
||||||
|
"""Return self to support ``async with`` usage."""
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, *_exc_info: object) -> None:
|
async def __aexit__(self, *_exc_info: object) -> None:
|
||||||
|
"""Close the client when leaving an ``async with`` block."""
|
||||||
await self.close()
|
await self.close()
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
@@ -99,6 +102,7 @@ class GatewayClient:
|
|||||||
return Session(client=self, session_id=reply.session_id, open_reply=reply)
|
return Session(client=self, session_id=reply.session_id, open_reply=reply)
|
||||||
|
|
||||||
async def open_session_raw(self, request: pb.OpenSessionRequest) -> pb.OpenSessionReply:
|
async def open_session_raw(self, request: pb.OpenSessionRequest) -> pb.OpenSessionReply:
|
||||||
|
"""Send an `OpenSession` RPC and return the raw reply."""
|
||||||
reply = await self._unary("open session", self.raw_stub.OpenSession, request)
|
reply = await self._unary("open session", self.raw_stub.OpenSession, request)
|
||||||
ensure_protocol_success("open session", reply.protocol_status, reply)
|
ensure_protocol_success("open session", reply.protocol_status, reply)
|
||||||
return reply
|
return reply
|
||||||
@@ -107,11 +111,13 @@ class GatewayClient:
|
|||||||
self,
|
self,
|
||||||
request: pb.CloseSessionRequest,
|
request: pb.CloseSessionRequest,
|
||||||
) -> pb.CloseSessionReply:
|
) -> pb.CloseSessionReply:
|
||||||
|
"""Send a `CloseSession` RPC and return the raw reply."""
|
||||||
reply = await self._unary("close session", self.raw_stub.CloseSession, request)
|
reply = await self._unary("close session", self.raw_stub.CloseSession, request)
|
||||||
ensure_protocol_success("close session", reply.protocol_status, reply)
|
ensure_protocol_success("close session", reply.protocol_status, reply)
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
async def invoke_raw(self, request: pb.MxCommandRequest) -> pb.MxCommandReply:
|
async def invoke_raw(self, request: pb.MxCommandRequest) -> pb.MxCommandReply:
|
||||||
|
"""Send an `Invoke` RPC and return the raw reply."""
|
||||||
reply = await self._unary("invoke", self.raw_stub.Invoke, request)
|
reply = await self._unary("invoke", self.raw_stub.Invoke, request)
|
||||||
ensure_protocol_success("invoke", reply.protocol_status, reply)
|
ensure_protocol_success("invoke", reply.protocol_status, reply)
|
||||||
return reply
|
return reply
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class MxGatewayError(Exception):
|
|||||||
protocol_status: pb.ProtocolStatus | None = None,
|
protocol_status: pb.ProtocolStatus | None = None,
|
||||||
raw_reply: Any | None = None,
|
raw_reply: Any | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Initialize with a message and the optional raw protocol context."""
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
self.protocol_status = protocol_status
|
self.protocol_status = protocol_status
|
||||||
self.raw_reply = raw_reply
|
self.raw_reply = raw_reply
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class GalaxyRepositoryClient:
|
|||||||
stub: Any,
|
stub: Any,
|
||||||
channel: grpc.aio.Channel | None = None,
|
channel: grpc.aio.Channel | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Initialize the client with resolved options and a gRPC stub."""
|
||||||
self.options = options
|
self.options = options
|
||||||
self.raw_stub = stub
|
self.raw_stub = stub
|
||||||
self._channel = channel
|
self._channel = channel
|
||||||
@@ -72,9 +73,11 @@ class GalaxyRepositoryClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def __aenter__(self) -> "GalaxyRepositoryClient":
|
async def __aenter__(self) -> "GalaxyRepositoryClient":
|
||||||
|
"""Return self to support ``async with`` usage."""
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, *_exc_info: object) -> None:
|
async def __aexit__(self, *_exc_info: object) -> None:
|
||||||
|
"""Close the client when leaving an ``async with`` block."""
|
||||||
await self.close()
|
await self.close()
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class ClientOptions:
|
|||||||
stream_timeout: float | None = None
|
stream_timeout: float | None = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
|
"""Validate options; raise `ValueError` for invalid combinations."""
|
||||||
if not self.endpoint:
|
if not self.endpoint:
|
||||||
raise ValueError("endpoint must not be empty")
|
raise ValueError("endpoint must not be empty")
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ class ClientOptions:
|
|||||||
raise ValueError("stream_timeout must be greater than zero")
|
raise ValueError("stream_timeout must be greater than zero")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
"""Return a repr that redacts the API key value."""
|
||||||
api_key = REDACTED if self.api_key else None
|
api_key = REDACTED if self.api_key else None
|
||||||
return (
|
return (
|
||||||
f"{type(self).__name__}(endpoint={self.endpoint!r}, "
|
f"{type(self).__name__}(endpoint={self.endpoint!r}, "
|
||||||
|
|||||||
@@ -21,15 +21,18 @@ class Session:
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
open_reply: pb.OpenSessionReply | None = None,
|
open_reply: pb.OpenSessionReply | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Initialize a session bound to a client and gateway session id."""
|
||||||
self.client = client
|
self.client = client
|
||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
self.open_reply = open_reply
|
self.open_reply = open_reply
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
|
||||||
async def __aenter__(self) -> "Session":
|
async def __aenter__(self) -> "Session":
|
||||||
|
"""Return self to support ``async with`` usage."""
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, *_exc_info: object) -> None:
|
async def __aexit__(self, *_exc_info: object) -> None:
|
||||||
|
"""Close the session when leaving an ``async with`` block."""
|
||||||
await self.close()
|
await self.close()
|
||||||
|
|
||||||
async def close(self, *, client_correlation_id: str = "") -> pb.CloseSessionReply:
|
async def close(self, *, client_correlation_id: str = "") -> pb.CloseSessionReply:
|
||||||
@@ -74,6 +77,7 @@ class Session:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def register(self, client_name: str, *, correlation_id: str = "") -> int:
|
async def register(self, client_name: str, *, correlation_id: str = "") -> int:
|
||||||
|
"""Invoke MXAccess `Register` and return the new `ServerHandle`."""
|
||||||
reply = await self.invoke(
|
reply = await self.invoke(
|
||||||
pb.MxCommand(
|
pb.MxCommand(
|
||||||
kind=pb.MX_COMMAND_KIND_REGISTER,
|
kind=pb.MX_COMMAND_KIND_REGISTER,
|
||||||
@@ -84,6 +88,7 @@ class Session:
|
|||||||
return reply.register.server_handle
|
return reply.register.server_handle
|
||||||
|
|
||||||
async def unregister(self, server_handle: int, *, correlation_id: str = "") -> None:
|
async def unregister(self, server_handle: int, *, correlation_id: str = "") -> None:
|
||||||
|
"""Invoke MXAccess `Unregister` for a previously registered `ServerHandle`."""
|
||||||
await self.invoke(
|
await self.invoke(
|
||||||
pb.MxCommand(
|
pb.MxCommand(
|
||||||
kind=pb.MX_COMMAND_KIND_UNREGISTER,
|
kind=pb.MX_COMMAND_KIND_UNREGISTER,
|
||||||
@@ -99,6 +104,7 @@ class Session:
|
|||||||
*,
|
*,
|
||||||
correlation_id: str = "",
|
correlation_id: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Invoke MXAccess `RemoveItem` for the given `ItemHandle`."""
|
||||||
await self.invoke(
|
await self.invoke(
|
||||||
pb.MxCommand(
|
pb.MxCommand(
|
||||||
kind=pb.MX_COMMAND_KIND_REMOVE_ITEM,
|
kind=pb.MX_COMMAND_KIND_REMOVE_ITEM,
|
||||||
@@ -117,6 +123,7 @@ class Session:
|
|||||||
*,
|
*,
|
||||||
correlation_id: str = "",
|
correlation_id: str = "",
|
||||||
) -> int:
|
) -> int:
|
||||||
|
"""Invoke MXAccess `AddItem` and return the new `ItemHandle`."""
|
||||||
reply = await self.invoke(
|
reply = await self.invoke(
|
||||||
pb.MxCommand(
|
pb.MxCommand(
|
||||||
kind=pb.MX_COMMAND_KIND_ADD_ITEM,
|
kind=pb.MX_COMMAND_KIND_ADD_ITEM,
|
||||||
@@ -137,6 +144,7 @@ class Session:
|
|||||||
*,
|
*,
|
||||||
correlation_id: str = "",
|
correlation_id: str = "",
|
||||||
) -> int:
|
) -> int:
|
||||||
|
"""Invoke MXAccess `AddItem2` with item context and return the new `ItemHandle`."""
|
||||||
reply = await self.invoke(
|
reply = await self.invoke(
|
||||||
pb.MxCommand(
|
pb.MxCommand(
|
||||||
kind=pb.MX_COMMAND_KIND_ADD_ITEM2,
|
kind=pb.MX_COMMAND_KIND_ADD_ITEM2,
|
||||||
@@ -157,6 +165,7 @@ class Session:
|
|||||||
*,
|
*,
|
||||||
correlation_id: str = "",
|
correlation_id: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Invoke MXAccess `Advise` to subscribe an existing `ItemHandle` to events."""
|
||||||
await self.invoke(
|
await self.invoke(
|
||||||
pb.MxCommand(
|
pb.MxCommand(
|
||||||
kind=pb.MX_COMMAND_KIND_ADVISE,
|
kind=pb.MX_COMMAND_KIND_ADVISE,
|
||||||
@@ -175,6 +184,7 @@ class Session:
|
|||||||
*,
|
*,
|
||||||
correlation_id: str = "",
|
correlation_id: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Invoke MXAccess `UnAdvise` to stop event delivery for an `ItemHandle`."""
|
||||||
await self.invoke(
|
await self.invoke(
|
||||||
pb.MxCommand(
|
pb.MxCommand(
|
||||||
kind=pb.MX_COMMAND_KIND_UN_ADVISE,
|
kind=pb.MX_COMMAND_KIND_UN_ADVISE,
|
||||||
@@ -193,6 +203,7 @@ class Session:
|
|||||||
*,
|
*,
|
||||||
correlation_id: str = "",
|
correlation_id: str = "",
|
||||||
) -> list[pb.SubscribeResult]:
|
) -> list[pb.SubscribeResult]:
|
||||||
|
"""Invoke MXAccess `AddItemBulk` and return one result per tag address."""
|
||||||
if tag_addresses is None:
|
if tag_addresses is None:
|
||||||
raise TypeError("tag_addresses is required")
|
raise TypeError("tag_addresses is required")
|
||||||
_ensure_bulk_size("tag_addresses", len(tag_addresses))
|
_ensure_bulk_size("tag_addresses", len(tag_addresses))
|
||||||
@@ -215,6 +226,7 @@ class Session:
|
|||||||
*,
|
*,
|
||||||
correlation_id: str = "",
|
correlation_id: str = "",
|
||||||
) -> list[pb.SubscribeResult]:
|
) -> list[pb.SubscribeResult]:
|
||||||
|
"""Invoke MXAccess `AdviseItemBulk` and return one result per item handle."""
|
||||||
if item_handles is None:
|
if item_handles is None:
|
||||||
raise TypeError("item_handles is required")
|
raise TypeError("item_handles is required")
|
||||||
_ensure_bulk_size("item_handles", len(item_handles))
|
_ensure_bulk_size("item_handles", len(item_handles))
|
||||||
@@ -237,6 +249,7 @@ class Session:
|
|||||||
*,
|
*,
|
||||||
correlation_id: str = "",
|
correlation_id: str = "",
|
||||||
) -> list[pb.SubscribeResult]:
|
) -> list[pb.SubscribeResult]:
|
||||||
|
"""Invoke MXAccess `RemoveItemBulk` and return one result per item handle."""
|
||||||
if item_handles is None:
|
if item_handles is None:
|
||||||
raise TypeError("item_handles is required")
|
raise TypeError("item_handles is required")
|
||||||
_ensure_bulk_size("item_handles", len(item_handles))
|
_ensure_bulk_size("item_handles", len(item_handles))
|
||||||
@@ -259,6 +272,7 @@ class Session:
|
|||||||
*,
|
*,
|
||||||
correlation_id: str = "",
|
correlation_id: str = "",
|
||||||
) -> list[pb.SubscribeResult]:
|
) -> list[pb.SubscribeResult]:
|
||||||
|
"""Invoke MXAccess `UnAdviseItemBulk` and return one result per item handle."""
|
||||||
if item_handles is None:
|
if item_handles is None:
|
||||||
raise TypeError("item_handles is required")
|
raise TypeError("item_handles is required")
|
||||||
_ensure_bulk_size("item_handles", len(item_handles))
|
_ensure_bulk_size("item_handles", len(item_handles))
|
||||||
@@ -281,6 +295,7 @@ class Session:
|
|||||||
*,
|
*,
|
||||||
correlation_id: str = "",
|
correlation_id: str = "",
|
||||||
) -> list[pb.SubscribeResult]:
|
) -> list[pb.SubscribeResult]:
|
||||||
|
"""Invoke MXAccess `SubscribeBulk` and return one result per tag address."""
|
||||||
if tag_addresses is None:
|
if tag_addresses is None:
|
||||||
raise TypeError("tag_addresses is required")
|
raise TypeError("tag_addresses is required")
|
||||||
_ensure_bulk_size("tag_addresses", len(tag_addresses))
|
_ensure_bulk_size("tag_addresses", len(tag_addresses))
|
||||||
@@ -303,6 +318,7 @@ class Session:
|
|||||||
*,
|
*,
|
||||||
correlation_id: str = "",
|
correlation_id: str = "",
|
||||||
) -> list[pb.SubscribeResult]:
|
) -> list[pb.SubscribeResult]:
|
||||||
|
"""Invoke MXAccess `UnsubscribeBulk` and return one result per item handle."""
|
||||||
if item_handles is None:
|
if item_handles is None:
|
||||||
raise TypeError("item_handles is required")
|
raise TypeError("item_handles is required")
|
||||||
_ensure_bulk_size("item_handles", len(item_handles))
|
_ensure_bulk_size("item_handles", len(item_handles))
|
||||||
@@ -327,6 +343,7 @@ class Session:
|
|||||||
user_id: int = 0,
|
user_id: int = 0,
|
||||||
correlation_id: str = "",
|
correlation_id: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Invoke MXAccess `Write` for an `ItemHandle` with the converted value."""
|
||||||
await self.invoke(
|
await self.invoke(
|
||||||
pb.MxCommand(
|
pb.MxCommand(
|
||||||
kind=pb.MX_COMMAND_KIND_WRITE,
|
kind=pb.MX_COMMAND_KIND_WRITE,
|
||||||
@@ -350,6 +367,7 @@ class Session:
|
|||||||
user_id: int = 0,
|
user_id: int = 0,
|
||||||
correlation_id: str = "",
|
correlation_id: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Invoke MXAccess `Write2` with both a value and a client-supplied timestamp."""
|
||||||
await self.invoke(
|
await self.invoke(
|
||||||
pb.MxCommand(
|
pb.MxCommand(
|
||||||
kind=pb.MX_COMMAND_KIND_WRITE2,
|
kind=pb.MX_COMMAND_KIND_WRITE2,
|
||||||
@@ -369,6 +387,7 @@ class Session:
|
|||||||
*,
|
*,
|
||||||
after_worker_sequence: int = 0,
|
after_worker_sequence: int = 0,
|
||||||
) -> AsyncIterator[pb.MxEvent]:
|
) -> AsyncIterator[pb.MxEvent]:
|
||||||
|
"""Return an async iterator of `MxEvent` messages for this session."""
|
||||||
return self.client.stream_events_raw(
|
return self.client.stream_events_raw(
|
||||||
pb.StreamEventsRequest(
|
pb.StreamEventsRequest(
|
||||||
session_id=self.session_id,
|
session_id=self.session_id,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ def version(output_json: bool) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def gateway_options(command: Callable[..., Any]) -> Callable[..., Any]:
|
def gateway_options(command: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
|
"""Apply the shared gateway connection options to a Click command."""
|
||||||
command = click.option("--endpoint", default="localhost:5000", show_default=True)(command)
|
command = click.option("--endpoint", default="localhost:5000", show_default=True)(command)
|
||||||
command = click.option("--api-key", default=None, help="Gateway API key.")(command)
|
command = click.option("--api-key", default=None, help="Gateway API key.")(command)
|
||||||
command = click.option(
|
command = click.option(
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
//! `mxgw` — the Rust test CLI for the MXAccess Gateway.
|
||||||
|
//!
|
||||||
|
//! The binary wraps [`mxgateway_client`] in a `clap`-driven command surface
|
||||||
|
//! used by the cross-language smoke matrix and by developers exercising the
|
||||||
|
//! gateway by hand. Every subcommand mirrors a single gateway/Galaxy RPC,
|
||||||
|
//! prints either a terse line or a JSON document with `--json`, and exits
|
||||||
|
//! non-zero on any failure.
|
||||||
|
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
//! API-key wrapper and the `tonic` interceptor that attaches it as a Bearer
|
||||||
|
//! token on every outbound gRPC call. The wrapper redacts its inner value in
|
||||||
|
//! `Debug`/`Display` so logs never leak the secret.
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use tonic::metadata::MetadataValue;
|
use tonic::metadata::MetadataValue;
|
||||||
@@ -5,14 +9,21 @@ use tonic::service::Interceptor;
|
|||||||
use tonic::{Request, Status};
|
use tonic::{Request, Status};
|
||||||
|
|
||||||
/// API key wrapper that avoids exposing raw credentials in formatted output.
|
/// API key wrapper that avoids exposing raw credentials in formatted output.
|
||||||
|
///
|
||||||
|
/// Use [`ApiKey::expose_secret`] when the underlying string is genuinely
|
||||||
|
/// needed (for example, building the `authorization` header).
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
pub struct ApiKey(String);
|
pub struct ApiKey(String);
|
||||||
|
|
||||||
impl ApiKey {
|
impl ApiKey {
|
||||||
|
/// Construct an [`ApiKey`] from the raw `mxgw_<key-id>_<secret>` string
|
||||||
|
/// returned by the gateway's `apikey` admin command.
|
||||||
pub fn new(value: impl Into<String>) -> Self {
|
pub fn new(value: impl Into<String>) -> Self {
|
||||||
Self(value.into())
|
Self(value.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the raw key value. Callers must not log or otherwise persist
|
||||||
|
/// the result.
|
||||||
pub fn expose_secret(&self) -> &str {
|
pub fn expose_secret(&self) -> &str {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
@@ -40,6 +51,9 @@ pub struct AuthInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AuthInterceptor {
|
impl AuthInterceptor {
|
||||||
|
/// Build an interceptor that injects the supplied API key on every
|
||||||
|
/// request. Pass `None` to disable authentication (useful for local
|
||||||
|
/// development against a gateway with `Authentication:Required = false`).
|
||||||
pub fn new(api_key: Option<ApiKey>) -> Self {
|
pub fn new(api_key: Option<ApiKey>) -> Self {
|
||||||
Self { api_key }
|
Self { api_key }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
//! High-level wrapper around the generated `MxAccessGateway` gRPC client.
|
||||||
|
//!
|
||||||
|
//! [`GatewayClient::connect`] builds an authenticated `tonic` channel using
|
||||||
|
//! the supplied [`ClientOptions`], applies the bearer-token interceptor, and
|
||||||
|
//! exposes typed methods for the unary and streaming RPCs. Most application
|
||||||
|
//! code should prefer [`GatewayClient::open_session`] and the [`Session`]
|
||||||
|
//! handle it returns, rather than the `*_raw` methods.
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use tonic::codegen::InterceptedService;
|
use tonic::codegen::InterceptedService;
|
||||||
@@ -14,11 +22,21 @@ use crate::generated::mxaccess_gateway::v1::{
|
|||||||
use crate::options::ClientOptions;
|
use crate::options::ClientOptions;
|
||||||
use crate::session::Session;
|
use crate::session::Session;
|
||||||
|
|
||||||
|
/// Generated gateway client wrapped in the auth interceptor that
|
||||||
|
/// [`GatewayClient`] uses internally.
|
||||||
pub type RawGatewayClient = MxAccessGatewayClient<InterceptedService<Channel, AuthInterceptor>>;
|
pub type RawGatewayClient = MxAccessGatewayClient<InterceptedService<Channel, AuthInterceptor>>;
|
||||||
|
|
||||||
|
/// Pinned, boxed [`MxEvent`] stream returned by
|
||||||
|
/// [`GatewayClient::stream_events`]. Errors are pre-mapped from
|
||||||
|
/// `tonic::Status` to [`Error`]; dropping the stream cancels the call.
|
||||||
pub type EventStream =
|
pub type EventStream =
|
||||||
std::pin::Pin<Box<dyn futures_core::Stream<Item = Result<MxEvent, Error>> + Send + 'static>>;
|
std::pin::Pin<Box<dyn futures_core::Stream<Item = Result<MxEvent, Error>> + Send + 'static>>;
|
||||||
|
|
||||||
/// Thin owner for the generated gateway client.
|
/// Thin async wrapper around the generated gateway client.
|
||||||
|
///
|
||||||
|
/// The wrapper is `Clone`: every clone shares the underlying tonic channel
|
||||||
|
/// (cheap, reference-counted) and the same call/stream timeouts. It is
|
||||||
|
/// designed to be cheap enough to clone per request handler.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GatewayClient {
|
pub struct GatewayClient {
|
||||||
inner: RawGatewayClient,
|
inner: RawGatewayClient,
|
||||||
@@ -27,6 +45,13 @@ pub struct GatewayClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GatewayClient {
|
impl GatewayClient {
|
||||||
|
/// Connect to the gateway endpoint described by `options` and return a
|
||||||
|
/// ready-to-use client.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::InvalidEndpoint`] if the endpoint URL or CA file is
|
||||||
|
/// malformed, and [`Error::Transport`] if the TCP/TLS handshake fails.
|
||||||
pub async fn connect(options: ClientOptions) -> Result<Self, Error> {
|
pub async fn connect(options: ClientOptions) -> Result<Self, Error> {
|
||||||
let mut endpoint =
|
let mut endpoint =
|
||||||
Channel::from_shared(options.endpoint().to_owned()).map_err(|source| {
|
Channel::from_shared(options.endpoint().to_owned()).map_err(|source| {
|
||||||
@@ -62,18 +87,30 @@ impl GatewayClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Borrow the underlying generated client. Use this only when you need
|
||||||
|
/// access to RPCs not surfaced by the wrapper.
|
||||||
pub fn raw_client(&mut self) -> &mut RawGatewayClient {
|
pub fn raw_client(&mut self) -> &mut RawGatewayClient {
|
||||||
&mut self.inner
|
&mut self.inner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Consume the wrapper and return the underlying generated client.
|
||||||
pub fn into_inner(self) -> RawGatewayClient {
|
pub fn into_inner(self) -> RawGatewayClient {
|
||||||
self.inner
|
self.inner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a [`Session`] handle from a previously opened session id. No
|
||||||
|
/// RPC is performed — this is the cheap counterpart to
|
||||||
|
/// [`GatewayClient::open_session`] for callers that already own the id.
|
||||||
pub fn session(&self, session_id: impl Into<String>) -> Session {
|
pub fn session(&self, session_id: impl Into<String>) -> Session {
|
||||||
Session::new(session_id, self.clone())
|
Session::new(session_id, self.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Issue an `OpenSession` RPC and return the raw reply without
|
||||||
|
/// validating its `protocol_status`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns the `tonic::Status` mapped through [`Error::from`].
|
||||||
pub async fn open_session_raw(
|
pub async fn open_session_raw(
|
||||||
&self,
|
&self,
|
||||||
request: OpenSessionRequest,
|
request: OpenSessionRequest,
|
||||||
@@ -83,12 +120,25 @@ impl GatewayClient {
|
|||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open a session, validate its `protocol_status`, and return a typed
|
||||||
|
/// [`Session`] handle bound to this client.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::ProtocolStatus`] if the gateway accepts the call
|
||||||
|
/// but reports a non-OK protocol status, plus any of the
|
||||||
|
/// [`Error`] variants produced by [`open_session_raw`](Self::open_session_raw).
|
||||||
pub async fn open_session(&self, request: OpenSessionRequest) -> Result<Session, Error> {
|
pub async fn open_session(&self, request: OpenSessionRequest) -> Result<Session, Error> {
|
||||||
let reply = self.open_session_raw(request).await?;
|
let reply = self.open_session_raw(request).await?;
|
||||||
ensure_protocol_success("open session", reply.protocol_status.as_ref())?;
|
ensure_protocol_success("open session", reply.protocol_status.as_ref())?;
|
||||||
Ok(Session::new(reply.session_id, self.clone()))
|
Ok(Session::new(reply.session_id, self.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Issue a `CloseSession` RPC and return the raw reply.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns the `tonic::Status` mapped through [`Error::from`].
|
||||||
pub async fn close_session_raw(
|
pub async fn close_session_raw(
|
||||||
&self,
|
&self,
|
||||||
request: CloseSessionRequest,
|
request: CloseSessionRequest,
|
||||||
@@ -98,16 +148,39 @@ impl GatewayClient {
|
|||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Issue an `Invoke` RPC and return the raw reply, even when the
|
||||||
|
/// command-level protocol status is non-OK.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns the `tonic::Status` mapped through [`Error::from`].
|
||||||
pub async fn invoke_raw(&self, request: MxCommandRequest) -> Result<MxCommandReply, Error> {
|
pub async fn invoke_raw(&self, request: MxCommandRequest) -> Result<MxCommandReply, Error> {
|
||||||
let mut client = self.inner.clone();
|
let mut client = self.inner.clone();
|
||||||
let response = client.invoke(self.unary_request(request)).await?;
|
let response = client.invoke(self.unary_request(request)).await?;
|
||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Issue an `Invoke` RPC and surface a non-OK reply as
|
||||||
|
/// [`Error::Command`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::Command`] when the reply's `protocol_status` is not
|
||||||
|
/// `Ok`, plus any errors propagated by
|
||||||
|
/// [`invoke_raw`](Self::invoke_raw).
|
||||||
pub async fn invoke(&self, request: MxCommandRequest) -> Result<MxCommandReply, Error> {
|
pub async fn invoke(&self, request: MxCommandRequest) -> Result<MxCommandReply, Error> {
|
||||||
ensure_command_success(self.invoke_raw(request).await?)
|
ensure_command_success(self.invoke_raw(request).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open the server-streaming `StreamEvents` RPC.
|
||||||
|
///
|
||||||
|
/// The returned [`EventStream`] yields `MxEvent` messages as the worker
|
||||||
|
/// produces them. Dropping the stream cancels the gRPC call cooperatively.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns the `tonic::Status` mapped through [`Error::from`] if the
|
||||||
|
/// server rejects the subscription.
|
||||||
pub async fn stream_events(&self, request: StreamEventsRequest) -> Result<EventStream, Error> {
|
pub async fn stream_events(&self, request: StreamEventsRequest) -> Result<EventStream, Error> {
|
||||||
let mut client = self.inner.clone();
|
let mut client = self.inner.clone();
|
||||||
let response = client.stream_events(self.stream_request(request)).await?;
|
let response = client.stream_events(self.stream_request(request)).await?;
|
||||||
|
|||||||
@@ -1,75 +1,136 @@
|
|||||||
|
//! Error types surfaced by the Rust client.
|
||||||
|
//!
|
||||||
|
//! [`Error`] is the umbrella enum returned by every async wrapper. It
|
||||||
|
//! classifies `tonic::Status` codes (auth, timeout, cancellation) and folds
|
||||||
|
//! gateway protocol failures and command-level rejections into structured
|
||||||
|
//! variants. Credentials embedded in status messages are scrubbed before the
|
||||||
|
//! message reaches a caller.
|
||||||
|
|
||||||
use thiserror::Error as ThisError;
|
use thiserror::Error as ThisError;
|
||||||
use tonic::Code;
|
use tonic::Code;
|
||||||
|
|
||||||
use crate::generated::mxaccess_gateway::v1::{MxCommandReply, ProtocolStatus, ProtocolStatusCode};
|
use crate::generated::mxaccess_gateway::v1::{MxCommandReply, ProtocolStatus, ProtocolStatusCode};
|
||||||
|
|
||||||
|
/// Top-level error type returned by the Rust client wrappers.
|
||||||
|
///
|
||||||
|
/// The variants distinguish transport/setup failures, classified gRPC status
|
||||||
|
/// codes, gateway protocol-level failures (`OpenSession`, `CloseSession`),
|
||||||
|
/// and command-level rejections that surface a populated [`MxCommandReply`].
|
||||||
#[derive(Debug, ThisError)]
|
#[derive(Debug, ThisError)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
/// Endpoint URL could not be parsed or its TLS material could not be
|
||||||
|
/// loaded.
|
||||||
#[error("invalid gateway endpoint `{endpoint}`: {detail}")]
|
#[error("invalid gateway endpoint `{endpoint}`: {detail}")]
|
||||||
InvalidEndpoint { endpoint: String, detail: String },
|
InvalidEndpoint {
|
||||||
|
/// Endpoint string supplied by the caller.
|
||||||
|
endpoint: String,
|
||||||
|
/// Human-readable explanation of the parse/load failure.
|
||||||
|
detail: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A caller-provided argument failed local validation before any RPC
|
||||||
|
/// was dispatched (for example, a bulk command exceeding the size cap).
|
||||||
#[error("invalid argument `{name}`: {detail}")]
|
#[error("invalid argument `{name}`: {detail}")]
|
||||||
InvalidArgument { name: String, detail: String },
|
InvalidArgument {
|
||||||
|
/// Name of the offending argument.
|
||||||
|
name: String,
|
||||||
|
/// Reason the argument was rejected.
|
||||||
|
detail: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Tonic transport-level failure (DNS, connect, TLS handshake).
|
||||||
#[error("gateway transport error: {0}")]
|
#[error("gateway transport error: {0}")]
|
||||||
Transport(#[from] tonic::transport::Error),
|
Transport(#[from] tonic::transport::Error),
|
||||||
|
|
||||||
|
/// Server returned `Unauthenticated` — the API key was missing or
|
||||||
|
/// rejected.
|
||||||
#[error("authentication failed: {message}")]
|
#[error("authentication failed: {message}")]
|
||||||
Authentication {
|
Authentication {
|
||||||
|
/// Redacted server-supplied detail message.
|
||||||
message: String,
|
message: String,
|
||||||
|
/// Original `tonic::Status` for callers that need the full context.
|
||||||
#[source]
|
#[source]
|
||||||
status: Box<tonic::Status>,
|
status: Box<tonic::Status>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Server returned `PermissionDenied` — the API key is valid but lacks
|
||||||
|
/// the required scope.
|
||||||
#[error("authorization failed: {message}")]
|
#[error("authorization failed: {message}")]
|
||||||
Authorization {
|
Authorization {
|
||||||
|
/// Redacted server-supplied detail message.
|
||||||
message: String,
|
message: String,
|
||||||
|
/// Original `tonic::Status`.
|
||||||
#[source]
|
#[source]
|
||||||
status: Box<tonic::Status>,
|
status: Box<tonic::Status>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Server returned `DeadlineExceeded`. Usually the per-call deadline
|
||||||
|
/// configured via [`crate::options::ClientOptions::with_call_timeout`].
|
||||||
#[error("gateway call timed out: {message}")]
|
#[error("gateway call timed out: {message}")]
|
||||||
Timeout {
|
Timeout {
|
||||||
|
/// Redacted server-supplied detail message.
|
||||||
message: String,
|
message: String,
|
||||||
|
/// Original `tonic::Status`.
|
||||||
#[source]
|
#[source]
|
||||||
status: Box<tonic::Status>,
|
status: Box<tonic::Status>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Server (or client) cancelled the call before a reply was produced.
|
||||||
#[error("gateway call cancelled: {message}")]
|
#[error("gateway call cancelled: {message}")]
|
||||||
Cancelled {
|
Cancelled {
|
||||||
|
/// Redacted server-supplied detail message.
|
||||||
message: String,
|
message: String,
|
||||||
|
/// Original `tonic::Status`.
|
||||||
#[source]
|
#[source]
|
||||||
status: Box<tonic::Status>,
|
status: Box<tonic::Status>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Any other `tonic::Status` that did not match a more specific variant.
|
||||||
#[error("gateway status error: {0}")]
|
#[error("gateway status error: {0}")]
|
||||||
Status(Box<tonic::Status>),
|
Status(Box<tonic::Status>),
|
||||||
|
|
||||||
|
/// Gateway accepted the call but the worker reply carried a non-OK
|
||||||
|
/// protocol status. The wrapped [`CommandError`] preserves the full
|
||||||
|
/// reply so callers can inspect the worker's status payload.
|
||||||
#[error("gateway command failed: {0}")]
|
#[error("gateway command failed: {0}")]
|
||||||
Command(#[from] Box<CommandError>),
|
Command(#[from] Box<CommandError>),
|
||||||
|
|
||||||
|
/// Protocol-level operation (open/close session) returned a non-OK
|
||||||
|
/// [`ProtocolStatus`] envelope.
|
||||||
#[error("gateway {operation} failed: {code:?}: {message}")]
|
#[error("gateway {operation} failed: {code:?}: {message}")]
|
||||||
ProtocolStatus {
|
ProtocolStatus {
|
||||||
|
/// Operation name, e.g. `"open session"`.
|
||||||
operation: &'static str,
|
operation: &'static str,
|
||||||
|
/// Decoded protocol status code from the server.
|
||||||
code: ProtocolStatusCode,
|
code: ProtocolStatusCode,
|
||||||
|
/// Detail message from the server.
|
||||||
message: String,
|
message: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrapper around an [`MxCommandReply`] whose `protocol_status` reported a
|
||||||
|
/// non-OK code.
|
||||||
|
///
|
||||||
|
/// The wrapper is heap-allocated inside [`Error::Command`] to keep the
|
||||||
|
/// containing enum small. Callers can recover the reply with
|
||||||
|
/// [`CommandError::reply`] or [`CommandError::into_reply`].
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CommandError {
|
pub struct CommandError {
|
||||||
reply: MxCommandReply,
|
reply: MxCommandReply,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandError {
|
impl CommandError {
|
||||||
|
/// Wrap an already-failed command reply.
|
||||||
pub fn new(reply: MxCommandReply) -> Self {
|
pub fn new(reply: MxCommandReply) -> Self {
|
||||||
Self { reply }
|
Self { reply }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Borrow the underlying reply (correlation id, status, payload).
|
||||||
pub fn reply(&self) -> &MxCommandReply {
|
pub fn reply(&self) -> &MxCommandReply {
|
||||||
&self.reply
|
&self.reply
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Consume the error and return the underlying reply.
|
||||||
pub fn into_reply(self) -> MxCommandReply {
|
pub fn into_reply(self) -> MxCommandReply {
|
||||||
self.reply
|
self.reply
|
||||||
}
|
}
|
||||||
@@ -118,6 +179,13 @@ impl From<tonic::Status> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Promote a non-OK protocol status carried inside an [`MxCommandReply`]
|
||||||
|
/// to an [`Error::Command`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::Command`] when `reply.protocol_status` is missing or
|
||||||
|
/// reports any code other than [`ProtocolStatusCode::Ok`].
|
||||||
pub fn ensure_command_success(reply: MxCommandReply) -> Result<MxCommandReply, Error> {
|
pub fn ensure_command_success(reply: MxCommandReply) -> Result<MxCommandReply, Error> {
|
||||||
let code = reply
|
let code = reply
|
||||||
.protocol_status
|
.protocol_status
|
||||||
@@ -132,6 +200,13 @@ pub fn ensure_command_success(reply: MxCommandReply) -> Result<MxCommandReply, E
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate a [`ProtocolStatus`] envelope returned by an open/close-session
|
||||||
|
/// reply.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::ProtocolStatus`] tagged with `operation` when `status`
|
||||||
|
/// is missing or reports any code other than [`ProtocolStatusCode::Ok`].
|
||||||
pub fn ensure_protocol_success(
|
pub fn ensure_protocol_success(
|
||||||
operation: &'static str,
|
operation: &'static str,
|
||||||
status: Option<&ProtocolStatus>,
|
status: Option<&ProtocolStatus>,
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
|
//! Generated tonic/prost bindings for the gateway, worker, and Galaxy
|
||||||
|
//! Repository protobuf contracts.
|
||||||
|
//!
|
||||||
|
//! Modules under this namespace are produced by `tonic-build` from the shared
|
||||||
|
//! `.proto` files in the contracts project. Treat them as build output: do not
|
||||||
|
//! hand-edit, and prefer the wrappers in [`crate::client`], [`crate::session`],
|
||||||
|
//! [`crate::galaxy`], and [`crate::value`] for application code.
|
||||||
|
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
|
/// Generated bindings for the public `mxaccess_gateway` gRPC service.
|
||||||
pub mod mxaccess_gateway {
|
pub mod mxaccess_gateway {
|
||||||
|
/// `mxaccess_gateway.v1` package — the v1 wire contract surfaced by the
|
||||||
|
/// gateway to language clients.
|
||||||
pub mod v1 {
|
pub mod v1 {
|
||||||
#![allow(clippy::large_enum_variant)]
|
#![allow(clippy::large_enum_variant)]
|
||||||
|
|
||||||
@@ -6,7 +19,10 @@ pub mod mxaccess_gateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generated bindings for the internal gateway↔worker IPC protocol.
|
||||||
pub mod mxaccess_worker {
|
pub mod mxaccess_worker {
|
||||||
|
/// `mxaccess_worker.v1` package — frame and envelope types used inside
|
||||||
|
/// the named-pipe transport between gateway and worker.
|
||||||
pub mod v1 {
|
pub mod v1 {
|
||||||
#![allow(clippy::large_enum_variant)]
|
#![allow(clippy::large_enum_variant)]
|
||||||
|
|
||||||
@@ -14,7 +30,10 @@ pub mod mxaccess_worker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generated bindings for the Galaxy Repository read-only browse service.
|
||||||
pub mod galaxy_repository {
|
pub mod galaxy_repository {
|
||||||
|
/// `galaxy_repository.v1` package — types for the Galaxy hierarchy
|
||||||
|
/// discovery and deploy-event watch RPCs.
|
||||||
pub mod v1 {
|
pub mod v1 {
|
||||||
#![allow(clippy::large_enum_variant)]
|
#![allow(clippy::large_enum_variant)]
|
||||||
|
|
||||||
|
|||||||
+18
-3
@@ -1,8 +1,15 @@
|
|||||||
//! Rust client scaffold for MXAccess Gateway.
|
//! Rust client for the MXAccess Gateway.
|
||||||
//!
|
//!
|
||||||
//! The crate compiles generated `tonic` bindings from the shared gateway
|
//! The crate compiles generated `tonic` bindings from the shared gateway
|
||||||
//! protobuf contracts and exposes a small handwritten surface for future client
|
//! protobuf contracts and exposes a small handwritten surface — connection
|
||||||
//! implementation work.
|
//! options, an authentication interceptor, gateway and Galaxy Repository
|
||||||
|
//! clients, an `MxValue` builder/projection, and protocol-error types — that
|
||||||
|
//! application code can use without touching the generated modules directly.
|
||||||
|
//!
|
||||||
|
//! See the project root `RustClientDesign.md` for the design rationale and
|
||||||
|
//! the cross-language client matrix.
|
||||||
|
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
@@ -14,11 +21,19 @@ pub mod session;
|
|||||||
pub mod value;
|
pub mod value;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
pub use auth::{ApiKey, AuthInterceptor};
|
pub use auth::{ApiKey, AuthInterceptor};
|
||||||
|
#[doc(inline)]
|
||||||
pub use client::{EventStream, GatewayClient};
|
pub use client::{EventStream, GatewayClient};
|
||||||
|
#[doc(inline)]
|
||||||
pub use error::{CommandError, Error};
|
pub use error::{CommandError, Error};
|
||||||
|
#[doc(inline)]
|
||||||
pub use galaxy::{DeployEventStream, GalaxyClient};
|
pub use galaxy::{DeployEventStream, GalaxyClient};
|
||||||
|
#[doc(inline)]
|
||||||
pub use options::ClientOptions;
|
pub use options::ClientOptions;
|
||||||
|
#[doc(inline)]
|
||||||
pub use session::Session;
|
pub use session::Session;
|
||||||
|
#[doc(inline)]
|
||||||
pub use value::{MxArrayProjection, MxArrayValue, MxStatus, MxValue, MxValueProjection};
|
pub use value::{MxArrayProjection, MxArrayValue, MxStatus, MxValue, MxValueProjection};
|
||||||
|
#[doc(inline)]
|
||||||
pub use version::{CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION};
|
pub use version::{CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION};
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
|
//! Connection options shared by [`crate::client::GatewayClient`] and
|
||||||
|
//! [`crate::galaxy::GalaxyClient`]. Build with [`ClientOptions::new`] and a
|
||||||
|
//! chain of `with_*` setters; the `Debug` impl redacts the API key.
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::auth::ApiKey;
|
use crate::auth::ApiKey;
|
||||||
|
|
||||||
|
/// Configuration for connecting to a gateway endpoint.
|
||||||
|
///
|
||||||
|
/// Defaults are 10s connect timeout, 30s call timeout, no streaming timeout,
|
||||||
|
/// and plaintext (h2c) transport. Set [`ClientOptions::with_plaintext`] to
|
||||||
|
/// `false` and supply [`ClientOptions::with_ca_file`] / a server-name override
|
||||||
|
/// for TLS deployments.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ClientOptions {
|
pub struct ClientOptions {
|
||||||
endpoint: String,
|
endpoint: String,
|
||||||
@@ -17,6 +27,8 @@ pub struct ClientOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ClientOptions {
|
impl ClientOptions {
|
||||||
|
/// Build options for the supplied gateway endpoint URL (for example,
|
||||||
|
/// `http://127.0.0.1:5000`). Other settings take their defaults.
|
||||||
pub fn new(endpoint: impl Into<String>) -> Self {
|
pub fn new(endpoint: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
endpoint: endpoint.into(),
|
endpoint: endpoint.into(),
|
||||||
@@ -30,69 +42,91 @@ impl ClientOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attach an API key. The key flows through [`crate::auth::AuthInterceptor`]
|
||||||
|
/// as the Bearer token on every request.
|
||||||
pub fn with_api_key(mut self, api_key: ApiKey) -> Self {
|
pub fn with_api_key(mut self, api_key: ApiKey) -> Self {
|
||||||
self.api_key = Some(api_key);
|
self.api_key = Some(api_key);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle h2c (plaintext) vs TLS. `true` (the default) skips the TLS
|
||||||
|
/// handshake and is suitable for loopback development.
|
||||||
pub fn with_plaintext(mut self, plaintext: bool) -> Self {
|
pub fn with_plaintext(mut self, plaintext: bool) -> Self {
|
||||||
self.plaintext = plaintext;
|
self.plaintext = plaintext;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trust roots PEM bundle for TLS connections. Ignored when
|
||||||
|
/// `plaintext` is `true`.
|
||||||
pub fn with_ca_file(mut self, ca_file: impl Into<PathBuf>) -> Self {
|
pub fn with_ca_file(mut self, ca_file: impl Into<PathBuf>) -> Self {
|
||||||
self.ca_file = Some(ca_file.into());
|
self.ca_file = Some(ca_file.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Override the SNI/server name used during the TLS handshake. Useful
|
||||||
|
/// when the dial-target host name does not match the certificate.
|
||||||
pub fn with_server_name_override(mut self, server_name_override: impl Into<String>) -> Self {
|
pub fn with_server_name_override(mut self, server_name_override: impl Into<String>) -> Self {
|
||||||
self.server_name_override = Some(server_name_override.into());
|
self.server_name_override = Some(server_name_override.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maximum time the transport waits for the initial TCP/TLS connection.
|
||||||
pub fn with_connect_timeout(mut self, connect_timeout: Duration) -> Self {
|
pub fn with_connect_timeout(mut self, connect_timeout: Duration) -> Self {
|
||||||
self.connect_timeout = connect_timeout;
|
self.connect_timeout = connect_timeout;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-call deadline applied to every unary RPC. Streaming RPCs use
|
||||||
|
/// [`ClientOptions::with_stream_timeout`] instead.
|
||||||
pub fn with_call_timeout(mut self, call_timeout: Duration) -> Self {
|
pub fn with_call_timeout(mut self, call_timeout: Duration) -> Self {
|
||||||
self.call_timeout = call_timeout;
|
self.call_timeout = call_timeout;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Optional deadline applied to streaming RPCs (for example,
|
||||||
|
/// `StreamEvents`). Without a stream timeout the stream lives until the
|
||||||
|
/// caller drops it or the server closes it.
|
||||||
pub fn with_stream_timeout(mut self, stream_timeout: Duration) -> Self {
|
pub fn with_stream_timeout(mut self, stream_timeout: Duration) -> Self {
|
||||||
self.stream_timeout = Some(stream_timeout);
|
self.stream_timeout = Some(stream_timeout);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configured endpoint URL.
|
||||||
pub fn endpoint(&self) -> &str {
|
pub fn endpoint(&self) -> &str {
|
||||||
&self.endpoint
|
&self.endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configured API key, if any.
|
||||||
pub fn api_key(&self) -> Option<&ApiKey> {
|
pub fn api_key(&self) -> Option<&ApiKey> {
|
||||||
self.api_key.as_ref()
|
self.api_key.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the transport runs in plaintext (h2c) mode.
|
||||||
pub fn plaintext(&self) -> bool {
|
pub fn plaintext(&self) -> bool {
|
||||||
self.plaintext
|
self.plaintext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Optional CA bundle path used to validate the server certificate.
|
||||||
pub fn ca_file(&self) -> Option<&PathBuf> {
|
pub fn ca_file(&self) -> Option<&PathBuf> {
|
||||||
self.ca_file.as_ref()
|
self.ca_file.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Optional SNI / server-name override for TLS handshakes.
|
||||||
pub fn server_name_override(&self) -> Option<&str> {
|
pub fn server_name_override(&self) -> Option<&str> {
|
||||||
self.server_name_override.as_deref()
|
self.server_name_override.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Connect timeout used during transport setup.
|
||||||
pub fn connect_timeout(&self) -> Duration {
|
pub fn connect_timeout(&self) -> Duration {
|
||||||
self.connect_timeout
|
self.connect_timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-call timeout for unary RPCs.
|
||||||
pub fn call_timeout(&self) -> Duration {
|
pub fn call_timeout(&self) -> Duration {
|
||||||
self.call_timeout
|
self.call_timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Optional per-call timeout for streaming RPCs.
|
||||||
pub fn stream_timeout(&self) -> Option<Duration> {
|
pub fn stream_timeout(&self) -> Option<Duration> {
|
||||||
self.stream_timeout
|
self.stream_timeout
|
||||||
}
|
}
|
||||||
|
|||||||
+139
-1
@@ -1,3 +1,13 @@
|
|||||||
|
//! Typed handle around an opened gateway session.
|
||||||
|
//!
|
||||||
|
//! [`Session`] wraps an `OpenSession` reply (just the session id) plus a
|
||||||
|
//! cloned [`GatewayClient`] and offers Rust-shaped methods for the
|
||||||
|
//! command surface that the worker exposes — `Register`, `AddItem`,
|
||||||
|
//! bulk subscribe variants, `Write`/`Write2`, and the event stream.
|
||||||
|
//!
|
||||||
|
//! Bulk commands enforce a 1000-item cap before contacting the worker, in
|
||||||
|
//! line with the gateway's documented `MAX_BULK_ITEMS`.
|
||||||
|
|
||||||
use crate::client::{EventStream, GatewayClient};
|
use crate::client::{EventStream, GatewayClient};
|
||||||
use crate::error::{ensure_protocol_success, Error};
|
use crate::error::{ensure_protocol_success, Error};
|
||||||
use crate::generated::mxaccess_gateway::v1::mx_command::Payload;
|
use crate::generated::mxaccess_gateway::v1::mx_command::Payload;
|
||||||
@@ -13,7 +23,13 @@ use crate::value::MxValue;
|
|||||||
|
|
||||||
const MAX_BULK_ITEMS: usize = 1_000;
|
const MAX_BULK_ITEMS: usize = 1_000;
|
||||||
|
|
||||||
/// Session identifier returned by the gateway.
|
/// Handle to an opened gateway session.
|
||||||
|
///
|
||||||
|
/// `Session` carries the gateway-issued session id and a cloned
|
||||||
|
/// [`GatewayClient`]. All methods are async and stateless on the client
|
||||||
|
/// side: the gateway tracks per-session worker state. Drop the handle and
|
||||||
|
/// call [`Session::close`] to release the worker; the gateway also reaps
|
||||||
|
/// orphaned sessions on its own schedule.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
id: String,
|
id: String,
|
||||||
@@ -28,10 +44,18 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Borrow the gateway-assigned session id.
|
||||||
pub fn id(&self) -> &str {
|
pub fn id(&self) -> &str {
|
||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convenience constructor that issues `OpenSession` with the supplied
|
||||||
|
/// client session name and returns the resulting [`Session`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Propagates errors from
|
||||||
|
/// [`GatewayClient::open_session`](crate::client::GatewayClient::open_session).
|
||||||
pub async fn open(client: GatewayClient, client_session_name: &str) -> Result<Self, Error> {
|
pub async fn open(client: GatewayClient, client_session_name: &str) -> Result<Self, Error> {
|
||||||
client
|
client
|
||||||
.open_session(OpenSessionRequest {
|
.open_session(OpenSessionRequest {
|
||||||
@@ -41,6 +65,12 @@ impl Session {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Issue `CloseSession` against this session id.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::ProtocolStatus`] if the gateway returns a non-OK
|
||||||
|
/// envelope and any transport/status errors propagated by tonic.
|
||||||
pub async fn close(&self) -> Result<(), Error> {
|
pub async fn close(&self) -> Result<(), Error> {
|
||||||
let reply = self
|
let reply = self
|
||||||
.client
|
.client
|
||||||
@@ -53,6 +83,12 @@ impl Session {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run MXAccess `Register` and return the assigned `ServerHandle`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::Command`] if the worker reports a non-OK status,
|
||||||
|
/// plus transport/status errors from tonic.
|
||||||
pub async fn register(&self, client_name: &str) -> Result<i32, Error> {
|
pub async fn register(&self, client_name: &str) -> Result<i32, Error> {
|
||||||
let reply = self
|
let reply = self
|
||||||
.invoke(
|
.invoke(
|
||||||
@@ -66,6 +102,13 @@ impl Session {
|
|||||||
Ok(register_server_handle(&reply))
|
Ok(register_server_handle(&reply))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run MXAccess `AddItem` against `server_handle` and return the
|
||||||
|
/// assigned `ItemHandle`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::Command`] when MXAccess rejects the item
|
||||||
|
/// definition, plus transport/status errors from tonic.
|
||||||
pub async fn add_item(&self, server_handle: i32, item_definition: &str) -> Result<i32, Error> {
|
pub async fn add_item(&self, server_handle: i32, item_definition: &str) -> Result<i32, Error> {
|
||||||
let reply = self
|
let reply = self
|
||||||
.invoke(
|
.invoke(
|
||||||
@@ -80,6 +123,12 @@ impl Session {
|
|||||||
Ok(add_item_handle(&reply))
|
Ok(add_item_handle(&reply))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run MXAccess `AddItem2` (item with a caller-supplied context string)
|
||||||
|
/// and return the assigned `ItemHandle`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Same conditions as [`Session::add_item`].
|
||||||
pub async fn add_item2(
|
pub async fn add_item2(
|
||||||
&self,
|
&self,
|
||||||
server_handle: i32,
|
server_handle: i32,
|
||||||
@@ -100,6 +149,12 @@ impl Session {
|
|||||||
Ok(add_item2_handle(&reply))
|
Ok(add_item2_handle(&reply))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run MXAccess `RemoveItem` for the given handle pair.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::Command`] on a non-OK worker status, plus the
|
||||||
|
/// usual transport/status errors.
|
||||||
pub async fn remove_item(&self, server_handle: i32, item_handle: i32) -> Result<(), Error> {
|
pub async fn remove_item(&self, server_handle: i32, item_handle: i32) -> Result<(), Error> {
|
||||||
self.invoke(
|
self.invoke(
|
||||||
MxCommandKind::RemoveItem,
|
MxCommandKind::RemoveItem,
|
||||||
@@ -112,6 +167,12 @@ impl Session {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run MXAccess `Advise` to start receiving change notifications for
|
||||||
|
/// the given item.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::Command`] when the worker reports a non-OK status.
|
||||||
pub async fn advise(&self, server_handle: i32, item_handle: i32) -> Result<(), Error> {
|
pub async fn advise(&self, server_handle: i32, item_handle: i32) -> Result<(), Error> {
|
||||||
self.invoke(
|
self.invoke(
|
||||||
MxCommandKind::Advise,
|
MxCommandKind::Advise,
|
||||||
@@ -124,6 +185,12 @@ impl Session {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run MXAccess `UnAdvise` to stop change notifications for the given
|
||||||
|
/// item.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::Command`] when the worker reports a non-OK status.
|
||||||
pub async fn un_advise(&self, server_handle: i32, item_handle: i32) -> Result<(), Error> {
|
pub async fn un_advise(&self, server_handle: i32, item_handle: i32) -> Result<(), Error> {
|
||||||
self.invoke(
|
self.invoke(
|
||||||
MxCommandKind::UnAdvise,
|
MxCommandKind::UnAdvise,
|
||||||
@@ -136,6 +203,13 @@ impl Session {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bulk variant of [`Session::add_item`]. Each tag address yields one
|
||||||
|
/// `SubscribeResult` in the returned vector.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::InvalidArgument`] when the input exceeds the
|
||||||
|
/// gateway's 1000-item bulk cap, plus the usual command-level errors.
|
||||||
pub async fn add_item_bulk(
|
pub async fn add_item_bulk(
|
||||||
&self,
|
&self,
|
||||||
server_handle: i32,
|
server_handle: i32,
|
||||||
@@ -155,6 +229,11 @@ impl Session {
|
|||||||
Ok(bulk_results(reply, BulkReplyKind::AddItemBulk))
|
Ok(bulk_results(reply, BulkReplyKind::AddItemBulk))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bulk variant of [`Session::advise`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Same conditions as [`Session::add_item_bulk`].
|
||||||
pub async fn advise_item_bulk(
|
pub async fn advise_item_bulk(
|
||||||
&self,
|
&self,
|
||||||
server_handle: i32,
|
server_handle: i32,
|
||||||
@@ -174,6 +253,11 @@ impl Session {
|
|||||||
Ok(bulk_results(reply, BulkReplyKind::AdviseItemBulk))
|
Ok(bulk_results(reply, BulkReplyKind::AdviseItemBulk))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bulk variant of [`Session::remove_item`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Same conditions as [`Session::add_item_bulk`].
|
||||||
pub async fn remove_item_bulk(
|
pub async fn remove_item_bulk(
|
||||||
&self,
|
&self,
|
||||||
server_handle: i32,
|
server_handle: i32,
|
||||||
@@ -193,6 +277,11 @@ impl Session {
|
|||||||
Ok(bulk_results(reply, BulkReplyKind::RemoveItemBulk))
|
Ok(bulk_results(reply, BulkReplyKind::RemoveItemBulk))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bulk variant of [`Session::un_advise`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Same conditions as [`Session::add_item_bulk`].
|
||||||
pub async fn un_advise_item_bulk(
|
pub async fn un_advise_item_bulk(
|
||||||
&self,
|
&self,
|
||||||
server_handle: i32,
|
server_handle: i32,
|
||||||
@@ -212,6 +301,11 @@ impl Session {
|
|||||||
Ok(bulk_results(reply, BulkReplyKind::UnAdviseItemBulk))
|
Ok(bulk_results(reply, BulkReplyKind::UnAdviseItemBulk))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bulk `Subscribe` (atomic add-and-advise) for a list of tag addresses.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Same conditions as [`Session::add_item_bulk`].
|
||||||
pub async fn subscribe_bulk(
|
pub async fn subscribe_bulk(
|
||||||
&self,
|
&self,
|
||||||
server_handle: i32,
|
server_handle: i32,
|
||||||
@@ -231,6 +325,12 @@ impl Session {
|
|||||||
Ok(bulk_results(reply, BulkReplyKind::SubscribeBulk))
|
Ok(bulk_results(reply, BulkReplyKind::SubscribeBulk))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bulk `Unsubscribe` (atomic un-advise-and-remove) for a list of
|
||||||
|
/// item handles.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Same conditions as [`Session::add_item_bulk`].
|
||||||
pub async fn unsubscribe_bulk(
|
pub async fn unsubscribe_bulk(
|
||||||
&self,
|
&self,
|
||||||
server_handle: i32,
|
server_handle: i32,
|
||||||
@@ -250,6 +350,12 @@ impl Session {
|
|||||||
Ok(bulk_results(reply, BulkReplyKind::UnsubscribeBulk))
|
Ok(bulk_results(reply, BulkReplyKind::UnsubscribeBulk))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run MXAccess `Write` (single-value, no caller-supplied timestamp).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::Command`] for non-OK worker statuses, plus the
|
||||||
|
/// usual transport/status errors.
|
||||||
pub async fn write(
|
pub async fn write(
|
||||||
&self,
|
&self,
|
||||||
server_handle: i32,
|
server_handle: i32,
|
||||||
@@ -270,6 +376,11 @@ impl Session {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run MXAccess `Write2` (single-value with caller-supplied timestamp).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Same conditions as [`Session::write`].
|
||||||
pub async fn write2(
|
pub async fn write2(
|
||||||
&self,
|
&self,
|
||||||
server_handle: i32,
|
server_handle: i32,
|
||||||
@@ -292,10 +403,23 @@ impl Session {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open the per-session event stream from the beginning.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns the `tonic::Status` mapped through [`Error::from`] when the
|
||||||
|
/// gateway rejects the subscription.
|
||||||
pub async fn events(&self) -> Result<EventStream, Error> {
|
pub async fn events(&self) -> Result<EventStream, Error> {
|
||||||
self.events_after(0).await
|
self.events_after(0).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open the per-session event stream, requesting only events whose
|
||||||
|
/// `worker_sequence` is greater than `after_worker_sequence`. Pass `0`
|
||||||
|
/// to receive every buffered event.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Same conditions as [`Session::events`].
|
||||||
pub async fn events_after(&self, after_worker_sequence: u64) -> Result<EventStream, Error> {
|
pub async fn events_after(&self, after_worker_sequence: u64) -> Result<EventStream, Error> {
|
||||||
self.client
|
self.client
|
||||||
.stream_events(StreamEventsRequest {
|
.stream_events(StreamEventsRequest {
|
||||||
@@ -305,6 +429,13 @@ impl Session {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Issue a raw `Invoke` for an arbitrary command, without filtering on
|
||||||
|
/// the protocol status. Useful when callers need the full reply for
|
||||||
|
/// commands not yet wrapped by `Session`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns the `tonic::Status` mapped through [`Error::from`].
|
||||||
pub async fn invoke_raw(
|
pub async fn invoke_raw(
|
||||||
&self,
|
&self,
|
||||||
kind: MxCommandKind,
|
kind: MxCommandKind,
|
||||||
@@ -315,6 +446,13 @@ impl Session {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Issue an `Invoke` for an arbitrary command and surface a non-OK
|
||||||
|
/// reply as [`Error::Command`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::Command`] for non-OK worker statuses plus any
|
||||||
|
/// errors propagated by [`invoke_raw`](Self::invoke_raw).
|
||||||
pub async fn invoke(
|
pub async fn invoke(
|
||||||
&self,
|
&self,
|
||||||
kind: MxCommandKind,
|
kind: MxCommandKind,
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
//! Rust-shaped wrappers around the wire `MxValue`, `MxArray`, and
|
||||||
|
//! `MxStatusProxy` types.
|
||||||
|
//!
|
||||||
|
//! [`MxValue`] keeps both the original protobuf message and a friendly
|
||||||
|
//! [`MxValueProjection`] enum, so callers can pass values through the wire
|
||||||
|
//! without losing information while still being able to pattern-match on
|
||||||
|
//! the typed variant. The same split applies to [`MxArrayValue`] and
|
||||||
|
//! [`MxArrayProjection`]. [`MxStatus`] wraps the MXAccess `MxStatusProxy`
|
||||||
|
//! status envelope.
|
||||||
|
|
||||||
use crate::generated::mxaccess_gateway::v1::mx_array::Values;
|
use crate::generated::mxaccess_gateway::v1::mx_array::Values;
|
||||||
use crate::generated::mxaccess_gateway::v1::mx_value::Kind;
|
use crate::generated::mxaccess_gateway::v1::mx_value::Kind;
|
||||||
use crate::generated::mxaccess_gateway::v1::{
|
use crate::generated::mxaccess_gateway::v1::{
|
||||||
@@ -6,6 +16,12 @@ use crate::generated::mxaccess_gateway::v1::{
|
|||||||
StringArray, TimestampArray,
|
StringArray, TimestampArray,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Owned `MxValue` carrying both the raw protobuf message and a typed
|
||||||
|
/// [`MxValueProjection`] view.
|
||||||
|
///
|
||||||
|
/// The constructors set both `data_type` and `variant_type` to the values
|
||||||
|
/// the worker expects so values built locally round-trip through MXAccess
|
||||||
|
/// without surprises.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct MxValue {
|
pub struct MxValue {
|
||||||
raw: ProtoMxValue,
|
raw: ProtoMxValue,
|
||||||
@@ -13,11 +29,14 @@ pub struct MxValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MxValue {
|
impl MxValue {
|
||||||
|
/// Wrap a protobuf [`ProtoMxValue`] and compute its
|
||||||
|
/// [`MxValueProjection`].
|
||||||
pub fn from_proto(raw: ProtoMxValue) -> Self {
|
pub fn from_proto(raw: ProtoMxValue) -> Self {
|
||||||
let projection = MxValueProjection::from_proto(&raw);
|
let projection = MxValueProjection::from_proto(&raw);
|
||||||
Self { raw, projection }
|
Self { raw, projection }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a boolean `MxValue` (`MxDataType::Boolean`, `VT_BOOL`).
|
||||||
pub fn bool(value: bool) -> Self {
|
pub fn bool(value: bool) -> Self {
|
||||||
Self::from_proto(ProtoMxValue {
|
Self::from_proto(ProtoMxValue {
|
||||||
data_type: MxDataType::Boolean as i32,
|
data_type: MxDataType::Boolean as i32,
|
||||||
@@ -27,6 +46,7 @@ impl MxValue {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a 32-bit integer `MxValue` (`MxDataType::Integer`, `VT_I4`).
|
||||||
pub fn int32(value: i32) -> Self {
|
pub fn int32(value: i32) -> Self {
|
||||||
Self::from_proto(ProtoMxValue {
|
Self::from_proto(ProtoMxValue {
|
||||||
data_type: MxDataType::Integer as i32,
|
data_type: MxDataType::Integer as i32,
|
||||||
@@ -36,6 +56,7 @@ impl MxValue {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a 64-bit integer `MxValue` (`MxDataType::Integer`, `VT_I8`).
|
||||||
pub fn int64(value: i64) -> Self {
|
pub fn int64(value: i64) -> Self {
|
||||||
Self::from_proto(ProtoMxValue {
|
Self::from_proto(ProtoMxValue {
|
||||||
data_type: MxDataType::Integer as i32,
|
data_type: MxDataType::Integer as i32,
|
||||||
@@ -45,6 +66,7 @@ impl MxValue {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a 32-bit float `MxValue` (`MxDataType::Float`, `VT_R4`).
|
||||||
pub fn float(value: f32) -> Self {
|
pub fn float(value: f32) -> Self {
|
||||||
Self::from_proto(ProtoMxValue {
|
Self::from_proto(ProtoMxValue {
|
||||||
data_type: MxDataType::Float as i32,
|
data_type: MxDataType::Float as i32,
|
||||||
@@ -54,6 +76,7 @@ impl MxValue {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a 64-bit float `MxValue` (`MxDataType::Double`, `VT_R8`).
|
||||||
pub fn double(value: f64) -> Self {
|
pub fn double(value: f64) -> Self {
|
||||||
Self::from_proto(ProtoMxValue {
|
Self::from_proto(ProtoMxValue {
|
||||||
data_type: MxDataType::Double as i32,
|
data_type: MxDataType::Double as i32,
|
||||||
@@ -63,6 +86,7 @@ impl MxValue {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a string `MxValue` (`MxDataType::String`, `VT_BSTR`).
|
||||||
pub fn string(value: impl Into<String>) -> Self {
|
pub fn string(value: impl Into<String>) -> Self {
|
||||||
Self::from_proto(ProtoMxValue {
|
Self::from_proto(ProtoMxValue {
|
||||||
data_type: MxDataType::String as i32,
|
data_type: MxDataType::String as i32,
|
||||||
@@ -72,14 +96,18 @@ impl MxValue {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Borrow the underlying protobuf message exactly as it will travel
|
||||||
|
/// over the wire.
|
||||||
pub fn raw(&self) -> &ProtoMxValue {
|
pub fn raw(&self) -> &ProtoMxValue {
|
||||||
&self.raw
|
&self.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Borrow the typed projection.
|
||||||
pub fn projection(&self) -> &MxValueProjection {
|
pub fn projection(&self) -> &MxValueProjection {
|
||||||
&self.projection
|
&self.projection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Consume the wrapper and return the underlying protobuf message.
|
||||||
pub fn into_proto(self) -> ProtoMxValue {
|
pub fn into_proto(self) -> ProtoMxValue {
|
||||||
self.raw
|
self.raw
|
||||||
}
|
}
|
||||||
@@ -97,18 +125,35 @@ impl From<ProtoMxValue> for MxValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Typed view over an [`MxValue`].
|
||||||
|
///
|
||||||
|
/// Mirrors the `MxValue::Kind` oneof on the wire, plus a [`MxValueProjection::Null`]
|
||||||
|
/// variant for `is_null=true` and a [`MxValueProjection::Unset`] variant for
|
||||||
|
/// values that arrive without a `kind` set.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum MxValueProjection {
|
pub enum MxValueProjection {
|
||||||
|
/// No `kind` was present on the wire.
|
||||||
Unset,
|
Unset,
|
||||||
|
/// `is_null = true` on the wire.
|
||||||
Null,
|
Null,
|
||||||
|
/// Boolean value.
|
||||||
Bool(bool),
|
Bool(bool),
|
||||||
|
/// 32-bit signed integer value.
|
||||||
Int32(i32),
|
Int32(i32),
|
||||||
|
/// 64-bit signed integer value.
|
||||||
Int64(i64),
|
Int64(i64),
|
||||||
|
/// 32-bit float value.
|
||||||
Float(f32),
|
Float(f32),
|
||||||
|
/// 64-bit float value.
|
||||||
Double(f64),
|
Double(f64),
|
||||||
|
/// UTF-8 string value.
|
||||||
String(String),
|
String(String),
|
||||||
|
/// Wall-clock timestamp.
|
||||||
Timestamp(prost_types::Timestamp),
|
Timestamp(prost_types::Timestamp),
|
||||||
|
/// Array value carrying a homogeneous element type.
|
||||||
Array(MxArrayValue),
|
Array(MxArrayValue),
|
||||||
|
/// Opaque variant payload that the gateway could not project to a typed
|
||||||
|
/// scalar.
|
||||||
Raw(Vec<u8>),
|
Raw(Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +178,8 @@ impl MxValueProjection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Owned `MxArray` carrying both the raw protobuf message and a typed
|
||||||
|
/// [`MxArrayProjection`] view of its elements.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct MxArrayValue {
|
pub struct MxArrayValue {
|
||||||
raw: MxArray,
|
raw: MxArray,
|
||||||
@@ -140,11 +187,14 @@ pub struct MxArrayValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MxArrayValue {
|
impl MxArrayValue {
|
||||||
|
/// Wrap a protobuf [`MxArray`] and compute its
|
||||||
|
/// [`MxArrayProjection`].
|
||||||
pub fn from_proto(raw: MxArray) -> Self {
|
pub fn from_proto(raw: MxArray) -> Self {
|
||||||
let projection = MxArrayProjection::from_proto(&raw);
|
let projection = MxArrayProjection::from_proto(&raw);
|
||||||
Self { raw, projection }
|
Self { raw, projection }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a one-dimensional string array (`VT_ARRAY|VT_BSTR`).
|
||||||
pub fn string(values: Vec<String>) -> Self {
|
pub fn string(values: Vec<String>) -> Self {
|
||||||
Self::from_proto(MxArray {
|
Self::from_proto(MxArray {
|
||||||
element_data_type: MxDataType::String as i32,
|
element_data_type: MxDataType::String as i32,
|
||||||
@@ -155,25 +205,40 @@ impl MxArrayValue {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Borrow the underlying protobuf array message.
|
||||||
pub fn raw(&self) -> &MxArray {
|
pub fn raw(&self) -> &MxArray {
|
||||||
&self.raw
|
&self.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Borrow the typed projection of the array's elements.
|
||||||
pub fn projection(&self) -> &MxArrayProjection {
|
pub fn projection(&self) -> &MxArrayProjection {
|
||||||
&self.projection
|
&self.projection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Typed view over an [`MxArrayValue`].
|
||||||
|
///
|
||||||
|
/// Each variant matches the corresponding `MxArray::Values` oneof arm on
|
||||||
|
/// the wire.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum MxArrayProjection {
|
pub enum MxArrayProjection {
|
||||||
|
/// No `values` oneof was present on the wire.
|
||||||
Unset,
|
Unset,
|
||||||
|
/// Boolean elements.
|
||||||
Bool(Vec<bool>),
|
Bool(Vec<bool>),
|
||||||
|
/// 32-bit signed integer elements.
|
||||||
Int32(Vec<i32>),
|
Int32(Vec<i32>),
|
||||||
|
/// 64-bit signed integer elements.
|
||||||
Int64(Vec<i64>),
|
Int64(Vec<i64>),
|
||||||
|
/// 32-bit float elements.
|
||||||
Float(Vec<f32>),
|
Float(Vec<f32>),
|
||||||
|
/// 64-bit float elements.
|
||||||
Double(Vec<f64>),
|
Double(Vec<f64>),
|
||||||
|
/// UTF-8 string elements.
|
||||||
String(Vec<String>),
|
String(Vec<String>),
|
||||||
|
/// Timestamp elements.
|
||||||
Timestamp(Vec<prost_types::Timestamp>),
|
Timestamp(Vec<prost_types::Timestamp>),
|
||||||
|
/// Opaque variant payloads, one per element.
|
||||||
Raw(Vec<Vec<u8>>),
|
Raw(Vec<Vec<u8>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,44 +260,57 @@ impl MxArrayProjection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Typed wrapper around the MXAccess `MxStatusProxy` envelope, the
|
||||||
|
/// per-value status that accompanies every `OnDataChange`/`Read` reply.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct MxStatus {
|
pub struct MxStatus {
|
||||||
raw: MxStatusProxy,
|
raw: MxStatusProxy,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MxStatus {
|
impl MxStatus {
|
||||||
|
/// Wrap a protobuf [`MxStatusProxy`].
|
||||||
pub fn from_proto(raw: MxStatusProxy) -> Self {
|
pub fn from_proto(raw: MxStatusProxy) -> Self {
|
||||||
Self { raw }
|
Self { raw }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Borrow the underlying protobuf message.
|
||||||
pub fn raw(&self) -> &MxStatusProxy {
|
pub fn raw(&self) -> &MxStatusProxy {
|
||||||
&self.raw
|
&self.raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `MXSTATUS_PROXY.Success` flag (0 = error, non-zero = good/warning).
|
||||||
pub fn success(&self) -> i32 {
|
pub fn success(&self) -> i32 {
|
||||||
self.raw.success
|
self.raw.success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decode the status category, or `None` if the wire value is not a
|
||||||
|
/// known [`MxStatusCategory`].
|
||||||
pub fn category(&self) -> Option<MxStatusCategory> {
|
pub fn category(&self) -> Option<MxStatusCategory> {
|
||||||
MxStatusCategory::try_from(self.raw.category).ok()
|
MxStatusCategory::try_from(self.raw.category).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decode the source (provider, gateway, worker) that flagged the
|
||||||
|
/// status, or `None` for unknown values.
|
||||||
pub fn detected_by(&self) -> Option<MxStatusSource> {
|
pub fn detected_by(&self) -> Option<MxStatusSource> {
|
||||||
MxStatusSource::try_from(self.raw.detected_by).ok()
|
MxStatusSource::try_from(self.raw.detected_by).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `MXSTATUS_PROXY.Detail` numeric reason code.
|
||||||
pub fn detail(&self) -> i32 {
|
pub fn detail(&self) -> i32 {
|
||||||
self.raw.detail
|
self.raw.detail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Raw, undecoded category integer (useful for forward compatibility).
|
||||||
pub fn raw_category(&self) -> i32 {
|
pub fn raw_category(&self) -> i32 {
|
||||||
self.raw.raw_category
|
self.raw.raw_category
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Raw, undecoded detected-by integer.
|
||||||
pub fn raw_detected_by(&self) -> i32 {
|
pub fn raw_detected_by(&self) -> i32 {
|
||||||
self.raw.raw_detected_by
|
self.raw.raw_detected_by
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Optional human-readable diagnostic from MXAccess.
|
||||||
pub fn diagnostic_text(&self) -> &str {
|
pub fn diagnostic_text(&self) -> &str {
|
||||||
&self.raw.diagnostic_text
|
&self.raw.diagnostic_text
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
//! Build-time version constants advertised by the Rust client.
|
||||||
|
//!
|
||||||
|
//! The protocol versions track the values the gateway and worker negotiate on
|
||||||
|
//! `OpenSession` and let test harnesses cross-check the wire contract.
|
||||||
|
|
||||||
|
/// Semantic version of this Rust client crate. Mirrors `Cargo.toml`.
|
||||||
pub const CLIENT_VERSION: &str = "0.1.0-dev";
|
pub const CLIENT_VERSION: &str = "0.1.0-dev";
|
||||||
|
|
||||||
|
/// Public gateway gRPC protocol version this client targets.
|
||||||
pub const GATEWAY_PROTOCOL_VERSION: u32 = 1;
|
pub const GATEWAY_PROTOCOL_VERSION: u32 = 1;
|
||||||
|
|
||||||
|
/// Internal worker IPC protocol version this client expects sessions to use.
|
||||||
pub const WORKER_PROTOCOL_VERSION: u32 = 1;
|
pub const WORKER_PROTOCOL_VERSION: u32 = 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user