Skip to main content

toon/encode/
normalize.rs

1use crate::{JsonArray, JsonObject, JsonPrimitive, JsonValue, StringOrNumberOrBoolOrNull};
2
3pub fn normalize_json_value(value: JsonValue) -> JsonValue {
4    match value {
5        JsonValue::Primitive(primitive) => JsonValue::Primitive(normalize_primitive(primitive)),
6        JsonValue::Array(items) => {
7            JsonValue::Array(items.into_iter().map(normalize_json_value).collect())
8        }
9        JsonValue::Object(entries) => JsonValue::Object(
10            entries
11                .into_iter()
12                .map(|(key, value)| (key, normalize_json_value(value)))
13                .collect(),
14        ),
15    }
16}
17
18#[must_use]
19pub fn normalize_primitive(value: JsonPrimitive) -> JsonPrimitive {
20    match value {
21        StringOrNumberOrBoolOrNull::Number(value) => {
22            if !value.is_finite() {
23                StringOrNumberOrBoolOrNull::Null
24            } else if value == 0.0 {
25                StringOrNumberOrBoolOrNull::Number(0.0)
26            } else {
27                StringOrNumberOrBoolOrNull::Number(value)
28            }
29        }
30        _ => value,
31    }
32}
33
34#[must_use]
35pub const fn is_json_primitive(value: &JsonValue) -> bool {
36    matches!(value, JsonValue::Primitive(_))
37}
38
39#[must_use]
40pub const fn is_json_array(value: &JsonValue) -> bool {
41    matches!(value, JsonValue::Array(_))
42}
43
44#[must_use]
45pub const fn is_json_object(value: &JsonValue) -> bool {
46    matches!(value, JsonValue::Object(_))
47}
48
49#[must_use]
50pub const fn is_empty_object(value: &JsonObject) -> bool {
51    value.is_empty()
52}
53
54#[must_use]
55pub fn is_array_of_primitives(value: &JsonArray) -> bool {
56    value
57        .iter()
58        .all(|item| matches!(item, JsonValue::Primitive(_)))
59}
60
61#[must_use]
62pub fn is_array_of_arrays(value: &JsonArray) -> bool {
63    value.iter().all(|item| matches!(item, JsonValue::Array(_)))
64}
65
66#[must_use]
67pub fn is_array_of_objects(value: &JsonArray) -> bool {
68    value
69        .iter()
70        .all(|item| matches!(item, JsonValue::Object(_)))
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    fn s(v: &str) -> JsonValue {
78        JsonValue::Primitive(StringOrNumberOrBoolOrNull::String(v.to_string()))
79    }
80
81    fn n(v: f64) -> JsonValue {
82        JsonValue::Primitive(StringOrNumberOrBoolOrNull::Number(v))
83    }
84
85    #[test]
86    fn normalize_primitive_passes_string_through() {
87        let v = StringOrNumberOrBoolOrNull::String("hello".into());
88        assert_eq!(
89            normalize_primitive(v),
90            StringOrNumberOrBoolOrNull::String("hello".into())
91        );
92    }
93
94    #[test]
95    fn normalize_primitive_preserves_finite_numbers() {
96        let v = StringOrNumberOrBoolOrNull::Number(3.5);
97        assert_eq!(
98            normalize_primitive(v),
99            StringOrNumberOrBoolOrNull::Number(3.5)
100        );
101    }
102
103    #[test]
104    fn normalize_primitive_turns_nan_into_null() {
105        let v = StringOrNumberOrBoolOrNull::Number(f64::NAN);
106        assert_eq!(normalize_primitive(v), StringOrNumberOrBoolOrNull::Null);
107    }
108
109    #[test]
110    fn normalize_primitive_turns_infinity_into_null() {
111        let v = StringOrNumberOrBoolOrNull::Number(f64::INFINITY);
112        assert_eq!(normalize_primitive(v), StringOrNumberOrBoolOrNull::Null);
113        let v = StringOrNumberOrBoolOrNull::Number(f64::NEG_INFINITY);
114        assert_eq!(normalize_primitive(v), StringOrNumberOrBoolOrNull::Null);
115    }
116
117    #[test]
118    fn normalize_primitive_zero_is_positive() {
119        let v = StringOrNumberOrBoolOrNull::Number(-0.0);
120        let normalized = normalize_primitive(v);
121        if let StringOrNumberOrBoolOrNull::Number(n) = normalized {
122            assert!(!n.is_sign_negative(), "expected positive zero");
123        } else {
124            panic!("expected Number variant");
125        }
126    }
127
128    #[test]
129    fn normalize_primitive_preserves_bool_and_null() {
130        assert_eq!(
131            normalize_primitive(StringOrNumberOrBoolOrNull::Bool(true)),
132            StringOrNumberOrBoolOrNull::Bool(true)
133        );
134        assert_eq!(
135            normalize_primitive(StringOrNumberOrBoolOrNull::Null),
136            StringOrNumberOrBoolOrNull::Null
137        );
138    }
139
140    #[test]
141    fn normalize_json_value_walks_array_and_replaces_nan() {
142        let v = JsonValue::Array(vec![n(1.0), n(f64::NAN), s("x")]);
143        let out = normalize_json_value(v);
144        if let JsonValue::Array(items) = out {
145            assert_eq!(items.len(), 3);
146            assert!(matches!(
147                &items[1],
148                JsonValue::Primitive(StringOrNumberOrBoolOrNull::Null)
149            ));
150        } else {
151            panic!("expected Array");
152        }
153    }
154
155    #[test]
156    fn normalize_json_value_walks_object() {
157        let v = JsonValue::Object(vec![
158            ("ok".to_string(), n(1.0)),
159            ("bad".to_string(), n(f64::INFINITY)),
160        ]);
161        let out = normalize_json_value(v);
162        if let JsonValue::Object(entries) = out {
163            assert!(matches!(
164                &entries[1].1,
165                JsonValue::Primitive(StringOrNumberOrBoolOrNull::Null)
166            ));
167        } else {
168            panic!("expected Object");
169        }
170    }
171
172    #[test]
173    fn is_json_primitive_detects_primitives() {
174        assert!(is_json_primitive(&n(1.0)));
175        assert!(!is_json_primitive(&JsonValue::Array(vec![])));
176        assert!(!is_json_primitive(&JsonValue::Object(vec![])));
177    }
178
179    #[test]
180    fn is_json_array_detects_arrays() {
181        assert!(is_json_array(&JsonValue::Array(vec![])));
182        assert!(!is_json_array(&n(1.0)));
183    }
184
185    #[test]
186    fn is_json_object_detects_objects() {
187        assert!(is_json_object(&JsonValue::Object(vec![])));
188        assert!(!is_json_object(&JsonValue::Array(vec![])));
189    }
190
191    #[test]
192    fn is_empty_object_detects_empty_and_non_empty() {
193        let empty: JsonObject = vec![];
194        let non_empty: JsonObject = vec![("a".into(), n(1.0))];
195        assert!(is_empty_object(&empty));
196        assert!(!is_empty_object(&non_empty));
197    }
198
199    #[test]
200    fn is_array_of_primitives_checks_every_item() {
201        let arr: JsonArray = vec![n(1.0), s("x")];
202        assert!(is_array_of_primitives(&arr));
203        let arr: JsonArray = vec![n(1.0), JsonValue::Array(vec![])];
204        assert!(!is_array_of_primitives(&arr));
205    }
206
207    #[test]
208    fn is_array_of_primitives_true_for_empty() {
209        let arr: JsonArray = vec![];
210        assert!(is_array_of_primitives(&arr));
211    }
212
213    #[test]
214    fn is_array_of_arrays_checks_every_item() {
215        let arr: JsonArray = vec![JsonValue::Array(vec![n(1.0)]), JsonValue::Array(vec![])];
216        assert!(is_array_of_arrays(&arr));
217        let arr: JsonArray = vec![JsonValue::Array(vec![]), n(1.0)];
218        assert!(!is_array_of_arrays(&arr));
219    }
220
221    #[test]
222    fn is_array_of_objects_checks_every_item() {
223        let arr: JsonArray = vec![JsonValue::Object(vec![])];
224        assert!(is_array_of_objects(&arr));
225        let arr: JsonArray = vec![JsonValue::Object(vec![]), n(1.0)];
226        assert!(!is_array_of_objects(&arr));
227    }
228}