pub fn sanitize_json(input: &str) -> String {
let trimmed = input.trim();
if trimmed.is_empty() {
return String::new();
}
let with_delimiters = fix_missing_delimiters(trimmed);
remove_trailing_commas(&with_delimiters)
}
fn remove_trailing_commas(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
let mut in_string = false;
let mut escape_next = false;
while let Some(c) = chars.next() {
if escape_next {
result.push(c);
escape_next = false;
continue;
}
match c {
'\\' if in_string => {
result.push(c);
escape_next = true;
}
'"' => {
in_string = !in_string;
result.push(c);
}
',' if !in_string => {
let mut peek_iter = chars.clone();
let next_non_ws = loop {
match peek_iter.next() {
Some(ws) if ws.is_whitespace() => continue,
other => break other,
}
};
if matches!(next_non_ws, Some('}') | Some(']')) {
continue;
}
result.push(c);
}
_ => {
result.push(c);
}
}
}
result
}
fn fix_missing_delimiters(input: &str) -> String {
let mut result = String::from(input);
let mut in_string = false;
let mut escape_next = false;
let mut stack: Vec<char> = Vec::new();
for c in input.chars() {
if escape_next {
escape_next = false;
continue;
}
match c {
'\\' if in_string => {
escape_next = true;
}
'"' => {
in_string = !in_string;
}
'{' if !in_string => {
stack.push('{');
}
'[' if !in_string => {
stack.push('[');
}
'}' if !in_string => {
if let Some(&top) = stack.last() {
if top == '{' {
stack.pop();
}
}
}
']' if !in_string => {
if let Some(&top) = stack.last() {
if top == '[' {
stack.pop();
}
}
}
_ => {}
}
}
if in_string {
result.push('"');
}
for &opener in stack.iter().rev() {
match opener {
'{' => result.push('}'),
'[' => result.push(']'),
_ => {}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trailing_comma_object() {
assert_eq!(sanitize_json(r#"{"a": 1,}"#), r#"{"a": 1}"#);
}
#[test]
fn test_trailing_comma_array() {
assert_eq!(sanitize_json(r#"[1, 2, 3,]"#), r#"[1, 2, 3]"#);
}
#[test]
fn test_trailing_comma_nested_object() {
assert_eq!(
sanitize_json(r#"{"outer": {"inner": 1,},}"#),
r#"{"outer": {"inner": 1}}"#
);
}
#[test]
fn test_trailing_comma_nested_array() {
assert_eq!(sanitize_json(r#"[[1, 2,], [3,],]"#), r#"[[1, 2], [3]]"#);
}
#[test]
fn test_trailing_comma_mixed() {
assert_eq!(
sanitize_json(r#"{"items": [1, 2,], "name": "test",}"#),
r#"{"items": [1, 2], "name": "test"}"#
);
}
#[test]
fn test_trailing_comma_with_whitespace() {
assert_eq!(sanitize_json(r#"{"a": 1 , }"#), r#"{"a": 1 }"#);
assert_eq!(sanitize_json("{\n \"a\": 1,\n}"), "{\n \"a\": 1\n}");
}
#[test]
fn test_comma_in_string_preserved() {
assert_eq!(
sanitize_json(r#"{"msg": "hello, world"}"#),
r#"{"msg": "hello, world"}"#
);
assert_eq!(sanitize_json(r#"{"msg": "a,}"}"#), r#"{"msg": "a,}"}"#);
}
#[test]
fn test_no_trailing_comma() {
assert_eq!(sanitize_json(r#"{"a": 1}"#), r#"{"a": 1}"#);
assert_eq!(sanitize_json(r#"[1, 2, 3]"#), r#"[1, 2, 3]"#);
}
#[test]
fn test_missing_closing_brace() {
assert_eq!(sanitize_json(r#"{"a": 1"#), r#"{"a": 1}"#);
}
#[test]
fn test_missing_closing_bracket() {
assert_eq!(sanitize_json(r#"["a", "b""#), r#"["a", "b"]"#);
}
#[test]
fn test_missing_multiple_braces() {
assert_eq!(sanitize_json(r#"{"a": {"b": 1"#), r#"{"a": {"b": 1}}"#);
}
#[test]
fn test_missing_multiple_brackets() {
assert_eq!(sanitize_json(r#"[[1, 2], [3"#), r#"[[1, 2], [3]]"#);
}
#[test]
fn test_missing_mixed_delimiters() {
assert_eq!(sanitize_json(r#"{"items": [1, 2"#), r#"{"items": [1, 2]}"#);
}
#[test]
fn test_brace_in_string_ignored() {
assert_eq!(sanitize_json(r#"{"msg": "{"}"#), r#"{"msg": "{"}"#);
}
#[test]
fn test_no_missing_delimiters() {
assert_eq!(sanitize_json(r#"{"a": 1}"#), r#"{"a": 1}"#);
assert_eq!(sanitize_json(r#"[1, 2]"#), r#"[1, 2]"#);
}
#[test]
fn test_trailing_comma_and_missing_brace() {
assert_eq!(sanitize_json(r#"{"a": 1,"#), r#"{"a": 1}"#);
}
#[test]
fn test_trailing_comma_and_missing_bracket() {
assert_eq!(sanitize_json(r#"[1, 2,"#), r#"[1, 2]"#);
}
#[test]
fn test_complex_llm_output() {
let input = r#"{
"type": "AddDerive",
"target": "User",
"derives": ["Debug", "Clone",],
"#;
let expected = r#"{
"type": "AddDerive",
"target": "User",
"derives": ["Debug", "Clone"]}"#;
assert_eq!(sanitize_json(input), expected);
}
#[test]
fn test_empty_input() {
assert_eq!(sanitize_json(""), "");
assert_eq!(sanitize_json(" "), "");
}
#[test]
fn test_whitespace_only() {
assert_eq!(sanitize_json(" \n\t "), "");
}
#[test]
fn test_simple_values() {
assert_eq!(sanitize_json("null"), "null");
assert_eq!(sanitize_json("true"), "true");
assert_eq!(sanitize_json("123"), "123");
assert_eq!(sanitize_json(r#""string""#), r#""string""#);
}
#[test]
fn test_escaped_quote_in_string() {
assert_eq!(
sanitize_json(r#"{"msg": "say \"hello\""}"#),
r#"{"msg": "say \"hello\""}"#
);
}
#[test]
fn test_escaped_backslash_in_string() {
assert_eq!(
sanitize_json(r#"{"path": "C:\\Users\\test"}"#),
r#"{"path": "C:\\Users\\test"}"#
);
}
#[test]
fn test_unclosed_string() {
assert_eq!(sanitize_json(r#"{"a": "test"#), r#"{"a": "test"}"#);
}
#[test]
fn test_deeply_nested() {
assert_eq!(
sanitize_json(r#"{"a": {"b": {"c": [1, 2,],"#),
r#"{"a": {"b": {"c": [1, 2]}}}"#
);
}
#[test]
fn test_llm_truncated_response() {
let input = r#"{"type": "RenameIdent", "from": "old_name", "to": "new_na"#;
let fixed = sanitize_json(input);
assert_eq!(
fixed,
r#"{"type": "RenameIdent", "from": "old_name", "to": "new_na"}"#
);
}
#[test]
fn test_llm_array_with_trailing_comma() {
let input = r#"{"intents": [
{"type": "AddDerive", "target": "User",},
{"type": "AddDerive", "target": "Post",},
]}"#;
let fixed = sanitize_json(input);
assert!(fixed.contains(r#""target": "User"}"#));
assert!(fixed.contains(r#""target": "Post"}"#));
assert!(!fixed.contains(",}"));
assert!(!fixed.contains(",]"));
}
}