toon_format/decode/
expansion.rs

1use indexmap::IndexMap;
2
3use crate::{
4    constants::QUOTED_KEY_MARKER,
5    types::{
6        is_identifier_segment,
7        JsonValue as Value,
8        PathExpansionMode,
9        ToonError,
10        ToonResult,
11    },
12};
13
14pub fn should_expand_key(key: &str, mode: PathExpansionMode) -> Option<Vec<String>> {
15    match mode {
16        PathExpansionMode::Off => None,
17        PathExpansionMode::Safe => {
18            // Quoted keys with dots shouldn't be expanded (they were explicitly quoted)
19            if key.starts_with(QUOTED_KEY_MARKER) {
20                return None;
21            }
22
23            if !key.contains('.') {
24                return None;
25            }
26
27            let segments: Vec<String> = key.split('.').map(String::from).collect();
28
29            if segments.len() < 2 {
30                return None;
31            }
32
33            // Only expand if all segments are valid identifiers (safety requirement)
34            if segments.iter().all(|s| is_identifier_segment(s)) {
35                Some(segments)
36            } else {
37                None
38            }
39        }
40    }
41}
42
43pub fn deep_merge_value(
44    target: &mut IndexMap<String, Value>,
45    segments: &[String],
46    value: Value,
47    strict: bool,
48) -> ToonResult<()> {
49    if segments.is_empty() {
50        return Ok(());
51    }
52
53    if segments.len() == 1 {
54        let key = &segments[0];
55
56        // Check for conflicts at leaf level
57        if let Some(existing) = target.get(key) {
58            if strict {
59                return Err(ToonError::DeserializationError(format!(
60                    "Path expansion conflict: key '{key}' already exists with value: {existing:?}",
61                )));
62            }
63        }
64
65        target.insert(key.clone(), value);
66        return Ok(());
67    }
68
69    let first_key = &segments[0];
70    let remaining_segments = &segments[1..];
71
72    // Get or create nested object, handling type conflicts
73    let nested_obj = if let Some(existing_value) = target.get_mut(first_key) {
74        match existing_value {
75            Value::Object(obj) => obj,
76            _ => {
77                if strict {
78                    return Err(ToonError::DeserializationError(format!(
79                        "Path expansion conflict: key '{first_key}' exists as non-object: \
80                         {existing_value:?}",
81                    )));
82                }
83                // Replace non-object with empty object in non-strict mode
84                *existing_value = Value::Object(IndexMap::new());
85                match existing_value {
86                    Value::Object(obj) => obj,
87                    _ => unreachable!(),
88                }
89            }
90        }
91    } else {
92        target.insert(first_key.clone(), Value::Object(IndexMap::new()));
93        match target.get_mut(first_key).unwrap() {
94            Value::Object(obj) => obj,
95            _ => unreachable!(),
96        }
97    };
98
99    // Recurse into nested object
100    deep_merge_value(nested_obj, remaining_segments, value, strict)
101}
102
103pub fn expand_paths_in_object(
104    obj: IndexMap<String, Value>,
105    mode: PathExpansionMode,
106    strict: bool,
107) -> ToonResult<IndexMap<String, Value>> {
108    let mut result = IndexMap::new();
109
110    for (key, mut value) in obj {
111        // Expand nested objects first (depth-first)
112        if let Value::Object(nested_obj) = value {
113            value = Value::Object(expand_paths_in_object(nested_obj, mode, strict)?);
114        }
115
116        // Strip marker from quoted keys
117        let clean_key = if key.starts_with(QUOTED_KEY_MARKER) {
118            key.strip_prefix(QUOTED_KEY_MARKER).unwrap().to_string()
119        } else {
120            key.clone()
121        };
122
123        if let Some(segments) = should_expand_key(&key, mode) {
124            deep_merge_value(&mut result, &segments, value, strict)?;
125        } else {
126            // Check for conflicts with expanded keys
127            if let Some(existing) = result.get(&clean_key) {
128                if strict {
129                    return Err(ToonError::DeserializationError(format!(
130                        "Key '{clean_key}' conflicts with existing value: {existing:?}",
131                    )));
132                }
133            }
134            result.insert(clean_key, value);
135        }
136    }
137
138    Ok(result)
139}
140
141pub fn expand_paths_recursive(
142    value: Value,
143    mode: PathExpansionMode,
144    strict: bool,
145) -> ToonResult<Value> {
146    match value {
147        Value::Object(obj) => {
148            let expanded = expand_paths_in_object(obj, mode, strict)?;
149            Ok(Value::Object(expanded))
150        }
151        Value::Array(arr) => {
152            let expanded: Result<Vec<_>, _> = arr
153                .into_iter()
154                .map(|v| expand_paths_recursive(v, mode, strict))
155                .collect();
156            Ok(Value::Array(expanded?))
157        }
158        _ => Ok(value),
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use serde_json::json;
165
166    use super::*;
167
168    #[test]
169    fn test_should_expand_key_off_mode() {
170        assert!(should_expand_key("a.b.c", PathExpansionMode::Off).is_none());
171    }
172
173    #[test]
174    fn test_should_expand_key_safe_mode() {
175        // Valid expansions
176        assert_eq!(
177            should_expand_key("a.b", PathExpansionMode::Safe),
178            Some(vec!["a".to_string(), "b".to_string()])
179        );
180        assert_eq!(
181            should_expand_key("a.b.c", PathExpansionMode::Safe),
182            Some(vec!["a".to_string(), "b".to_string(), "c".to_string()])
183        );
184
185        // No dots
186        assert!(should_expand_key("simple", PathExpansionMode::Safe).is_none());
187
188        // Invalid segments (not IdentifierSegments)
189        assert!(should_expand_key("a.bad-key", PathExpansionMode::Safe).is_none());
190        assert!(should_expand_key("123.key", PathExpansionMode::Safe).is_none());
191    }
192
193    #[test]
194    fn test_deep_merge_simple() {
195        let mut target = IndexMap::new();
196        deep_merge_value(
197            &mut target,
198            &["a".to_string(), "b".to_string()],
199            Value::from(json!(1)),
200            true,
201        )
202        .unwrap();
203
204        let expected = json!({"a": {"b": 1}});
205        assert_eq!(Value::Object(target), Value::from(expected));
206    }
207
208    #[test]
209    fn test_deep_merge_multiple_paths() {
210        let mut target = IndexMap::new();
211
212        deep_merge_value(
213            &mut target,
214            &["a".to_string(), "b".to_string()],
215            Value::from(json!(1)),
216            true,
217        )
218        .unwrap();
219
220        deep_merge_value(
221            &mut target,
222            &["a".to_string(), "c".to_string()],
223            Value::from(json!(2)),
224            true,
225        )
226        .unwrap();
227
228        let expected = json!({"a": {"b": 1, "c": 2}});
229        assert_eq!(Value::Object(target), Value::from(expected));
230    }
231
232    #[test]
233    fn test_deep_merge_conflict_strict() {
234        let mut target = IndexMap::new();
235        target.insert("a".to_string(), Value::from(json!({"b": 1})));
236
237        let result = deep_merge_value(
238            &mut target,
239            &["a".to_string(), "b".to_string()],
240            Value::from(json!(2)),
241            true,
242        );
243
244        assert!(result.is_err());
245    }
246
247    #[test]
248    fn test_deep_merge_conflict_non_strict() {
249        let mut target = IndexMap::new();
250        target.insert("a".to_string(), Value::from(json!({"b": 1})));
251
252        deep_merge_value(
253            &mut target,
254            &["a".to_string(), "b".to_string()],
255            Value::from(json!(2)),
256            false,
257        )
258        .unwrap();
259
260        let expected = json!({"a": {"b": 2}});
261        assert_eq!(Value::Object(target), Value::from(expected));
262    }
263
264    #[test]
265    fn test_expand_paths_in_object() {
266        let mut obj = IndexMap::new();
267        obj.insert("a.b.c".to_string(), Value::from(json!(1)));
268        obj.insert("simple".to_string(), Value::from(json!(2)));
269
270        let result = expand_paths_in_object(obj, PathExpansionMode::Safe, true).unwrap();
271
272        let expected = json!({"a": {"b": {"c": 1}}, "simple": 2});
273        assert_eq!(Value::Object(result), Value::from(expected));
274    }
275
276    #[test]
277    fn test_expand_paths_with_merge() {
278        let mut obj = IndexMap::new();
279        obj.insert("a.b".to_string(), Value::from(json!(1)));
280        obj.insert("a.c".to_string(), Value::from(json!(2)));
281
282        let result = expand_paths_in_object(obj, PathExpansionMode::Safe, true).unwrap();
283
284        let expected = json!({"a": {"b": 1, "c": 2}});
285        assert_eq!(Value::Object(result), Value::from(expected));
286    }
287}