Files
mxaccessgw/clients/rust/src/session.rs
T
Joseph Doherty 4a0f88b17d Resolve Client.Rust-022..029: MalformedReply, correlation ids, clippy
Client.Rust-022  Restored Error::MalformedReply for register / add_item /
                 add_item2 and the bulk-subscribe / read-bulk / write-bulk
                 dispatch arms so malformed-but-OK replies fail loudly
                 instead of returning Vec::new().
Client.Rust-023  Restored next_correlation_id and routed every CLI close /
                 stream-alarms / acknowledge-alarm / bench-read-bulk call
                 through it so each call carries a unique opaque token.
Client.Rust-024  Added round-trip tests for read_bulk / write_bulk /
                 write2_bulk / write_secured_bulk / write_secured2_bulk
                 plus stream_alarms and percentile_summary unit tests.
Client.Rust-025  RustClientDesign.md re-synced — new bulk SDK, alarms
                 surface, Error variants, CLI command list, and the
                 Windows stack workaround.
Client.Rust-026  Session::read_bulk now borrows a tag slice; bench-read-
                 bulk binds tags once outside the warm-up / steady-state
                 loops.
Client.Rust-027  .cargo/config.toml selector tightened to
                 cfg(all(windows, target_env = "msvc")) and comment
                 rewritten to match reality (release + debug ship the
                 8 MB reservation).
Client.Rust-028  run_batch removed the empty-line break; stdin EOF is
                 the only terminator.
Client.Rust-029  Re-applied Client.Rust-001 / 002 / 012 — added the
                 missing doc comments, renamed BulkReplyKind variants,
                 and replaced the clone-on-copy with a deref under lock
                 so cargo clippy -D warnings is clean.

All resolved at 2026-05-24; cargo fmt + check + clippy + test all green
(55 tests).

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

775 lines
25 KiB
Rust

