Skip to main content

lean_ctx/core/
jsonc.rs

1use serde_json::Value;
2
3/// Strip `//` line comments and `/* */` block comments from JSONC,
4/// then parse with serde_json. String contents are preserved verbatim.
5pub fn parse_jsonc(input: &str) -> Result<Value, serde_json::Error> {
6    let stripped = strip_json_comments(input);
7    serde_json::from_str(&stripped)
8}
9
10fn strip_json_comments(input: &str) -> String {
11    let bytes = input.as_bytes();
12    let len = bytes.len();
13    let mut out = String::with_capacity(len);
14    let mut i = 0;
15
16    while i < len {
17        let b = bytes[i];
18
19        if b == b'"' {
20            out.push('"');
21            i += 1;
22            while i < len {
23                let c = bytes[i];
24                out.push(c as char);
25                i += 1;
26                if c == b'\\' && i < len {
27                    out.push(bytes[i] as char);
28                    i += 1;
29                } else if c == b'"' {
30                    break;
31                }
32            }
33            continue;
34        }
35
36        if b == b'/' && i + 1 < len {
37            if bytes[i + 1] == b'/' {
38                i += 2;
39                while i < len && bytes[i] != b'\n' {
40                    i += 1;
41                }
42                continue;
43            }
44            if bytes[i + 1] == b'*' {
45                i += 2;
46                while i + 1 < len {
47                    if bytes[i] == b'*' && bytes[i + 1] == b'/' {
48                        i += 2;
49                        break;
50                    }
51                    i += 1;
52                }
53                continue;
54            }
55        }
56
57        out.push(b as char);
58        i += 1;
59    }
60
61    out
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn strips_line_comments() {
70        let input = r#"{
71  // this is a comment
72  "key": "value"
73}"#;
74        let v = parse_jsonc(input).unwrap();
75        assert_eq!(v["key"], "value");
76    }
77
78    #[test]
79    fn strips_block_comments() {
80        let input = r#"{
81  /* block
82     comment */
83  "key": "value"
84}"#;
85        let v = parse_jsonc(input).unwrap();
86        assert_eq!(v["key"], "value");
87    }
88
89    #[test]
90    fn preserves_slashes_in_strings() {
91        let input = r#"{"url": "https://example.com/path"}"#;
92        let v = parse_jsonc(input).unwrap();
93        assert_eq!(v["url"], "https://example.com/path");
94    }
95
96    #[test]
97    fn preserves_comment_like_content_in_strings() {
98        let input = r#"{"note": "see // inline", "code": "/* not a comment */"}"#;
99        let v = parse_jsonc(input).unwrap();
100        assert_eq!(v["note"], "see // inline");
101        assert_eq!(v["code"], "/* not a comment */");
102    }
103
104    #[test]
105    fn handles_escaped_quotes_in_strings() {
106        let input = r#"{"msg": "say \"hello\" // world"}"#;
107        let v = parse_jsonc(input).unwrap();
108        assert_eq!(v["msg"], r#"say "hello" // world"#);
109    }
110
111    #[test]
112    fn handles_trailing_comma_free_json() {
113        let input = r#"{
114  "a": 1,
115  // comment between entries
116  "b": 2
117}"#;
118        let v = parse_jsonc(input).unwrap();
119        assert_eq!(v["a"], 1);
120        assert_eq!(v["b"], 2);
121    }
122
123    #[test]
124    fn empty_input() {
125        assert!(parse_jsonc("").is_err());
126    }
127
128    #[test]
129    fn pure_json_passthrough() {
130        let input = r#"{"key": "value", "num": 42}"#;
131        let v = parse_jsonc(input).unwrap();
132        assert_eq!(v["key"], "value");
133        assert_eq!(v["num"], 42);
134    }
135
136    #[test]
137    fn real_opencode_config_with_comments() {
138        let input = r#"{
139  // OpenCode configuration
140  "$schema": "https://opencode.ai/config.json",
141  "mcp": {
142    /* existing tool */
143    "my-tool": {
144      "type": "local",
145      "command": ["my-tool"],
146      "enabled": true
147    }
148  }
149}"#;
150        let v = parse_jsonc(input).unwrap();
151        assert_eq!(v["$schema"], "https://opencode.ai/config.json");
152        assert!(v["mcp"]["my-tool"]["enabled"].as_bool().unwrap());
153    }
154}