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                    && let JsonContext::Object { needs_comma, .. } = context
66                    && needs_comma
67                {
68                    out.push("\n".to_string());
69                    out.push(" ".repeat(depth * indent));
70                }
71                out.push("}".to_string());
72
73                if let Some(parent) = stack.last_mut() {
74                    match parent {
75                        JsonContext::Object {
76                            needs_comma,
77                            expect_value,
78                        } => {
79                            *expect_value = false;
80                            *needs_comma = true;
81                        }
82                        JsonContext::Array { needs_comma } => {
83                            *needs_comma = true;
84                        }
85                    }
86                }
87            }
88            JsonStreamEvent::StartArray { .. } => {
89                if let Some(parent) = parent {
90                    match parent {
91                        JsonContext::Array { needs_comma } => {
92                            if *needs_comma {
93                                out.push(",".to_string());
94                            }
95                            if indent > 0 {
96                                out.push("\n".to_string());
97                                out.push(" ".repeat(depth * indent));
98                            }
99                        }
100                        JsonContext::Object { .. } => {}
101                    }
102                }
103
104                out.push("[".to_string());
105                stack.push(JsonContext::Array { needs_comma: false });
106                depth += 1;
107            }
108            JsonStreamEvent::EndArray => {
109                let Some(context) = stack.pop() else {
110                    return Err(ToonError::message("Mismatched endArray event"));
111                };
112                if !matches!(context, JsonContext::Array { .. }) {
113                    return Err(ToonError::message("Mismatched endArray event"));
114                }
115                depth = depth.saturating_sub(1);
116                if indent > 0
117                    && let JsonContext::Array { needs_comma } = context
118                    && needs_comma
119                {
120                    out.push("\n".to_string());
121                    out.push(" ".repeat(depth * indent));
122                }
123                out.push("]".to_string());
124
125                if let Some(parent) = stack.last_mut() {
126                    match parent {
127                        JsonContext::Object {
128                            needs_comma,
129                            expect_value,
130                        } => {
131                            *expect_value = false;
132                            *needs_comma = true;
133                        }
134                        JsonContext::Array { needs_comma } => {
135                            *needs_comma = true;
136                        }
137                    }
138                }
139            }
140            JsonStreamEvent::Key { key, .. } => {
141                let Some(JsonContext::Object {
142                    needs_comma,
143                    expect_value,
144                }) = stack.last_mut()
145                else {
146                    return Err(ToonError::message("Key event outside of object context"));
147                };
148
149                if *needs_comma {
150                    out.push(",".to_string());
151                }
152                if indent > 0 {
153                    out.push("\n".to_string());
154                    out.push(" ".repeat(depth * indent));
155                }
156
157                out.push(serde_json::to_string(&key).unwrap_or_else(|_| "\"\"".to_string()));
158                out.push(if indent > 0 { ": " } else { ":" }.to_string());
159
160                *expect_value = true;
161                *needs_comma = true;
162            }
163            JsonStreamEvent::Primitive { value } => {
164                if let Some(parent) = stack.last_mut() {
165                    match parent {
166                        JsonContext::Array { needs_comma } => {
167                            if *needs_comma {
168                                out.push(",".to_string());
169                            }
170                            if indent > 0 {
171                                out.push("\n".to_string());
172                                out.push(" ".repeat(depth * indent));
173                            }
174                        }
175                        JsonContext::Object { expect_value, .. } => {
176                            if !*expect_value {
177                                return Err(ToonError::message(
178                                    "Primitive event in object without preceding key",
179                                ));
180                            }
181                        }
182                    }
183                }
184
185                out.push(stringify_primitive(&value));
186
187                if let Some(parent) = stack.last_mut() {
188                    match parent {
189                        JsonContext::Object { expect_value, .. } => {
190                            *expect_value = false;
191                        }
192                        JsonContext::Array { needs_comma } => {
193                            *needs_comma = true;
194                        }
195                    }
196                }
197            }
198        }
199    }
200
201    if !stack.is_empty() {
202        return Err(ToonError::message(
203            "Incomplete event stream: unclosed objects or arrays",
204        ));
205    }
206
207    Ok(out)
208}
209
210fn stringify_primitive(value: &crate::JsonPrimitive) -> String {
211    match value {
212        crate::StringOrNumberOrBoolOrNull::Null => "null".to_string(),
213        crate::StringOrNumberOrBoolOrNull::Bool(value) => value.to_string(),
214        crate::StringOrNumberOrBoolOrNull::Number(value) => serde_json::Number::from_f64(*value)
215            .map_or_else(|| "null".to_string(), |num| num.to_string()),
216        crate::StringOrNumberOrBoolOrNull::String(value) => {
217            serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
218        }
219    }
220}