# Rust Client Workspace

The Rust client workspace contains the MXAccess Gateway client library, a
test CLI, and tests for generated contract wiring plus wrapper behavior. The
library uses
the shared protobuf inputs documented in
`../../docs/ClientProtoGeneration.md` so the Rust bindings compile against
the same public gateway and worker contracts as the server.

## Layout

```text
clients/rust/
  Cargo.toml
  build.rs
  src/
  tests/
  crates/mxgw-cli/
```

`build.rs` reads the `.proto` files from
`../../src/ZB.MOM.WW.MxGateway.Contracts/Protos` and generates `tonic`/`prost` bindings
into Cargo build output. `src/generated.rs` declares the Rust modules that
include those generated files. `src/generated` remains reserved for checked-in
generator output if the crate later changes to source-tree generation.

## Build And Test

Run the Rust workspace checks from `clients/rust`:

```powershell
cargo fmt --all --check
cargo test --workspace
cargo check --workspace
cargo clippy --workspace --all-targets -- -D warnings
```

The build script uses `protoc` from `PATH` or the Windows path recorded in
`../../docs/ToolchainLinks.md`.

## Packaging

Create local release artifacts from `clients/rust`:

```powershell
cargo build --workspace --release
cargo install --path crates/mxgw-cli --locked --force
```

`cargo check --workspace` regenerates the `tonic` and `prost` modules into
Cargo build output through `build.rs`.

## CLI

The CLI exposes version, session, command, event stream, write, and smoke
commands over the same client wrapper used by tests:

```powershell
cargo run -p mxgw-cli -- version --json
cargo run -p mxgw-cli -- open-session --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
cargo run -p mxgw-cli -- register --session-id <session-id> --client-name mxgw-rust-cli --json
cargo run -p mxgw-cli -- add-item --session-id <session-id> --server-handle 1 --item TestChildObject.TestInt --json
cargo run -p mxgw-cli -- advise --session-id <session-id> --server-handle 1 --item-handle 1 --json
cargo run -p mxgw-cli -- stream-events --session-id <session-id> --max-events 1 --json
cargo run -p mxgw-cli -- stream-alarms --session-id <session-id> --max-messages 1 --json
cargo run -p mxgw-cli -- acknowledge-alarm --session-id <session-id> --alarm-reference "\\Galaxy\Area001.Pump001.PumpFault" --json
cargo run -p mxgw-cli -- write --session-id <session-id> --server-handle 1 --item-handle 1 --value-type int32 --value 123 --json
```

Use `--tls`, `--ca-file`, and `--server-name-override` for TLS endpoints. The
CLI reads the API key from `--api-key` or from `--api-key-env`, which defaults
to `MXGATEWAY_API_KEY`. API keys are redacted by the library option and secret
types.

```powershell
cargo run -p mxgw-cli -- smoke --endpoint https://mxgateway.example.local:5001 --tls --ca-file C:\certs\mxgateway-ca.pem --server-name-override mxgateway.example.local --api-key-env MXGATEWAY_API_KEY --item TestChildObject.TestInt --json
```

### TLS trust (pin-only)

The gateway can auto-generate its own self-signed certificate (it has no PKI).
Unlike the other clients, the Rust client is **not** lenient: tonic 0.13.1
exposes no public hook to inject a custom certificate verifier, so TLS over Rust
cannot accept an *arbitrary* self-signed certificate. A TLS connection requires
one of two trust paths:

- `--ca-file` / `ClientOptions::with_ca_file(...)` to pin a CA (export the
  gateway's self-signed certificate and pin it). This is the path for a
  self-signed gateway.
- `--require-certificate-validation` / `with_require_certificate_validation(true)`
  to verify against the operating system's trust roots (`tls-native-roots`). This
  only succeeds for a certificate that chains to a root the host already trusts —
  i.e. a gateway fronted by a publicly- or enterprise-CA-issued certificate, not a
  bare self-signed one.

TLS with neither set fails `connect` with a clear, actionable error rather
than accepting the certificate. See
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).

## Library Surface

`ClientOptions` configures endpoint, API key, plaintext or TLS transport,
timeouts, custom CA files, and server name override. `GatewayClient::connect`
creates an authenticated `tonic` client and attaches `authorization: Bearer
<api-key>` metadata to unary and streaming calls.

`GatewayClient` exposes raw generated calls through `open_session_raw`,
`close_session_raw`, `invoke_raw`, `stream_events`, `query_active_alarms`,
`stream_alarms`, `acknowledge_alarm`, and `raw_client`. `stream_alarms`
returns an `AlarmFeedStream` async stream of alarm-feed messages and
shares the gateway's central alarm monitor with every other client. The
session helpers keep MXAccess handles visible:

```rust
let session = client.open_session(request).await?;
let server_handle = session.register("mxgw-rust").await?;
let item_handle = session.add_item(server_handle, "TestChildObject.TestInt").await?;
session.advise(server_handle, item_handle).await?;
let mut events = session.events().await?;
session.close().await?;
```

`MxValue`, `MxArrayValue`, and `MxStatus` wrap generated protobuf messages while
preserving the raw message for parity diagnostics. Command replies whose
protocol status is not `PROTOCOL_STATUS_CODE_OK` become `Error::Command` and
retain the raw `MxCommandReply`.

## Galaxy Repository browse

The Galaxy Repository service exposes a read-only browse over the AVEVA System
Platform Galaxy Repository (ZB SQL database). It uses the same API-key auth as
the gateway service but requires the `metadata:read` scope on the server.

