# 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 The workspace is rooted at `clients/rust/`. The top-level crate `zb-mom-ww-mxgateway-client` is declared by `clients/rust/Cargo.toml` itself (flat layout — its `src/` sits directly under the workspace root, not nested inside `crates/`). The only `[workspace.members]` entry is the `mxgw-cli` binary subcrate under `crates/mxgw-cli/`. ```text clients/rust/ Cargo.toml # workspace root + top-level crate `zb-mom-ww-mxgateway-client` Cargo.lock build.rs # tonic-build proto generation README.md RustClientDesign.md src/ lib.rs client.rs session.rs galaxy.rs options.rs auth.rs error.rs value.rs version.rs generated.rs # `pub mod` wrappers around files under `src/generated/` generated/ # tonic-build output (not hand-edited) tests/ client_behavior.rs proto_fixtures.rs crates/ mxgw-cli/ # sole workspace member — binary `mxgw` Cargo.toml src/main.rs ``` Expected dependencies: - `tonic` - `prost` - `prost-types` - `tokio` - `tokio-stream` - `thiserror` - `clap` - `serde` - `serde_json` - `tracing` ## 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 events(&self) -> Result>, Error>; pub async fn close(&self) -> Result<(), Error>; } ``` ## 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 { Transport(tonic::transport::Error), Status(tonic::Status), Authentication(String), Authorization(String), Session(SessionError), Worker(WorkerError), Command(CommandError), MxAccess(MxAccessError), Timeout, Cancelled, } ``` Preserve raw command replies in `CommandError` where applicable. ## Test CLI Binary: `mxgw`. Use `clap` derive. Commands: ```text mxgw version mxgw smoke --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt mxgw stream-events --session-id --json mxgw write --session-id --server-handle 1 --item-handle 1 --type int32 --value 123 ``` 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)