use regex::Regex;
use std::sync::LazyLock;
static TRAILING_COMMA: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r",(\s*[\]}])").expect("TRAILING_COMMA is a compile-time-constant regex")
});
pub fn parse_and_fix(payload: &str) -> Result<Vec<serde_json::Value>, crate::error::A2uiError> {
let normalized = normalize_smart_quotes(payload);
match parse_inner(&normalized) {
Ok(vals) => Ok(vals),
Err(first_err) => {
let fixed = TRAILING_COMMA.replace_all(&normalized, "$1").into_owned();
if fixed == normalized {
return Err(first_err);
}
parse_inner(&fixed).map_err(|_| first_err)
}
}
}
fn parse_inner(payload: &str) -> Result<Vec<serde_json::Value>, crate::error::A2uiError> {
let value: serde_json::Value = serde_json::from_str(payload)?;
match value {
serde_json::Value::Array(arr) => Ok(arr),
other => {
if other.is_object() {
Ok(vec![other])
} else {
Err(crate::error::A2uiError::Validation(
"payload is not a JSON list or object".into(),
))
}
}
}
}
fn normalize_smart_quotes(s: &str) -> String {
s.replace('\u{201C}', "\"")
.replace('\u{201D}', "\"")
.replace('\u{2018}', "'")
.replace('\u{2019}', "'")
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn clean_json_passes_through() {
let payload = r#"[{"id":"root","component":"Text"}]"#;
let vals = parse_and_fix(payload).unwrap();
assert_eq!(vals.len(), 1);
assert_eq!(vals[0]["id"], json!("root"));
}
#[test]
fn smart_quotes_normalized() {
let payload = "[{\u{201C}id\u{201D}: \u{201C}root\u{201D}}]";
let vals = parse_and_fix(payload).unwrap();
assert_eq!(vals[0]["id"], json!("root"));
}
#[test]
fn trailing_comma_removed() {
let payload = r#"[{"id":"root",},{"id":"c1",}]"#;
let vals = parse_and_fix(payload).unwrap();
assert_eq!(vals.len(), 2);
assert_eq!(vals[0]["id"], json!("root"));
assert_eq!(vals[1]["id"], json!("c1"));
}
#[test]
fn single_object_wrapped_in_list() {
let payload = r#"{"id":"root","component":"Text"}"#;
let vals = parse_and_fix(payload).unwrap();
assert_eq!(vals.len(), 1);
assert_eq!(vals[0]["id"], json!("root"));
}
#[test]
fn broken_json_errors() {
let payload = r#"not json at all {{{"#;
assert!(parse_and_fix(payload).is_err());
}
}