use serde_json::Value;
use rho_sdk::floor_char_boundary;
use crate::cli_runtime::stream_effect::MAX_TOOL_PAYLOAD_CHARS;
pub(super) const MAX_INPUT_JSON_CHARS: usize = MAX_TOOL_PAYLOAD_CHARS.saturating_mul(16);
const EAGER_PARSE_CHARS: usize = MAX_TOOL_PAYLOAD_CHARS;
#[derive(Debug, Clone, Default, PartialEq)]
pub(super) struct StreamedInputJson {
raw: String,
scan: JsonScan,
closed: bool,
}
impl StreamedInputJson {
pub(super) fn push(&mut self, fragment: &str) -> Option<Value> {
if fragment.is_empty() || self.closed {
return None;
}
let room = MAX_INPUT_JSON_CHARS.saturating_sub(self.raw.len());
let end = floor_char_boundary(fragment, room);
if end == 0 {
return None;
}
let appended = &fragment[..end];
let truncated = end < fragment.len();
self.raw.push_str(appended);
let crossed_boundary = self.scan.advance(appended);
let due = crossed_boundary || truncated || self.raw.len() <= EAGER_PARSE_CHARS;
if !due {
return None;
}
let parsed = parse_assembled_input(&self.raw);
if crossed_boundary && self.scan.depth == 0 {
self.closed = true;
self.raw = String::new();
}
parsed
}
#[cfg(test)]
pub(super) fn len(&self) -> usize {
self.raw.len()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct JsonScan {
depth: usize,
in_string: bool,
escaped: bool,
}
impl JsonScan {
fn advance(&mut self, text: &str) -> bool {
let mut crossed = false;
for byte in text.bytes() {
if self.in_string {
match byte {
_ if self.escaped => self.escaped = false,
b'\\' => self.escaped = true,
b'"' => self.in_string = false,
_ => {}
}
continue;
}
match byte {
b'"' => self.in_string = true,
b'{' | b'[' => self.depth += 1,
b'}' | b']' => {
self.depth = self.depth.saturating_sub(1);
crossed |= self.depth == 0;
}
b',' => crossed |= self.depth == 1,
_ => {}
}
}
crossed
}
}
fn parse_assembled_input(raw: &str) -> Option<Value> {
if let Ok(value) = serde_json::from_str(raw) {
return Some(value);
}
for suffix in ["}", "\"}"] {
if let Ok(value) = serde_json::from_str::<Value>(&format!("{raw}{suffix}")) {
if value.is_object() {
return Some(value);
}
}
}
None
}
#[cfg(test)]
#[path = "input_json_tests.rs"]
mod tests;