Skip to main content

toon/decode/
event_builder.rs

1use std::collections::HashSet;
2
3use crate::error::{Result, ToonError};
4use crate::{JsonPrimitive, JsonStreamEvent, JsonValue};
5
6#[derive(Debug, Clone, PartialEq)]
7pub enum NodeValue {
8    Primitive(JsonPrimitive),
9    Array(Vec<Self>),
10    Object(ObjectNode),
11}
12
13#[derive(Debug, Clone, PartialEq)]
14pub struct ObjectNode {
15    pub entries: Vec<(String, NodeValue)>,
16    pub quoted_keys: HashSet<String>,
17}
18
19#[derive(Debug, Clone)]
20enum BuildContext {
21    Object {
22        entries: Vec<(String, NodeValue)>,
23        current_key: Option<String>,
24        quoted_keys: HashSet<String>,
25    },
26    Array {
27        items: Vec<NodeValue>,
28    },
29}
30
31#[derive(Debug, Clone)]
32struct BuildState {
33    stack: Vec<BuildContext>,
34    root: Option<NodeValue>,
35}
36
37/// Build a decoded node tree from a stream of events.
38///
39/// # Errors
40///
41/// Returns an error if the event stream is malformed (mismatched start/end
42/// events, missing keys, or incomplete stacks).
43pub fn build_node_from_events(
44    events: impl IntoIterator<Item = JsonStreamEvent>,
45) -> Result<NodeValue> {
46    let mut state = BuildState {
47        stack: Vec::new(),
48        root: None,
49    };
50
51    for event in events {
52        apply_event(&mut state, event)?;
53    }
54
55    finalize_state(state)
56}
57
58pub fn node_to_json(value: NodeValue) -> JsonValue {
59    match value {
60        NodeValue::Primitive(value) => JsonValue::Primitive(value),
61        NodeValue::Array(items) => JsonValue::Array(items.into_iter().map(node_to_json).collect()),
62        NodeValue::Object(obj) => JsonValue::Object(
63            obj.entries
64                .into_iter()
65                .map(|(key, value)| (key, node_to_json(value)))
66                .collect(),
67        ),
68    }
69}
70
71#[allow(clippy::too_many_lines)]
72fn apply_event(state: &mut BuildState, event: JsonStreamEvent) -> Result<()> {
73    match event {
74        JsonStreamEvent::StartObject => {
75            state.stack.push(BuildContext::Object {
76                entries: Vec::new(),
77                current_key: None,
78                quoted_keys: HashSet::new(),
79            });
80        }
81        JsonStreamEvent::EndObject => {
82            let Some(context) = state.stack.pop() else {
83                return Err(ToonError::unexpected_event("endObject", "with empty stack"));
84            };
85            let BuildContext::Object {
86                entries,
87                quoted_keys,
88                ..
89            } = context
90            else {
91                return Err(ToonError::mismatched_end("Object", "Array"));
92            };
93            let node = NodeValue::Object(ObjectNode {
94                entries,
95                quoted_keys,
96            });
97            if let Some(parent) = state.stack.last_mut() {
98                match parent {
99                    BuildContext::Object {
100                        entries,
101                        current_key,
102                        ..
103                    } => {
104                        let Some(key) = current_key.take() else {
105                            return Err(ToonError::message(
106                                "Object endObject event without preceding key",
107                            ));
108                        };
109                        entries.push((key, node));
110                    }
111                    BuildContext::Array { items } => {
112                        items.push(node);
113                    }
114                }
115            } else {
116                state.root = Some(node);
117            }
118        }
119        JsonStreamEvent::StartArray { .. } => {
120            state.stack.push(BuildContext::Array { items: Vec::new() });
121        }
122        JsonStreamEvent::EndArray => {
123            let Some(context) = state.stack.pop() else {
124                return Err(ToonError::unexpected_event("endArray", "with empty stack"));
125            };
126            let BuildContext::Array { items } = context else {
127                return Err(ToonError::mismatched_end("Array", "Object"));
128            };
129            let node = NodeValue::Array(items);
130            if let Some(parent) = state.stack.last_mut() {
131                match parent {
132                    BuildContext::Object {
133                        entries,
134                        current_key,
135                        ..
136                    } => {
137                        let Some(key) = current_key.take() else {
138                            return Err(ToonError::message(
139                                "Array endArray event without preceding key",
140                            ));
141                        };
142                        entries.push((key, node));
143                    }
144                    BuildContext::Array { items } => {
145                        items.push(node);
146                    }
147                }
148            } else {
149                state.root = Some(node);
150            }
151        }
152        JsonStreamEvent::Key { key, was_quoted } => {
153            let Some(BuildContext::Object {
154                current_key,
155                quoted_keys,
156                ..
157            }) = state.stack.last_mut()
158            else {
159                return Err(ToonError::unexpected_event(
160                    "Key",
161                    "outside of object context",
162                ));
163            };
164            *current_key = Some(key.clone());
165            if was_quoted {
166                quoted_keys.insert(key);
167            }
168        }
169        JsonStreamEvent::Primitive { value } => {
170            if state.stack.is_empty() {
171                state.root = Some(NodeValue::Primitive(value));
172                return Ok(());
173            }
174
175            match state.stack.last_mut() {
176                Some(BuildContext::Object {
177                    entries,
178                    current_key,
179                    ..
180                }) => {
181                    let Some(key) = current_key.take() else {
182                        return Err(ToonError::message(
183                            "Primitive event without preceding key in object",
184                        ));
185                    };
186                    entries.push((key, NodeValue::Primitive(value)));
187                }
188                Some(BuildContext::Array { items }) => {
189                    items.push(NodeValue::Primitive(value));
190                }
191                None => {}
192            }
193        }
194    }
195
196    Ok(())
197}
198
199fn finalize_state(state: BuildState) -> Result<NodeValue> {
200    if !state.stack.is_empty() {
201        return Err(ToonError::event_stream(
202            "Incomplete event stream: stack not empty at end",
203        ));
204    }
205
206    state
207        .root
208        .ok_or_else(|| ToonError::event_stream("No root value built from events"))
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::StringOrNumberOrBoolOrNull;
215
216    fn prim(v: &str) -> JsonStreamEvent {
217        JsonStreamEvent::Primitive {
218            value: StringOrNumberOrBoolOrNull::String(v.to_string()),
219        }
220    }
221
222    fn prim_num(n: f64) -> JsonStreamEvent {
223        JsonStreamEvent::Primitive {
224            value: StringOrNumberOrBoolOrNull::Number(n),
225        }
226    }
227
228    fn key(k: &str) -> JsonStreamEvent {
229        JsonStreamEvent::Key {
230            key: k.to_string(),
231            was_quoted: false,
232        }
233    }
234
235    fn quoted_key(k: &str) -> JsonStreamEvent {
236        JsonStreamEvent::Key {
237            key: k.to_string(),
238            was_quoted: true,
239        }
240    }
241
242    #[test]
243    fn build_root_primitive() {
244        let root = build_node_from_events([prim_num(42.0)]).unwrap();
245        assert!(matches!(
246            root,
247            NodeValue::Primitive(StringOrNumberOrBoolOrNull::Number(n)) if (n - 42.0).abs() < f64::EPSILON
248        ));
249    }
250
251    #[test]
252    fn build_empty_object() {
253        let root =
254            build_node_from_events([JsonStreamEvent::StartObject, JsonStreamEvent::EndObject])
255                .unwrap();
256        if let NodeValue::Object(obj) = root {
257            assert!(obj.entries.is_empty());
258        } else {
259            panic!("expected object");
260        }
261    }
262
263    #[test]
264    fn build_empty_array() {
265        let root = build_node_from_events([
266            JsonStreamEvent::StartArray { length: 0 },
267            JsonStreamEvent::EndArray,
268        ])
269        .unwrap();
270        assert!(matches!(root, NodeValue::Array(items) if items.is_empty()));
271    }
272
273    #[test]
274    fn build_object_with_primitive_entry() {
275        let root = build_node_from_events([
276            JsonStreamEvent::StartObject,
277            key("name"),
278            prim("Alice"),
279            JsonStreamEvent::EndObject,
280        ])
281        .unwrap();
282        if let NodeValue::Object(obj) = root {
283            assert_eq!(obj.entries.len(), 1);
284            assert_eq!(obj.entries[0].0, "name");
285            assert!(!obj.quoted_keys.contains("name"));
286        } else {
287            panic!("expected object");
288        }
289    }
290
291    #[test]
292    fn quoted_keys_are_tracked() {
293        let root = build_node_from_events([
294            JsonStreamEvent::StartObject,
295            quoted_key("weird key"),
296            prim("x"),
297            JsonStreamEvent::EndObject,
298        ])
299        .unwrap();
300        if let NodeValue::Object(obj) = root {
301            assert!(obj.quoted_keys.contains("weird key"));
302        } else {
303            panic!("expected object");
304        }
305    }
306
307    #[test]
308    fn nested_object_round_trips() {
309        let root = build_node_from_events([
310            JsonStreamEvent::StartObject,
311            key("outer"),
312            JsonStreamEvent::StartObject,
313            key("inner"),
314            prim_num(1.0),
315            JsonStreamEvent::EndObject,
316            JsonStreamEvent::EndObject,
317        ])
318        .unwrap();
319        if let NodeValue::Object(obj) = root {
320            let (k, v) = &obj.entries[0];
321            assert_eq!(k, "outer");
322            assert!(matches!(v, NodeValue::Object(_)));
323        } else {
324            panic!("expected object");
325        }
326    }
327
328    #[test]
329    fn array_in_object() {
330        let root = build_node_from_events([
331            JsonStreamEvent::StartObject,
332            key("nums"),
333            JsonStreamEvent::StartArray { length: 2 },
334            prim_num(1.0),
335            prim_num(2.0),
336            JsonStreamEvent::EndArray,
337            JsonStreamEvent::EndObject,
338        ])
339        .unwrap();
340        if let NodeValue::Object(obj) = root {
341            let (_, v) = &obj.entries[0];
342            assert!(matches!(v, NodeValue::Array(items) if items.len() == 2));
343        } else {
344            panic!("expected object");
345        }
346    }
347
348    #[test]
349    fn object_in_array() {
350        let root = build_node_from_events([
351            JsonStreamEvent::StartArray { length: 1 },
352            JsonStreamEvent::StartObject,
353            key("k"),
354            prim_num(1.0),
355            JsonStreamEvent::EndObject,
356            JsonStreamEvent::EndArray,
357        ])
358        .unwrap();
359        if let NodeValue::Array(items) = root {
360            assert!(matches!(&items[0], NodeValue::Object(_)));
361        } else {
362            panic!("expected array");
363        }
364    }
365
366    #[test]
367    fn unbalanced_end_object_errors() {
368        let result = build_node_from_events([JsonStreamEvent::EndObject]);
369        assert!(result.is_err());
370    }
371
372    #[test]
373    fn unbalanced_end_array_errors() {
374        let result = build_node_from_events([JsonStreamEvent::EndArray]);
375        assert!(result.is_err());
376    }
377
378    #[test]
379    fn mismatched_end_errors() {
380        let result =
381            build_node_from_events([JsonStreamEvent::StartObject, JsonStreamEvent::EndArray]);
382        assert!(result.is_err());
383    }
384
385    #[test]
386    fn key_outside_object_errors() {
387        let result = build_node_from_events([key("dangling"), prim("x")]);
388        assert!(result.is_err());
389    }
390
391    #[test]
392    fn incomplete_stream_errors() {
393        let result = build_node_from_events([JsonStreamEvent::StartObject]);
394        assert!(result.is_err());
395    }
396
397    #[test]
398    fn empty_stream_errors() {
399        let result = build_node_from_events([]);
400        assert!(result.is_err());
401    }
402
403    #[test]
404    fn primitive_in_object_without_key_errors() {
405        let result = build_node_from_events([
406            JsonStreamEvent::StartObject,
407            prim("no-key"),
408            JsonStreamEvent::EndObject,
409        ]);
410        assert!(result.is_err());
411    }
412
413    #[test]
414    fn node_to_json_preserves_structure() {
415        let root = NodeValue::Object(ObjectNode {
416            entries: vec![(
417                "a".to_string(),
418                NodeValue::Array(vec![NodeValue::Primitive(
419                    StringOrNumberOrBoolOrNull::Number(1.0),
420                )]),
421            )],
422            quoted_keys: HashSet::new(),
423        });
424        let json = node_to_json(root);
425        if let JsonValue::Object(entries) = json {
426            let (k, v) = &entries[0];
427            assert_eq!(k, "a");
428            assert!(matches!(v, JsonValue::Array(items) if items.len() == 1));
429        } else {
430            panic!("expected object");
431        }
432    }
433}