[`GalaxyClient`](src/galaxy.rs) wraps the generated Galaxy bindings the same
way [`GatewayClient`](src/client.rs) wraps the gateway bindings:

```rust
let mut galaxy = GalaxyClient::connect(
    ClientOptions::new("http://localhost:5000")
        .with_api_key(ApiKey::new(api_key)),
).await?;

let ok = galaxy.test_connection().await?;
let last_deploy = galaxy.get_last_deploy_time().await?; // Option<prost_types::Timestamp>
let objects = galaxy.discover_hierarchy().await?;       // Vec<GalaxyObject>
```

`get_last_deploy_time` returns `None` when the server reports
`present = false`. `discover_hierarchy` returns the generated
`GalaxyObject` proto type (re-exported via
`zb_mom_ww_mxgateway_client::generated::galaxy_repository::v1`) with all attributes
attached.

The CLI ships matching subcommands under `galaxy`:

```powershell
cargo run -p mxgw-cli -- galaxy test-connection --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
cargo run -p mxgw-cli -- galaxy last-deploy-time --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
cargo run -p mxgw-cli -- galaxy discover-hierarchy --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
```

### Browsing lazily

For UI trees or OPC UA bridges, use `browse_children_raw` to walk one level at a
time instead of paging the full hierarchy. Pass a default request for root
objects; subsequent calls set `parent_gobject_id`, `parent_tag_name`, or
`parent_contained_path`. Filter fields match `discover_hierarchy`. Each response
pairs `children` with `child_has_children` so you know which nodes to expand. See
[Galaxy Repository](../../docs/GalaxyRepository.md#browsechildren) for full
request and filter semantics.

```rust
use zb_mom_ww_mxgateway_client::generated::galaxy_repository::v1::BrowseChildrenRequest;

let reply = galaxy.browse_children_raw(BrowseChildrenRequest::default()).await?;
for (child, has_children) in reply.children.iter().zip(reply.child_has_children.iter()) {
    println!("{} expand={}", child.tag_name, has_children);
}
```

#### High-level walker

For UI trees, the client provides a `LazyBrowseNode` walker that handles
sibling pagination and the `child_has_children` hint for you:

```rust
let mut client = GalaxyClient::connect(
    ClientOptions::new("http://localhost:5000").with_api_key(ApiKey::new(api_key)),
).await?;
let roots = client.browse(None).await?;
for root in &roots {
    if root.has_children_hint() {
        root.expand().await?;
    }
    for child in root.children().await {
        let kind = if child.has_children_hint() { "has children" } else { "leaf" };
        println!("{} ({kind})", child.object().tag_name);
    }
}
```

`expand` is idempotent — calling it twice fires only one RPC,
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
`browse` again from the root.

### Watching deploy events

`watch_deploy_events` opens the `WatchDeployEvents` server stream. The
server emits a bootstrap [`DeployEvent`](src/galaxy.rs) describing the
current cache state on subscribe, then one event each time the cached
`galaxy.time_of_last_deploy` changes. `sequence` is monotonic per server
start; gaps signal that the per-subscriber buffer dropped older events.
Pass `last_seen_deploy_time` to suppress the bootstrap event when the
client's cached deploy time matches the server's.

```rust
use futures_util::StreamExt;

let mut stream = galaxy.watch_deploy_events(None).await?;
while let Some(event) = stream.next().await {
    let event = event?;
    println!(
        "seq={} objects={} attributes={}",
        event.sequence, event.object_count, event.attribute_count,
    );
}
// Drop the stream to cancel the gRPC call.
```

The matching CLI subcommand prints one line per event (`--json` switches to
one JSON object per event). `--last-seen-deploy-time` accepts an RFC3339
timestamp and is forwarded to the server. `--max-events` (default 0 = no
cap) lets you stop after a fixed number of events; otherwise the command
runs until the stream ends or `Ctrl+C` is pressed.

```powershell
cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY
cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
cargo run -p mxgw-cli -- galaxy watch --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --last-seen-deploy-time 2026-04-28T15:30:00Z
```

## Integration Checks

Run live checks only when a gateway and MXAccess-backed worker are available:

```powershell
$env:MXGATEWAY_INTEGRATION = '1'
$env:MXGATEWAY_ENDPOINT = 'http://127.0.0.1:5000'
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
$env:MXGATEWAY_TEST_ITEM = 'TestChildObject.TestInt'
cargo run -p mxgw-cli -- smoke --endpoint $env:MXGATEWAY_ENDPOINT --plaintext --api-key-env MXGATEWAY_API_KEY --item $env:MXGATEWAY_TEST_ITEM --json
```

## Related Documentation

- [Client Packaging](../../docs/ClientPackaging.md)
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
- [Rust Client Detailed Design](./RustClientDesign.md)
- [Rust Style Guide](../../docs/style-guides/RustStyleGuide.md)

## Installing from the Gitea Cargo registry

The crate publishes to the internal Gitea Cargo registry. Register the
registry once in your global `~/.cargo/config.toml`:

```toml
[registries.dohertj2-gitea]
index = "sparse+https://gitea.dohertylan.com/api/packages/dohertj2/cargo/"
```

Authentication: cargo reads credentials from `~/.cargo/credentials.toml`:

```toml
[registries.dohertj2-gitea]
token = "Bearer <your-gitea-token>"
```

Then add the dependency:

```toml
[dependencies]
zb-mom-ww-mxgateway-client = { version = "0.1.1", registry = "dohertj2-gitea" }
```
