Skip to main content

toon/cli/
json_stream.rs

1use crate::JsonStreamEvent;
2use crate::error::{Result, ToonError};
3
4#[derive(Debug, Clone)]
5enum JsonContext {
6    Object {
7        needs_comma: bool,
8        expect_value: bool,
9    },
10    Array {
11        needs_comma: bool,
12    },
13}
14
15/// Convert JSON stream events into JSON string chunks.
16///
17/// # Errors
18///
19/// Returns an error if the event stream is malformed (mismatched start/end
20/// events or primitives without keys in an object).
21#[allow(clippy::too_many_lines)]
22pub fn json_stream_from_events(
23    events: impl IntoIterator<Item = JsonStreamEvent>,
24    indent: usize,
25) -> Result<Vec<String>> {
26    let mut stack: Vec<JsonContext> = Vec::new();
27    let mut depth = 0usize;
28    let mut out = Vec::new();
29
30    for event in events {
31        let parent = stack.last_mut();
32        match event {
33            JsonStreamEvent::StartObject => {
34                if let Some(parent) = parent {
35                    match parent {
36                        JsonContext::Array { needs_comma } => {
37                            if *needs_comma {
38                                out.push(",".to_string());
39                            }
40                            if indent > 0 {
41                                out.push("\n".to_string());
42                                out.push(" ".repeat(depth * indent));
43                            }
44                        }
45                        JsonContext::Object { .. } => {}
46                    }
47                }
48
49                out.push("{".to_string());
50                stack.push(JsonContext::Object {
51                    needs_comma: false,
52                    expect_value: false,
53                });
54                depth += 1;
55            }
56            JsonStreamEvent::EndObject => {
57                let Some(context) = stack.pop() else {
58                    return Err(ToonError::message("Mismatched endObject event"));
59                };
60                if !matches!(context, JsonContext::Object { .. }) {
61                    return Err(ToonError::message("Mismatched endObject event"));
62                }
63                depth = depth.saturating_sub(1);
64                if indent > 0 {
65                    if let JsonContext::Object { needs_comma, .. } = context {
66                        if needs_comma {
67                            out.push("\n".to_string());
68                            out.push(" ".repeat(depth * indent));
69                        }
70                    }
71                }
72                out.push("}".to_string());
73
74                if let Some(parent) = stack.last_mut() {
75                    match parent {
76                        JsonContext::Object {
77                            needs_comma,
78                            expect_value,
79                        } => {
80                            *expect_value = false;
81                            *needs_comma = true;
82                        }
83                        JsonContext::Array { needs_comma } => {
84                            *needs_comma = true;
85                        }
86                    }
87                }
88            }
89            JsonStreamEvent::StartArray { .. } => {
90                if let Some(parent) = parent {
91                    match parent {
92                        JsonContext::Array { needs_comma } => {
93                            if *needs_comma {
94                                out.push(",".to_string());
95                            }
96                            if indent > 0 {
97                                out.push("\n".to_string());
98                                out.push(" ".repeat(depth * indent));
99                            }
100                        }
101                        JsonContext::Object { .. } => {}
102                    }
103                }
104
105                out.push("[".to_string());
106                stack.push(JsonContext::Array { needs_comma: false });
107                depth += 1;
108            }
109            JsonStreamEvent::EndArray => {
110                let Some(context) = stack.pop() else {
111                    return Err(ToonError::message("Mismatched endArray event"));
112                };
113                if !matches!(context, JsonContext::Array { .. }) {
114                    return Err(ToonError::message("Mismatched endArray event"));
115                }
116                depth = depth.saturating_sub(1);
117                if indent > 0 {
118                    if let JsonContext::Array { needs_comma } = context {
119                        if needs_comma {
120                            out.push("\n".to_string());
121                            out.push(" ".repeat(depth * indent));
122                        }
123                    }
124                }
125                out.push("]".to_string());
126
127                if let Some(parent) = stack.last_mut() {
128                    match parent {
129                        JsonContext::Object {
130                            needs_comma,
131                            expect_value,
132                        } => {
133                            *expect_value = false;
134                            *needs_comma = true;
135                        }
136                        JsonContext::Array { needs_comma } => {
137                            *needs_comma = true;
138                        }
139                    }
140                }
141            }
142            JsonStreamEvent::Key { key, .. } => {
143                let Some(JsonContext::Object {
144                    needs_comma,
145                    expect_value,
146                }) = stack.last_mut()
147                else {
148                    return Err(ToonError::message("Key event outside of object context"));
149                };
150
151                if *needs_comma {
152                    out.push(",".to_string());
153                }
154                if indent > 0 {
155                    out.push("\n".to_string());
156                    out.push(" ".repeat(depth * indent));
157                }
158
159                out.push(serde_json::to_string(&key).unwrap_or_else(|_| "\"\"".to_string()));
160                out.push(if indent > 0 { ": " } else { ":" }.to_string());
161
162                *expect_value = true;
163                *needs_comma = true;
164            }
165            JsonStreamEvent::Primitive { value } => {
166                if let Some(parent) = stack.last_mut() {
167                    match parent {
168                        JsonContext::Array { needs_comma } => {
169                            if *needs_comma {
170                                out.push(",".to_string());
171                            }
172                            if indent > 0 {
173                                out.push("\n".to_string());
174                                out.push(" ".repeat(depth * indent));
175                            }
176                        }
177                        JsonContext::Object { expect_value, .. } => {
178                            if !*expect_value {
179                                return Err(ToonError::message(
180                                    "Primitive event in object without preceding key",
181                                ));
182                            }
183                        }
184                    }
185                }
186
187                out.push(stringify_primitive(&value));
188
189                if let Some(parent) = stack.last_mut() {
190                    match parent {
191                        JsonContext::Object { expect_value, .. } => {
192                            *expect_value = false;
193                        }
194                        JsonContext::Array { needs_comma } => {
195                            *needs_comma = true;
196                        }
197                    }
198                }
199            }
200        }
201    }
202
203    if !stack.is_empty() {
204        return Err(ToonError::message(
205            "Incomplete event stream: unclosed objects or arrays",
206        ));
207    }
208
209    Ok(out)
210}
211
212fn stringify_primitive(value: &crate::JsonPrimitive) -> String {
213    match value {
214        crate::StringOrNumberOrBoolOrNull::Null => "null".to_string(),
215        crate::StringOrNumberOrBoolOrNull::Bool(value) => value.to_string(),
216        crate::StringOrNumberOrBoolOrNull::Number(value) => serde_json::Number::from_f64(*value)
217            .map_or_else(|| "null".to_string(), |num| num.to_string()),
218        crate::StringOrNumberOrBoolOrNull::String(value) => {
219            serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
220        }
221    }
222}