Files
mxaccessgw/clients/rust/src/options.rs
T
Joseph Doherty 0d8a28d2fe Fix all MxGateway.Client.Rust code-review findings
Resolves Client.Rust-001 through Client.Rust-011.

Build/test/clippy gate (Client.Rust-001/002/003):
- options.rs: doc comments on with_max_grpc_message_bytes /
  max_grpc_message_bytes (#![warn(missing_docs)])
- session.rs: rename BulkReplyKind variants to drop the shared `Bulk`
  suffix (clippy::enum_variant_names)
- galaxy.rs: deref instead of clone on Option<Timestamp>
  (clippy::clone_on_copy — an extra violation the gate also hit)
- mxgw-cli: assert version_json against GATEWAY/WORKER_PROTOCOL_VERSION
  constants instead of the stale literal 2
`cargo clippy --workspace --all-targets -- -D warnings` now passes.

Correctness / error handling:
- version.rs: CLIENT_VERSION = env!("CARGO_PKG_VERSION") (Client.Rust-004)
- session.rs: register/add_item/add_item2 handle extractors and
  bulk_results now return Err(Error::MalformedReply) instead of a
  silent 0 / empty vec on a shapeless OK reply (Client.Rust-005/006)
- error.rs: new Error::Unavailable classifies Code::Unavailable /
  ResourceExhausted as transient (Client.Rust-010)
- session.rs: per-call unique correlation ids via an atomic counter
  (Client.Rust-011)

Other:
- value.rs: MxValue/MxArrayValue compute the projection on demand
  instead of caching it, so a wire-only value pays no projection cost
  (Client.Rust-008)
- RustClientDesign.md: correct the crate layout, drop the unused
  `tracing` dependency (Client.Rust-007)
- client_behavior.rs: tests for the bulk-size cap, a mid-stream status
  fault, and the unreadable-CA-file path (Client.Rust-009)

cargo fmt / test --workspace (27 tests) / clippy all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:08:55 -04:00

190 lines
6.2 KiB
Rust

//! 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;
const DEFAULT_MAX_GRPC_MESSAGE_BYTES: usize = 16 * 1024 * 1024;
/// 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,
api_key: Option<ApiKey>,
plaintext: bool,
ca_file: Option<PathBuf>,
server_name_override: Option<String>,
connect_timeout: Duration,
call_timeout: Duration,
stream_timeout: Option<Duration>,
max_grpc_message_bytes: usize,
}
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<String>) -> Self {
Self {
endpoint: endpoint.into(),
api_key: None,
plaintext: true,
ca_file: None,
server_name_override: None,
connect_timeout: Duration::from_secs(10),
call_timeout: Duration::from_secs(30),
stream_timeout: None,
max_grpc_message_bytes: DEFAULT_MAX_GRPC_MESSAGE_BYTES,
}
}
/// 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<PathBuf>) -> 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<String>) -> 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
}
/// Maximum encoded/decoded gRPC message size, in bytes, the transport
/// will accept. Defaults to 16 MiB.
pub fn with_max_grpc_message_bytes(mut self, max_grpc_message_bytes: usize) -> Self {
self.max_grpc_message_bytes = max_grpc_message_bytes;
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<Duration> {
self.stream_timeout
}
/// Configured maximum gRPC message size in bytes.
pub fn max_grpc_message_bytes(&self) -> usize {
self.max_grpc_message_bytes
}
}
impl Default for ClientOptions {
fn default() -> Self {
Self::new("http://127.0.0.1:5000")
}
}
impl fmt::Debug for ClientOptions {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ClientOptions")
.field("endpoint", &self.endpoint)
.field("api_key", &self.api_key.as_ref().map(|_| "<redacted>"))
.field("plaintext", &self.plaintext)
.field("ca_file", &self.ca_file)
.field("server_name_override", &self.server_name_override)
.field("connect_timeout", &self.connect_timeout)
.field("call_timeout", &self.call_timeout)
.field("stream_timeout", &self.stream_timeout)
.field("max_grpc_message_bytes", &self.max_grpc_message_bytes)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::ClientOptions;
use crate::auth::ApiKey;
#[test]
fn debug_redacts_api_key() {
let options =
ClientOptions::new("http://localhost:5000").with_api_key(ApiKey::new("mxgw_secret"));
let debug = format!("{options:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("mxgw_secret"));
}
}