[M5] mxaccess-asb: F25 step 9 — Write operation

Closes the highest-value remaining IASBIDataV2 op. With Write landed,
the read+write+subscribe path is functionally complete in-memory.

API additions:
* `MinimalWriteValue { value: AsbVariant }` — carries just the Value
  payload. Optional ArrayElementIndex / Comment / HasQT / Status /
  Timestamp fields are deferred to a later iteration once a live
  capture confirms the WCF DataContract XML form.
* `build_write_request_body(items, values, write_handle)` per
  `AsbContracts.cs:181-194`:
  ```xml
  <WriteBasicRequest xmlns="urn:msg.data.asb.iom:2">
    <Items><ASBIData>{ItemIdentity[] binary}</ASBIData></Items>
    <Values>
      <WriteValue><Value><ASBIData>{Variant binary}</ASBIData></Value></WriteValue>
      ...
    </Values>
    <WriteHandle>{i32}</WriteHandle>
  </WriteBasicRequest>
  ```
  Items array uses the IAsbCustomSerializableType binary fast-path;
  each Value's inner Variant also uses the fast-path. WriteHandle is
  an Int32 (opaque correlation echoed in PublishWriteComplete).
* `decode_write_response` — per-item Status array (mirrors the
  unregister/register pattern).
* `AsbClient::write(items, values, write_handle)` — thin wrapper.

4 new tests:
* `write_request_body_carries_items_values_and_write_handle` — body
  shape sanity (WriteHandle = 7 Int32, WriteValue element present).
* `write_request_body_pairs_items_and_values_arrays` — 2 items + 2
  values produces 2 WriteValue elements.
* `write_response_round_trips_status_array` — Status decode.
* `write_response_missing_status_fails` — graceful MissingField
  error.

Workspace: 695 tests pass (was 691, +4).

Stubbed for next F25 iterations:
* `PublishWriteComplete` — empty request, `ItemWriteComplete[]`
  response.
* `DeleteMonitoredItems` — mirrors AddMonitoredItems pattern.
* Optional WriteValue fields (Comment / Timestamp / etc.) once a
  live capture confirms the wire-byte layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-05 13:04:11 -04:00
parent b543eb1f84
commit 0441a2e693
4 changed files with 231 additions and 14 deletions
+25 -4
View File
@@ -56,16 +56,16 @@ use crate::contracts::{ItemIdentity, ItemStatus};
use crate::envelope::{ConnectionValidator, EnvelopeError, SoapEnvelope};
use crate::operations::{
AddMonitoredItemsResponse, ConnectResponse, CreateSubscriptionResponse,
DeleteSubscriptionResponse, MinimalMonitoredItem, OperationError, PublishResponse,
ReadResponse, RegisterItemsResponse, UnregisterItemsResponse,
DeleteSubscriptionResponse, MinimalMonitoredItem, MinimalWriteValue, OperationError,
PublishResponse, ReadResponse, RegisterItemsResponse, UnregisterItemsResponse, WriteResponse,
build_add_monitored_items_request_body, build_authenticate_me_request_body,
build_connect_request_body, build_create_subscription_request_body,
build_delete_subscription_request_body, build_disconnect_request_body,
build_keep_alive_request_body, build_publish_request_body, build_read_request_body,
build_register_items_request_body, build_unregister_items_request_body,
decode_add_monitored_items_response, decode_connect_response,
build_write_request_body, decode_add_monitored_items_response, decode_connect_response,
decode_create_subscription_response, decode_publish_response, decode_read_response,
decode_register_items_response, decode_unregister_items_response,
decode_register_items_response, decode_unregister_items_response, decode_write_response,
};
use crate::{actions, decode_envelope, encode_envelope};
@@ -341,6 +341,27 @@ impl<T: AsyncRead + AsyncWrite + Unpin + Send> AsbClient<T> {
Ok(decode_read_response(&response.body_tokens)?)
}
/// `Write` operation — sends a signed `WriteIn` SOAP envelope and
/// decodes the `WriteResponse` (per-item Status array).
///
/// `items.len()` must equal `values.len()`; the .NET reference
/// pairs them positionally per `MxAsbDataClient.cs` Write path.
/// `write_handle` is an opaque correlation ID echoed in the
/// PublishWriteComplete callback (irrelevant for fire-and-forget
/// writes; pass `0`).
pub async fn write(
&mut self,
items: &[ItemIdentity],
values: &[MinimalWriteValue],
write_handle: u32,
) -> Result<WriteResponse, ClientError> {
let body = build_write_request_body(items, values, write_handle);
let response = self
.send_signed_envelope(actions::WRITE, body, false)
.await?;
Ok(decode_write_response(&response.body_tokens)?)
}
/// `CreateSubscription` operation — allocates a server-side
/// subscription and returns its ID. Caller threads the ID through
/// subsequent `add_monitored_items` / `publish` /