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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user