Files
mxaccessgw/clients/rust/RustClientDesign.md
T
Joseph Doherty 397d3c5c4f rename: apply ZB.MOM.WW prefix to all client SDKs + fix pre-existing alarm-RPC breaks
Rename across every client surface using each language's idiomatic convention:

  * .NET   clients/dotnet/MxGateway.Client[.Cli|.Tests]/
             -> clients/dotnet/ZB.MOM.WW.MxGateway.Client[.Cli|.Tests]/
             namespaces -> ZB.MOM.WW.MxGateway.Client[.Cli|.Tests]
             contracts ProjectReference repointed to ZB.MOM.WW.MxGateway.Contracts
             sln migrated to slnx (dotnet sln migrate)
  * Python src/mxgateway -> src/zb_mom_ww_mxgateway
             src/mxgateway_cli -> src/zb_mom_ww_mxgateway_cli
             distribution: mxaccess-gateway-client -> zb-mom-ww-mxaccess-gateway-client
  * Rust   crate: mxgateway-client -> zb-mom-ww-mxgateway-client
             build.rs proto path repointed
  * Java   subprojects: mxgateway-{client,cli} -> zb-mom-ww-mxgateway-{client,cli}
             packages com.dohertylan.mxgateway -> com.zb.mom.ww.mxgateway
             group   com.dohertylan.mxgateway -> com.zb.mom.ww.mxgateway
             rootProject mxaccessgw-java -> zb-mom-ww-mxaccessgw-java
  * Go     generate-proto.ps1 proto path repointed; module path and
             package mxgateway kept (Go convention).
  * proto-inputs.json: generatedOutputs.python updated to new package path.
  * scripts/run-client-e2e-tests.ps1: Java CLI install path + gradle task
             updated to zb-mom-ww-mxgateway-cli.

CLI binary names (mxgw, mxgw-py, mxgw-go, mxgateway-cli) and wire-level
identifiers (MXGATEWAY_* env vars, the mxgw_<id>_<secret> API key
prefix, protobuf package names like mxaccess_gateway.v1, all MXAccess
references) intentionally NOT renamed.

Fix pre-existing alarms-over-gateway breaks unblocked by the rename:

  * mxaccess_gateway.proto: add missing public message QueryActiveAlarmsRequest
    {session_id, client_correlation_id, alarm_filter_prefix} and missing
    rpc QueryActiveAlarms(QueryActiveAlarmsRequest) returns
    (stream ActiveAlarmSnapshot). All four typed clients referenced
    these but they were absent from the proto.
  * MxAccessGatewayService.QueryActiveAlarms: implement the new RPC on
    the server, streaming from IGatewayAlarmService.CurrentAlarms with
    optional alarm_filter_prefix filter.
  * clients/dotnet/.../DiscoverHierarchyOptions.cs: add the hand-written
    .NET POCO that wraps DiscoverHierarchyRequest (referenced by
    GalaxyRepositoryClient.DiscoverHierarchyAsync but never authored).
  * Drop retired session_id field references from
    AcknowledgeAlarmRequest/AcknowledgeAlarmReply test fixtures across
    .NET, Rust, Go, and Python clients.
  * Rust integration test: add the missing stream_alarms impl on the
    fake MxAccessGateway server (the trait gained the method, fake
    didn't).
  * Rust CLI test: bump expected gatewayProtocolVersion 2 -> 3.

Regenerated artifacts updated in this commit:
  * src/ZB.MOM.WW.MxGateway.Contracts/Generated/{MxaccessGateway,MxaccessGatewayGrpc}.cs
  * clients/python/src/zb_mom_ww_mxgateway/generated/*_pb2{,_grpc}.py
  * clients/go/internal/generated/*.pb.go
(C# regenerated by Grpc.Tools on contracts build; Python and Go via
their generate-proto.ps1 scripts; Rust regenerates from .proto via
tonic-build at compile time so no checked-in artefact.)

Verification: 472 server tests, 275 worker tests (9 dev-rig skipped),
18 integration tests (live MxAccess + LDAP + Galaxy), 57 .NET client
tests, 32 Rust workspace tests, 39 Python tests, all Go packages, and
gradle build for Java all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:09:34 -04:00

5.2 KiB

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 for handwritten code and the Protobuf Style Guide for generated contract inputs.

Crate Layout

Recommended layout:

clients/rust/
  Cargo.toml
  build.rs
  crates/
    zb-mom-ww-mxgateway-client/
      src/lib.rs
      src/client.rs
      src/session.rs
      src/options.rs
      src/auth.rs
      src/value.rs
      src/error.rs
      src/generated/
    mxgw-cli/
      src/main.rs
  tests/

Expected dependencies:

  • tonic
  • prost
  • prost-types
  • tokio
  • tokio-stream
  • thiserror
  • clap
  • serde
  • serde_json
  • tracing

Library API

Suggested API:

pub struct GatewayClient { /* tonic channel + generated client */ }

pub struct ClientOptions {
    pub endpoint: String,
    pub api_key: String,
    pub plaintext: bool,
    pub ca_file: Option<PathBuf>,
    pub server_name_override: Option<String>,
    pub connect_timeout: Duration,
    pub call_timeout: Duration,
}

impl GatewayClient {
    pub async fn connect(options: ClientOptions) -> Result<Self, Error>;
    pub async fn open_session(&self, options: OpenSessionOptions) -> Result<Session, Error>;
    pub async fn invoke(&self, request: MxCommandRequest) -> Result<MxCommandReply, Error>;
}

Session:

pub struct Session {
    pub id: String,
}

impl Session {
    pub async fn register(&self, client_name: &str) -> Result<i32, Error>;
    pub async fn add_item(&self, server_handle: i32, item: &str) -> Result<i32, Error>;
    pub async fn add_item2(&self, server_handle: i32, item: &str, context: &str) -> Result<i32, Error>;
    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<String>) -> Result<Vec<SubscribeResult>, Error>;
    pub async fn advise_item_bulk(&self, server_handle: i32, item_handles: Vec<i32>) -> Result<Vec<SubscribeResult>, Error>;
    pub async fn remove_item_bulk(&self, server_handle: i32, item_handles: Vec<i32>) -> Result<Vec<SubscribeResult>, Error>;
    pub async fn un_advise_item_bulk(&self, server_handle: i32, item_handles: Vec<i32>) -> Result<Vec<SubscribeResult>, Error>;
    pub async fn subscribe_bulk(&self, server_handle: i32, tag_addresses: Vec<String>) -> Result<Vec<SubscribeResult>, Error>;
    pub async fn unsubscribe_bulk(&self, server_handle: i32, item_handles: Vec<i32>) -> Result<Vec<SubscribeResult>, 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<impl Stream<Item = Result<MxEvent, Error>>, Error>;
    pub async fn close(&self) -> Result<(), Error>;
}

Authentication

Use a tonic interceptor or request extension layer to add:

authorization: Bearer <api key>

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<Item = Result<MxEvent, Error>>. 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:

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:

mxgw version
mxgw smoke --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt
mxgw stream-events --session-id <id> --json
mxgw write --session-id <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:

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.