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
|
the 2 new ones). Sending only the 2 changed values overwrites the attribute
|
||||||
with a 2-element array.
|
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
|
## Galaxy Repository browse
|
||||||
|
|
||||||
The Galaxy Repository service exposes a read-only browse over the AVEVA System
|
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::{
|
use crate::generated::mxaccess_gateway::v1::{
|
||||||
AddItem2Command, AddItemBulkCommand, AddItemCommand, AdviseCommand, AdviseItemBulkCommand,
|
AddItem2Command, AddItemBulkCommand, AddItemCommand, AdviseCommand, AdviseItemBulkCommand,
|
||||||
BulkReadResult, BulkWriteResult, CloseSessionRequest, MxCommand, MxCommandKind, MxCommandReply,
|
BulkReadResult, BulkWriteResult, CloseSessionRequest, MxCommand, MxCommandKind, MxCommandReply,
|
||||||
MxCommandRequest, MxValue as ProtoMxValue, OpenSessionRequest, ReadBulkCommand,
|
MxCommandRequest, MxDataType, MxSparseArray, MxSparseElement, MxValue as ProtoMxValue,
|
||||||
RegisterCommand, RemoveItemBulkCommand, RemoveItemCommand, StreamEventsRequest,
|
OpenSessionRequest, ReadBulkCommand, RegisterCommand, RemoveItemBulkCommand, RemoveItemCommand,
|
||||||
SubscribeBulkCommand, SubscribeResult, UnAdviseCommand, UnAdviseItemBulkCommand,
|
StreamEventsRequest, SubscribeBulkCommand, SubscribeResult, UnAdviseCommand,
|
||||||
UnsubscribeBulkCommand, Write2BulkCommand, Write2BulkEntry, Write2Command, WriteBulkCommand,
|
UnAdviseItemBulkCommand, UnsubscribeBulkCommand, Write2BulkCommand, Write2BulkEntry,
|
||||||
WriteBulkEntry, WriteCommand, WriteSecured2BulkCommand, WriteSecured2BulkEntry,
|
Write2Command, WriteBulkCommand, WriteBulkEntry, WriteCommand, WriteSecured2BulkCommand,
|
||||||
WriteSecuredBulkCommand, WriteSecuredBulkEntry,
|
WriteSecured2BulkEntry, WriteSecuredBulkCommand, WriteSecuredBulkEntry,
|
||||||
};
|
};
|
||||||
use crate::value::MxValue;
|
use crate::value::MxValue;
|
||||||
|
|
||||||
@@ -547,6 +547,62 @@ impl Session {
|
|||||||
Ok(())
|
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).
|
/// Run MXAccess `Write2` (single-value with caller-supplied timestamp).
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|||||||
@@ -173,7 +173,11 @@ impl MxValueProjection {
|
|||||||
Some(Kind::TimestampValue(value)) => Self::Timestamp(*value),
|
Some(Kind::TimestampValue(value)) => Self::Timestamp(*value),
|
||||||
Some(Kind::ArrayValue(value)) => Self::Array(MxArrayValue::from_proto(value.clone())),
|
Some(Kind::ArrayValue(value)) => Self::Array(MxArrayValue::from_proto(value.clone())),
|
||||||
Some(Kind::RawValue(value)) => Self::Raw(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,
|
AddItem2Reply, AddItemReply, AlarmConditionState, AlarmFeedMessage, AlarmTransitionKind,
|
||||||
BulkReadReply, BulkReadResult, BulkSubscribeReply, BulkWriteReply, BulkWriteResult,
|
BulkReadReply, BulkReadResult, BulkSubscribeReply, BulkWriteReply, BulkWriteResult,
|
||||||
CloseSessionReply, CloseSessionRequest, MxCommandKind, MxCommandReply, MxDataType, MxEvent,
|
CloseSessionReply, CloseSessionRequest, MxCommandKind, MxCommandReply, MxDataType, MxEvent,
|
||||||
MxEventFamily, MxStatusCategory, MxStatusProxy, MxStatusSource, MxValue,
|
MxEventFamily, MxSparseArray, MxSparseElement, MxStatusCategory, MxStatusProxy, MxStatusSource,
|
||||||
OnAlarmTransitionEvent, OpenSessionReply, OpenSessionRequest, ProtocolStatus,
|
MxValue, OnAlarmTransitionEvent, OpenSessionReply, OpenSessionRequest, ProtocolStatus,
|
||||||
ProtocolStatusCode, QueryActiveAlarmsRequest, RegisterReply, SessionState, StreamAlarmsRequest,
|
ProtocolStatusCode, QueryActiveAlarmsRequest, RegisterReply, SessionState, StreamAlarmsRequest,
|
||||||
StreamEventsRequest, SubscribeResult, Write2BulkEntry, WriteBulkEntry, WriteSecured2BulkEntry,
|
StreamEventsRequest, SubscribeResult, Write2BulkEntry, WriteBulkEntry, WriteSecured2BulkEntry,
|
||||||
WriteSecuredBulkEntry,
|
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))
|
.find(|case| case["id"].as_str() == Some(id))
|
||||||
.unwrap_or_else(|| panic!("missing fixture case {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