Skip to main content

toon/encode/
replacer.rs

1use crate::encode::normalize::normalize_json_value;
2use crate::options::{EncodeReplacer, PathSegment};
3use crate::{JsonArray, JsonObject, JsonValue};
4
5pub fn apply_replacer(root: &JsonValue, replacer: &EncodeReplacer) -> JsonValue {
6    let replaced_root = replacer("", root, &[]);
7    if let Some(value) = replaced_root {
8        let normalized = normalize_json_value(value);
9        return transform_children(normalized, replacer, &[]);
10    }
11
12    transform_children(root.clone(), replacer, &[])
13}
14
15fn transform_children(
16    value: JsonValue,
17    replacer: &EncodeReplacer,
18    path: &[PathSegment],
19) -> JsonValue {
20    match value {
21        JsonValue::Object(entries) => JsonValue::Object(transform_object(entries, replacer, path)),
22        JsonValue::Array(values) => JsonValue::Array(transform_array(values, replacer, path)),
23        JsonValue::Primitive(value) => JsonValue::Primitive(value),
24    }
25}
26
27fn transform_object(
28    entries: JsonObject,
29    replacer: &EncodeReplacer,
30    path: &[PathSegment],
31) -> JsonObject {
32    let mut result = Vec::new();
33
34    for (key, value) in entries {
35        let mut next_path = path.to_vec();
36        next_path.push(PathSegment::Key(key.clone()));
37
38        let replacement = replacer(&key, &value, &next_path);
39        if let Some(next_value) = replacement {
40            let normalized = normalize_json_value(next_value);
41            let transformed = transform_children(normalized, replacer, &next_path);
42            result.push((key, transformed));
43        }
44    }
45
46    result
47}
48
49fn transform_array(
50    values: JsonArray,
51    replacer: &EncodeReplacer,
52    path: &[PathSegment],
53) -> JsonArray {
54    let mut result = Vec::new();
55
56    for (idx, value) in values.into_iter().enumerate() {
57        let mut next_path = path.to_vec();
58        next_path.push(PathSegment::Index(idx));
59
60        let key = idx.to_string();
61        let replacement = replacer(&key, &value, &next_path);
62        if let Some(next_value) = replacement {
63            let normalized = normalize_json_value(next_value);
64            let transformed = transform_children(normalized, replacer, &next_path);
65            result.push(transformed);
66        }
67    }
68
69    result
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::StringOrNumberOrBoolOrNull;
76    use std::sync::Arc;
77    use std::sync::atomic::{AtomicUsize, Ordering};
78
79    fn s(v: &str) -> JsonValue {
80        JsonValue::Primitive(StringOrNumberOrBoolOrNull::String(v.to_string()))
81    }
82
83    fn n(v: f64) -> JsonValue {
84        JsonValue::Primitive(StringOrNumberOrBoolOrNull::Number(v))
85    }
86
87    fn identity_replacer() -> EncodeReplacer {
88        Arc::new(|_key, value, _path| Some(value.clone()))
89    }
90
91    #[test]
92    fn identity_replacer_returns_equivalent_tree_for_primitive() {
93        let root = n(42.0);
94        let out = apply_replacer(&root, &identity_replacer());
95        assert_eq!(out, n(42.0));
96    }
97
98    #[test]
99    fn identity_replacer_returns_equivalent_tree_for_object() {
100        let root = JsonValue::Object(vec![("a".into(), n(1.0)), ("b".into(), s("hi"))]);
101        let out = apply_replacer(&root, &identity_replacer());
102        assert_eq!(out, root);
103    }
104
105    #[test]
106    fn returning_none_for_root_falls_back_to_original_transform() {
107        let replacer: EncodeReplacer = Arc::new(|key, value, _path| {
108            if key.is_empty() {
109                None
110            } else {
111                Some(value.clone())
112            }
113        });
114        let root = JsonValue::Object(vec![("a".into(), n(1.0))]);
115        let out = apply_replacer(&root, &replacer);
116        assert_eq!(out, root);
117    }
118
119    #[test]
120    fn replacer_can_drop_object_entries_by_returning_none() {
121        let replacer: EncodeReplacer = Arc::new(|key, value, _path| {
122            if key == "drop" {
123                None
124            } else {
125                Some(value.clone())
126            }
127        });
128        let root = JsonValue::Object(vec![("keep".into(), n(1.0)), ("drop".into(), n(2.0))]);
129        let out = apply_replacer(&root, &replacer);
130        assert_eq!(out, JsonValue::Object(vec![("keep".into(), n(1.0))]));
131    }
132
133    #[test]
134    fn replacer_can_drop_array_elements_by_returning_none() {
135        let replacer: EncodeReplacer = Arc::new(|_key, value, path| {
136            let PathSegment::Index(idx) = path.last()? else {
137                return Some(value.clone());
138            };
139            if *idx == 1 { None } else { Some(value.clone()) }
140        });
141        let root = JsonValue::Array(vec![n(1.0), n(2.0), n(3.0)]);
142        let out = apply_replacer(&root, &replacer);
143        assert_eq!(out, JsonValue::Array(vec![n(1.0), n(3.0)]));
144    }
145
146    #[test]
147    fn replacer_can_transform_values() {
148        let replacer: EncodeReplacer = Arc::new(|_key, value, _path| {
149            if let JsonValue::Primitive(StringOrNumberOrBoolOrNull::Number(num)) = value {
150                Some(JsonValue::from(num * 2.0))
151            } else {
152                Some(value.clone())
153            }
154        });
155        let root = JsonValue::Array(vec![n(1.0), n(2.0)]);
156        let out = apply_replacer(&root, &replacer);
157        assert_eq!(out, JsonValue::Array(vec![n(2.0), n(4.0)]));
158    }
159
160    #[test]
161    fn replacer_normalizes_nan_returned_from_replacement() {
162        let replacer: EncodeReplacer =
163            Arc::new(|_key, _value, _path| Some(JsonValue::from(f64::NAN)));
164        let root = n(1.0);
165        let out = apply_replacer(&root, &replacer);
166        assert!(matches!(
167            out,
168            JsonValue::Primitive(StringOrNumberOrBoolOrNull::Null)
169        ));
170    }
171
172    #[test]
173    fn replacer_receives_path_segments() {
174        let collected: Arc<std::sync::Mutex<Vec<Vec<PathSegment>>>> =
175            Arc::new(std::sync::Mutex::new(Vec::new()));
176        let collected_inner = collected.clone();
177        let replacer: EncodeReplacer = Arc::new(move |_key, value, path| {
178            collected_inner.lock().unwrap().push(path.to_vec());
179            Some(value.clone())
180        });
181        let root = JsonValue::Object(vec![("arr".into(), JsonValue::Array(vec![n(1.0), n(2.0)]))]);
182        let _ = apply_replacer(&root, &replacer);
183        // Take a snapshot and drop the lock before running assertions so no
184        // panicking assertion poisons the mutex.
185        let paths: Vec<Vec<PathSegment>> = collected.lock().unwrap().clone();
186        // root visit + "arr" + arr[0] + arr[1]
187        assert!(paths.iter().any(Vec::is_empty));
188        assert!(
189            paths
190                .iter()
191                .any(|p| p.len() == 1 && matches!(&p[0], PathSegment::Key(k) if k == "arr"))
192        );
193        assert!(paths.iter().any(|p| {
194            p.last()
195                .is_some_and(|seg| matches!(seg, PathSegment::Index(0)))
196        }));
197        assert!(paths.iter().any(|p| {
198            p.last()
199                .is_some_and(|seg| matches!(seg, PathSegment::Index(1)))
200        }));
201    }
202
203    #[test]
204    fn replacer_call_counts_are_reasonable() {
205        let count = Arc::new(AtomicUsize::new(0));
206        let count_inner = count.clone();
207        let replacer: EncodeReplacer = Arc::new(move |_key, value, _path| {
208            count_inner.fetch_add(1, Ordering::SeqCst);
209            Some(value.clone())
210        });
211        let root = JsonValue::Object(vec![
212            ("a".into(), n(1.0)),
213            ("b".into(), JsonValue::Array(vec![n(2.0), n(3.0)])),
214        ]);
215        let _ = apply_replacer(&root, &replacer);
216        let n_calls = count.load(Ordering::SeqCst);
217        // root + a + b + b[0] + b[1] = 5 calls
218        assert_eq!(n_calls, 5);
219    }
220}