Files
mxaccess/rust/crates/mxaccess-codec/src/value.rs
T
Joseph Doherty 68aa2e30ab [M3] codec+galaxy: MxValueKind::for_data_type + GalaxyTagMetadata::resolve_write_kind
Last codec-side prerequisite before F13 (NmxClient high-level write
wrappers) can land. Two small additions, both wire-byte-direct ports
of the .NET reference's MxDataType → MxValueKind lookup logic.

mxaccess-codec
- MxValueKind::for_data_type(MxDataType, is_array) -> Option<MxValueKind>:
  fuses NmxWriteMessage.cs:58-86 (TryGetValueKind's 12 base mappings
  for data types 1..=6 scalar+array) with the two scalar fallbacks the
  .NET GalaxyTagMetadata.ProjectWriteValue layers on top
  (GalaxyRepositoryTagResolver.cs:65-69): ElapsedTime → Int32,
  InternationalizedString → String. Returns None for any other
  combination — including arrays of those two types and unsupported
  scalars (ReferenceType, StatusType, Enum, etc.).
- 6 new tests covering the base table, both fallbacks, the array-of-
  unsupported rejection, and the no-mapping branch for ReferenceType /
  StatusType / Enum / DataQualityType / BigString / Unknown / NoData /
  End sentinels.

mxaccess-galaxy
- GalaxyTagMetadata::resolve_write_kind() -> Result<MxValueKind,
  UnsupportedDataType>: pure delegation to MxValueKind::for_data_type
  + a typed error carrying (mx_data_type, is_array) for diagnostics.
- GalaxyTagMetadata::is_writable() — Ok-side accessor for browse UIs.
- UnsupportedDataType public error type (re-exported from lib.rs).
- 7 new tests: Double scalar → Float64, Boolean array → BoolArray,
  ElapsedTime scalar → Int32 (the fallback path), array-of-ElapsedTime
  rejected, InternationalizedString → String, ReferenceType rejected,
  Unknown sentinel rejected.

Test count delta: 446 -> 459 (+13; codec 215 -> 221, galaxy 49 -> 56).
All four DoD gates green.

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

649 lines
26 KiB
Rust

