Skip to main content

nx_value/
lib.rs

1//! NX value model for JSON-like object hierarchies.
2//!
3//! `NxValue` is intended to be a stable, serde-compatible data IR that can be used as input/output
4//! across the NX API surface.
5
6use std::collections::BTreeMap;
7use std::fs::File;
8use std::io::{Read, Write};
9use std::path::Path;
10
11use serde::de::Error as _;
12use serde::de::{MapAccess, SeqAccess, Visitor};
13use serde::ser::{SerializeMap, SerializeSeq};
14use serde::{Deserialize, Deserializer, Serialize, Serializer};
15
16/// A JSON-like tree value used as the stable NX API value type.
17///
18/// It is the Rust form of a canonical value, which `docs/nx-ir-format.md` (*Host values*) defines:
19/// a host builds one and passes it as it is, whether it read the value from JSON or computed it,
20/// and JSON and MessagePack are encodings of it for a wire or a store. It can hold nothing that is
21/// not a canonical value, so a runtime given one has nothing to refuse for its kind.
22///
23/// Constant union cases are represented as [`NxValue::String`] carrying the bare authored case
24/// name. The declaring union is recovered from the target schema (declared NX type, typed DTO
25/// property, or other type annotation) at the point where the value is consumed; the IR itself
26/// carries no in-payload union type wrapper.
27///
28/// NX has no null: its absent value is the empty sequence, [`NxValue::empty`]. [`NxValue::Null`]
29/// is the host's spelling of absence, which NX reads as the empty value. NX writes it in two
30/// places: an entry call's result whose type is a standalone `T?` and holds nothing, and a
31/// present empty field of an update record — a cleared field — which is how key presence carries
32/// "cleared" through JSON. An absent optional record field is an omitted key.
33#[derive(Debug, Clone, PartialEq)]
34pub enum NxValue {
35    Bool(bool),
36    Int32(i32),
37    Int(i64),
38    Float32(f32),
39    Float(f64),
40    /// String value.
41    ///
42    /// A string value may represent either a plain string or a constant union case. The two are
43    /// distinguishable only against the target schema — a schema-less consumer cannot tell
44    /// them apart from the payload alone.
45    String(String),
46    /// The host's `null`: JSON `null`, MessagePack `nil`. NX reads it as the empty value.
47    Null,
48    Array(Vec<NxValue>),
49    /// Record value (ordered properties).
50    ///
51    /// When serialized to JSON, `type_name` is encoded as a `"$type"` string property if present.
52    Record {
53        type_name: Option<String>,
54        properties: BTreeMap<String, NxValue>,
55    },
56}
57
58impl NxValue {
59    /// The empty value: the empty sequence, which is also the absent value.
60    pub fn empty() -> Self {
61        NxValue::Array(Vec::new())
62    }
63
64    /// True for the empty value, or for a host `null`, which NX reads as the empty value.
65    pub fn is_empty_value(&self) -> bool {
66        match self {
67            NxValue::Null => true,
68            NxValue::Array(elements) => elements.is_empty(),
69            _ => false,
70        }
71    }
72
73    /// Deserialize a value from a JSON string.
74    pub fn from_json_str(source: &str) -> Result<Self, serde_json::Error> {
75        serde_json::from_str(source)
76    }
77
78    /// Deserialize a value from a JSON reader.
79    pub fn from_json_reader<R: Read>(reader: R) -> Result<Self, serde_json::Error> {
80        serde_json::from_reader(reader)
81    }
82
83    /// Deserialize a value from a JSON file.
84    pub fn from_json_file(path: impl AsRef<Path>) -> Result<Self, NxValueIoError> {
85        let file = File::open(path.as_ref())?;
86        Ok(Self::from_json_reader(file)?)
87    }
88
89    /// Serialize a value to a compact JSON string.
90    pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
91        serde_json::to_string(self)
92    }
93
94    /// Serialize a value to a pretty JSON string.
95    pub fn to_json_string_pretty(&self) -> Result<String, serde_json::Error> {
96        serde_json::to_string_pretty(self)
97    }
98
99    /// Serialize a value to JSON using the provided writer.
100    pub fn to_json_writer<W: Write>(&self, writer: W) -> Result<(), serde_json::Error> {
101        serde_json::to_writer(writer, self)
102    }
103
104    /// Serialize a value to a JSON file (compact form).
105    pub fn to_json_file(&self, path: impl AsRef<Path>) -> Result<(), NxValueIoError> {
106        let mut file = File::create(path.as_ref())?;
107        self.to_json_writer(&mut file)?;
108        file.flush()?;
109        Ok(())
110    }
111
112    /// Serialize a value to a JSON file (pretty form).
113    pub fn to_json_file_pretty(&self, path: impl AsRef<Path>) -> Result<(), NxValueIoError> {
114        let mut file = File::create(path.as_ref())?;
115        let json = self.to_json_string_pretty()?;
116        file.write_all(json.as_bytes())?;
117        file.flush()?;
118        Ok(())
119    }
120
121    /// Serialize a value to MessagePack bytes.
122    pub fn to_msgpack_vec(&self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
123        rmp_serde::to_vec(self)
124    }
125
126    /// Deserialize a value from MessagePack bytes.
127    pub fn from_msgpack_slice(bytes: &[u8]) -> Result<Self, rmp_serde::decode::Error> {
128        rmp_serde::from_slice(bytes)
129    }
130}
131
132impl Serialize for NxValue {
133    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
134        match self {
135            NxValue::Bool(value) => serializer.serialize_bool(*value),
136            NxValue::Int32(value) => serializer.serialize_i32(*value),
137            NxValue::Int(value) => serializer.serialize_i64(*value),
138            NxValue::Float32(value) => serializer.serialize_f32(*value),
139            NxValue::Float(value) => serializer.serialize_f64(*value),
140            NxValue::String(value) => serializer.serialize_str(value),
141            NxValue::Null => serializer.serialize_unit(),
142            NxValue::Array(elements) => {
143                let mut seq = serializer.serialize_seq(Some(elements.len()))?;
144                for element in elements {
145                    seq.serialize_element(element)?;
146                }
147                seq.end()
148            }
149            NxValue::Record {
150                type_name,
151                properties,
152            } => {
153                let len = properties.len() + usize::from(type_name.is_some());
154                let mut map = serializer.serialize_map(Some(len))?;
155
156                if let Some(type_name) = type_name {
157                    map.serialize_entry("$type", type_name)?;
158                }
159
160                for (key, value) in properties {
161                    map.serialize_entry(key, value)?;
162                }
163
164                map.end()
165            }
166        }
167    }
168}
169
170impl<'de> Deserialize<'de> for NxValue {
171    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
172        struct NxValueVisitor;
173
174        impl<'de> Visitor<'de> for NxValueVisitor {
175            type Value = NxValue;
176
177            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178                formatter.write_str("a JSON-like value")
179            }
180
181            fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
182                Ok(NxValue::Null)
183            }
184
185            fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
186                Ok(NxValue::Null)
187            }
188
189            fn visit_bool<E: serde::de::Error>(self, v: bool) -> Result<Self::Value, E> {
190                Ok(NxValue::Bool(v))
191            }
192
193            fn visit_i32<E: serde::de::Error>(self, v: i32) -> Result<Self::Value, E> {
194                Ok(NxValue::Int32(v))
195            }
196
197            fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
198                Ok(NxValue::Int(v))
199            }
200
201            fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
202                if v <= i64::MAX as u64 {
203                    Ok(NxValue::Int(v as i64))
204                } else {
205                    Ok(NxValue::Float(v as f64))
206                }
207            }
208
209            fn visit_f32<E: serde::de::Error>(self, v: f32) -> Result<Self::Value, E> {
210                Ok(NxValue::Float32(v))
211            }
212
213            fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Self::Value, E> {
214                Ok(NxValue::Float(v))
215            }
216
217            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
218                Ok(NxValue::String(v.to_owned()))
219            }
220
221            fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
222                Ok(NxValue::String(v))
223            }
224
225            fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
226                let mut values = Vec::new();
227                while let Some(value) = seq.next_element::<NxValue>()? {
228                    values.push(value);
229                }
230                Ok(NxValue::Array(values))
231            }
232
233            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
234                let mut type_name = None;
235                let mut properties = BTreeMap::new();
236                while let Some((key, value)) = map.next_entry::<String, NxValue>()? {
237                    if key == "$type" {
238                        match value {
239                            NxValue::String(name) => {
240                                type_name = Some(name);
241                            }
242                            other => {
243                                return Err(A::Error::custom(format!(
244                                    "expected \"$type\" to be a string, got {:?}",
245                                    other
246                                )));
247                            }
248                        }
249                        continue;
250                    }
251
252                    properties.insert(key, value);
253                }
254
255                Ok(NxValue::Record {
256                    type_name,
257                    properties,
258                })
259            }
260        }
261
262        deserializer.deserialize_any(NxValueVisitor)
263    }
264}
265
266/// Errors for JSON file IO helpers.
267#[derive(Debug)]
268pub enum NxValueIoError {
269    Io(std::io::Error),
270    Json(serde_json::Error),
271}
272
273impl std::fmt::Display for NxValueIoError {
274    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275        match self {
276            NxValueIoError::Io(e) => write!(f, "io error: {}", e),
277            NxValueIoError::Json(e) => write!(f, "json error: {}", e),
278        }
279    }
280}
281
282impl std::error::Error for NxValueIoError {
283    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
284        match self {
285            NxValueIoError::Io(e) => Some(e),
286            NxValueIoError::Json(e) => Some(e),
287        }
288    }
289}
290
291impl From<std::io::Error> for NxValueIoError {
292    fn from(value: std::io::Error) -> Self {
293        NxValueIoError::Io(value)
294    }
295}
296
297impl From<serde_json::Error> for NxValueIoError {
298    fn from(value: serde_json::Error) -> Self {
299        NxValueIoError::Json(value)
300    }
301}
302
303/// The README's example, compiled as a doc test.
304#[cfg(doctest)]
305#[doc = include_str!("../README.md")]
306struct ReadmeDoctests;
307
308pub mod color;
309pub mod math;
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use std::io::Cursor;
315
316    #[test]
317    fn json_round_trip_in_memory() {
318        let mut obj = BTreeMap::new();
319        obj.insert("a".to_string(), NxValue::Int(1));
320        obj.insert("b".to_string(), NxValue::Bool(true));
321        obj.insert(
322            "c".to_string(),
323            NxValue::Array(vec![NxValue::Int(2), NxValue::String("x".to_string())]),
324        );
325
326        let value = NxValue::Record {
327            type_name: None,
328            properties: obj,
329        };
330        let json = value.to_json_string().unwrap();
331        let decoded = NxValue::from_json_str(&json).unwrap();
332        assert_eq!(decoded, value);
333    }
334
335    #[test]
336    fn json_round_trip_primitives() {
337        let cases = [
338            ("true", NxValue::Bool(true)),
339            ("false", NxValue::Bool(false)),
340            ("0", NxValue::Int(0)),
341            ("-1", NxValue::Int(-1)),
342            ("2.75", NxValue::Float(2.75)),
343            ("\"hello\"", NxValue::String("hello".to_string())),
344            ("[]", NxValue::Array(vec![])),
345            (
346                "{}",
347                NxValue::Record {
348                    type_name: None,
349                    properties: BTreeMap::new(),
350                },
351            ),
352        ];
353
354        for (json, expected) in cases {
355            let decoded = NxValue::from_json_str(json).unwrap();
356            assert_eq!(decoded, expected);
357            let encoded = decoded.to_json_string().unwrap();
358            let decoded_again = NxValue::from_json_str(&encoded).unwrap();
359            assert_eq!(decoded_again, expected);
360        }
361    }
362
363    #[test]
364    fn json_round_trip_nested_structures() {
365        let value = NxValue::Record {
366            type_name: None,
367            properties: BTreeMap::from([
368                (
369                    "a".to_string(),
370                    NxValue::Array(vec![NxValue::Int(1), NxValue::Int(2)]),
371                ),
372                (
373                    "b".to_string(),
374                    NxValue::Record {
375                        type_name: None,
376                        properties: BTreeMap::from([
377                            ("x".to_string(), NxValue::Bool(false)),
378                            ("y".to_string(), NxValue::empty()),
379                        ]),
380                    },
381                ),
382            ]),
383        };
384
385        let json = value.to_json_string().unwrap();
386        let decoded = NxValue::from_json_str(&json).unwrap();
387        assert_eq!(decoded, value);
388    }
389
390    /// The canonical encoding of an occurrence-typed value, row by row: `+` and `*` are arrays, a
391    /// present `?` is its item, a host `null` decodes to [`NxValue::Null`], which NX reads as the
392    /// empty value, and `Null` is written as `null`.
393    #[test]
394    fn json_encoding_of_occurrence_typed_values() {
395        // `+` and `*` encode as arrays, the empty `*` as the empty array.
396        assert_eq!(
397            NxValue::Array(vec![NxValue::Int(1), NxValue::Int(2)])
398                .to_json_string()
399                .unwrap(),
400            "[1,2]"
401        );
402        assert_eq!(NxValue::empty().to_json_string().unwrap(), "[]");
403        // A present `?` is its item; an absent standalone `?` is `null`.
404        assert_eq!(NxValue::Int(1).to_json_string().unwrap(), "1");
405        assert_eq!(NxValue::Null.to_json_string().unwrap(), "null");
406        // Host `null` and `[]` are both read as the empty value.
407        assert_eq!(NxValue::from_json_str("null").unwrap(), NxValue::Null);
408        assert_eq!(NxValue::from_json_str("[]").unwrap(), NxValue::empty());
409        assert!(NxValue::from_json_str("null").unwrap().is_empty_value());
410        // A missing key stays missing: an absent optional field is an omitted key.
411        let book = NxValue::from_json_str(r#"{"$type":"Book","title":"A"}"#).unwrap();
412        let NxValue::Record { properties, .. } = &book else {
413            panic!("a record");
414        };
415        assert!(!properties.contains_key("author"));
416        assert_eq!(
417            book.to_json_string().unwrap(),
418            r#"{"$type":"Book","title":"A"}"#
419        );
420        // A cleared update-record field is `Null`, and round-trips as `null`.
421        let cleared = NxValue::Record {
422            type_name: Some("Book.Update".to_string()),
423            properties: BTreeMap::from([("author".to_string(), NxValue::Null)]),
424        };
425        assert_eq!(
426            cleared.to_json_string().unwrap(),
427            r#"{"$type":"Book.Update","author":null}"#
428        );
429        assert_eq!(
430            NxValue::from_json_str(r#"{"$type":"Book.Update","author":null}"#).unwrap(),
431            cleared
432        );
433        // The encoding is structural: an empty field encodes as it is stored.
434        let plain = NxValue::Record {
435            type_name: Some("Box".to_string()),
436            properties: BTreeMap::from([("items".to_string(), NxValue::empty())]),
437        };
438        assert_eq!(
439            plain.to_json_string().unwrap(),
440            r#"{"$type":"Box","items":[]}"#
441        );
442    }
443
444    #[test]
445    fn msgpack_writes_null_as_nil() {
446        let bytes = NxValue::Null.to_msgpack_vec().unwrap();
447        assert_eq!(bytes, vec![0xc0]);
448        assert_eq!(NxValue::from_msgpack_slice(&bytes).unwrap(), NxValue::Null);
449    }
450
451    #[test]
452    fn json_round_trip_constant_case_as_bare_string() {
453        let value = NxValue::String("active".to_string());
454
455        let json = value.to_json_string().unwrap();
456        assert_eq!(json, "\"active\"");
457        let decoded = NxValue::from_json_str(&json).unwrap();
458        assert_eq!(decoded, value);
459    }
460
461    #[test]
462    fn json_parses_large_u64_as_float() {
463        let value = NxValue::from_json_str("18446744073709551615").unwrap();
464        assert!(matches!(value, NxValue::Float(_)));
465    }
466
467    #[test]
468    fn json_number_boundaries() {
469        let max_i64 = NxValue::from_json_str("9223372036854775807").unwrap();
470        assert_eq!(max_i64, NxValue::Int(i64::MAX));
471
472        let min_i64 = NxValue::from_json_str("-9223372036854775808").unwrap();
473        assert_eq!(min_i64, NxValue::Int(i64::MIN));
474
475        let just_over_max_i64 = NxValue::from_json_str("9223372036854775808").unwrap();
476        assert!(matches!(just_over_max_i64, NxValue::Float(_)));
477    }
478
479    #[test]
480    fn json_supports_exponent_notation() {
481        let value = NxValue::from_json_str("1e3").unwrap();
482        assert_eq!(value, NxValue::Float(1000.0));
483    }
484
485    #[test]
486    fn json_preserves_negative_zero_sign() {
487        let value = NxValue::from_json_str("-0.0").unwrap();
488        match value {
489            NxValue::Float(f) => {
490                assert_eq!(f.to_bits(), (-0.0f64).to_bits());
491            }
492            _ => panic!("Expected float"),
493        }
494    }
495
496    #[test]
497    fn json_object_serialization_is_deterministic() {
498        let mut obj = BTreeMap::new();
499        obj.insert("b".to_string(), NxValue::Int(2));
500        obj.insert("a".to_string(), NxValue::Int(1));
501
502        let value = NxValue::Record {
503            type_name: None,
504            properties: obj,
505        };
506        let json = value.to_json_string().unwrap();
507        assert_eq!(json, "{\"a\":1,\"b\":2}");
508    }
509
510    #[test]
511    fn json_reader_writer_round_trip() {
512        let value = NxValue::Array(vec![
513            NxValue::String("x".to_string()),
514            NxValue::Int(1),
515            NxValue::Bool(false),
516        ]);
517
518        let mut buffer = Vec::new();
519        value.to_json_writer(&mut buffer).unwrap();
520
521        let decoded = NxValue::from_json_reader(Cursor::new(buffer)).unwrap();
522        assert_eq!(decoded, value);
523    }
524
525    #[test]
526    fn json_pretty_string_round_trip() {
527        let value = NxValue::Record {
528            type_name: None,
529            properties: BTreeMap::from([(
530                "a".to_string(),
531                NxValue::Array(vec![NxValue::Int(1), NxValue::Int(2)]),
532            )]),
533        };
534
535        let json = value.to_json_string_pretty().unwrap();
536        let decoded = NxValue::from_json_str(&json).unwrap();
537        assert_eq!(decoded, value);
538    }
539
540    #[test]
541    fn json_invalid_input_errors() {
542        assert!(NxValue::from_json_str("{]").is_err());
543        assert!(NxValue::from_json_str("[1,]").is_err());
544    }
545
546    #[test]
547    fn json_missing_file_errors() {
548        let tmp = tempfile::tempdir().unwrap();
549        let missing = tmp.path().join("missing.json");
550
551        let err = NxValue::from_json_file(&missing).unwrap_err();
552        assert!(matches!(err, NxValueIoError::Io(_)));
553    }
554
555    #[test]
556    fn json_file_helpers_round_trip() {
557        let tmp = tempfile::tempdir().unwrap();
558        let path = tmp.path().join("value.json");
559
560        let mut obj = BTreeMap::new();
561        obj.insert("name".to_string(), NxValue::String("Ada".to_string()));
562        obj.insert("age".to_string(), NxValue::Int(42));
563        let value = NxValue::Record {
564            type_name: None,
565            properties: obj,
566        };
567
568        value.to_json_file_pretty(&path).unwrap();
569        let decoded = NxValue::from_json_file(&path).unwrap();
570        assert_eq!(decoded, value);
571    }
572
573    #[test]
574    fn msgpack_round_trip_in_memory() {
575        let mut obj = BTreeMap::new();
576        obj.insert("name".to_string(), NxValue::String("Ada".to_string()));
577        obj.insert("age".to_string(), NxValue::Int(42));
578
579        let value = NxValue::Record {
580            type_name: Some("User".to_string()),
581            properties: obj,
582        };
583
584        let bytes = value.to_msgpack_vec().unwrap();
585        let decoded = NxValue::from_msgpack_slice(&bytes).unwrap();
586        assert_eq!(decoded, value);
587    }
588
589    #[test]
590    fn msgpack_round_trip_constant_case_as_bare_string() {
591        let value = NxValue::String("active".to_string());
592
593        let bytes = value.to_msgpack_vec().unwrap();
594        let decoded = NxValue::from_msgpack_slice(&bytes).unwrap();
595        assert_eq!(decoded, value);
596    }
597
598    #[test]
599    fn int32_round_trip_msgpack() {
600        let value = NxValue::Int32(42);
601        let bytes = value.to_msgpack_vec().unwrap();
602        let decoded = NxValue::from_msgpack_slice(&bytes).unwrap();
603        // MessagePack may widen small integers; the value should be numerically equal
604        match decoded {
605            NxValue::Int32(n) => assert_eq!(n, 42),
606            NxValue::Int(n) => assert_eq!(n, 42),
607            other => panic!("Expected integer, got {:?}", other),
608        }
609    }
610
611    #[test]
612    fn float32_serializes_to_json() {
613        let value = NxValue::Float32(2.75);
614        let json = value.to_json_string().unwrap();
615        // JSON serializes f32 as a number
616        let decoded = NxValue::from_json_str(&json).unwrap();
617        // JSON always deserializes as f64, so we get Float back
618        assert!(matches!(decoded, NxValue::Float(_)));
619    }
620
621    #[test]
622    fn int32_serializes_to_json() {
623        let value = NxValue::Int32(42);
624        let json = value.to_json_string().unwrap();
625        assert_eq!(json, "42");
626        // JSON always deserializes integers via visit_i64/visit_u64
627        let decoded = NxValue::from_json_str(&json).unwrap();
628        assert_eq!(decoded, NxValue::Int(42));
629    }
630}
631
632#[cfg(test)]
633mod color_corpus_tests;