//! Typed handle around an opened gateway session.
//!
//! [`Session`] wraps an `OpenSession` reply (just the session id) plus a
//! cloned [`GatewayClient`] and offers Rust-shaped methods for the
//! command surface that the worker exposes — `Register`, `AddItem`,
//! bulk subscribe variants, `Write`/`Write2`, and the event stream.
//!
//! Bulk commands enforce a 1000-item cap before contacting the worker, in
//! line with the gateway's documented `MAX_BULK_ITEMS`.
use std::sync::atomic::{AtomicU64, Ordering};
use crate::client::{EventStream, GatewayClient};
use crate::error::{ensure_protocol_success, Error};
use crate::generated::mxaccess_gateway::v1::mx_command::Payload;
use crate::generated::mxaccess_gateway::v1::mx_command_reply;
use crate::generated::mxaccess_gateway::v1::{
AddItem2Command, AddItemBulkCommand, AddItemCommand, AdviseCommand, AdviseItemBulkCommand,
BulkReadResult, BulkWriteResult, CloseSessionRequest, MxCommand, MxCommandKind, MxCommandReply,
MxCommandRequest, MxValue as ProtoMxValue, OpenSessionRequest, ReadBulkCommand,
RegisterCommand, RemoveItemBulkCommand, RemoveItemCommand, StreamEventsRequest,
SubscribeBulkCommand, SubscribeResult, UnAdviseCommand, UnAdviseItemBulkCommand,
UnsubscribeBulkCommand, Write2BulkCommand, Write2BulkEntry, Write2Command, WriteBulkCommand,
WriteBulkEntry, WriteCommand, WriteSecured2BulkCommand, WriteSecured2BulkEntry,
WriteSecuredBulkCommand, WriteSecuredBulkEntry,
};
use crate::value::MxValue;
const MAX_BULK_ITEMS: usize = 1_000;
/// Process-wide monotonic sequence used by [`next_correlation_id`].
static CORRELATION_SEQUENCE: AtomicU64 = AtomicU64::new(1);
/// Build a per-call correlation id that embeds the supplied `label`.
///
/// The returned token is opaque and guaranteed to be unique within the
/// current process: every call increments a process-wide atomic counter,
/// so concurrent CLI smokes and library callers on the same machine produce
/// distinct ids that gateway logs can tell apart. The token carries no
/// embedded secret beyond `label`.
///
/// The exact textual format (currently `rust-client-{label}-{N}`) is *not*
/// part of the public contract — callers must not parse it. The crate root
/// re-exports this helper as
/// [`zb_mom_ww_mxgateway_client::next_correlation_id`] so out-of-tree
/// consumers can build correlation ids without referencing the `session`
/// module path.
pub fn next_correlation_id(label: &str) -> String {
let sequence = CORRELATION_SEQUENCE.fetch_add(1, Ordering::Relaxed);
format!("rust-client-{label}-{sequence}")
}
/// Handle to an opened gateway session.
///
/// `Session` carries the gateway-issued session id and a cloned
/// [`GatewayClient`]. All methods are async and stateless on the client
/// side: the gateway tracks per-session worker state. Drop the handle and
/// call [`Session::close`] to release the worker; the gateway also reaps
/// orphaned sessions on its own schedule.
#[derive(Clone)]
pub struct Session {
id: String,
client: GatewayClient,
}
impl Session {
pub(crate) fn new(id: impl Into<String>, client: GatewayClient) -> Self {
Self {
id: id.into(),
client,
}
}
/// Borrow the gateway-assigned session id.
pub fn id(&self) -> &str {
&self.id
}
/// Convenience constructor that issues `OpenSession` with the supplied
/// client session name and returns the resulting [`Session`].
///
/// # Errors
///
/// Propagates errors from
/// [`GatewayClient::open_session`](crate::client::GatewayClient::open_session).
pub async fn open(client: GatewayClient, client_session_name: &str) -> Result<Self, Error> {
client
.open_session(OpenSessionRequest {
client_session_name: client_session_name.to_owned(),
..OpenSessionRequest::default()
})
.await
}
/// Issue `CloseSession` against this session id.
///
/// # Errors
///
/// Returns [`Error::ProtocolStatus`] if the gateway returns a non-OK
/// envelope and any transport/status errors propagated by tonic.
pub async fn close(&self) -> Result<(), Error> {
let reply = self
.client
.close_session_raw(CloseSessionRequest {
session_id: self.id.clone(),
client_correlation_id: next_correlation_id("close-session"),
})
.await?;
ensure_protocol_success("close session", reply.protocol_status.as_ref())?;
Ok(())
}
/// Run MXAccess `Register` and return the assigned `ServerHandle`.
///
/// # Errors
///
/// Returns [`Error::Command`] if the worker reports a non-OK status,
/// plus transport/status errors from tonic.
pub async fn register(&self, client_name: &str) -> Result<i32, Error> {
let reply = self
.invoke(
MxCommandKind::Register,
Payload::Register(RegisterCommand {
client_name: client_name.to_owned(),
}),
)
.await?;
register_server_handle(&reply)
}
/// Run MXAccess `AddItem` against `server_handle` and return the
/// assigned `ItemHandle`.
///
/// # Errors
///
/// Returns [`Error::Command`] when MXAccess rejects the item
/// definition, plus transport/status errors from tonic.
pub async fn add_item(&self, server_handle: i32, item_definition: &str) -> Result<i32, Error> {
let reply = self
.invoke(
MxCommandKind::AddItem,
Payload::AddItem(AddItemCommand {
server_handle,
item_definition: item_definition.to_owned(),
}),
)
.await?;
add_item_handle(&reply)
}
/// Run MXAccess `AddItem2` (item with a caller-supplied context string)
/// and return the assigned `ItemHandle`.
///
/// # Errors
///
/// Same conditions as [`Session::add_item`].
pub async fn add_item2(
&self,
server_handle: i32,
item_definition: &str,
item_context: &str,
) -> Result<i32, Error> {
let reply = self
.invoke(
MxCommandKind::AddItem2,
Payload::AddItem2(AddItem2Command {
server_handle,
item_definition: item_definition.to_owned(),
item_context: item_context.to_owned(),
}),
)
.await?;
add_item2_handle(&reply)
}
/// Run MXAccess `RemoveItem` for the given handle pair.
///
/// # Errors
///
/// Returns [`Error::Command`] on a non-OK worker status, plus the
/// usual transport/status errors.
pub async fn remove_item(&self, server_handle: i32, item_handle: i32) -> Result<(), Error> {
self.invoke(
MxCommandKind::RemoveItem,
Payload::RemoveItem(RemoveItemCommand {
server_handle,
item_handle,
}),
)
.await?;
Ok(())
}
/// Run MXAccess `Advise` to start receiving change notifications for
/// the given item.
///
/// # Errors
///
/// Returns [`Error::Command`] when the worker reports a non-OK status.
pub async fn advise(&self, server_handle: i32, item_handle: i32) -> Result<(), Error> {
self.invoke(
MxCommandKind::Advise,
Payload::Advise(AdviseCommand {
server_handle,
item_handle,
}),
)
.await?;
Ok(())
}
/// Run MXAccess `UnAdvise` to stop change notifications for the given
/// item.
///
/// # Errors
///
/// Returns [`Error::Command`] when the worker reports a non-OK status.
pub async fn un_advise(&self, server_handle: i32, item_handle: i32) -> Result<(), Error> {
self.invoke(
MxCommandKind::UnAdvise,
Payload::UnAdvise(UnAdviseCommand {
server_handle,
item_handle,
}),
)
.await?;
Ok(())
}
/// Bulk variant of [`Session::add_item`]. Each tag address yields one
/// `SubscribeResult` in the returned vector.
///
/// # Errors
///
/// Returns [`Error::InvalidArgument`] when the input exceeds the
/// gateway's 1000-item bulk cap, plus the usual command-level errors.
pub async fn add_item_bulk(
&self,
server_handle: i32,
tag_addresses: Vec<String>,
) -> Result<Vec<SubscribeResult>, Error> {
ensure_bulk_size("tag_addresses", tag_addresses.len())?;
let reply = self
.invoke(
MxCommandKind::AddItemBulk,
Payload::AddItemBulk(AddItemBulkCommand {
server_handle,
tag_addresses,
}),
)
.await?;
bulk_results(reply, BulkReplyKind::AddItem)
}
/// Bulk variant of [`Session::advise`].
///
/// # Errors
///
/// Same conditions as [`Session::add_item_bulk`].
pub async fn advise_item_bulk(
&self,
server_handle: i32,
item_handles: Vec<i32>,
) -> Result<Vec<SubscribeResult>, Error> {
ensure_bulk_size("item_handles", item_handles.len())?;
let reply = self
.invoke(
MxCommandKind::AdviseItemBulk,
Payload::AdviseItemBulk(AdviseItemBulkCommand {
server_handle,
item_handles,
}),
)
.await?;
bulk_results(reply, BulkReplyKind::AdviseItem)
}
/// Bulk variant of [`Session::remove_item`].
///
/// # Errors
///
/// Same conditions as [`Session::add_item_bulk`].
pub async fn remove_item_bulk(
&self,
server_handle: i32,
item_handles: Vec<i32>,
) -> Result<Vec<SubscribeResult>, Error> {
ensure_bulk_size("item_handles", item_handles.len())?;
let reply = self
.invoke(
MxCommandKind::RemoveItemBulk,
Payload::RemoveItemBulk(RemoveItemBulkCommand {
server_handle,
item_handles,
}),
)
.await?;
bulk_results(reply, BulkReplyKind::RemoveItem)
}
/// Bulk variant of [`Session::un_advise`].
///
/// # Errors
///
/// Same conditions as [`Session::add_item_bulk`].
pub async fn un_advise_item_bulk(
&self,
server_handle: i32,
item_handles: Vec<i32>,
) -> Result<Vec<SubscribeResult>, Error> {
ensure_bulk_size("item_handles", item_handles.len())?;
let reply = self
.invoke(
MxCommandKind::UnAdviseItemBulk,
Payload::UnAdviseItemBulk(UnAdviseItemBulkCommand {
server_handle,
item_handles,
}),
)
.await?;
bulk_results(reply, BulkReplyKind::UnAdviseItem)
}
/// Bulk `Subscribe` (atomic add-and-advise) for a list of tag addresses.
///
/// # Errors
///
/// Same conditions as [`Session::add_item_bulk`].
pub async fn subscribe_bulk(
&self,
server_handle: i32,
tag_addresses: Vec<String>,
) -> Result<Vec<SubscribeResult>, Error> {
ensure_bulk_size("tag_addresses", tag_addresses.len())?;
let reply = self
.invoke(
MxCommandKind::SubscribeBulk,
Payload::SubscribeBulk(SubscribeBulkCommand {
server_handle,
tag_addresses,
}),
)
.await?;
bulk_results(reply, BulkReplyKind::Subscribe)
}
/// Bulk `Unsubscribe` (atomic un-advise-and-remove) for a list of
/// item handles.
///
/// # Errors
///
/// Same conditions as [`Session::add_item_bulk`].
pub async fn unsubscribe_bulk(
&self,
server_handle: i32,
item_handles: Vec<i32>,
) -> Result<Vec<SubscribeResult>, Error> {
ensure_bulk_size("item_handles", item_handles.len())?;
let reply = self
.invoke(
MxCommandKind::UnsubscribeBulk,
Payload::UnsubscribeBulk(UnsubscribeBulkCommand {
server_handle,
item_handles,
}),
)
.await?;
bulk_results(reply, BulkReplyKind::Unsubscribe)
}
/// Bulk `Read` — snapshot the current value for each requested tag.
///
/// MXAccess COM has no synchronous `Read`; the worker satisfies this by
/// returning the most recent cached `OnDataChange` value when the tag is
/// already advised (`was_cached = true`), or by taking a full AddItem +
/// Advise + wait + UnAdvise + RemoveItem snapshot lifecycle otherwise.
/// `timeout_ms == 0` lets the worker pick its default (1000 ms).
/// Per-tag failures appear as `BulkReadResult` entries with
/// `was_successful = false`; the call never errors on per-tag failure.
///
/// # Errors
///
/// Same conditions as [`Session::add_item_bulk`].
pub async fn read_bulk<S: AsRef<str>>(
&self,
server_handle: i32,
tag_addresses: &[S],
timeout_ms: u32,
) -> Result<Vec<BulkReadResult>, Error> {
ensure_bulk_size("tag_addresses", tag_addresses.len())?;
let reply = self
.invoke(
MxCommandKind::ReadBulk,
Payload::ReadBulk(ReadBulkCommand {
server_handle,
tag_addresses: tag_addresses
.iter()
.map(|tag| tag.as_ref().to_owned())
.collect(),
timeout_ms,
}),
)
.await?;
match reply.payload {
Some(mx_command_reply::Payload::ReadBulk(reply)) => Ok(reply.results),
_ => Err(Error::MalformedReply {
detail: "read_bulk reply did not carry a ReadBulk payload".to_owned(),
}),
}
}
/// Bulk `Write` (sequential MXAccess Write per entry, on the worker's STA).
///
/// Per-entry MXAccess failures are reported as `BulkWriteResult` entries
/// with `was_successful = false`; the call never errors on per-entry
/// failure. Protocol-level failures still surface as [`Error::Command`].
///
/// # Errors
///
/// Same conditions as [`Session::add_item_bulk`], plus the usual
/// transport/status errors.
pub async fn write_bulk(
&self,
server_handle: i32,
entries: Vec<WriteBulkEntry>,
) -> Result<Vec<BulkWriteResult>, Error> {
ensure_bulk_size("entries", entries.len())?;
let reply = self
.invoke(
MxCommandKind::WriteBulk,
Payload::WriteBulk(WriteBulkCommand {
server_handle,
entries,
}),
)
.await?;
bulk_write_results(reply, BulkWriteReplyKind::Write)
}
/// Bulk `Write2` (timestamped) — see [`Session::write_bulk`].
///
/// # Errors
///
/// Same conditions as [`Session::write_bulk`].
pub async fn write2_bulk(
&self,
server_handle: i32,
entries: Vec<Write2BulkEntry>,
) -> Result<Vec<BulkWriteResult>, Error> {
ensure_bulk_size("entries", entries.len())?;
let reply = self
.invoke(
MxCommandKind::Write2Bulk,
Payload::Write2Bulk(Write2BulkCommand {
server_handle,
entries,
}),
)
.await?;
bulk_write_results(reply, BulkWriteReplyKind::Write2)
}
/// Bulk `WriteSecured` — credential-sensitive values follow the same
/// redaction contract as the single-item `write_secured` path.
///
/// # Errors
///
/// Same conditions as [`Session::write_bulk`].
pub async fn write_secured_bulk(
&self,
server_handle: i32,
entries: Vec<WriteSecuredBulkEntry>,
) -> Result<Vec<BulkWriteResult>, Error> {
ensure_bulk_size("entries", entries.len())?;
let reply = self
.invoke(
MxCommandKind::WriteSecuredBulk,
Payload::WriteSecuredBulk(WriteSecuredBulkCommand {
server_handle,
entries,
}),
)
.await?;
bulk_write_results(reply, BulkWriteReplyKind::WriteSecured)
}
/// Bulk `WriteSecured2` (timestamped) — see [`Session::write_secured_bulk`].
///
/// # Errors
///
/// Same conditions as [`Session::write_bulk`].
pub async fn write_secured2_bulk(
&self,
server_handle: i32,
entries: Vec<WriteSecured2BulkEntry>,
) -> Result<Vec<BulkWriteResult>, Error> {
ensure_bulk_size("entries", entries.len())?;
let reply = self
.invoke(
MxCommandKind::WriteSecured2Bulk,
Payload::WriteSecured2Bulk(WriteSecured2BulkCommand {
server_handle,
entries,
}),
)
.await?;
bulk_write_results(reply, BulkWriteReplyKind::WriteSecured2)
}
/// Run MXAccess `Write` (single-value, no caller-supplied timestamp).
///
/// # Errors
///
/// Returns [`Error::Command`] for non-OK worker statuses, plus the
/// usual transport/status errors.
pub async fn write(
&self,
server_handle: i32,
item_handle: i32,
value: MxValue,
user_id: i32,
) -> Result<(), Error> {
self.invoke(
MxCommandKind::Write,
Payload::Write(WriteCommand {
server_handle,
item_handle,
value: Some(value.into_proto()),
user_id,
}),
)
.await?;
Ok(())
}
/// Run MXAccess `Write2` (single-value with caller-supplied timestamp).
///
/// # Errors
///
/// Same conditions as [`Session::write`].
pub async fn write2(
&self,
server_handle: i32,
item_handle: i32,
value: MxValue,
timestamp_value: MxValue,
user_id: i32,
) -> Result<(), Error> {
self.invoke(
MxCommandKind::Write2,
Payload::Write2(Write2Command {
server_handle,
item_handle,
value: Some(value.into_proto()),
timestamp_value: Some(timestamp_value.into_proto()),
user_id,
}),
)
.await?;
Ok(())
}
/// Open the per-session event stream from the beginning.
///
/// # Errors
///
/// Returns the `tonic::Status` mapped through [`Error::from`] when the
/// gateway rejects the subscription.
pub async fn events(&self) -> Result<EventStream, Error> {
self.events_after(0).await
}
/// Open the per-session event stream, requesting only events whose
/// `worker_sequence` is greater than `after_worker_sequence`. Pass `0`
/// to receive every buffered event.
///
/// # Errors
///
/// Same conditions as [`Session::events`].
pub async fn events_after(&self, after_worker_sequence: u64) -> Result<EventStream, Error> {
self.client
.stream_events(StreamEventsRequest {
session_id: self.id.clone(),
after_worker_sequence,
})
.await
}
/// Issue a raw `Invoke` for an arbitrary command, without filtering on
/// the protocol status. Useful when callers need the full reply for
/// commands not yet wrapped by `Session`.
///
/// # Errors
///
/// Returns the `tonic::Status` mapped through [`Error::from`].
pub async fn invoke_raw(
&self,
kind: MxCommandKind,
payload: Payload,
) -> Result<MxCommandReply, Error> {
self.client
.invoke_raw(self.command_request(kind, payload))
.await
}
/// Issue an `Invoke` for an arbitrary command and surface a non-OK
/// reply as [`Error::Command`].
///
/// # Errors
///
/// Returns [`Error::Command`] for non-OK worker statuses plus any
/// errors propagated by [`invoke_raw`](Self::invoke_raw).
pub async fn invoke(
&self,
kind: MxCommandKind,
payload: Payload,
) -> Result<MxCommandReply, Error> {
self.client
.invoke(self.command_request(kind, payload))
.await
}
fn command_request(&self, kind: MxCommandKind, payload: Payload) -> MxCommandRequest {
MxCommandRequest {
session_id: self.id.clone(),
client_correlation_id: next_correlation_id(kind.as_str_name()),
command: Some(MxCommand {
kind: kind as i32,
payload: Some(payload),
}),
}
}
}
fn ensure_bulk_size(name: &'static str, len: usize) -> Result<(), Error> {
if len > MAX_BULK_ITEMS {
Err(Error::InvalidArgument {
name: name.to_owned(),
detail: format!("bulk commands are limited to {MAX_BULK_ITEMS} item(s)"),
})
} else {
Ok(())
}
}
fn register_server_handle(reply: &MxCommandReply) -> Result<i32, Error> {
match reply.payload.as_ref() {
Some(mx_command_reply::Payload::Register(register)) => Ok(register.server_handle),
_ => reply
.return_value
.as_ref()
.and_then(int32_reply_value)
.ok_or_else(|| Error::MalformedReply {
detail: "register reply lacked a server_handle payload or int32 return_value"
.to_owned(),
}),
}
}
fn add_item_handle(reply: &MxCommandReply) -> Result<i32, Error> {
match reply.payload.as_ref() {
Some(mx_command_reply::Payload::AddItem(add_item)) => Ok(add_item.item_handle),
_ => reply
.return_value
.as_ref()
.and_then(int32_reply_value)
.ok_or_else(|| Error::MalformedReply {
detail: "add_item reply lacked an item_handle payload or int32 return_value"
.to_owned(),
}),
}
}
fn add_item2_handle(reply: &MxCommandReply) -> Result<i32, Error> {
match reply.payload.as_ref() {
Some(mx_command_reply::Payload::AddItem2(add_item)) => Ok(add_item.item_handle),
_ => reply
.return_value
.as_ref()
.and_then(int32_reply_value)
.ok_or_else(|| Error::MalformedReply {
detail: "add_item2 reply lacked an item_handle payload or int32 return_value"
.to_owned(),
}),
}
}
enum BulkReplyKind {
AddItem,
AdviseItem,
RemoveItem,
UnAdviseItem,
Subscribe,
Unsubscribe,
}
fn bulk_results(reply: MxCommandReply, kind: BulkReplyKind) -> Result<Vec<SubscribeResult>, Error> {
match (reply.payload, kind) {
(Some(mx_command_reply::Payload::AddItemBulk(reply)), BulkReplyKind::AddItem) => {
Ok(reply.results)
}
(Some(mx_command_reply::Payload::AdviseItemBulk(reply)), BulkReplyKind::AdviseItem) => {
Ok(reply.results)
}
(Some(mx_command_reply::Payload::RemoveItemBulk(reply)), BulkReplyKind::RemoveItem) => {
Ok(reply.results)
}
(Some(mx_command_reply::Payload::UnAdviseItemBulk(reply)), BulkReplyKind::UnAdviseItem) => {
Ok(reply.results)
}
(Some(mx_command_reply::Payload::SubscribeBulk(reply)), BulkReplyKind::Subscribe) => {
Ok(reply.results)
}
(Some(mx_command_reply::Payload::UnsubscribeBulk(reply)), BulkReplyKind::Unsubscribe) => {
Ok(reply.results)
}
_ => Err(Error::MalformedReply {
detail: "bulk subscribe reply did not carry the expected payload arm".to_owned(),
}),
}
}
enum BulkWriteReplyKind {
Write,
Write2,
WriteSecured,
WriteSecured2,
}
fn bulk_write_results(
reply: MxCommandReply,
kind: BulkWriteReplyKind,
) -> Result<Vec<BulkWriteResult>, Error> {
match (reply.payload, kind) {
(Some(mx_command_reply::Payload::WriteBulk(reply)), BulkWriteReplyKind::Write) => {
Ok(reply.results)
}
(Some(mx_command_reply::Payload::Write2Bulk(reply)), BulkWriteReplyKind::Write2) => {
Ok(reply.results)
}
(
Some(mx_command_reply::Payload::WriteSecuredBulk(reply)),
BulkWriteReplyKind::WriteSecured,
) => Ok(reply.results),
(
Some(mx_command_reply::Payload::WriteSecured2Bulk(reply)),
BulkWriteReplyKind::WriteSecured2,
) => Ok(reply.results),
_ => Err(Error::MalformedReply {
detail: "bulk write reply did not carry the expected payload arm".to_owned(),
}),
}
}
fn int32_reply_value(value: &ProtoMxValue) -> Option<i32> {
match value.kind.as_ref()? {
crate::generated::mxaccess_gateway::v1::mx_value::Kind::Int32Value(value) => Some(*value),
_ => None,
}
}