Skip to main content

nodedb_types/
conversion.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Conversions between `serde_json::Value` and `nodedb_types::Value`.
4//!
5//! Shared across crates to avoid duplicating JSON-to-Value logic.
6
7use crate::Value;
8
9/// A JSON number as the `Value` that keeps its exact number: an `i64` as an
10/// `Integer`, a larger `u64` through [`Value::from_u64`], anything else as a
11/// `Float`.
12fn json_number(n: &serde_json::Number) -> Value {
13    if let Some(i) = n.as_i64() {
14        Value::Integer(i)
15    } else if let Some(u) = n.as_u64() {
16        Value::from_u64(u)
17    } else {
18        n.as_f64().map_or(Value::Null, Value::Float)
19    }
20}
21
22/// Convert a `serde_json::Value` to a `Value` by consuming ownership.
23///
24/// Nested objects are preserved as `Value::Object`.
25pub fn json_to_value(v: serde_json::Value) -> Value {
26    match v {
27        serde_json::Value::Null => Value::Null,
28        serde_json::Value::Bool(b) => Value::Bool(b),
29        serde_json::Value::Number(n) => json_number(&n),
30        serde_json::Value::String(s) => Value::String(s),
31        serde_json::Value::Array(arr) => Value::Array(arr.into_iter().map(json_to_value).collect()),
32        serde_json::Value::Object(obj) => Value::Object(
33            obj.into_iter()
34                .map(|(k, v)| (k, json_to_value(v)))
35                .collect(),
36        ),
37    }
38}
39
40/// Convert a `&serde_json::Value` to a `Value`, borrowing the input and
41/// cloning only the leaves it must own.
42///
43/// Structure-preserving all the way down, exactly like [`json_to_value`];
44/// this variant exists only so a caller holding a reference does not have to
45/// clone a whole subtree just to convert it.
46///
47/// Nested objects and arrays MUST stay nested. An earlier version of this
48/// function rendered objects as their JSON text for "tabular display", which
49/// silently made every nested field of a row unreadable: a client
50/// deserializing the row back into the struct it was written from got a
51/// string where an object belonged, and a field that genuinely held a JSON
52/// string was indistinguishable from one that had been flattened. Rendering
53/// a value as text is a property of a textual wire protocol, so it belongs to
54/// that protocol's encoder — never to a shared conversion every caller reaches
55/// for.
56pub fn json_to_value_ref(v: &serde_json::Value) -> Value {
57    match v {
58        serde_json::Value::Null => Value::Null,
59        serde_json::Value::Bool(b) => Value::Bool(*b),
60        serde_json::Value::Number(n) => json_number(n),
61        serde_json::Value::String(s) => Value::String(s.clone()),
62        serde_json::Value::Array(arr) => Value::Array(arr.iter().map(json_to_value_ref).collect()),
63        serde_json::Value::Object(map) => Value::Object(
64            map.iter()
65                .map(|(k, v)| (k.clone(), json_to_value_ref(v)))
66                .collect(),
67        ),
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn owned_preserves_nested_objects() {
77        let v = serde_json::json!({"a": 1, "b": {"nested": true}});
78        let val = json_to_value(v);
79        match val {
80            Value::Object(map) => {
81                assert_eq!(map.get("a"), Some(&Value::Integer(1)));
82                assert!(matches!(map.get("b"), Some(Value::Object(_))));
83            }
84            _ => panic!("expected Object"),
85        }
86    }
87
88    /// The by-reference conversion must agree with the owned one all the way
89    /// down. It once flattened objects into JSON text, which made every
90    /// nested field of a row unreadable to a client deserializing it back
91    /// into the struct it was written from.
92    #[test]
93    fn by_reference_preserves_nested_objects_and_arrays() {
94        let v = serde_json::json!({"a": 1, "b": {"nested": true}, "c": [{"deep": "x"}]});
95        assert_eq!(json_to_value_ref(&v), json_to_value(v.clone()));
96
97        let Value::Object(map) = json_to_value_ref(&v) else {
98            panic!("expected Object");
99        };
100        assert!(matches!(map.get("b"), Some(Value::Object(_))));
101        let Some(Value::Array(items)) = map.get("c") else {
102            panic!("expected Array");
103        };
104        assert!(matches!(items.first(), Some(Value::Object(_))));
105    }
106
107    #[test]
108    fn primitives_roundtrip() {
109        assert_eq!(json_to_value(serde_json::Value::Null), Value::Null);
110        assert_eq!(
111            json_to_value(serde_json::Value::Bool(true)),
112            Value::Bool(true)
113        );
114        assert_eq!(json_to_value(serde_json::json!(42)), Value::Integer(42));
115        let max = serde_json::json!(u64::MAX);
116        let exact = Value::Decimal(rust_decimal::Decimal::from(u64::MAX));
117        assert_eq!(json_to_value_ref(&max), exact);
118        assert_eq!(json_to_value(max), exact);
119        assert_eq!(
120            json_to_value(serde_json::json!("hello")),
121            Value::String("hello".into())
122        );
123    }
124}