//! 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 { 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), /// `i32` array, wire `0x42` /// (`NmxSubscriptionMessage.cs:173,296-310`). Int32Array(Vec), /// `f32` array, wire `0x43` /// (`NmxSubscriptionMessage.cs:173,312-326`). Float32Array(Vec), /// `f64` array, wire `0x44` /// (`NmxSubscriptionMessage.cs:173,328-342`). Float64Array(Vec), /// String array, wire `0x45` on encode AND decode /// (`NmxWriteMessage.cs:107`, `NmxSubscriptionMessage.cs:173,274`). StringArray(Vec), /// `DateTime` array; decoded from wire `0x46` /// (`NmxSubscriptionMessage.cs:173,275`), but encoded as `0x45` /// (`NmxWriteMessage.cs:107`). Elements are raw `FILETIME` ticks. DateTimeArray(Vec), } 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); } }