# Rust Client Detailed Design ## Purpose Provide an async Rust client crate for MXAccess Gateway, plus a test CLI and unit tests. The Rust client should use `tonic` and `tokio`. Follow the [Rust Style Guide](../../docs/style-guides/RustStyleGuide.md) for handwritten code and the [Protobuf Style Guide](../../docs/style-guides/ProtobufStyleGuide.md) for generated contract inputs. ## Crate Layout Actual layout — the `mxgateway-client` library crate is the workspace root, with the `mxgw` test CLI as a workspace member: ```text clients/rust/ # `mxgateway-client` library crate (workspace root) Cargo.toml build.rs src/ lib.rs client.rs session.rs galaxy.rs options.rs auth.rs value.rs version.rs error.rs generated.rs crates/ mxgw-cli/ # `mxgw` test CLI (workspace member) Cargo.toml src/main.rs tests/ client_behavior.rs proto_fixtures.rs ``` Dependencies: - `tonic` - `prost` - `prost-types` - `tokio` - `tokio-stream` - `thiserror` - `clap` - `serde` - `serde_json` ## Library API Suggested API: ```rust pub struct GatewayClient { /* tonic channel + generated client */ } pub struct ClientOptions { pub endpoint: String, pub api_key: String, pub plaintext: bool, pub ca_file: Option, pub server_name_override: Option, pub connect_timeout: Duration, pub call_timeout: Duration, } impl GatewayClient { pub async fn connect(options: ClientOptions) -> Result; pub async fn open_session(&self, options: OpenSessionOptions) -> Result; pub async fn invoke(&self, request: MxCommandRequest) -> Result; } ``` Session: ```rust pub struct Session { pub id: String, } impl Session { pub async fn register(&self, client_name: &str) -> Result; pub async fn add_item(&self, server_handle: i32, item: &str) -> Result; pub async fn add_item2(&self, server_handle: i32, item: &str, context: &str) -> Result; pub async fn advise(&self, server_handle: i32, item_handle: i32) -> Result<(), Error>; pub async fn add_item_bulk(&self, server_handle: i32, tag_addresses: Vec) -> Result, Error>; pub async fn advise_item_bulk(&self, server_handle: i32, item_handles: Vec) -> Result, Error>; pub async fn remove_item_bulk(&self, server_handle: i32, item_handles: Vec) -> Result, Error>; pub async fn un_advise_item_bulk(&self, server_handle: i32, item_handles: Vec) -> Result, Error>; pub async fn subscribe_bulk(&self, server_handle: i32, tag_addresses: Vec) -> Result, Error>; pub async fn unsubscribe_bulk(&self, server_handle: i32, item_handles: Vec) -> Result, Error>; pub async fn write(&self, server_handle: i32, item_handle: i32, value: MxValue, user_id: i32) -> Result<(), Error>; pub async fn write_bulk(&self, server_handle: i32, entries: Vec) -> Result, Error>; pub async fn write2_bulk(&self, server_handle: i32, entries: Vec) -> Result, Error>; pub async fn write_secured_bulk(&self, server_handle: i32, entries: Vec) -> Result, Error>; pub async fn write_secured2_bulk(&self, server_handle: i32, entries: Vec) -> Result, Error>; pub async fn read_bulk>(&self, server_handle: i32, tag_addresses: &[S], timeout_ms: u32) -> Result, Error>; pub async fn events(&self) -> Result>, Error>; pub async fn close(&self) -> Result<(), Error>; } ``` The four bulk-write helpers (`write_bulk`, `write2_bulk`, `write_secured_bulk`, `write_secured2_bulk`) and `read_bulk` mirror the worker's bulk command shapes in `mxaccess_gateway.proto` and use the same correlation-id discipline as the unary helpers — `next_correlation_id` is part of the public SDK surface, re-exported at the crate root (`mxgateway_client::next_correlation_id`), so that consumers constructing raw `MxCommandRequest`/`CloseSessionRequest` payloads outside the `Session` helpers (notably the `mxgw` test CLI's `ping` and `close-session` subcommands) share the same id generation. The returned id is documented as an opaque token with three guaranteed properties (embeds the caller's label, unique within a process, carries no secret); its textual format is intentionally *not* part of the contract. The per-entry fields that the matching MXAccess COM calls accept once per batch — `user_id` (`WriteBulkEntry`/`Write2BulkEntry`), `timestamp_value` (`Write2BulkEntry`/`WriteSecured2BulkEntry`), and `current_user_id` / `verifier_user_id` (`WriteSecuredBulkEntry`/`WriteSecured2BulkEntry`) — live on the entry structs themselves rather than as trailing positional arguments on the helper, matching the protobuf shapes in `mxaccess_gateway.proto` (`WriteBulkCommand` / `Write2BulkCommand` / `WriteSecuredBulkCommand` / `WriteSecured2BulkCommand`). `read_bulk` is generic over `AsRef` so callers can pass `&[String]` or `&[&str]` without cloning at the call site. ## Authentication Use a `tonic` interceptor or request extension layer to add: ```text authorization: Bearer ``` Use `SecretString` or equivalent if a dependency is acceptable. Always redact API keys in `Debug` output. ## TLS Support: - plaintext channel for local development, - native or rustls TLS depending on project preference, - custom CA file, - domain override. ## Streaming Expose event streams as a `Stream>`. Dropping the stream should cancel the underlying gRPC stream. Do not buffer unboundedly in the client. If a helper channel is used, make it bounded. ## Error Handling Use `thiserror`: ```rust pub enum Error { InvalidEndpoint { endpoint: String, detail: String }, InvalidArgument { name: String, detail: String }, Transport(tonic::transport::Error), Authentication { message: String, status: Box }, Authorization { message: String, status: Box }, Timeout { message: String, status: Box }, Cancelled { message: String, status: Box }, Unavailable { message: String, status: Box }, Status(Box), Command(Box), ProtocolStatus { operation: &'static str, code: ProtocolStatusCode, message: String }, MalformedReply { detail: String }, } ``` `Unavailable` classifies the transient `Code::Unavailable` / `Code::ResourceExhausted` statuses so callers can decide whether to retry without unwrapping the raw status. `MalformedReply` surfaces OK replies whose payload does not carry the data the command contract requires (for example, an `AddItem` reply missing the item handle, or a `WriteBulk` reply carrying the wrong payload arm). `InvalidEndpoint` is returned when the endpoint URL fails to parse or its TLS material cannot be loaded. Preserve raw command replies in `CommandError` where applicable. ## Test CLI Binary: `mxgw`. Use `clap` derive. Commands (see `clients/rust/README.md` for full argument lists): ```text mxgw version mxgw ping mxgw open-session mxgw close-session --session-id mxgw register --session-id --client-name mxgw add-item --session-id --server-handle --item mxgw advise --session-id --server-handle --item-handle mxgw subscribe-bulk --session-id --server-handle --items mxgw unsubscribe-bulk --session-id --server-handle --item-handles <1,2,3> mxgw read-bulk --session-id --server-handle --items --timeout-ms 1500 mxgw write --session-id --server-handle 1 --item-handle 1 --value-type int32 --value 123 mxgw write2 --session-id --server-handle 1 --item-handle 1 --value-type int32 --value 123 --timestamp mxgw write-bulk --session-id --server-handle --item-handles <1,2> --value-type int32 --values <1,2> mxgw write2-bulk --session-id --server-handle --item-handles <1,2> --value-type int32 --values <1,2> --timestamp mxgw write-secured-bulk --session-id --server-handle --item-handles <1,2> --value-type int32 --values <1,2> mxgw write-secured2-bulk --session-id --server-handle --item-handles <1,2> --value-type int32 --values <1,2> --timestamp mxgw stream-events --session-id --json mxgw bench-read-bulk --duration-seconds 30 --bulk-size 6 --json mxgw smoke --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt mxgw galaxy test-connection mxgw galaxy last-deploy-time mxgw galaxy discover-hierarchy mxgw galaxy watch [--last-seen-deploy-time ] [--max-events N] ``` JSON output should use `serde_json`. ## Unit Tests Use a fake `tonic` server started on a local ephemeral port, or abstract the generated client behind a trait for unit tests. Required tests: - generated client compiles from proto, - auth metadata injection, - TLS/plaintext endpoint construction, - value conversion, - command request construction, - error mapping from `tonic::Status`, - event stream order, - stream cancellation, - CLI parsing, - JSON redaction. ## Integration Tests Skip unless: ```text MXGATEWAY_INTEGRATION=1 ``` Use `tokio::test`. Run bounded smoke flow and ensure `CloseSession` is attempted with `drop` fallback docs, but do not rely on `Drop` for async close. ## Related Documentation - [Client Libraries Detailed Design](../../docs/ClientLibrariesDesign.md) - [Client Proto Generation](../../docs/ClientProtoGeneration.md) - [Client Packaging](../../docs/ClientPackaging.md) - [Rust Style Guide](../../docs/style-guides/RustStyleGuide.md)