From 8d3352f2c66a8f225d93bd1c66ef9a69deabea9c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 30 Apr 2026 12:04:46 -0400 Subject: [PATCH] Add idiomatic documentation to Go, Java, Python, and Rust clients --- clients/go/cmd/mxgw-go/main.go | 5 + clients/go/mxgateway/client.go | 22 +- clients/go/mxgateway/errors.go | 20 +- clients/go/mxgateway/galaxy.go | 30 ++- clients/go/mxgateway/options.go | 31 ++- clients/go/mxgateway/session.go | 4 +- clients/go/mxgateway/types.go | 184 +++++++++----- .../mxgateway/cli/MxGatewayCli.java | 24 ++ .../client/DeployEventSubscription.java | 5 + .../client/GalaxyRepositoryClient.java | 89 ++++++- .../mxgateway/client/MxAccessException.java | 18 ++ .../mxgateway/client/MxEventStream.java | 10 + .../client/MxGatewayAuthInterceptor.java | 10 + .../MxGatewayAuthenticationException.java | 10 + .../MxGatewayAuthorizationException.java | 10 + .../mxgateway/client/MxGatewayClient.java | 115 +++++++++ .../client/MxGatewayClientOptions.java | 118 +++++++++ .../client/MxGatewayClientVersion.java | 21 ++ .../client/MxGatewayCommandException.java | 22 ++ .../client/MxGatewayEventSubscription.java | 13 + .../mxgateway/client/MxGatewayException.java | 18 ++ .../mxgateway/client/MxGatewaySecrets.java | 24 ++ .../mxgateway/client/MxGatewaySession.java | 240 ++++++++++++++++++ .../client/MxGatewaySessionException.java | 15 ++ .../client/MxGatewayWorkerException.java | 15 ++ .../mxgateway/client/MxStatuses.java | 61 +++++ .../dohertylan/mxgateway/client/MxValues.java | 84 ++++++ clients/python/src/mxgateway/auth.py | 3 + clients/python/src/mxgateway/client.py | 6 + clients/python/src/mxgateway/errors.py | 1 + clients/python/src/mxgateway/galaxy.py | 3 + clients/python/src/mxgateway/options.py | 2 + clients/python/src/mxgateway/session.py | 19 ++ clients/python/src/mxgateway_cli/commands.py | 1 + clients/rust/crates/mxgw-cli/src/main.rs | 10 + clients/rust/src/auth.rs | 14 + clients/rust/src/client.rs | 75 +++++- clients/rust/src/error.rs | 79 +++++- clients/rust/src/generated.rs | 19 ++ clients/rust/src/lib.rs | 21 +- clients/rust/src/options.rs | 34 +++ clients/rust/src/session.rs | 140 +++++++++- clients/rust/src/value.rs | 78 ++++++ clients/rust/src/version.rs | 10 + 44 files changed, 1631 insertions(+), 102 deletions(-) diff --git a/clients/go/cmd/mxgw-go/main.go b/clients/go/cmd/mxgw-go/main.go index e2f1173..0fff337 100644 --- a/clients/go/cmd/mxgw-go/main.go +++ b/clients/go/cmd/mxgw-go/main.go @@ -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 import ( diff --git a/clients/go/mxgateway/client.go b/clients/go/mxgateway/client.go index 439d655..2aac029 100644 --- a/clients/go/mxgateway/client.go +++ b/clients/go/mxgateway/client.go @@ -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 import ( @@ -219,10 +230,15 @@ func resolveTransportCredentials(opts Options) (credentials.TransportCredentials // OpenSessionOptions describes fields used to create an OpenSessionRequest. type OpenSessionOptions struct { - RequestedBackend string - ClientSessionName string + // RequestedBackend selects the gateway worker backend (empty for default). + 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 - 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. diff --git a/clients/go/mxgateway/errors.go b/clients/go/mxgateway/errors.go index 890f14b..92dd486 100644 --- a/clients/go/mxgateway/errors.go +++ b/clients/go/mxgateway/errors.go @@ -8,10 +8,13 @@ import ( // GatewayError wraps transport-level gRPC failures. 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 } +// Error returns the formatted gateway error message. func (e *GatewayError) Error() string { if e == nil { return "" @@ -22,6 +25,7 @@ func (e *GatewayError) Error() string { return fmt.Sprintf("mxgateway: %s failed: %v", e.Op, e.Err) } +// Unwrap returns the wrapped transport error. func (e *GatewayError) Unwrap() error { if e == nil { return nil @@ -32,11 +36,15 @@ func (e *GatewayError) Unwrap() error { // CommandError reports a non-OK gateway protocol status and keeps the raw // command reply when one exists. 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 - 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 { if e == nil { return "" @@ -53,10 +61,13 @@ func (e *CommandError) Error() string { // MxAccessError reports HRESULT or MXSTATUS_PROXY failures returned by MXAccess. type MxAccessError struct { + // Command is the wrapped CommandError when the protocol status carried one. 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 { if e == nil { return "" @@ -73,6 +84,7 @@ func (e *MxAccessError) Error() string { return "mxgateway: MXAccess command failed" } +// Unwrap returns the wrapped CommandError, when one is present. func (e *MxAccessError) Unwrap() error { if e == nil { return nil diff --git a/clients/go/mxgateway/galaxy.go b/clients/go/mxgateway/galaxy.go index 949bb6c..a5da3d4 100644 --- a/clients/go/mxgateway/galaxy.go +++ b/clients/go/mxgateway/galaxy.go @@ -20,16 +20,26 @@ type RawGalaxyRepositoryClient = pb.GalaxyRepositoryClient // Generated protobuf aliases for Galaxy Repository messages. type ( - TestConnectionRequest = pb.TestConnectionRequest - TestConnectionReply = pb.TestConnectionReply - GetLastDeployTimeRequest = pb.GetLastDeployTimeRequest - GetLastDeployTimeReply = pb.GetLastDeployTimeReply - DiscoverHierarchyRequest = pb.DiscoverHierarchyRequest - DiscoverHierarchyReply = pb.DiscoverHierarchyReply - GalaxyObject = pb.GalaxyObject - GalaxyAttribute = pb.GalaxyAttribute - WatchDeployEventsRequest = pb.WatchDeployEventsRequest - DeployEvent = pb.DeployEvent + // TestConnectionRequest is the request for Galaxy Repository TestConnection. + TestConnectionRequest = pb.TestConnectionRequest + // TestConnectionReply is the reply for Galaxy Repository TestConnection. + TestConnectionReply = pb.TestConnectionReply + // GetLastDeployTimeRequest is the request for GetLastDeployTime. + GetLastDeployTimeRequest = pb.GetLastDeployTimeRequest + // GetLastDeployTimeReply is the reply for GetLastDeployTime. + GetLastDeployTimeReply = pb.GetLastDeployTimeReply + // DiscoverHierarchyRequest is the request for DiscoverHierarchy. + 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. diff --git a/clients/go/mxgateway/options.go b/clients/go/mxgateway/options.go index 782634d..30a8c23 100644 --- a/clients/go/mxgateway/options.go +++ b/clients/go/mxgateway/options.go @@ -11,16 +11,29 @@ import ( // Options configures gateway connections. type Options struct { - Endpoint string - APIKey string - Plaintext bool - CACertFile string - ServerNameOverride string - DialTimeout time.Duration - CallTimeout time.Duration - TLSConfig *tls.Config + // Endpoint is the gateway host:port address to dial. + Endpoint string + // APIKey is the bearer token attached to outgoing gRPC metadata. + APIKey string + // Plaintext disables TLS and uses insecure credentials when true. + Plaintext bool + // CACertFile points to a PEM file used to verify the gateway certificate. + 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 - 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 diff --git a/clients/go/mxgateway/session.go b/clients/go/mxgateway/session.go index 4a6099c..8e00fd1 100644 --- a/clients/go/mxgateway/session.go +++ b/clients/go/mxgateway/session.go @@ -18,8 +18,10 @@ const maxBulkItems = 1000 // EventResult carries either the next ordered event or a terminal stream error. type EventResult struct { + // Event is the next event from the stream when Err is nil. 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. diff --git a/clients/go/mxgateway/types.go b/clients/go/mxgateway/types.go index 399fe08..edba63c 100644 --- a/clients/go/mxgateway/types.go +++ b/clients/go/mxgateway/types.go @@ -12,77 +12,145 @@ type RawEventStream = pb.MxAccessGateway_StreamEventsClient // Generated protobuf aliases keep raw contract access available from the public // mxgateway package while generated code remains under internal/generated. type ( - OpenSessionRequest = pb.OpenSessionRequest - OpenSessionReply = pb.OpenSessionReply - CloseSessionRequest = pb.CloseSessionRequest - CloseSessionReply = pb.CloseSessionReply - StreamEventsRequest = pb.StreamEventsRequest - MxCommandRequest = pb.MxCommandRequest - MxCommandReply = pb.MxCommandReply - MxCommand = pb.MxCommand - MxEvent = pb.MxEvent - MxValue = pb.MxValue - Value = pb.MxValue - MxArray = pb.MxArray - MxStatusProxy = pb.MxStatusProxy - ProtocolStatus = pb.ProtocolStatus - RegisterCommand = pb.RegisterCommand - UnregisterCommand = pb.UnregisterCommand - AddItemCommand = pb.AddItemCommand - AddItem2Command = pb.AddItem2Command - RemoveItemCommand = pb.RemoveItemCommand - AdviseCommand = pb.AdviseCommand - UnAdviseCommand = pb.UnAdviseCommand - AddItemBulkCommand = pb.AddItemBulkCommand - AdviseItemBulkCommand = pb.AdviseItemBulkCommand - RemoveItemBulkCommand = pb.RemoveItemBulkCommand + // OpenSessionRequest is the gateway OpenSession request message. + OpenSessionRequest = pb.OpenSessionRequest + // OpenSessionReply is the gateway OpenSession reply message. + OpenSessionReply = pb.OpenSessionReply + // CloseSessionRequest is the gateway CloseSession request message. + CloseSessionRequest = pb.CloseSessionRequest + // CloseSessionReply is the gateway CloseSession reply message. + CloseSessionReply = pb.CloseSessionReply + // StreamEventsRequest is the gateway StreamEvents request message. + StreamEventsRequest = pb.StreamEventsRequest + // MxCommandRequest carries one MXAccess command for Invoke. + MxCommandRequest = pb.MxCommandRequest + // MxCommandReply is the reply to an MXAccess command Invoke. + MxCommandReply = pb.MxCommandReply + // MxCommand is the discriminated union of MXAccess command payloads. + MxCommand = pb.MxCommand + // MxEvent is one ordered event delivered on a session event stream. + MxEvent = pb.MxEvent + // MxValue is the protobuf representation of an MXAccess value. + MxValue = pb.MxValue + // Value is an alias for MxValue retained for symmetry with other clients. + Value = pb.MxValue + // MxArray is the protobuf representation of an MXAccess array value. + 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 - SubscribeBulkCommand = pb.SubscribeBulkCommand - UnsubscribeBulkCommand = pb.UnsubscribeBulkCommand - WriteCommand = pb.WriteCommand - Write2Command = pb.Write2Command - RegisterReply = pb.RegisterReply - AddItemReply = pb.AddItemReply - AddItem2Reply = pb.AddItem2Reply - SubscribeResult = pb.SubscribeResult - BulkSubscribeReply = pb.BulkSubscribeReply + // SubscribeBulkCommand combines AddItem and Advise for a list of tags. + SubscribeBulkCommand = pb.SubscribeBulkCommand + // UnsubscribeBulkCommand combines UnAdvise and RemoveItem for a list of items. + UnsubscribeBulkCommand = pb.UnsubscribeBulkCommand + // WriteCommand is the payload of an MXAccess Write command. + WriteCommand = pb.WriteCommand + // Write2Command is the payload of an MXAccess Write2 command. + Write2Command = pb.Write2Command + // 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 ( - MxCommandKind = pb.MxCommandKind - MxDataType = pb.MxDataType - MxEventFamily = pb.MxEventFamily - MxStatusCategory = pb.MxStatusCategory - MxStatusSource = pb.MxStatusSource + // MxCommandKind discriminates which MXAccess command an MxCommand carries. + MxCommandKind = pb.MxCommandKind + // MxDataType is the MXAccess data type tag on values and arrays. + MxDataType = pb.MxDataType + // 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 - 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 ( - CommandKindRegister = pb.MxCommandKind_MX_COMMAND_KIND_REGISTER - CommandKindUnregister = pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER - CommandKindAddItem = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM - CommandKindAddItem2 = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2 - CommandKindRemoveItem = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM - CommandKindAdvise = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE - CommandKindUnAdvise = pb.MxCommandKind_MX_COMMAND_KIND_UN_ADVISE - CommandKindAddItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM_BULK - CommandKindAdviseItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_ITEM_BULK - CommandKindRemoveItemBulk = pb.MxCommandKind_MX_COMMAND_KIND_REMOVE_ITEM_BULK + // CommandKindRegister selects the MXAccess Register command. + CommandKindRegister = pb.MxCommandKind_MX_COMMAND_KIND_REGISTER + // CommandKindUnregister selects the MXAccess Unregister command. + CommandKindUnregister = pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER + // CommandKindAddItem selects the MXAccess AddItem command. + CommandKindAddItem = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM + // CommandKindAddItem2 selects the MXAccess AddItem2 command. + CommandKindAddItem2 = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2 + // CommandKindRemoveItem selects the MXAccess RemoveItem command. + 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 - CommandKindSubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK - CommandKindUnsubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_UNSUBSCRIBE_BULK - CommandKindWrite = pb.MxCommandKind_MX_COMMAND_KIND_WRITE - CommandKindWrite2 = pb.MxCommandKind_MX_COMMAND_KIND_WRITE2 + // CommandKindSubscribeBulk selects the AddItem+Advise combined bulk command. + CommandKindSubscribeBulk = pb.MxCommandKind_MX_COMMAND_KIND_SUBSCRIBE_BULK + // CommandKindUnsubscribeBulk selects the UnAdvise+RemoveItem combined bulk command. + 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 + // DataTypeBoolean denotes an MXAccess Boolean value. DataTypeBoolean = pb.MxDataType_MX_DATA_TYPE_BOOLEAN + // DataTypeInteger denotes an MXAccess Integer value. DataTypeInteger = pb.MxDataType_MX_DATA_TYPE_INTEGER - DataTypeFloat = pb.MxDataType_MX_DATA_TYPE_FLOAT - DataTypeDouble = pb.MxDataType_MX_DATA_TYPE_DOUBLE - DataTypeString = pb.MxDataType_MX_DATA_TYPE_STRING - DataTypeTime = pb.MxDataType_MX_DATA_TYPE_TIME + // DataTypeFloat denotes an MXAccess Float (single precision) value. + DataTypeFloat = pb.MxDataType_MX_DATA_TYPE_FLOAT + // DataTypeDouble denotes an MXAccess Double (double precision) value. + 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 ) diff --git a/clients/java/mxgateway-cli/src/main/java/com/dohertylan/mxgateway/cli/MxGatewayCli.java b/clients/java/mxgateway-cli/src/main/java/com/dohertylan/mxgateway/cli/MxGatewayCli.java index e948b76..f9da2a4 100644 --- a/clients/java/mxgateway-cli/src/main/java/com/dohertylan/mxgateway/cli/MxGatewayCli.java +++ b/clients/java/mxgateway-cli/src/main/java/com/dohertylan/mxgateway/cli/MxGatewayCli.java @@ -38,6 +38,10 @@ import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Option; import picocli.CommandLine.Spec; +/** + * Picocli entry point for the {@code mxgw-java} test CLI used by the + * cross-language smoke matrix. + */ @Command( name = "mxgw-java", mixinStandardHelpOptions = true, @@ -48,6 +52,9 @@ public final class MxGatewayCli implements Callable { @Spec private CommandSpec spec; + /** + * Builds a CLI bound to the default gRPC client factory. + */ public MxGatewayCli() { this(new GrpcMxGatewayCliClientFactory()); } @@ -56,11 +63,25 @@ public final class MxGatewayCli implements Callable { this.clientFactory = clientFactory; } + /** + * Process entry point. + * + * @param args command-line arguments + */ public static void main(String[] args) { int exitCode = commandLine(new GrpcMxGatewayCliClientFactory()).execute(args); 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) { return execute(new GrpcMxGatewayCliClientFactory(), out, err, args); } @@ -280,6 +301,9 @@ public final class MxGatewayCli implements Callable { return values; } + /** + * Picocli subcommand that prints the client and protocol version numbers. + */ @Command(name = "version", description = "Prints the Java client version.") public static final class VersionCommand implements Callable { @Spec diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventSubscription.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventSubscription.java index a42d4be..da610af 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventSubscription.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/DeployEventSubscription.java @@ -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() { cancelled.set(true); ClientCallStreamObserver stream = requestStream.get(); diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClient.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClient.java index 9e142d4..15607a4 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClient.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/GalaxyRepositoryClient.java @@ -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. + * + * @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) { this.ownedChannel = null; @@ -64,25 +68,49 @@ public final class GalaxyRepositoryClient implements AutoCloseable { 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) { 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() { return withDeadline(blockingStub); } + /** + * Returns the underlying future stub with the per-call deadline applied. + * + * @return the future stub + */ public GalaxyRepositoryGrpc.GalaxyRepositoryFutureStub rawFutureStub() { return withDeadline(futureStub); } + /** + * Returns the underlying async stub. Stream deadlines are applied per call. + * + * @return the async stub + */ public GalaxyRepositoryGrpc.GalaxyRepositoryStub rawAsyncStub() { 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() { 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 testConnectionAsync() { return toCompletable(rawFutureStub().testConnection(TestConnectionRequest.getDefaultInstance())) .thenApply(TestConnectionReply::getOk); } /** - * Invoke the {@code GetLastDeployTime} RPC. Returns {@link Optional#empty()} - * when the server reports {@code present=false}. + * Invokes the {@code GetLastDeployTime} RPC. + * + * @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 getLastDeployTime() { 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> getLastDeployTimeAsync() { return toCompletable(rawFutureStub().getLastDeployTime(GetLastDeployTimeRequest.getDefaultInstance())) .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 * the proto message without an extra DTO layer. + * + * @return the Galaxy object hierarchy + * @throws MxGatewayException on transport or protocol failure */ public List discoverHierarchy() { 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> discoverHierarchyAsync() { return toCompletable(rawFutureStub().discoverHierarchy(DiscoverHierarchyRequest.getDefaultInstance())) .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 * the underlying gRPC call. * - * @param lastSeenDeployTime optional. When non-null, the bootstrap event is - * suppressed if the cached deploy time matches. + * @param lastSeenDeployTime optional. When non-{@code null}, the bootstrap + * event is suppressed if the cached deploy time matches. + * @return an iterator-style stream of deploy events */ public DeployEventStream watchDeployEvents(Instant lastSeenDeployTime) { DeployEventStream stream = new DeployEventStream(16); @@ -163,15 +217,23 @@ public final class GalaxyRepositoryClient implements AutoCloseable { /** * Iterator-style alias for {@link #watchDeployEvents(Instant)} matching the * task-spec signature. + * + * @param lastSeenDeployTime optional cached deploy time for bootstrap suppression + * @return an iterator over deploy events */ public Iterator watchDeployEventsIterator(Instant 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 * 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( Instant lastSeenDeployTime, StreamObserver 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 { if (ownedChannel != null) { ownedChannel.shutdown(); diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxAccessException.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxAccessException.java index ecbe9ea..8b89674 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxAccessException.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxAccessException.java @@ -3,11 +3,29 @@ package com.dohertylan.mxgateway.client; import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply; 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 { + /** + * 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) { 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) { super(operation, reply == null ? null : reply.getProtocolStatus(), reply); } diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxEventStream.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxEventStream.java index 84bc634..0f669cd 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxEventStream.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxEventStream.java @@ -12,6 +12,16 @@ import java.util.concurrent.BlockingQueue; import mxaccess_gateway.v1.MxaccessGateway.MxEvent; import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest; +/** + * Iterator-style adaptor over the gateway {@code StreamEvents} server-streaming + * RPC. + * + *

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, AutoCloseable { private static final Object END = new Object(); diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayAuthInterceptor.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayAuthInterceptor.java index e92ba45..94f229b 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayAuthInterceptor.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayAuthInterceptor.java @@ -8,12 +8,22 @@ import io.grpc.ForwardingClientCall; import io.grpc.Metadata; 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 { static final Metadata.Key AUTHORIZATION_HEADER = Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER); 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) { this.apiKey = apiKey == null ? "" : apiKey; } diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayAuthenticationException.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayAuthenticationException.java index bdd3a69..d89c50a 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayAuthenticationException.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayAuthenticationException.java @@ -1,6 +1,16 @@ 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 { + /** + * 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) { super(message, cause); } diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayAuthorizationException.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayAuthorizationException.java index 8bc6909..3761491 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayAuthorizationException.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayAuthorizationException.java @@ -1,6 +1,16 @@ 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 { + /** + * 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) { super(message, cause); } diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClient.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClient.java index e8dd2e3..db8a59a 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClient.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClient.java @@ -25,6 +25,15 @@ import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest; import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode; import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest; +/** + * Idiomatic Java wrapper around the generated {@code MxAccessGateway} gRPC + * stubs. + * + *

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 { private final ManagedChannel ownedChannel; private final MxGatewayClientOptions options; @@ -41,6 +50,14 @@ public final class MxGatewayClient implements AutoCloseable { 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) { this.ownedChannel = null; this.options = Objects.requireNonNull(options, "options"); @@ -50,27 +67,64 @@ public final class MxGatewayClient implements AutoCloseable { 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) { 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() { return withDeadline(blockingStub); } + /** + * Returns the underlying future stub with the per-call deadline applied. + * + * @return the future stub + */ public MxAccessGatewayGrpc.MxAccessGatewayFutureStub rawFutureStub() { return withDeadline(futureStub); } + /** + * Returns the underlying async stub. Stream deadlines are applied per call. + * + * @return the async stub + */ public MxAccessGatewayGrpc.MxAccessGatewayStub rawAsyncStub() { 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) { OpenSessionReply reply = openSessionRaw(request); 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) { return openSession(OpenSessionRequest.newBuilder() .setClientSessionName(clientSessionName) @@ -81,6 +135,13 @@ public final class MxGatewayClient implements AutoCloseable { .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) { try { 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 openSessionAsync(OpenSessionRequest request) { CompletableFuture future = toCompletable(rawFutureStub().openSession(request)); 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) { try { 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 invokeAsync(MxCommandRequest request) { CompletableFuture future = toCompletable(rawFutureStub().invoke(request)); 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) { try { 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) { MxEventStream stream = new MxEventStream(16); withStreamDeadline(rawAsyncStub()).streamEvents(request, stream.observer()); 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( StreamEventsRequest request, StreamObserver observer) { 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 { if (ownedChannel != null) { ownedChannel.shutdown(); diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientOptions.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientOptions.java index 083403a..5ef5dd4 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientOptions.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientOptions.java @@ -4,6 +4,13 @@ import java.nio.file.Path; import java.time.Duration; import java.util.Objects; +/** + * Immutable configuration for {@link MxGatewayClient} and + * {@link GalaxyRepositoryClient}. + * + *

Captures the gateway endpoint, API key, transport security selection and + * call/stream timeouts. Instances are constructed via {@link #builder()}. + */ public final class MxGatewayClientOptions { private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10); private static final Duration DEFAULT_CALL_TIMEOUT = Duration.ofSeconds(30); @@ -28,42 +35,93 @@ public final class MxGatewayClientOptions { streamTimeout = builder.streamTimeout; } + /** + * Returns a fresh builder with default timeouts and no endpoint set. + * + * @return a new {@link Builder} + */ public static Builder 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() { return endpoint; } + /** + * Returns the configured API key, or an empty string if none was supplied. + * + * @return the raw API key + */ public String 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() { 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() { 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() { return caCertificatePath; } + /** + * Returns the TLS server-name override, or an empty string when none was supplied. + * + * @return the server-name override + */ public String serverNameOverride() { return serverNameOverride; } + /** + * Returns the channel connect timeout. + * + * @return the connect timeout duration + */ public Duration connectTimeout() { return connectTimeout; } + /** + * Returns the per-call deadline applied to unary RPCs. + * + * @return the call timeout duration + */ public Duration 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() { return streamTimeout; } @@ -100,6 +158,9 @@ public final class MxGatewayClientOptions { return value; } + /** + * Mutable builder for {@link MxGatewayClientOptions}. + */ public static final class Builder { private String endpoint; private String apiKey; @@ -113,46 +174,103 @@ public final class MxGatewayClientOptions { 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) { endpoint = value; 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) { apiKey = value; 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) { plaintext = value; 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) { caCertificatePath = value; 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) { serverNameOverride = value; 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) { connectTimeout = Objects.requireNonNull(value, "connectTimeout"); 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) { callTimeout = Objects.requireNonNull(value, "callTimeout"); 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) { streamTimeout = Objects.requireNonNull(value, "streamTimeout"); 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() { return new MxGatewayClientOptions(this); } diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientVersion.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientVersion.java index d94791d..983d782 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientVersion.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayClientVersion.java @@ -1,5 +1,11 @@ package com.dohertylan.mxgateway.client; +/** + * Reports the client and protocol version numbers compiled into this build. + * + *

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 { private static final int GATEWAY_PROTOCOL_VERSION = 1; private static final int WORKER_PROTOCOL_VERSION = 1; @@ -8,14 +14,29 @@ public final class MxGatewayClientVersion { private MxGatewayClientVersion() { } + /** + * Returns the human-readable client release version. + * + * @return the client version string + */ public static String clientVersion() { return CLIENT_VERSION; } + /** + * Returns the gRPC gateway protocol version this client targets. + * + * @return the gateway protocol version + */ public static int gatewayProtocolVersion() { return GATEWAY_PROTOCOL_VERSION; } + /** + * Returns the worker IPC protocol version this client targets. + * + * @return the worker protocol version + */ public static int workerProtocolVersion() { return WORKER_PROTOCOL_VERSION; } diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayCommandException.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayCommandException.java index ec645d1..0dfd577 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayCommandException.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayCommandException.java @@ -3,20 +3,42 @@ package com.dohertylan.mxgateway.client; import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply; 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 { private final ProtocolStatus protocolStatus; 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) { super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus)); this.protocolStatus = protocolStatus; 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() { 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() { return reply; } diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayEventSubscription.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayEventSubscription.java index ef40e09..a7bd55f 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayEventSubscription.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayEventSubscription.java @@ -8,6 +8,14 @@ import java.util.concurrent.atomic.AtomicBoolean; import mxaccess_gateway.v1.MxaccessGateway.MxEvent; import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest; +/** + * Cancellable handle returned by the async {@code streamEvents} variant. + * + *

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 { private final AtomicReference> requestStream = new AtomicReference<>(); 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() { cancelled.set(true); ClientCallStreamObserver stream = requestStream.get(); diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayException.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayException.java index 01698d1..e93b13d 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayException.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayException.java @@ -1,10 +1,28 @@ package com.dohertylan.mxgateway.client; +/** + * Base unchecked exception thrown by the MXAccess Gateway Java client. + * + *

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 { + /** + * Creates a new exception with the supplied message. + * + * @param message human-readable description of the failure + */ public MxGatewayException(String 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) { super(message, cause); } diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewaySecrets.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewaySecrets.java index be7fa84..be5deed 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewaySecrets.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewaySecrets.java @@ -1,9 +1,24 @@ package com.dohertylan.mxgateway.client; +/** + * Helpers for redacting secrets such as gateway API keys from log output. + * + *

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 { 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 ""} + * for keys eight characters or shorter, or a masked form preserving + * the leading and trailing four characters + */ public static String redactApiKey(String apiKey) { if (apiKey == null || apiKey.isEmpty()) { return ""; @@ -17,6 +32,15 @@ public final class MxGatewaySecrets { + 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 ""} + */ public static String redactCredentials(String value) { if (value == null || value.isBlank()) { return value == null ? "" : value; diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewaySession.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewaySession.java index d2f697b..51b686a 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewaySession.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewaySession.java @@ -30,6 +30,14 @@ import mxaccess_gateway.v1.MxaccessGateway.UnregisterCommand; import mxaccess_gateway.v1.MxaccessGateway.Write2Command; import mxaccess_gateway.v1.MxaccessGateway.WriteCommand; +/** + * Typed handle for a single MXAccess gateway session. + * + *

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 { private static final SecureRandom RANDOM = new SecureRandom(); @@ -42,19 +50,45 @@ public final class MxGatewaySession implements AutoCloseable { 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) { return new MxGatewaySession( client, OpenSessionReply.newBuilder().setSessionId(sessionId).build()); } + /** + * Returns the gateway-assigned session id. + * + * @return the session id + */ public String sessionId() { return openReply.getSessionId(); } + /** + * Returns the original {@link OpenSessionReply} that this session was opened with. + * + * @return the open-session reply + */ public OpenSessionReply 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() { if (closeReply == null) { closeReply = client.closeSessionRaw(CloseSessionRequest.newBuilder() @@ -70,6 +104,13 @@ public final class MxGatewaySession implements AutoCloseable { 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) { MxCommandReply reply = registerRaw(clientName); if (reply.hasRegister()) { @@ -78,6 +119,13 @@ public final class MxGatewaySession implements AutoCloseable { 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) { return invokeCommand(MxCommand.newBuilder() .setKind(MxCommandKind.MX_COMMAND_KIND_REGISTER) @@ -85,6 +133,12 @@ public final class MxGatewaySession implements AutoCloseable { .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) { invokeCommand(MxCommand.newBuilder() .setKind(MxCommandKind.MX_COMMAND_KIND_UNREGISTER) @@ -92,6 +146,14 @@ public final class MxGatewaySession implements AutoCloseable { .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) { MxCommandReply reply = addItemRaw(serverHandle, itemDefinition); if (reply.hasAddItem()) { @@ -100,6 +162,14 @@ public final class MxGatewaySession implements AutoCloseable { 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) { return invokeCommand(MxCommand.newBuilder() .setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM) @@ -109,6 +179,15 @@ public final class MxGatewaySession implements AutoCloseable { .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) { MxCommandReply reply = addItem2Raw(serverHandle, itemDefinition, itemContext); if (reply.hasAddItem2()) { @@ -117,6 +196,15 @@ public final class MxGatewaySession implements AutoCloseable { 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) { return invokeCommand(MxCommand.newBuilder() .setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM2) @@ -127,10 +215,25 @@ public final class MxGatewaySession implements AutoCloseable { .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) { 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) { return invokeCommand(MxCommand.newBuilder() .setKind(MxCommandKind.MX_COMMAND_KIND_REMOVE_ITEM) @@ -140,10 +243,25 @@ public final class MxGatewaySession implements AutoCloseable { .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) { 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) { return invokeCommand(MxCommand.newBuilder() .setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE) @@ -153,10 +271,25 @@ public final class MxGatewaySession implements AutoCloseable { .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) { 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) { return invokeCommand(MxCommand.newBuilder() .setKind(MxCommandKind.MX_COMMAND_KIND_UN_ADVISE) @@ -166,6 +299,15 @@ public final class MxGatewaySession implements AutoCloseable { .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 addItemBulk(int serverHandle, List tagAddresses) { Objects.requireNonNull(tagAddresses, "tagAddresses"); MxCommandReply reply = invokeCommand(MxCommand.newBuilder() @@ -177,6 +319,15 @@ public final class MxGatewaySession implements AutoCloseable { 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 adviseItemBulk(int serverHandle, List itemHandles) { Objects.requireNonNull(itemHandles, "itemHandles"); MxCommandReply reply = invokeCommand(MxCommand.newBuilder() @@ -188,6 +339,15 @@ public final class MxGatewaySession implements AutoCloseable { 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 removeItemBulk(int serverHandle, List itemHandles) { Objects.requireNonNull(itemHandles, "itemHandles"); MxCommandReply reply = invokeCommand(MxCommand.newBuilder() @@ -199,6 +359,15 @@ public final class MxGatewaySession implements AutoCloseable { 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 unAdviseItemBulk(int serverHandle, List itemHandles) { Objects.requireNonNull(itemHandles, "itemHandles"); MxCommandReply reply = invokeCommand(MxCommand.newBuilder() @@ -210,6 +379,16 @@ public final class MxGatewaySession implements AutoCloseable { 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 subscribeBulk(int serverHandle, List tagAddresses) { Objects.requireNonNull(tagAddresses, "tagAddresses"); MxCommandReply reply = invokeCommand(MxCommand.newBuilder() @@ -221,6 +400,16 @@ public final class MxGatewaySession implements AutoCloseable { 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 unsubscribeBulk(int serverHandle, List itemHandles) { Objects.requireNonNull(itemHandles, "itemHandles"); MxCommandReply reply = invokeCommand(MxCommand.newBuilder() @@ -232,10 +421,29 @@ public final class MxGatewaySession implements AutoCloseable { 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) { 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) { return invokeCommand(MxCommand.newBuilder() .setKind(MxCommandKind.MX_COMMAND_KIND_WRITE) @@ -247,6 +455,16 @@ public final class MxGatewaySession implements AutoCloseable { .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) { invokeCommand(MxCommand.newBuilder() .setKind(MxCommandKind.MX_COMMAND_KIND_WRITE2) @@ -259,10 +477,24 @@ public final class MxGatewaySession implements AutoCloseable { .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() { 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) { return client.streamEvents(StreamEventsRequest.newBuilder() .setSessionId(sessionId()) @@ -270,6 +502,14 @@ public final class MxGatewaySession implements AutoCloseable { .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) { return client.invoke(MxCommandRequest.newBuilder() .setSessionId(sessionId()) diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewaySessionException.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewaySessionException.java index d9a7493..07c9447 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewaySessionException.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewaySessionException.java @@ -2,14 +2,29 @@ package com.dohertylan.mxgateway.client; 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 { 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) { super(MxGatewayErrors.protocolStatusMessage(operation, 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() { return protocolStatus; } diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayWorkerException.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayWorkerException.java index ec45b05..bf39852 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayWorkerException.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxGatewayWorkerException.java @@ -2,14 +2,29 @@ package com.dohertylan.mxgateway.client; 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 { 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) { super(MxGatewayErrors.protocolStatusMessage(operation, 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() { return protocolStatus; } diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxStatuses.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxStatuses.java index ff2f1d2..8980778 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxStatuses.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxStatuses.java @@ -4,43 +4,104 @@ import mxaccess_gateway.v1.MxaccessGateway.MxStatusCategory; import mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy; import mxaccess_gateway.v1.MxaccessGateway.MxStatusSource; +/** + * Helpers for inspecting {@link MxStatusProxy} values returned by the gateway. + * + *

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 { 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) { 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) { 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) { + /** + * Returns the raw success flag (non-zero indicates success). + * + * @return the success flag value + */ public int success() { return raw.getSuccess(); } + /** + * Returns the high-level status category. + * + * @return the status category enum value + */ public MxStatusCategory category() { return raw.getCategory(); } + /** + * Returns which subsystem detected the status. + * + * @return the detection source enum value + */ public MxStatusSource detectedBy() { return raw.getDetectedBy(); } + /** + * Returns the detail code accompanying the status. + * + * @return the raw detail code + */ public int detail() { return raw.getDetail(); } + /** + * Returns the raw, unmapped category code from MXAccess. + * + * @return the raw category integer + */ public int rawCategory() { return raw.getRawCategory(); } + /** + * Returns the raw, unmapped detection-source code from MXAccess. + * + * @return the raw detection-source integer + */ public int rawDetectedBy() { return raw.getRawDetectedBy(); } + /** + * Returns the diagnostic text supplied by MXAccess, if any. + * + * @return the diagnostic message, possibly empty + */ public String diagnosticText() { return raw.getDiagnosticText(); } diff --git a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxValues.java b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxValues.java index 6d44b93..1c99571 100644 --- a/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxValues.java +++ b/clients/java/mxgateway-client/src/main/java/com/dohertylan/mxgateway/client/MxValues.java @@ -17,10 +17,24 @@ import mxaccess_gateway.v1.MxaccessGateway.RawArray; import mxaccess_gateway.v1.MxaccessGateway.StringArray; 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. + * + *

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 { private MxValues() { } + /** + * Builds a boolean {@link MxValue}. + * + * @param value the boolean payload + * @return a populated {@code MxValue} + */ public static MxValue boolValue(boolean value) { return MxValue.newBuilder() .setDataType(MxDataType.MX_DATA_TYPE_BOOLEAN) @@ -29,6 +43,12 @@ public final class MxValues { .build(); } + /** + * Builds a 32-bit integer {@link MxValue}. + * + * @param value the int32 payload + * @return a populated {@code MxValue} + */ public static MxValue int32Value(int value) { return MxValue.newBuilder() .setDataType(MxDataType.MX_DATA_TYPE_INTEGER) @@ -37,6 +57,12 @@ public final class MxValues { .build(); } + /** + * Builds a 64-bit integer {@link MxValue}. + * + * @param value the int64 payload + * @return a populated {@code MxValue} + */ public static MxValue int64Value(long value) { return MxValue.newBuilder() .setDataType(MxDataType.MX_DATA_TYPE_INTEGER) @@ -45,6 +71,12 @@ public final class MxValues { .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) { return MxValue.newBuilder() .setDataType(MxDataType.MX_DATA_TYPE_FLOAT) @@ -53,6 +85,12 @@ public final class MxValues { .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) { return MxValue.newBuilder() .setDataType(MxDataType.MX_DATA_TYPE_DOUBLE) @@ -61,6 +99,12 @@ public final class MxValues { .build(); } + /** + * Builds a string {@link MxValue}. + * + * @param value the string payload + * @return a populated {@code MxValue} + */ public static MxValue stringValue(String value) { return MxValue.newBuilder() .setDataType(MxDataType.MX_DATA_TYPE_STRING) @@ -69,6 +113,12 @@ public final class MxValues { .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) { return MxValue.newBuilder() .setDataType(MxDataType.MX_DATA_TYPE_TIME) @@ -80,6 +130,14 @@ public final class MxValues { .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) { if (value == null || value.getIsNull()) { 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) { if (array == 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 values) { return MxArray.newBuilder() .setElementDataType(MxDataType.MX_DATA_TYPE_STRING) @@ -126,6 +198,12 @@ public final class MxValues { .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 values) { return MxArray.newBuilder() .setElementDataType(MxDataType.MX_DATA_TYPE_INTEGER) @@ -135,6 +213,12 @@ public final class MxValues { .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) { return value == null ? "KIND_NOT_SET" : value.getKindCase().name(); } diff --git a/clients/python/src/mxgateway/auth.py b/clients/python/src/mxgateway/auth.py index 3c5d041..8d21f08 100644 --- a/clients/python/src/mxgateway/auth.py +++ b/clients/python/src/mxgateway/auth.py @@ -14,13 +14,16 @@ class ApiKey: value: str def __post_init__(self) -> None: + """Validate that the API key value is non-empty.""" if not self.value: raise ValueError("api_key must not be empty") def __repr__(self) -> str: + """Return a repr that redacts the secret value.""" return f"{type(self).__name__}({REDACTED!r})" def bearer_value(self) -> str: + """Return the value formatted as an HTTP `Bearer` token.""" return f"Bearer {self.value}" diff --git a/clients/python/src/mxgateway/client.py b/clients/python/src/mxgateway/client.py index 75baed8..c785d39 100644 --- a/clients/python/src/mxgateway/client.py +++ b/clients/python/src/mxgateway/client.py @@ -25,6 +25,7 @@ class GatewayClient: stub: Any, channel: grpc.aio.Channel | None = None, ) -> None: + """Initialize the client with resolved options and a gRPC stub.""" self.options = options self.raw_stub = stub self._channel = channel @@ -63,9 +64,11 @@ class GatewayClient: ) async def __aenter__(self) -> "GatewayClient": + """Return self to support ``async with`` usage.""" return self async def __aexit__(self, *_exc_info: object) -> None: + """Close the client when leaving an ``async with`` block.""" await self.close() async def close(self) -> None: @@ -99,6 +102,7 @@ class GatewayClient: return Session(client=self, session_id=reply.session_id, open_reply=reply) 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) ensure_protocol_success("open session", reply.protocol_status, reply) return reply @@ -107,11 +111,13 @@ class GatewayClient: self, request: pb.CloseSessionRequest, ) -> pb.CloseSessionReply: + """Send a `CloseSession` RPC and return the raw reply.""" reply = await self._unary("close session", self.raw_stub.CloseSession, request) ensure_protocol_success("close session", reply.protocol_status, reply) return reply 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) ensure_protocol_success("invoke", reply.protocol_status, reply) return reply diff --git a/clients/python/src/mxgateway/errors.py b/clients/python/src/mxgateway/errors.py index 9939d21..7f689af 100644 --- a/clients/python/src/mxgateway/errors.py +++ b/clients/python/src/mxgateway/errors.py @@ -19,6 +19,7 @@ class MxGatewayError(Exception): protocol_status: pb.ProtocolStatus | None = None, raw_reply: Any | None = None, ) -> None: + """Initialize with a message and the optional raw protocol context.""" super().__init__(message) self.protocol_status = protocol_status self.raw_reply = raw_reply diff --git a/clients/python/src/mxgateway/galaxy.py b/clients/python/src/mxgateway/galaxy.py index 5495279..8ad6617 100644 --- a/clients/python/src/mxgateway/galaxy.py +++ b/clients/python/src/mxgateway/galaxy.py @@ -34,6 +34,7 @@ class GalaxyRepositoryClient: stub: Any, channel: grpc.aio.Channel | None = None, ) -> None: + """Initialize the client with resolved options and a gRPC stub.""" self.options = options self.raw_stub = stub self._channel = channel @@ -72,9 +73,11 @@ class GalaxyRepositoryClient: ) async def __aenter__(self) -> "GalaxyRepositoryClient": + """Return self to support ``async with`` usage.""" return self async def __aexit__(self, *_exc_info: object) -> None: + """Close the client when leaving an ``async with`` block.""" await self.close() async def close(self) -> None: diff --git a/clients/python/src/mxgateway/options.py b/clients/python/src/mxgateway/options.py index 446330d..c0577d1 100644 --- a/clients/python/src/mxgateway/options.py +++ b/clients/python/src/mxgateway/options.py @@ -23,6 +23,7 @@ class ClientOptions: stream_timeout: float | None = None def __post_init__(self) -> None: + """Validate options; raise `ValueError` for invalid combinations.""" if not self.endpoint: raise ValueError("endpoint must not be empty") @@ -34,6 +35,7 @@ class ClientOptions: raise ValueError("stream_timeout must be greater than zero") def __repr__(self) -> str: + """Return a repr that redacts the API key value.""" api_key = REDACTED if self.api_key else None return ( f"{type(self).__name__}(endpoint={self.endpoint!r}, " diff --git a/clients/python/src/mxgateway/session.py b/clients/python/src/mxgateway/session.py index 659f209..75f647b 100644 --- a/clients/python/src/mxgateway/session.py +++ b/clients/python/src/mxgateway/session.py @@ -21,15 +21,18 @@ class Session: session_id: str, open_reply: pb.OpenSessionReply | None = None, ) -> None: + """Initialize a session bound to a client and gateway session id.""" self.client = client self.session_id = session_id self.open_reply = open_reply self._closed = False async def __aenter__(self) -> "Session": + """Return self to support ``async with`` usage.""" return self async def __aexit__(self, *_exc_info: object) -> None: + """Close the session when leaving an ``async with`` block.""" await self.close() 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: + """Invoke MXAccess `Register` and return the new `ServerHandle`.""" reply = await self.invoke( pb.MxCommand( kind=pb.MX_COMMAND_KIND_REGISTER, @@ -84,6 +88,7 @@ class Session: return reply.register.server_handle async def unregister(self, server_handle: int, *, correlation_id: str = "") -> None: + """Invoke MXAccess `Unregister` for a previously registered `ServerHandle`.""" await self.invoke( pb.MxCommand( kind=pb.MX_COMMAND_KIND_UNREGISTER, @@ -99,6 +104,7 @@ class Session: *, correlation_id: str = "", ) -> None: + """Invoke MXAccess `RemoveItem` for the given `ItemHandle`.""" await self.invoke( pb.MxCommand( kind=pb.MX_COMMAND_KIND_REMOVE_ITEM, @@ -117,6 +123,7 @@ class Session: *, correlation_id: str = "", ) -> int: + """Invoke MXAccess `AddItem` and return the new `ItemHandle`.""" reply = await self.invoke( pb.MxCommand( kind=pb.MX_COMMAND_KIND_ADD_ITEM, @@ -137,6 +144,7 @@ class Session: *, correlation_id: str = "", ) -> int: + """Invoke MXAccess `AddItem2` with item context and return the new `ItemHandle`.""" reply = await self.invoke( pb.MxCommand( kind=pb.MX_COMMAND_KIND_ADD_ITEM2, @@ -157,6 +165,7 @@ class Session: *, correlation_id: str = "", ) -> None: + """Invoke MXAccess `Advise` to subscribe an existing `ItemHandle` to events.""" await self.invoke( pb.MxCommand( kind=pb.MX_COMMAND_KIND_ADVISE, @@ -175,6 +184,7 @@ class Session: *, correlation_id: str = "", ) -> None: + """Invoke MXAccess `UnAdvise` to stop event delivery for an `ItemHandle`.""" await self.invoke( pb.MxCommand( kind=pb.MX_COMMAND_KIND_UN_ADVISE, @@ -193,6 +203,7 @@ class Session: *, correlation_id: str = "", ) -> list[pb.SubscribeResult]: + """Invoke MXAccess `AddItemBulk` and return one result per tag address.""" if tag_addresses is None: raise TypeError("tag_addresses is required") _ensure_bulk_size("tag_addresses", len(tag_addresses)) @@ -215,6 +226,7 @@ class Session: *, correlation_id: str = "", ) -> list[pb.SubscribeResult]: + """Invoke MXAccess `AdviseItemBulk` and return one result per item handle.""" if item_handles is None: raise TypeError("item_handles is required") _ensure_bulk_size("item_handles", len(item_handles)) @@ -237,6 +249,7 @@ class Session: *, correlation_id: str = "", ) -> list[pb.SubscribeResult]: + """Invoke MXAccess `RemoveItemBulk` and return one result per item handle.""" if item_handles is None: raise TypeError("item_handles is required") _ensure_bulk_size("item_handles", len(item_handles)) @@ -259,6 +272,7 @@ class Session: *, correlation_id: str = "", ) -> list[pb.SubscribeResult]: + """Invoke MXAccess `UnAdviseItemBulk` and return one result per item handle.""" if item_handles is None: raise TypeError("item_handles is required") _ensure_bulk_size("item_handles", len(item_handles)) @@ -281,6 +295,7 @@ class Session: *, correlation_id: str = "", ) -> list[pb.SubscribeResult]: + """Invoke MXAccess `SubscribeBulk` and return one result per tag address.""" if tag_addresses is None: raise TypeError("tag_addresses is required") _ensure_bulk_size("tag_addresses", len(tag_addresses)) @@ -303,6 +318,7 @@ class Session: *, correlation_id: str = "", ) -> list[pb.SubscribeResult]: + """Invoke MXAccess `UnsubscribeBulk` and return one result per item handle.""" if item_handles is None: raise TypeError("item_handles is required") _ensure_bulk_size("item_handles", len(item_handles)) @@ -327,6 +343,7 @@ class Session: user_id: int = 0, correlation_id: str = "", ) -> None: + """Invoke MXAccess `Write` for an `ItemHandle` with the converted value.""" await self.invoke( pb.MxCommand( kind=pb.MX_COMMAND_KIND_WRITE, @@ -350,6 +367,7 @@ class Session: user_id: int = 0, correlation_id: str = "", ) -> None: + """Invoke MXAccess `Write2` with both a value and a client-supplied timestamp.""" await self.invoke( pb.MxCommand( kind=pb.MX_COMMAND_KIND_WRITE2, @@ -369,6 +387,7 @@ class Session: *, after_worker_sequence: int = 0, ) -> AsyncIterator[pb.MxEvent]: + """Return an async iterator of `MxEvent` messages for this session.""" return self.client.stream_events_raw( pb.StreamEventsRequest( session_id=self.session_id, diff --git a/clients/python/src/mxgateway_cli/commands.py b/clients/python/src/mxgateway_cli/commands.py index 428e9d6..93f3ba8 100644 --- a/clients/python/src/mxgateway_cli/commands.py +++ b/clients/python/src/mxgateway_cli/commands.py @@ -42,6 +42,7 @@ def version(output_json: bool) -> None: 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("--api-key", default=None, help="Gateway API key.")(command) command = click.option( diff --git a/clients/rust/crates/mxgw-cli/src/main.rs b/clients/rust/crates/mxgw-cli/src/main.rs index c3de660..667e058 100644 --- a/clients/rust/crates/mxgw-cli/src/main.rs +++ b/clients/rust/crates/mxgw-cli/src/main.rs @@ -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::path::PathBuf; use std::process::ExitCode; diff --git a/clients/rust/src/auth.rs b/clients/rust/src/auth.rs index b63c51f..95d66a8 100644 --- a/clients/rust/src/auth.rs +++ b/clients/rust/src/auth.rs @@ -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 tonic::metadata::MetadataValue; @@ -5,14 +9,21 @@ use tonic::service::Interceptor; use tonic::{Request, Status}; /// 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)] pub struct ApiKey(String); impl ApiKey { + /// Construct an [`ApiKey`] from the raw `mxgw__` string + /// returned by the gateway's `apikey` admin command. pub fn new(value: impl Into) -> Self { Self(value.into()) } + /// Return the raw key value. Callers must not log or otherwise persist + /// the result. pub fn expose_secret(&self) -> &str { &self.0 } @@ -40,6 +51,9 @@ pub struct 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) -> Self { Self { api_key } } diff --git a/clients/rust/src/client.rs b/clients/rust/src/client.rs index 2bfdb85..c59a975 100644 --- a/clients/rust/src/client.rs +++ b/clients/rust/src/client.rs @@ -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 tonic::codegen::InterceptedService; @@ -14,11 +22,21 @@ use crate::generated::mxaccess_gateway::v1::{ use crate::options::ClientOptions; use crate::session::Session; +/// Generated gateway client wrapped in the auth interceptor that +/// [`GatewayClient`] uses internally. pub type RawGatewayClient = MxAccessGatewayClient>; + +/// 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 = std::pin::Pin> + 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)] pub struct GatewayClient { inner: RawGatewayClient, @@ -27,6 +45,13 @@ pub struct 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 { let mut endpoint = 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 { &mut self.inner } + /// Consume the wrapper and return the underlying generated client. pub fn into_inner(self) -> RawGatewayClient { 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) -> Session { 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( &self, request: OpenSessionRequest, @@ -83,12 +120,25 @@ impl GatewayClient { 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 { let reply = self.open_session_raw(request).await?; ensure_protocol_success("open session", reply.protocol_status.as_ref())?; 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( &self, request: CloseSessionRequest, @@ -98,16 +148,39 @@ impl GatewayClient { 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 { let mut client = self.inner.clone(); let response = client.invoke(self.unary_request(request)).await?; 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 { 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 { let mut client = self.inner.clone(); let response = client.stream_events(self.stream_request(request)).await?; diff --git a/clients/rust/src/error.rs b/clients/rust/src/error.rs index 29c4f32..f4c8c9a 100644 --- a/clients/rust/src/error.rs +++ b/clients/rust/src/error.rs @@ -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 tonic::Code; 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)] pub enum Error { + /// Endpoint URL could not be parsed or its TLS material could not be + /// loaded. #[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}")] - 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}")] Transport(#[from] tonic::transport::Error), + /// Server returned `Unauthenticated` — the API key was missing or + /// rejected. #[error("authentication failed: {message}")] Authentication { + /// Redacted server-supplied detail message. message: String, + /// Original `tonic::Status` for callers that need the full context. #[source] status: Box, }, + /// Server returned `PermissionDenied` — the API key is valid but lacks + /// the required scope. #[error("authorization failed: {message}")] Authorization { + /// Redacted server-supplied detail message. message: String, + /// Original `tonic::Status`. #[source] status: Box, }, + /// Server returned `DeadlineExceeded`. Usually the per-call deadline + /// configured via [`crate::options::ClientOptions::with_call_timeout`]. #[error("gateway call timed out: {message}")] Timeout { + /// Redacted server-supplied detail message. message: String, + /// Original `tonic::Status`. #[source] status: Box, }, + /// Server (or client) cancelled the call before a reply was produced. #[error("gateway call cancelled: {message}")] Cancelled { + /// Redacted server-supplied detail message. message: String, + /// Original `tonic::Status`. #[source] status: Box, }, + /// Any other `tonic::Status` that did not match a more specific variant. #[error("gateway status error: {0}")] Status(Box), + /// 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}")] Command(#[from] Box), + /// Protocol-level operation (open/close session) returned a non-OK + /// [`ProtocolStatus`] envelope. #[error("gateway {operation} failed: {code:?}: {message}")] ProtocolStatus { + /// Operation name, e.g. `"open session"`. operation: &'static str, + /// Decoded protocol status code from the server. code: ProtocolStatusCode, + /// Detail message from the server. 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)] pub struct CommandError { reply: MxCommandReply, } impl CommandError { + /// Wrap an already-failed command reply. pub fn new(reply: MxCommandReply) -> Self { Self { reply } } + /// Borrow the underlying reply (correlation id, status, payload). pub fn reply(&self) -> &MxCommandReply { &self.reply } + /// Consume the error and return the underlying reply. pub fn into_reply(self) -> MxCommandReply { self.reply } @@ -118,6 +179,13 @@ impl From 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 { let code = reply .protocol_status @@ -132,6 +200,13 @@ pub fn ensure_command_success(reply: MxCommandReply) -> Result, diff --git a/clients/rust/src/generated.rs b/clients/rust/src/generated.rs index fbd9dbc..845a5d3 100644 --- a/clients/rust/src/generated.rs +++ b/clients/rust/src/generated.rs @@ -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 { + /// `mxaccess_gateway.v1` package — the v1 wire contract surfaced by the + /// gateway to language clients. pub mod v1 { #![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 { + /// `mxaccess_worker.v1` package — frame and envelope types used inside + /// the named-pipe transport between gateway and worker. pub mod v1 { #![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 { + /// `galaxy_repository.v1` package — types for the Galaxy hierarchy + /// discovery and deploy-event watch RPCs. pub mod v1 { #![allow(clippy::large_enum_variant)] diff --git a/clients/rust/src/lib.rs b/clients/rust/src/lib.rs index ac95672..9d3c9b7 100644 --- a/clients/rust/src/lib.rs +++ b/clients/rust/src/lib.rs @@ -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 -//! protobuf contracts and exposes a small handwritten surface for future client -//! implementation work. +//! protobuf contracts and exposes a small handwritten surface — connection +//! 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 client; @@ -14,11 +21,19 @@ pub mod session; pub mod value; pub mod version; +#[doc(inline)] pub use auth::{ApiKey, AuthInterceptor}; +#[doc(inline)] pub use client::{EventStream, GatewayClient}; +#[doc(inline)] pub use error::{CommandError, Error}; +#[doc(inline)] pub use galaxy::{DeployEventStream, GalaxyClient}; +#[doc(inline)] pub use options::ClientOptions; +#[doc(inline)] pub use session::Session; +#[doc(inline)] pub use value::{MxArrayProjection, MxArrayValue, MxStatus, MxValue, MxValueProjection}; +#[doc(inline)] pub use version::{CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION}; diff --git a/clients/rust/src/options.rs b/clients/rust/src/options.rs index 4246335..9c3517c 100644 --- a/clients/rust/src/options.rs +++ b/clients/rust/src/options.rs @@ -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::path::PathBuf; use std::time::Duration; 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)] pub struct ClientOptions { endpoint: String, @@ -17,6 +27,8 @@ pub struct 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) -> Self { Self { 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 { self.api_key = Some(api_key); 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 { self.plaintext = plaintext; self } + /// Trust roots PEM bundle for TLS connections. Ignored when + /// `plaintext` is `true`. pub fn with_ca_file(mut self, ca_file: impl Into) -> Self { self.ca_file = Some(ca_file.into()); 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) -> Self { self.server_name_override = Some(server_name_override.into()); self } + /// Maximum time the transport waits for the initial TCP/TLS connection. pub fn with_connect_timeout(mut self, connect_timeout: Duration) -> Self { self.connect_timeout = connect_timeout; 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 { self.call_timeout = call_timeout; 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 { self.stream_timeout = Some(stream_timeout); self } + /// Configured endpoint URL. pub fn endpoint(&self) -> &str { &self.endpoint } + /// Configured API key, if any. pub fn api_key(&self) -> Option<&ApiKey> { self.api_key.as_ref() } + /// Whether the transport runs in plaintext (h2c) mode. pub fn plaintext(&self) -> bool { self.plaintext } + /// Optional CA bundle path used to validate the server certificate. pub fn ca_file(&self) -> Option<&PathBuf> { self.ca_file.as_ref() } + /// Optional SNI / server-name override for TLS handshakes. pub fn server_name_override(&self) -> Option<&str> { self.server_name_override.as_deref() } + /// Connect timeout used during transport setup. pub fn connect_timeout(&self) -> Duration { self.connect_timeout } + /// Per-call timeout for unary RPCs. pub fn call_timeout(&self) -> Duration { self.call_timeout } + /// Optional per-call timeout for streaming RPCs. pub fn stream_timeout(&self) -> Option { self.stream_timeout } diff --git a/clients/rust/src/session.rs b/clients/rust/src/session.rs index 93dd5f4..cc8a78f 100644 --- a/clients/rust/src/session.rs +++ b/clients/rust/src/session.rs @@ -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::error::{ensure_protocol_success, Error}; use crate::generated::mxaccess_gateway::v1::mx_command::Payload; @@ -13,7 +23,13 @@ use crate::value::MxValue; 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)] pub struct Session { id: String, @@ -28,10 +44,18 @@ impl Session { } } + /// Borrow the gateway-assigned session id. pub fn id(&self) -> &str { &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 { client .open_session(OpenSessionRequest { @@ -41,6 +65,12 @@ impl Session { .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> { let reply = self .client @@ -53,6 +83,12 @@ impl Session { 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 { let reply = self .invoke( @@ -66,6 +102,13 @@ impl Session { 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 { let reply = self .invoke( @@ -80,6 +123,12 @@ impl Session { 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( &self, server_handle: i32, @@ -100,6 +149,12 @@ impl Session { 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> { self.invoke( MxCommandKind::RemoveItem, @@ -112,6 +167,12 @@ impl Session { 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> { self.invoke( MxCommandKind::Advise, @@ -124,6 +185,12 @@ impl Session { 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> { self.invoke( MxCommandKind::UnAdvise, @@ -136,6 +203,13 @@ impl Session { 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( &self, server_handle: i32, @@ -155,6 +229,11 @@ impl Session { 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( &self, server_handle: i32, @@ -174,6 +253,11 @@ impl Session { 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( &self, server_handle: i32, @@ -193,6 +277,11 @@ impl Session { 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( &self, server_handle: i32, @@ -212,6 +301,11 @@ impl Session { 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( &self, server_handle: i32, @@ -231,6 +325,12 @@ impl Session { 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( &self, server_handle: i32, @@ -250,6 +350,12 @@ impl Session { 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( &self, server_handle: i32, @@ -270,6 +376,11 @@ impl Session { Ok(()) } + /// Run MXAccess `Write2` (single-value with caller-supplied timestamp). + /// + /// # Errors + /// + /// Same conditions as [`Session::write`]. pub async fn write2( &self, server_handle: i32, @@ -292,10 +403,23 @@ impl Session { 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 { 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 { self.client .stream_events(StreamEventsRequest { @@ -305,6 +429,13 @@ impl Session { .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( &self, kind: MxCommandKind, @@ -315,6 +446,13 @@ impl Session { .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( &self, kind: MxCommandKind, diff --git a/clients/rust/src/value.rs b/clients/rust/src/value.rs index 6547b98..4da694a 100644 --- a/clients/rust/src/value.rs +++ b/clients/rust/src/value.rs @@ -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_value::Kind; use crate::generated::mxaccess_gateway::v1::{ @@ -6,6 +16,12 @@ use crate::generated::mxaccess_gateway::v1::{ 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)] pub struct MxValue { raw: ProtoMxValue, @@ -13,11 +29,14 @@ pub struct MxValue { } impl MxValue { + /// Wrap a protobuf [`ProtoMxValue`] and compute its + /// [`MxValueProjection`]. pub fn from_proto(raw: ProtoMxValue) -> Self { let projection = MxValueProjection::from_proto(&raw); Self { raw, projection } } + /// Build a boolean `MxValue` (`MxDataType::Boolean`, `VT_BOOL`). pub fn bool(value: bool) -> Self { Self::from_proto(ProtoMxValue { 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 { Self::from_proto(ProtoMxValue { 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 { Self::from_proto(ProtoMxValue { 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 { Self::from_proto(ProtoMxValue { 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 { Self::from_proto(ProtoMxValue { 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) -> Self { Self::from_proto(ProtoMxValue { 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 { &self.raw } + /// Borrow the typed projection. pub fn projection(&self) -> &MxValueProjection { &self.projection } + /// Consume the wrapper and return the underlying protobuf message. pub fn into_proto(self) -> ProtoMxValue { self.raw } @@ -97,18 +125,35 @@ impl From 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)] pub enum MxValueProjection { + /// No `kind` was present on the wire. Unset, + /// `is_null = true` on the wire. Null, + /// Boolean value. Bool(bool), + /// 32-bit signed integer value. Int32(i32), + /// 64-bit signed integer value. Int64(i64), + /// 32-bit float value. Float(f32), + /// 64-bit float value. Double(f64), + /// UTF-8 string value. String(String), + /// Wall-clock timestamp. Timestamp(prost_types::Timestamp), + /// Array value carrying a homogeneous element type. Array(MxArrayValue), + /// Opaque variant payload that the gateway could not project to a typed + /// scalar. Raw(Vec), } @@ -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)] pub struct MxArrayValue { raw: MxArray, @@ -140,11 +187,14 @@ pub struct MxArrayValue { } impl MxArrayValue { + /// Wrap a protobuf [`MxArray`] and compute its + /// [`MxArrayProjection`]. pub fn from_proto(raw: MxArray) -> Self { let projection = MxArrayProjection::from_proto(&raw); Self { raw, projection } } + /// Build a one-dimensional string array (`VT_ARRAY|VT_BSTR`). pub fn string(values: Vec) -> Self { Self::from_proto(MxArray { element_data_type: MxDataType::String as i32, @@ -155,25 +205,40 @@ impl MxArrayValue { }) } + /// Borrow the underlying protobuf array message. pub fn raw(&self) -> &MxArray { &self.raw } + /// Borrow the typed projection of the array's elements. pub fn projection(&self) -> &MxArrayProjection { &self.projection } } +/// Typed view over an [`MxArrayValue`]. +/// +/// Each variant matches the corresponding `MxArray::Values` oneof arm on +/// the wire. #[derive(Clone, Debug, PartialEq)] pub enum MxArrayProjection { + /// No `values` oneof was present on the wire. Unset, + /// Boolean elements. Bool(Vec), + /// 32-bit signed integer elements. Int32(Vec), + /// 64-bit signed integer elements. Int64(Vec), + /// 32-bit float elements. Float(Vec), + /// 64-bit float elements. Double(Vec), + /// UTF-8 string elements. String(Vec), + /// Timestamp elements. Timestamp(Vec), + /// Opaque variant payloads, one per element. Raw(Vec>), } @@ -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)] pub struct MxStatus { raw: MxStatusProxy, } impl MxStatus { + /// Wrap a protobuf [`MxStatusProxy`]. pub fn from_proto(raw: MxStatusProxy) -> Self { Self { raw } } + /// Borrow the underlying protobuf message. pub fn raw(&self) -> &MxStatusProxy { &self.raw } + /// `MXSTATUS_PROXY.Success` flag (0 = error, non-zero = good/warning). pub fn success(&self) -> i32 { self.raw.success } + /// Decode the status category, or `None` if the wire value is not a + /// known [`MxStatusCategory`]. pub fn category(&self) -> Option { 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::try_from(self.raw.detected_by).ok() } + /// `MXSTATUS_PROXY.Detail` numeric reason code. pub fn detail(&self) -> i32 { self.raw.detail } + /// Raw, undecoded category integer (useful for forward compatibility). pub fn raw_category(&self) -> i32 { self.raw.raw_category } + /// Raw, undecoded detected-by integer. pub fn raw_detected_by(&self) -> i32 { self.raw.raw_detected_by } + /// Optional human-readable diagnostic from MXAccess. pub fn diagnostic_text(&self) -> &str { &self.raw.diagnostic_text } diff --git a/clients/rust/src/version.rs b/clients/rust/src/version.rs index aee4d39..c49ebb9 100644 --- a/clients/rust/src/version.rs +++ b/clients/rust/src/version.rs @@ -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"; + +/// Public gateway gRPC protocol version this client targets. 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;