toon_format/encode/
folding.rs

1use crate::types::{
2    is_identifier_segment,
3    JsonValue as Value,
4    KeyFoldingMode,
5};
6
7/// Result of chain analysis for folding.
8pub struct FoldableChain {
9    /// The folded key path (e.g., "a.b.c")
10    pub folded_key: String,
11    /// The leaf value at the end of the chain
12    pub leaf_value: Value,
13    /// Number of segments that were folded
14    pub depth_folded: usize,
15}
16
17/// Check if a value is a single-key object suitable for folding.
18fn is_single_key_object(value: &Value) -> Option<(&String, &Value)> {
19    if let Value::Object(obj) = value {
20        if obj.len() == 1 {
21            return obj.iter().next();
22        }
23    }
24    None
25}
26
27/// Analyze if a key-value pair can be folded into dotted notation.
28pub fn analyze_foldable_chain(
29    key: &str,
30    value: &Value,
31    flatten_depth: usize,
32    existing_keys: &[&String],
33) -> Option<FoldableChain> {
34    if !is_identifier_segment(key) {
35        return None;
36    }
37
38    let mut segments = vec![key.to_string()];
39    let mut current_value = value;
40
41    // Follow single-key object chain until we hit a multi-key object or leaf
42    while let Some((next_key, next_value)) = is_single_key_object(current_value) {
43        if segments.len() >= flatten_depth {
44            break;
45        }
46
47        if !is_identifier_segment(next_key) {
48            break;
49        }
50
51        segments.push(next_key.clone());
52        current_value = next_value;
53    }
54
55    // Must fold at least 2 segments to be worthwhile
56    if segments.len() < 2 {
57        return None;
58    }
59
60    let folded_key = segments.join(".");
61
62    // Don't fold if it would collide with an existing key
63    if existing_keys.contains(&&folded_key) {
64        return None;
65    }
66
67    Some(FoldableChain {
68        folded_key,
69        leaf_value: current_value.clone(),
70        depth_folded: segments.len(),
71    })
72}
73
74pub fn should_fold(mode: KeyFoldingMode, chain: &Option<FoldableChain>) -> bool {
75    match mode {
76        KeyFoldingMode::Off => false,
77        KeyFoldingMode::Safe => chain.is_some(),
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use serde_json::json;
84
85    use super::*;
86
87    #[test]
88    fn test_is_single_key_object() {
89        let val = Value::from(json!({"a": 1}));
90        assert!(is_single_key_object(&val).is_some());
91
92        let val = Value::from(json!({"a": 1, "b": 2}));
93        assert!(is_single_key_object(&val).is_none());
94
95        let val = Value::from(json!(42));
96        assert!(is_single_key_object(&val).is_none());
97    }
98
99    #[test]
100    fn test_analyze_simple_chain() {
101        let val = Value::from(json!({"b": {"c": 1}}));
102        let existing: Vec<&String> = vec![];
103
104        let result = analyze_foldable_chain("a", &val, usize::MAX, &existing);
105        assert!(result.is_some());
106
107        let chain = result.unwrap();
108        assert_eq!(chain.folded_key, "a.b.c");
109        assert_eq!(chain.depth_folded, 3);
110        assert_eq!(chain.leaf_value, Value::from(json!(1)));
111    }
112
113    #[test]
114    fn test_analyze_with_flatten_depth() {
115        let val = Value::from(json!({"b": {"c": {"d": 1}}}));
116        let existing: Vec<&String> = vec![];
117
118        let result = analyze_foldable_chain("a", &val, 2, &existing);
119        assert!(result.is_some());
120
121        let chain = result.unwrap();
122        assert_eq!(chain.folded_key, "a.b");
123        assert_eq!(chain.depth_folded, 2);
124    }
125
126    #[test]
127    fn test_analyze_stops_at_multi_key() {
128        let val = Value::from(json!({"b": {"c": 1, "d": 2}}));
129        let existing: Vec<&String> = vec![];
130
131        let result = analyze_foldable_chain("a", &val, usize::MAX, &existing);
132        assert!(result.is_some());
133
134        let chain = result.unwrap();
135        assert_eq!(chain.folded_key, "a.b");
136        assert_eq!(chain.depth_folded, 2);
137    }
138
139    #[test]
140    fn test_analyze_rejects_non_identifier() {
141        let val = Value::from(json!({"c": 1}));
142        let existing: Vec<&String> = vec![];
143
144        let result = analyze_foldable_chain("bad-key", &val, usize::MAX, &existing);
145        assert!(result.is_none());
146    }
147
148    #[test]
149    fn test_analyze_detects_collision() {
150        let val = Value::from(json!({"b": 1}));
151        let existing_key = String::from("a.b");
152        let existing: Vec<&String> = vec![&existing_key];
153
154        let result = analyze_foldable_chain("a", &val, usize::MAX, &existing);
155        assert!(result.is_none());
156    }
157
158    #[test]
159    fn test_analyze_too_short_chain() {
160        let val = Value::from(json!(42));
161        let existing: Vec<&String> = vec![];
162
163        let result = analyze_foldable_chain("a", &val, usize::MAX, &existing);
164        assert!(result.is_none());
165    }
166}