use anyhow::Result;
use serde_json::Value as JsonValue;
pub fn try_parse_partial_json(partial_json: &str) -> Result<Option<JsonValue>> {
let trimmed = partial_json.trim();
if trimmed.is_empty() {
return Ok(None);
}
let extracted = extract_from_markdown(trimmed);
if let Ok(value) = serde_json::from_str::<JsonValue>(&extracted) {
return Ok(Some(value));
}
let attempts = generate_completion_attempts(&extracted);
for attempt in attempts {
if let Ok(value) = serde_json::from_str::<JsonValue>(&attempt) {
return Ok(Some(value));
}
}
Ok(None)
}
fn extract_from_markdown(text: &str) -> String {
if let Some(start) = text.find("```json") {
let json_start = start + 7;
if let Some(end_offset) = text[json_start..].find("```") {
let json_end = json_start + end_offset;
return text[json_start..json_end].trim().to_string();
}
return text[json_start..].trim().to_string();
}
if let Some(start) = text.find("```") {
let content_start = start + 3;
if let Some(end) = text[content_start..].find("```") {
let content_end = content_start + end;
let content = text[content_start..content_end].trim();
if content.starts_with('{') || content.starts_with('[') {
return content.to_string();
}
} else {
let content = text[content_start..].trim();
if content.starts_with('{') || content.starts_with('[') {
return content.to_string();
}
}
}
if let Some(start) = text.find('{') {
if let Some(end) = text.rfind('}') {
if end > start {
return text[start..=end].to_string();
}
}
return text[start..].to_string();
}
if let Some(start) = text.find('[') {
if let Some(end) = text.rfind(']') {
if end > start {
return text[start..=end].to_string();
}
}
return text[start..].to_string();
}
text.to_string()
}
fn generate_completion_attempts(json: &str) -> Vec<String> {
let json = json.trim();
let mut attempts = Vec::new();
let open_braces = json.matches('{').count();
let close_braces = json.matches('}').count();
let open_brackets = json.matches('[').count();
let close_brackets = json.matches(']').count();
let mut completion = json.to_string();
if has_incomplete_string(&completion) {
completion.push('"');
}
for _ in 0..(open_brackets.saturating_sub(close_brackets)) {
completion.push(']');
}
for _ in 0..(open_braces.saturating_sub(close_braces)) {
completion.push('}');
}
attempts.push(completion);
let mut aggressive = json.to_string();
if json.trim_end().ends_with(':') {
aggressive.push_str("null");
} else if json.trim_end().ends_with(',') {
aggressive = aggressive.trim_end().trim_end_matches(',').to_string();
}
if has_incomplete_string(&aggressive) {
aggressive.push('"');
}
for _ in 0..(open_brackets.saturating_sub(close_brackets)) {
aggressive.push(']');
}
for _ in 0..(open_braces.saturating_sub(close_braces)) {
aggressive.push('}');
}
attempts.push(aggressive);
if let Some(last_comma) = json.rfind(',') {
let mut truncated = json[..=last_comma].to_string();
truncated = truncated.trim_end().trim_end_matches(',').to_string();
for _ in 0..(open_brackets.saturating_sub(close_brackets)) {
truncated.push(']');
}
for _ in 0..(open_braces.saturating_sub(close_braces)) {
truncated.push('}');
}
attempts.push(truncated);
}
attempts
}
fn has_incomplete_string(json: &str) -> bool {
let mut in_string = false;
let mut escape_next = false;
let mut last_quote_pos = None;
for (i, c) in json.chars().enumerate() {
if escape_next {
escape_next = false;
continue;
}
match c {
'\\' if in_string => escape_next = true,
'"' => {
in_string = !in_string;
if in_string {
last_quote_pos = Some(i);
} else {
last_quote_pos = None;
}
}
_ => {}
}
}
in_string && last_quote_pos.is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_complete_json() {
let json = r#"{"name": "John", "age": 30}"#;
let result = try_parse_partial_json(json).unwrap();
assert!(result.is_some());
let value = result.unwrap();
assert_eq!(value["name"], "John");
assert_eq!(value["age"], 30);
}
#[test]
fn test_incomplete_object() {
let partial = r#"{"name": "John", "age": 30"#;
let result = try_parse_partial_json(partial).unwrap();
assert!(result.is_some());
let value = result.unwrap();
assert_eq!(value["name"], "John");
assert_eq!(value["age"], 30);
}
#[test]
fn test_incomplete_string() {
let partial = r#"{"name": "Joh"#;
let result = try_parse_partial_json(partial).unwrap();
assert!(result.is_some());
let value = result.unwrap();
assert_eq!(value["name"], "Joh");
}
#[test]
fn test_incomplete_array() {
let partial = r#"{"items": [1, 2, 3"#;
let result = try_parse_partial_json(partial).unwrap();
assert!(result.is_some());
let value = result.unwrap();
assert_eq!(value["items"].as_array().unwrap().len(), 3);
}
#[test]
fn test_nested_incomplete() {
let partial = r#"{"person": {"name": "John", "age": 30"#;
let result = try_parse_partial_json(partial).unwrap();
assert!(result.is_some());
let value = result.unwrap();
assert_eq!(value["person"]["name"], "John");
assert_eq!(value["person"]["age"], 30);
}
#[test]
fn test_markdown_extraction() {
let partial = r#"Here's the data:
```json
{"name": "John", "age": 30
```"#;
let result = try_parse_partial_json(partial).unwrap();
assert!(result.is_some());
}
#[test]
fn test_markdown_incomplete() {
let partial = r#"```json
{"name": "John", "age": 30"#;
let result = try_parse_partial_json(partial).unwrap();
assert!(result.is_some());
}
#[test]
fn test_trailing_comma() {
let partial = r#"{"name": "John", "age": 30,"#;
let result = try_parse_partial_json(partial).unwrap();
assert!(result.is_some());
let value = result.unwrap();
assert_eq!(value["name"], "John");
}
#[test]
fn test_empty_input() {
let result = try_parse_partial_json("").unwrap();
assert!(result.is_none());
}
#[test]
fn test_incomplete_field_name() {
let partial = r#"{"name": "John", "ag"#;
let _result = try_parse_partial_json(partial).unwrap();
}
}