//! Value model — `MxValueKind`, `MxDataType`, and `MxValue`.
//!
//! Ports `src/MxNativeCodec/MxValueKind.cs`, `src/MxNativeCodec/MxDataType.cs`,
//! and the wire-kind/value mapping that `NmxSubscriptionMessage.cs` and
//! `NmxWriteMessage.cs` use. `MxValueKind` carries the on-the-wire numeric
//! tags observed in NMX subscription / write bodies; `MxDataType` is the
//! attribute-model side of the .NET enum and is independent of the wire
//! kind; `MxValue` is the runtime carrier used by codecs.
//!
//! The wire-kind values are not encoded as an enum in the .NET reference
//! (`MxValueKind.cs:3-18` uses default integer ordering) — the `0x01..0x07`
//! and `0x41..0x46` byte tags come from the encoder/decoder switches in
//! `NmxWriteMessage.cs:94-110` and `NmxSubscriptionMessage.cs:164-176`.
#![allow(clippy::indexing_slicing)]
/// On-the-wire value kind tag.
///
/// Per `NmxWriteMessage.cs:94-110` (`GetWireKind`) and
/// `NmxSubscriptionMessage.cs:164-176` (`DecodeValue`). The byte values are
/// the actual wire tags written into / read out of NMX message bodies; they
/// are NOT the `int` ordinals of the C# `MxValueKind` enum
/// (`MxValueKind.cs:3-18`).
///
/// Encoder asymmetry: both `StringArray` and `DateTimeArray` are written as
/// `0x45` on the wire (`NmxWriteMessage.cs:107`), but the decoder
/// distinguishes `0x46` for `DateTimeArray`
/// (`NmxSubscriptionMessage.cs:173,275`). [`MxValue::kind`] reflects the
/// encoder behaviour — see its docs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
#[repr(u8)]
pub enum MxValueKind {
/// Sentinel for unrecognised wire tags. Not on the wire; used by
/// [`MxValueKind::from_u8`] to surface "unknown kind" without panicking.
#[default]
Unknown = 0x00,
/// `MxValueKind.cs:5` / wire `0x01` (`NmxWriteMessage.cs:98`,
/// `NmxSubscriptionMessage.cs:166`).
Boolean = 0x01,
/// `MxValueKind.cs:6` / wire `0x02` (`NmxWriteMessage.cs:99`,
/// `NmxSubscriptionMessage.cs:167`).
Int32 = 0x02,
/// `MxValueKind.cs:7` / wire `0x03` (`NmxWriteMessage.cs:100`,
/// `NmxSubscriptionMessage.cs:168`).
Float32 = 0x03,
/// `MxValueKind.cs:8` / wire `0x04` (`NmxWriteMessage.cs:101`,
/// `NmxSubscriptionMessage.cs:169`).
Float64 = 0x04,
/// `MxValueKind.cs:9` / wire `0x05` (`NmxWriteMessage.cs:102`,
/// `NmxSubscriptionMessage.cs:170`). Encoder collapses `String` and
/// `DateTime` to the same tag (`NmxWriteMessage.cs:102`).
String = 0x05,
/// `MxValueKind.cs:10` / wire `0x06` on the decode path
/// (`NmxSubscriptionMessage.cs:171`). Encoder collapses to `0x05`
/// (`NmxWriteMessage.cs:102`).
DateTime = 0x06,
/// `MxValueKind.cs:11` / wire `0x07` (`NmxSubscriptionMessage.cs:172,253`).
/// Decoder reads a signed `i32` milliseconds value.
ElapsedTime = 0x07,
/// `MxValueKind.cs:12` / wire `0x41` (`NmxWriteMessage.cs:103`,
/// `NmxSubscriptionMessage.cs:173,270`).
BoolArray = 0x41,
/// `MxValueKind.cs:13` / wire `0x42` (`NmxWriteMessage.cs:104`,
/// `NmxSubscriptionMessage.cs:173,271`).
Int32Array = 0x42,
/// `MxValueKind.cs:14` / wire `0x43` (`NmxWriteMessage.cs:105`,
/// `NmxSubscriptionMessage.cs:173,272`).
Float32Array = 0x43,
/// `MxValueKind.cs:15` / wire `0x44` (`NmxWriteMessage.cs:106`,
/// `NmxSubscriptionMessage.cs:173,273`).
Float64Array = 0x44,
/// `MxValueKind.cs:16` / wire `0x45` (`NmxWriteMessage.cs:107`,
/// `NmxSubscriptionMessage.cs:173,274`). Encoder collapses
/// `StringArray` and `DateTimeArray` to this tag
/// (`NmxWriteMessage.cs:107`).
StringArray = 0x45,
/// `MxValueKind.cs:17` / wire `0x46` on the decode path
/// (`NmxSubscriptionMessage.cs:173,275`). Encoder collapses to `0x45`
/// (`NmxWriteMessage.cs:107`).
DateTimeArray = 0x46,
// ElapsedTimeArray (0x47) is not enumerated by `MxValueKind.cs` and is
// not handled by either the encoder or decoder. Known gap, parity with
// .NET reference.
}
impl MxValueKind {
/// Decode a wire byte into a kind. Returns [`MxValueKind::Unknown`] for
/// any tag not in `0x01..=0x07` or `0x41..=0x46` — mirrors the fall-
/// through arms of `NmxSubscriptionMessage.DecodeValue`
/// (`NmxSubscriptionMessage.cs:174`) and `ToValueKindOrNull` in the
/// .NET reference.
pub fn from_u8(value: u8) -> Self {
match value {
0x01 => Self::Boolean,
0x02 => Self::Int32,
0x03 => Self::Float32,
0x04 => Self::Float64,
0x05 => Self::String,
0x06 => Self::DateTime,
0x07 => Self::ElapsedTime,
0x41 => Self::BoolArray,
0x42 => Self::Int32Array,
0x43 => Self::Float32Array,
0x44 => Self::Float64Array,
0x45 => Self::StringArray,
0x46 => Self::DateTimeArray,
_ => Self::Unknown,
}
}
/// Encode the kind to its wire byte. `Unknown` returns `0x00`; do not
/// emit `Unknown` to the wire — the .NET encoder
/// (`NmxWriteMessage.cs:108`) throws `ArgumentOutOfRangeException` for
/// any kind not in its match.
pub fn to_u8(self) -> u8 {
self as u8
}
/// Map a model-side `(MxDataType, is_array)` pair to the wire-side
/// `MxValueKind` the LMX server expects on a Write body.
///
/// Mirrors `NmxWriteMessage.GetValueKind` + `TryGetValueKind`
/// (`NmxWriteMessage.cs:58-86`) **plus** the two scalar fallbacks the
/// .NET `GalaxyTagMetadata.ProjectWriteValue`
/// (`GalaxyRepositoryTagResolver.cs:53-72`) layers on top:
///
/// - `ElapsedTime` (scalar) → `Int32`. The .NET reference converts a
/// `TimeSpan` value to `int totalMilliseconds` at `cs:67-68`; the
/// wire kind is `Int32` regardless of the source CLR type.
/// - `InternationalizedString` (scalar) → `String`
/// (`cs:69`).
///
/// Returns `None` for any other combination — including arrays of
/// `ElapsedTime` / `InternationalizedString` / `Enum` / `BigString`,
/// which the .NET reference explicitly rejects at `cs:60-63`.
///
/// The 12 base mappings (data types 1..=6, scalar and array each):
///
/// ```text
/// (Boolean, false) → Boolean (Boolean, true) → BoolArray
/// (Integer, false) → Int32 (Integer, true) → Int32Array
/// (Float, false) → Float32 (Float, true) → Float32Array
/// (Double, false) → Float64 (Double, true) → Float64Array
/// (String, false) → String (String, true) → StringArray
/// (Time, false) → DateTime (Time, true) → DateTimeArray
/// ```
#[must_use]
pub fn for_data_type(data_type: MxDataType, is_array: bool) -> Option<MxValueKind> {
match (data_type, is_array) {
(MxDataType::Boolean, false) => Some(MxValueKind::Boolean),
(MxDataType::Integer, false) => Some(MxValueKind::Int32),
(MxDataType::Float, false) => Some(MxValueKind::Float32),
(MxDataType::Double, false) => Some(MxValueKind::Float64),
(MxDataType::String, false) => Some(MxValueKind::String),
(MxDataType::Time, false) => Some(MxValueKind::DateTime),
(MxDataType::Boolean, true) => Some(MxValueKind::BoolArray),
(MxDataType::Integer, true) => Some(MxValueKind::Int32Array),
(MxDataType::Float, true) => Some(MxValueKind::Float32Array),
(MxDataType::Double, true) => Some(MxValueKind::Float64Array),
(MxDataType::String, true) => Some(MxValueKind::StringArray),
(MxDataType::Time, true) => Some(MxValueKind::DateTimeArray),
// ProjectWriteValue scalar fallbacks (`cs:65-69`):
(MxDataType::ElapsedTime, false) => Some(MxValueKind::Int32),
(MxDataType::InternationalizedString, false) => Some(MxValueKind::String),
// Everything else (arrays of unsupported types, or unsupported
// scalars like ReferenceType / StatusType / Enum / etc.) is
// rejected. Mirrors the `_ => Return(default, out valueKind,
// success: false)` arm at `cs:84` plus the
// `ArgumentOutOfRangeException` paths at `cs:62,70`.
_ => None,
}
}
}
/// Attribute-model data type — port of `MxDataType.cs:3-24`.
///
/// This is the model-side attribute classification, distinct from the wire
/// `MxValueKind`. The numeric values are the explicit `short` discriminants
/// from `MxDataType.cs:3` (`enum MxDataType : short`). Used by the runtime
/// model and by `RegisterMxReferences` results, not by NMX value bodies.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
#[repr(i16)]
pub enum MxDataType {
/// `MxDataType.cs:5` — sentinel for "no type known".
#[default]
Unknown = -1,
/// `MxDataType.cs:6`.
NoData = 0,
/// `MxDataType.cs:7`.
Boolean = 1,
/// `MxDataType.cs:8`.
Integer = 2,
/// `MxDataType.cs:9`.
Float = 3,
/// `MxDataType.cs:10`.
Double = 4,
/// `MxDataType.cs:11`.
String = 5,
/// `MxDataType.cs:12`.
Time = 6,
/// `MxDataType.cs:13`.
ElapsedTime = 7,
/// `MxDataType.cs:14`.
ReferenceType = 8,
/// `MxDataType.cs:15`.
StatusType = 9,
/// `MxDataType.cs:16`.
Enum = 10,
/// `MxDataType.cs:17`.
SecurityClassificationEnum = 11,
/// `MxDataType.cs:18`.
DataQualityType = 12,
/// `MxDataType.cs:19`.
QualifiedEnum = 13,
/// `MxDataType.cs:20`.
QualifiedStruct = 14,
/// `MxDataType.cs:21`.
InternationalizedString = 15,
/// `MxDataType.cs:22`.
BigString = 16,
/// `MxDataType.cs:23` — terminator sentinel from the .NET enum.
End = 17,
}
impl MxDataType {
/// Decode the model-side type id. Out-of-range values map to
/// [`MxDataType::Unknown`]. Mirrors the `Unknown = -1` sentinel from
/// `MxDataType.cs:5`.
pub fn from_i16(value: i16) -> Self {
match value {
0 => Self::NoData,
1 => Self::Boolean,
2 => Self::Integer,
3 => Self::Float,
4 => Self::Double,
5 => Self::String,
6 => Self::Time,
7 => Self::ElapsedTime,
8 => Self::ReferenceType,
9 => Self::StatusType,
10 => Self::Enum,
11 => Self::SecurityClassificationEnum,
12 => Self::DataQualityType,
13 => Self::QualifiedEnum,
14 => Self::QualifiedStruct,
15 => Self::InternationalizedString,
16 => Self::BigString,
17 => Self::End,
_ => Self::Unknown,
}
}
pub fn to_i16(self) -> i16 {
self as i16
}
}
/// Runtime carrier for a decoded MXAccess value.
///
/// Variant set tracks `NmxSubscriptionMessage.DecodeValue`
/// (`NmxSubscriptionMessage.cs:164-176`): the seven scalar wire kinds
/// (`0x01..=0x07`) plus the six array wire kinds (`0x41..=0x46`,
/// minus `ElapsedTimeArray` which has no .NET support).
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum MxValue {
/// Scalar boolean, wire `0x01` (`NmxSubscriptionMessage.cs:166`).
Boolean(bool),
/// Scalar `i32`, wire `0x02` (`NmxSubscriptionMessage.cs:167`).
Int32(i32),
/// Scalar `f32`, wire `0x03` (`NmxSubscriptionMessage.cs:168`).
Float32(f32),
/// Scalar `f64`, wire `0x04` (`NmxSubscriptionMessage.cs:169`).
Float64(f64),
/// Scalar UTF-16LE string, wire `0x05`
/// (`NmxSubscriptionMessage.cs:170,178-210`).
String(String),
/// Windows `FILETIME` ticks (100ns since 1601-01-01 UTC), wire `0x06`
/// (`NmxSubscriptionMessage.cs:171,212-243`). Carries the raw `i64`
/// rather than a `DateTime` to preserve byte-for-byte parity even when
/// the value falls outside `chrono`/`time` clamp ranges.
DateTime(i64),
/// Signed milliseconds, wire `0x07`
/// (`NmxSubscriptionMessage.cs:172,245-254`). Wire is `i32`; widened to
/// `i64` here to allow the model to be unambiguous about sign and to
/// avoid forcing the `std::time::Duration` (unsigned) shape.
ElapsedTime(i64),
/// Boolean array, wire `0x41`
/// (`NmxSubscriptionMessage.cs:173,280-294`). On the wire each element
/// is an `i16` per `elementWidth==sizeof(short)` check.
BoolArray(Vec<bool>),
/// `i32` array, wire `0x42`
/// (`NmxSubscriptionMessage.cs:173,296-310`).
Int32Array(Vec<i32>),
/// `f32` array, wire `0x43`
/// (`NmxSubscriptionMessage.cs:173,312-326`).
Float32Array(Vec<f32>),
/// `f64` array, wire `0x44`
/// (`NmxSubscriptionMessage.cs:173,328-342`).
Float64Array(Vec<f64>),
/// String array, wire `0x45` on encode AND decode
/// (`NmxWriteMessage.cs:107`, `NmxSubscriptionMessage.cs:173,274`).
StringArray(Vec<String>),
/// `DateTime` array; decoded from wire `0x46`
/// (`NmxSubscriptionMessage.cs:173,275`), but encoded as `0x45`
/// (`NmxWriteMessage.cs:107`). Elements are raw `FILETIME` ticks.
DateTimeArray(Vec<i64>),
}
impl MxValue {
/// Wire kind for this value. Mirrors `NmxWriteMessage.GetWireKind`
/// (`NmxWriteMessage.cs:94-110`) — i.e. the *encoder* behaviour.
///
/// **Encoder collapse:** both [`MxValue::StringArray`] and
/// [`MxValue::DateTimeArray`] return [`MxValueKind::StringArray`]
/// (`0x45`) here, matching `NmxWriteMessage.cs:107`. The decoder is
/// asymmetric (`0x46` round-trips back into `DateTimeArray`); see
/// [`MxValueKind`] docs.
pub fn kind(&self) -> MxValueKind {
match self {
Self::Boolean(_) => MxValueKind::Boolean,
Self::Int32(_) => MxValueKind::Int32,
Self::Float32(_) => MxValueKind::Float32,
Self::Float64(_) => MxValueKind::Float64,
Self::String(_) => MxValueKind::String,
// Per NmxWriteMessage.cs:102 the encoder collapses DateTime to
// the String wire tag (0x05). Returning `DateTime` here keeps
// the model side honest; encoders should re-map via
// `MxValueKind::to_u8` semantics or call out to a future
// `to_wire_byte` helper.
Self::DateTime(_) => MxValueKind::DateTime,
Self::ElapsedTime(_) => MxValueKind::ElapsedTime,
Self::BoolArray(_) => MxValueKind::BoolArray,
Self::Int32Array(_) => MxValueKind::Int32Array,
Self::Float32Array(_) => MxValueKind::Float32Array,
Self::Float64Array(_) => MxValueKind::Float64Array,
// Encoder collapse: NmxWriteMessage.cs:107 maps both
// StringArray and DateTimeArray to wire 0x45. Matches the
// .NET encoder; the decoder asymmetrically uses 0x46 for
// DateTimeArray (NmxSubscriptionMessage.cs:275).
Self::StringArray(_) => MxValueKind::StringArray,
Self::DateTimeArray(_) => MxValueKind::StringArray,
}
}
/// Model-side data type hint. This is best-effort: the wire never
/// carries `MxDataType` — it's the model classification used by
/// register results and the public API. Arrays return their scalar
/// element's `MxDataType` (the .NET reference does not have an
/// "array of X" `MxDataType` discriminant).
pub fn data_type(&self) -> MxDataType {
match self {
Self::Boolean(_) | Self::BoolArray(_) => MxDataType::Boolean,
Self::Int32(_) | Self::Int32Array(_) => MxDataType::Integer,
Self::Float32(_) | Self::Float32Array(_) => MxDataType::Float,
Self::Float64(_) | Self::Float64Array(_) => MxDataType::Double,
Self::String(_) | Self::StringArray(_) => MxDataType::String,
Self::DateTime(_) | Self::DateTimeArray(_) => MxDataType::Time,
Self::ElapsedTime(_) => MxDataType::ElapsedTime,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
const ALL_KINDS: &[MxValueKind] = &[
MxValueKind::Unknown,
MxValueKind::Boolean,
MxValueKind::Int32,
MxValueKind::Float32,
MxValueKind::Float64,
MxValueKind::String,
MxValueKind::DateTime,
MxValueKind::ElapsedTime,
MxValueKind::BoolArray,
MxValueKind::Int32Array,
MxValueKind::Float32Array,
MxValueKind::Float64Array,
MxValueKind::StringArray,
MxValueKind::DateTimeArray,
];
#[test]
fn value_kind_round_trip() {
for &kind in ALL_KINDS {
assert_eq!(MxValueKind::from_u8(kind.to_u8()), kind, "{kind:?}");
}
}
#[test]
fn value_kind_unknown_byte_maps_to_unknown() {
assert_eq!(MxValueKind::from_u8(0xff), MxValueKind::Unknown);
// Tags between scalar (0x07) and array (0x41) ranges are not
// assigned in the .NET reference.
assert_eq!(MxValueKind::from_u8(0x08), MxValueKind::Unknown);
assert_eq!(MxValueKind::from_u8(0x40), MxValueKind::Unknown);
// 0x47 (would-be ElapsedTimeArray) is a documented gap — must
// currently surface as Unknown.
assert_eq!(MxValueKind::from_u8(0x47), MxValueKind::Unknown);
}
#[test]
fn value_kind_to_u8_matches_wire_tags() {
// Spot-check the wire bytes against the .cs sources.
assert_eq!(MxValueKind::Boolean.to_u8(), 0x01);
assert_eq!(MxValueKind::Int32.to_u8(), 0x02);
assert_eq!(MxValueKind::Float32.to_u8(), 0x03);
assert_eq!(MxValueKind::Float64.to_u8(), 0x04);
assert_eq!(MxValueKind::String.to_u8(), 0x05);
assert_eq!(MxValueKind::DateTime.to_u8(), 0x06);
assert_eq!(MxValueKind::ElapsedTime.to_u8(), 0x07);
assert_eq!(MxValueKind::BoolArray.to_u8(), 0x41);
assert_eq!(MxValueKind::Int32Array.to_u8(), 0x42);
assert_eq!(MxValueKind::Float32Array.to_u8(), 0x43);
assert_eq!(MxValueKind::Float64Array.to_u8(), 0x44);
assert_eq!(MxValueKind::StringArray.to_u8(), 0x45);
assert_eq!(MxValueKind::DateTimeArray.to_u8(), 0x46);
}
#[test]
fn data_type_round_trip() {
let all = [
MxDataType::Unknown,
MxDataType::NoData,
MxDataType::Boolean,
MxDataType::Integer,
MxDataType::Float,
MxDataType::Double,
MxDataType::String,
MxDataType::Time,
MxDataType::ElapsedTime,
MxDataType::ReferenceType,
MxDataType::StatusType,
MxDataType::Enum,
MxDataType::SecurityClassificationEnum,
MxDataType::DataQualityType,
MxDataType::QualifiedEnum,
MxDataType::QualifiedStruct,
MxDataType::InternationalizedString,
MxDataType::BigString,
MxDataType::End,
];
for dt in all {
assert_eq!(MxDataType::from_i16(dt.to_i16()), dt, "{dt:?}");
}
}
#[test]
fn data_type_out_of_range_maps_to_unknown() {
assert_eq!(MxDataType::from_i16(-2), MxDataType::Unknown);
assert_eq!(MxDataType::from_i16(18), MxDataType::Unknown);
assert_eq!(MxDataType::from_i16(i16::MAX), MxDataType::Unknown);
assert_eq!(MxDataType::from_i16(i16::MIN), MxDataType::Unknown);
}
#[test]
fn value_kind_for_each_variant() {
assert_eq!(MxValue::Boolean(true).kind(), MxValueKind::Boolean);
assert_eq!(MxValue::Int32(0).kind(), MxValueKind::Int32);
assert_eq!(MxValue::Float32(0.0).kind(), MxValueKind::Float32);
assert_eq!(MxValue::Float64(0.0).kind(), MxValueKind::Float64);
assert_eq!(MxValue::String(String::new()).kind(), MxValueKind::String);
assert_eq!(MxValue::DateTime(0).kind(), MxValueKind::DateTime);
assert_eq!(MxValue::ElapsedTime(0).kind(), MxValueKind::ElapsedTime);
assert_eq!(MxValue::BoolArray(vec![]).kind(), MxValueKind::BoolArray);
assert_eq!(MxValue::Int32Array(vec![]).kind(), MxValueKind::Int32Array);
assert_eq!(
MxValue::Float32Array(vec![]).kind(),
MxValueKind::Float32Array
);
assert_eq!(
MxValue::Float64Array(vec![]).kind(),
MxValueKind::Float64Array
);
}
/// Both `StringArray` and `DateTimeArray` collapse to the same wire
/// kind on the encode path. This mirrors `NmxWriteMessage.cs:107`:
///
/// ```text
/// MxValueKind.StringArray or MxValueKind.DateTimeArray => 0x45,
/// ```
#[test]
fn string_and_datetime_arrays_collapse_to_string_array_wire_kind() {
let s = MxValue::StringArray(vec!["a".to_string()]);
let d = MxValue::DateTimeArray(vec![0_i64]);
assert_eq!(s.kind(), MxValueKind::StringArray);
assert_eq!(d.kind(), MxValueKind::StringArray);
assert_eq!(s.kind(), d.kind());
assert_eq!(s.kind().to_u8(), 0x45);
assert_eq!(d.kind().to_u8(), 0x45);
}
#[test]
fn data_type_for_each_value() {
assert_eq!(MxValue::Boolean(false).data_type(), MxDataType::Boolean);
assert_eq!(MxValue::Int32(0).data_type(), MxDataType::Integer);
assert_eq!(MxValue::Float32(0.0).data_type(), MxDataType::Float);
assert_eq!(MxValue::Float64(0.0).data_type(), MxDataType::Double);
assert_eq!(
MxValue::String(String::new()).data_type(),
MxDataType::String
);
assert_eq!(MxValue::DateTime(0).data_type(), MxDataType::Time);
assert_eq!(MxValue::ElapsedTime(0).data_type(), MxDataType::ElapsedTime);
assert_eq!(MxValue::BoolArray(vec![]).data_type(), MxDataType::Boolean);
assert_eq!(MxValue::DateTimeArray(vec![]).data_type(), MxDataType::Time);
}
#[test]
fn defaults_match_dotnet_sentinels() {
// MxValueKind::Unknown is the from_u8 fall-through and the Default
// (matches `ToValueKindOrNull` semantics in
// `NmxSubscriptionMessage.cs:174`).
assert_eq!(MxValueKind::default(), MxValueKind::Unknown);
// MxDataType::Unknown == -1 per `MxDataType.cs:5`.
assert_eq!(MxDataType::default(), MxDataType::Unknown);
assert_eq!(MxDataType::default().to_i16(), -1);
}
#[test]
fn for_data_type_scalar_base_table() {
// Mirrors NmxWriteMessage.cs:72-77 (scalar arms of TryGetValueKind).
assert_eq!(
MxValueKind::for_data_type(MxDataType::Boolean, false),
Some(MxValueKind::Boolean)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Integer, false),
Some(MxValueKind::Int32)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Float, false),
Some(MxValueKind::Float32)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Double, false),
Some(MxValueKind::Float64)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::String, false),
Some(MxValueKind::String)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Time, false),
Some(MxValueKind::DateTime)
);
}
#[test]
fn for_data_type_array_base_table() {
// Mirrors NmxWriteMessage.cs:78-83 (array arms of TryGetValueKind).
assert_eq!(
MxValueKind::for_data_type(MxDataType::Boolean, true),
Some(MxValueKind::BoolArray)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Integer, true),
Some(MxValueKind::Int32Array)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Float, true),
Some(MxValueKind::Float32Array)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Double, true),
Some(MxValueKind::Float64Array)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::String, true),
Some(MxValueKind::StringArray)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Time, true),
Some(MxValueKind::DateTimeArray)
);
}
#[test]
fn for_data_type_elapsed_time_scalar_falls_back_to_int32() {
// GalaxyRepositoryTagResolver.cs:67-68: ElapsedTime scalar maps to
// Int32 (caller is expected to convert TimeSpan to milliseconds).
assert_eq!(
MxValueKind::for_data_type(MxDataType::ElapsedTime, false),
Some(MxValueKind::Int32)
);
}
#[test]
fn for_data_type_internationalized_string_scalar_falls_back_to_string() {
// GalaxyRepositoryTagResolver.cs:69.
assert_eq!(
MxValueKind::for_data_type(MxDataType::InternationalizedString, false),
Some(MxValueKind::String)
);
}
#[test]
fn for_data_type_array_of_unsupported_returns_none() {
// GalaxyRepositoryTagResolver.cs:60-63 explicitly rejects array of
// unsupported types — no fallback applies in the array case.
assert_eq!(
MxValueKind::for_data_type(MxDataType::ElapsedTime, true),
None
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::InternationalizedString, true),
None
);
assert_eq!(MxValueKind::for_data_type(MxDataType::Enum, true), None);
assert_eq!(
MxValueKind::for_data_type(MxDataType::BigString, true),
None
);
}
#[test]
fn for_data_type_unsupported_scalars_return_none() {
// ReferenceType, StatusType, Enum, etc. are not in either the base
// table or the ProjectWriteValue fallbacks → None.
assert_eq!(
MxValueKind::for_data_type(MxDataType::ReferenceType, false),
None
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::StatusType, false),
None
);
assert_eq!(MxValueKind::for_data_type(MxDataType::Enum, false), None);
assert_eq!(
MxValueKind::for_data_type(MxDataType::DataQualityType, false),
None
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::BigString, false),
None
);
assert_eq!(MxValueKind::for_data_type(MxDataType::Unknown, false), None);
assert_eq!(MxValueKind::for_data_type(MxDataType::NoData, false), None);
assert_eq!(MxValueKind::for_data_type(MxDataType::End, false), None);
}
}