Skip to main content

apif_parser/
json_stream_parser.rs

1//! Streaming JSON parser for RESPONSE sections.
2//!
3//! Parses multiple JSON values from a single section block by tracking
4//! depth, string state, and escape characters — a state-machine approach
5//! that handles JSON5 with comments.
6
7use crate::json_mod;
8
9/// Parse multiple JSON values from a single content string.
10///
11/// Used for streaming response sections where multiple JSON objects
12/// are concatenated (e.g., NDJSON or concatenated JSON5).
13///
14/// Returns `Some(values)` if 2+ values were successfully parsed, `None` otherwise.
15pub fn parse_response_json_values(content: &str) -> Option<Vec<serde_json::Value>> {
16    let mut values = Vec::new();
17    let mut current_lines: Vec<&str> = Vec::new();
18    let mut depth: i32 = 0;
19    let mut in_string = false;
20    let mut escaped = false;
21    let mut started = false;
22
23    for line in content.lines() {
24        let trimmed = line.trim();
25        if trimmed.is_empty() && current_lines.is_empty() {
26            continue;
27        }
28
29        current_lines.push(line);
30
31        let mut chars = line.chars().peekable();
32        while let Some(ch) = chars.next() {
33            if escaped {
34                escaped = false;
35                continue;
36            }
37
38            if ch == '\\' {
39                escaped = true;
40                continue;
41            }
42
43            if ch == '"' {
44                in_string = !in_string;
45                started = true;
46                continue;
47            }
48
49            if in_string {
50                continue;
51            }
52
53            if ch == '#' {
54                break;
55            }
56            if ch == '/' && chars.next_if_eq(&'/').is_some() {
57                break;
58            }
59
60            match ch {
61                '{' | '[' => {
62                    depth += 1;
63                    started = true;
64                }
65                '}' | ']' => {
66                    depth -= 1;
67                    started = true;
68                    if depth < 0 {
69                        return None;
70                    }
71                }
72                c if !c.is_whitespace() => {
73                    started = true;
74                }
75                _ => {}
76            }
77        }
78
79        if started && depth == 0 {
80            let chunk = current_lines.join("\n");
81            let chunk = chunk.trim();
82            if chunk.is_empty() {
83                current_lines.clear();
84                started = false;
85                continue;
86            }
87
88            let value = json_mod::from_str(chunk).ok()?;
89            values.push(value);
90            current_lines.clear();
91            started = false;
92        }
93    }
94
95    if !current_lines.is_empty() {
96        return None;
97    }
98
99    if values.len() >= 2 {
100        Some(values)
101    } else {
102        None
103    }
104}