feat(client-rust): add write_array_elements default-fill helper and document semantics

Handles the new MxSparseArray wire type (proto field 19 on MxValue::Kind):
- value.rs: map SparseArrayValue to MxValueProjection::Unset (write-only; never emitted on read path)
- session.rs: add write_array_elements() that builds the sparse proto value and delegates to write()
- tests: three unit tests asserting proto shape, empty-elements case, and read-path Unset projection
- README: document write_array_elements default-fill semantics and bare-name [] normalisation
This commit is contained in:
Joseph Doherty
2026-06-18 03:02:15 -04:00
parent b7f29f3048
commit 3a8f2bed4e
4 changed files with 191 additions and 9 deletions
+33
View File
@@ -168,6 +168,39 @@ the unchanged elements included. For example, to change 2 elements of a
the 2 new ones). Sending only the 2 changed values overwrites the attribute
with a 2-element array.
#### Default-fill partial array writes
When you only need to set a handful of indices and want every other position to
take the element type's default (zero / `false` / empty string / Unix epoch for
timestamps), use `Session::write_array_elements` instead:
```rust
// Write a 10-element integer array; index 0 = 42, index 7 = 99,
// all other indices default to 0 (not preserved from the previous value).
session
.write_array_elements(
server_handle,
item_handle,
MxDataType::Integer,
10,
[(0, MxValue::int32(42)), (7, MxValue::int32(99))],
user_id,
)
.await?;
```
The gateway expands the sparse representation into a full `MxArray` before
forwarding to the worker — the worker and MXAccess COM never see the sparse
form. Unmentioned indices are reset to the type default, **not** preserved from
the existing attribute value.
#### Bare-name array AddItem normalisation
`AddItem` for a bare array attribute name (e.g. `Tank01.Temperature`) is
automatically normalised to `Tank01.Temperature[]` by the gateway so the
worker can resolve the full array. You do not need to append `[]` in client
code; the gateway handles it.
## Galaxy Repository browse
The Galaxy Repository service exposes a read-only browse over the AVEVA System
+62 -6
View File
@@ -17,12 +17,12 @@ 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,
MxCommandRequest, MxDataType, MxSparseArray, MxSparseElement, 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;
@@ -547,6 +547,62 @@ impl Session {
Ok(())
}
/// Write a sparse, default-filled array: only the given elements
/// (index → scalar value) are set; every unmentioned index up to
/// `total_length` is written as the element type's default (a reset,
/// **not** a preserve). The gateway expands the sparse representation into
/// a whole-array write before forwarding to the worker.
///
/// This is a convenience wrapper around [`Session::write`] that builds the
/// `MxSparseArray` wire value for you. Call [`Session::write`] directly
/// if you need to pass a pre-built [`MxValue`] carrying a full
/// `MxArray`.
///
/// # Errors
///
/// Returns [`Error::InvalidArgument`] (propagated from the gateway) if
/// `total_length` is zero, exceeds the gateway's maximum array length, or
/// any element index is out of range. Returns [`Error::Command`] for
/// non-OK worker statuses, plus the usual transport/status errors.
pub async fn write_array_elements(
&self,
server_handle: i32,
item_handle: i32,
element_data_type: MxDataType,
total_length: u32,
elements: impl IntoIterator<Item = (u32, MxValue)>,
user_id: i32,
) -> Result<(), Error> {
use crate::generated::mxaccess_gateway::v1::mx_value::Kind;
let sparse_elements: Vec<MxSparseElement> = elements
.into_iter()
.map(|(index, value)| MxSparseElement {
index,
value: Some(value.into_proto()),
})
.collect();
let sparse_value = ProtoMxValue {
data_type: element_data_type as i32,
variant_type: String::new(),
kind: Some(Kind::SparseArrayValue(MxSparseArray {
element_data_type: element_data_type as i32,
total_length,
elements: sparse_elements,
})),
..ProtoMxValue::default()
};
self.write(
server_handle,
item_handle,
MxValue::from_proto(sparse_value),
user_id,
)
.await
}
/// Run MXAccess `Write2` (single-value with caller-supplied timestamp).
///
/// # Errors
+5 -1
View File
@@ -173,7 +173,11 @@ impl MxValueProjection {
Some(Kind::TimestampValue(value)) => Self::Timestamp(*value),
Some(Kind::ArrayValue(value)) => Self::Array(MxArrayValue::from_proto(value.clone())),
Some(Kind::RawValue(value)) => Self::Raw(value.clone()),
None => Self::Unset,
// SparseArrayValue is write-only: the gateway expands it before forwarding
// to the worker and never emits it in events or read replies. Map it to
// Unset so any read-side code that encounters a stale or mis-routed
// sparse value degrades gracefully rather than panicking.
Some(Kind::SparseArrayValue(_)) | None => Self::Unset,
}
}
}
+91 -2
View File
@@ -24,8 +24,8 @@ use zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::{
AddItem2Reply, AddItemReply, AlarmConditionState, AlarmFeedMessage, AlarmTransitionKind,
BulkReadReply, BulkReadResult, BulkSubscribeReply, BulkWriteReply, BulkWriteResult,
CloseSessionReply, CloseSessionRequest, MxCommandKind, MxCommandReply, MxDataType, MxEvent,
MxEventFamily, MxStatusCategory, MxStatusProxy, MxStatusSource, MxValue,
OnAlarmTransitionEvent, OpenSessionReply, OpenSessionRequest, ProtocolStatus,
MxEventFamily, MxSparseArray, MxSparseElement, MxStatusCategory, MxStatusProxy, MxStatusSource,
MxValue, OnAlarmTransitionEvent, OpenSessionReply, OpenSessionRequest, ProtocolStatus,
ProtocolStatusCode, QueryActiveAlarmsRequest, RegisterReply, SessionState, StreamAlarmsRequest,
StreamEventsRequest, SubscribeResult, Write2BulkEntry, WriteBulkEntry, WriteSecured2BulkEntry,
WriteSecuredBulkEntry,
@@ -1091,3 +1091,92 @@ fn case_by_id<'a>(cases: &'a [Value], id: &str) -> &'a Value {
.find(|case| case["id"].as_str() == Some(id))
.unwrap_or_else(|| panic!("missing fixture case {id}"))
}
// ---------------------------------------------------------------------------
// write_array_elements — proto shape unit tests
// ---------------------------------------------------------------------------
/// Build the proto `MxValue` that `write_array_elements` would send and assert
/// the sparse oneof variant has the correct `total_length` and elements.
fn sparse_int32_value(
total_length: u32,
elements: impl IntoIterator<Item = (u32, i32)>,
) -> MxValue {
let sparse_elements: Vec<MxSparseElement> = elements
.into_iter()
.map(|(index, v)| MxSparseElement {
index,
value: Some(MxValue {
data_type: MxDataType::Integer as i32,
variant_type: "VT_I4".to_owned(),
kind: Some(Kind::Int32Value(v)),
..MxValue::default()
}),
})
.collect();
MxValue {
data_type: MxDataType::Integer as i32,
variant_type: String::new(),
kind: Some(Kind::SparseArrayValue(MxSparseArray {
element_data_type: MxDataType::Integer as i32,
total_length,
elements: sparse_elements,
})),
..MxValue::default()
}
}
#[test]
fn write_array_elements_proto_shape_has_sparse_oneof_kind() {
let proto = sparse_int32_value(5, [(0, 10), (3, 30)]);
let Kind::SparseArrayValue(ref sparse) = proto.kind.as_ref().unwrap() else {
panic!("expected SparseArrayValue kind, got {:?}", proto.kind);
};
assert_eq!(sparse.total_length, 5, "total_length must round-trip");
assert_eq!(sparse.elements.len(), 2, "two elements supplied");
assert_eq!(sparse.element_data_type, MxDataType::Integer as i32);
let elem0 = &sparse.elements[0];
assert_eq!(elem0.index, 0);
assert_eq!(
elem0.value.as_ref().unwrap().kind,
Some(Kind::Int32Value(10))
);
let elem3 = &sparse.elements[1];
assert_eq!(elem3.index, 3);
assert_eq!(
elem3.value.as_ref().unwrap().kind,
Some(Kind::Int32Value(30))
);
}
#[test]
fn write_array_elements_empty_elements_is_valid_all_defaults() {
let proto = sparse_int32_value(8, []);
let Kind::SparseArrayValue(ref sparse) = proto.kind.as_ref().unwrap() else {
panic!("expected SparseArrayValue kind");
};
assert_eq!(sparse.total_length, 8);
assert!(
sparse.elements.is_empty(),
"no elements means every index defaults"
);
}
#[test]
fn sparse_array_value_round_trips_through_client_mx_value_projection_as_unset() {
// SparseArrayValue is write-only. If it ever arrives on the read path
// (e.g. a future version bug), the projection should degrade to Unset
// rather than panic, because the enum variant is not readable.
let proto = sparse_int32_value(4, [(1, 99)]);
let client_value = ClientMxValue::from_proto(proto);
assert_eq!(
client_value.projection(),
&MxValueProjection::Unset,
"write-only SparseArrayValue must project to Unset, not panic"
);
}