use regex::Regex;
pub struct JSONExtractor;
impl JSONExtractor {
pub fn new() -> Self {
Self
}
pub fn extract_from_response(&self, response: &str) -> String {
const PATTERNS: &[(&str, bool)] = &[
(r#"```json\s*([\s\S]*?)\s*```"#, true), (r#"```\s*(\{[\s\S]*?\})\s*```"#, true), (r#"```\s*(\[[\s\S]*?\])\s*```"#, true), ];
for (pattern, dotall) in PATTERNS {
if let Some(json) = self.try_extract(response, pattern, *dotall) {
return json;
}
}
if let Some(json) = self.find_complete_json_object(response) {
return json;
}
response.to_string()
}
fn try_extract(&self, text: &str, pattern: &str, dotall: bool) -> Option<String> {
let regex = if dotall {
Regex::new(pattern).ok()?
} else {
Regex::new(pattern).ok()?
};
if let Some(captures) = regex.captures(text) {
if let Some(matched) = captures.get(1) {
let extracted = matched.as_str().trim();
if self.looks_like_json(extracted) {
return Some(extracted.to_string());
}
}
}
None
}
fn looks_like_json(&self, s: &str) -> bool {
let trimmed = s.trim();
trimmed.starts_with('{') && trimmed.ends_with('}')
|| trimmed.starts_with('[') && trimmed.ends_with(']')
}
fn find_complete_json_object(&self, text: &str) -> Option<String> {
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
let mut start = None;
for (i, &c) in chars.iter().enumerate() {
if c == '{' {
start = Some(i);
break;
}
}
let start = start?;
let mut brace_count = 0;
let mut in_string = false;
let mut escape_next = false;
for i in start..len {
let c = chars[i];
if escape_next {
escape_next = false;
continue;
}
if c == '\\' {
escape_next = true;
continue;
}
if c == '"' {
in_string = !in_string;
continue;
}
if !in_string {
if c == '{' {
brace_count += 1;
} else if c == '}' {
brace_count -= 1;
if brace_count == 0 {
let json_str: String = chars[start..=i].iter().collect();
return Some(json_str);
}
}
}
}
None
}
}
impl Default for JSONExtractor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_json_from_code_block() {
let extractor = JSONExtractor::new();
let response = r#"Here's the JSON:
```json
{"key": "value"}
```
"#;
let result = extractor.extract_from_response(response);
assert!(result.contains("key") && result.contains("value"));
}
#[test]
fn test_extract_plain_json() {
let extractor = JSONExtractor::new();
let response = r#"The result is {"key": "value"}"#;
let result = extractor.extract_from_response(response);
assert!(result.contains("key"));
}
#[test]
fn test_extract_nested_json() {
let extractor = JSONExtractor::new();
let response = r#"Here's the result: {"action": [{"type": "navigate", "params": {"url": "https://example.com"}}]}"#;
let result = extractor.extract_from_response(response);
assert!(result.contains("action"));
assert!(result.contains("navigate"));
}
#[test]
fn test_fallback_to_original() {
let extractor = JSONExtractor::new();
let response = "No JSON here, just plain text";
let result = extractor.extract_from_response(response);
assert_eq!(result, "No JSON here, just plain text");
}
}