use serde_json::Value;
#[derive(Debug, Clone, thiserror::Error)]
pub enum PartialJsonError {
#[error("Incomplete JSON: {0}")]
Incomplete(String),
#[error("Invalid JSON: {0}")]
Invalid(String),
}
pub struct PartialJsonParser {
buffer: String,
depth: usize,
in_string: bool,
escape_next: bool,
}
impl PartialJsonParser {
pub fn new() -> Self {
Self {
buffer: String::new(),
depth: 0,
in_string: false,
escape_next: false,
}
}
pub fn push_and_parse(&mut self, token: &str) -> Result<Value, PartialJsonError> {
for ch in token.chars() {
if self.escape_next {
self.escape_next = false;
continue;
}
if ch == '\\' && self.in_string {
self.escape_next = true;
continue;
}
if ch == '"' {
self.in_string = !self.in_string;
continue;
}
if !self.in_string {
match ch {
'{' | '[' => self.depth += 1,
'}' | ']' => {
if self.depth > 0 {
self.depth -= 1;
}
}
_ => {}
}
}
}
if token.is_char_boundary(0) {
self.buffer.push_str(token);
} else {
let mut pos = 0;
while pos < token.len() && !token.is_char_boundary(pos) {
pos += 1;
}
self.buffer.push_str(&token[pos..]);
}
if let Ok(value) = serde_json::from_str::<Value>(&self.buffer) {
return Ok(value);
}
if self.depth > 0
|| self.buffer.trim().starts_with('{')
|| self.buffer.trim().starts_with('[')
{
let repaired = Self::repair_partial_json(&self.buffer);
if let Ok(value) = serde_json::from_str::<Value>(&repaired) {
return Ok(value);
}
}
Err(PartialJsonError::Incomplete(format!(
"Buffer has {} chars, depth={}",
self.buffer.len(),
self.depth
)))
}
pub fn finalize(self) -> Result<Value, PartialJsonError> {
if let Ok(value) = serde_json::from_str::<Value>(&self.buffer) {
return Ok(value);
}
let repaired = Self::repair_partial_json(&self.buffer);
serde_json::from_str::<Value>(&repaired).map_err(|e| {
PartialJsonError::Invalid(format!(
"Failed to parse final buffer ({} chars): {}. Buffer: {}",
self.buffer.len(),
e,
&self.buffer[..std::cmp::min(200, self.buffer.len())]
))
})
}
pub fn buffer(&self) -> &str {
&self.buffer
}
pub fn is_in_string(&self) -> bool {
self.in_string
}
pub fn depth(&self) -> usize {
self.depth
}
pub(crate) fn repair_partial_json(text: &str) -> String {
let mut repaired = text.trim().to_string();
let mut in_string = false;
let mut escape_next = false;
let mut open_braces = 0usize;
let mut close_braces = 0usize;
let mut open_brackets = 0usize;
let mut close_brackets = 0usize;
let mut unescaped_quote_count = 0usize;
for ch in repaired.chars() {
if escape_next {
escape_next = false;
continue;
}
if ch == '\\' && in_string {
escape_next = true;
continue;
}
if ch == '"' {
unescaped_quote_count += 1;
in_string = !in_string;
continue;
}
if !in_string {
match ch {
'{' => open_braces += 1,
'}' => close_braces += 1,
'[' => open_brackets += 1,
']' => close_brackets += 1,
_ => {}
}
}
}
if unescaped_quote_count % 2 != 0 {
repaired.push('"');
}
for _ in close_braces..open_braces {
repaired.push('}');
}
for _ in close_brackets..open_brackets {
repaired.push(']');
}
repaired = Self::remove_trailing_commas(&repaired);
repaired
}
pub(crate) fn remove_trailing_commas(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == ',' && i + 1 < chars.len() {
let next_non_ws = chars[i + 1..].iter().find(|c| !c.is_whitespace());
if next_non_ws == Some(&'}') || next_non_ws == Some(&']') {
i += 1;
continue;
}
}
result.push(chars[i]);
i += 1;
}
result
}
}
impl Default for PartialJsonParser {
fn default() -> Self {
Self::new()
}
}