use serde_json::Value;
pub fn from_str(json_str: &str) -> Result<Value, anyhow::Error> {
let cleaned = tokenize_strip_comments(json_str);
json5::from_str(&cleaned).map_err(|e| anyhow::anyhow!("Failed to parse JSON5: {}", e))
}
fn tokenize_strip_comments(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'"' => {
out.push(ch);
while let Some(c) = chars.next() {
out.push(c);
if c == '\\' {
if let Some(escaped) = chars.next() {
out.push(escaped);
}
} else if c == '"' {
break;
}
}
}
'/' => {
if let Some(kind) = chars.next_if_map(|next| match next {
'/' | '*' => Ok(next),
_ => Err(next),
}) {
if kind == '/' {
for c in chars.by_ref() {
if c == '\n' {
out.push(c);
break;
}
}
} else {
loop {
match chars.next() {
Some('*') => {
if chars.next_if_eq(&'/').is_some() {
break;
}
}
Some(c) if c == '\n' => {
out.push(c);
}
Some(_) => {}
None => break,
}
}
}
} else {
out.push(ch)
}
}
'#' => {
for c in chars.by_ref() {
if c == '\n' {
out.push(c);
break;
}
}
}
c => {
out.push(c);
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_json5_simple() {
let input = r#"{key: "value"}"#;
let expected = json!({"key": "value"});
assert_eq!(from_str(input).unwrap(), expected);
}
#[test]
fn test_parse_json5_comments() {
let input = r#"{
// This is a comment
key: "value" /* block comment */
}"#;
let expected = json!({"key": "value"});
assert_eq!(from_str(input).unwrap(), expected);
}
#[test]
fn test_parse_json5_trailing_comma() {
let input = r#"{
key: "value",
}"#;
let expected = json!({"key": "value"});
assert_eq!(from_str(input).unwrap(), expected);
}
#[test]
fn test_parse_json5_unquoted_keys() {
let input = r#"{
key: "value",
number: 123,
}"#;
let expected = json!({
"key": "value",
"number": 123
});
assert_eq!(from_str(input).unwrap(), expected);
}
#[test]
fn test_parse_hash_comments() {
let input = r#"{
key: "value", # inline comment
num: 1
}"#;
let expected = json!({"key": "value", "num": 1});
assert_eq!(from_str(input).unwrap(), expected);
}
#[test]
fn test_hash_in_string_not_comment() {
let input = r#"{
url: "https://example.com/path#anchor"
}"#;
let expected = json!({"url": "https://example.com/path#anchor"});
assert_eq!(from_str(input).unwrap(), expected);
}
#[test]
fn test_tokenize_inline_slash_comment() {
let input = r#"{
"ipsToDecorations": {
"10.0.0.1": {
"decoration": "web-frontend",
// "environment": "production"
}
}
}"#;
let result = from_str(input).unwrap();
assert_eq!(
result["ipsToDecorations"]["10.0.0.1"]["decoration"],
"web-frontend"
);
}
#[test]
fn test_tokenize_trailing_comment_after_json() {
let input = r#"{
"key": "value"
}
// trailing comment
"#;
let result = from_str(input).unwrap();
assert_eq!(result["key"], "value");
}
#[test]
fn test_tokenize_block_comment_multiline() {
let input = r#"{
/* this is
a multiline
block comment */
"key": "value"
}"#;
let result = from_str(input).unwrap();
assert_eq!(result["key"], "value");
}
#[test]
fn test_tokenize_slash_in_string_preserved() {
let input = r#"{"url": "http://example.com", "path": "a/b/c"}"#;
let result = from_str(input).unwrap();
assert_eq!(result["url"], "http://example.com");
assert_eq!(result["path"], "a/b/c");
}
#[test]
fn test_tokenize_escaped_quotes_in_string() {
let input = r#"{"text": "say \"hello\" // not a comment"}"#;
let result = from_str(input).unwrap();
assert_eq!(result["text"], "say \"hello\" // not a comment");
}
#[test]
fn test_tokenize_hash_preserves_newlines() {
let input = "{\n # comment line 1\n # comment line 2\n \"key\": \"value\"\n}";
let result = from_str(input).unwrap();
assert_eq!(result["key"], "value");
}
}