1use serde_json::Value;
2
3pub fn from_str(json_str: &str) -> Result<Value, anyhow::Error> {
6 let cleaned = tokenize_strip_comments(json_str);
7 json5::from_str(&cleaned).map_err(|e| anyhow::anyhow!("Failed to parse JSON5: {}", e))
8}
9
10fn tokenize_strip_comments(input: &str) -> String {
18 let mut out = String::with_capacity(input.len());
19 let mut chars = input.chars().peekable();
20
21 while let Some(ch) = chars.next() {
22 match ch {
23 '"' => {
24 out.push(ch);
25 while let Some(c) = chars.next() {
26 out.push(c);
27 if c == '\\' {
28 if let Some(escaped) = chars.next() {
29 out.push(escaped);
30 }
31 } else if c == '"' {
32 break;
33 }
34 }
35 }
36 '/' => {
37 if let Some(kind) = chars.next_if_map(|next| match next {
38 '/' | '*' => Ok(next),
39 _ => Err(next),
40 }) {
41 if kind == '/' {
42 for c in chars.by_ref() {
44 if c == '\n' {
45 out.push(c);
46 break;
47 }
48 }
49 } else {
50 loop {
52 match chars.next() {
53 Some('*') => {
54 if chars.next_if_eq(&'/').is_some() {
55 break;
56 }
57 }
58 Some(c) if c == '\n' => {
59 out.push(c);
60 }
61 Some(_) => {}
62 None => break,
63 }
64 }
65 }
66 } else {
67 out.push(ch)
68 }
69 }
70 '#' => {
71 for c in chars.by_ref() {
73 if c == '\n' {
74 out.push(c);
75 break;
76 }
77 }
78 }
79 c => {
80 out.push(c);
81 }
82 }
83 }
84
85 out
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use serde_json::json;
92
93 #[test]
94 fn test_parse_json5_simple() {
95 let input = r#"{key: "value"}"#;
96 let expected = json!({"key": "value"});
97 assert_eq!(from_str(input).unwrap(), expected);
98 }
99
100 #[test]
101 fn test_parse_json5_comments() {
102 let input = r#"{
103 // This is a comment
104 key: "value" /* block comment */
105 }"#;
106 let expected = json!({"key": "value"});
107 assert_eq!(from_str(input).unwrap(), expected);
108 }
109
110 #[test]
111 fn test_parse_json5_trailing_comma() {
112 let input = r#"{
113 key: "value",
114 }"#;
115 let expected = json!({"key": "value"});
116 assert_eq!(from_str(input).unwrap(), expected);
117 }
118
119 #[test]
120 fn test_parse_json5_unquoted_keys() {
121 let input = r#"{
122 key: "value",
123 number: 123,
124 }"#;
125 let expected = json!({
126 "key": "value",
127 "number": 123
128 });
129 assert_eq!(from_str(input).unwrap(), expected);
130 }
131
132 #[test]
133 fn test_parse_hash_comments() {
134 let input = r#"{
135 key: "value", # inline comment
136 num: 1
137 }"#;
138 let expected = json!({"key": "value", "num": 1});
139 assert_eq!(from_str(input).unwrap(), expected);
140 }
141
142 #[test]
143 fn test_hash_in_string_not_comment() {
144 let input = r#"{
145 url: "https://example.com/path#anchor"
146 }"#;
147 let expected = json!({"url": "https://example.com/path#anchor"});
148 assert_eq!(from_str(input).unwrap(), expected);
149 }
150
151 #[test]
152 fn test_tokenize_inline_slash_comment() {
153 let input = r#"{
154 "ipsToDecorations": {
155 "10.0.0.1": {
156 "decoration": "web-frontend",
157 // "environment": "production"
158 }
159 }
160}"#;
161 let result = from_str(input).unwrap();
162 assert_eq!(
163 result["ipsToDecorations"]["10.0.0.1"]["decoration"],
164 "web-frontend"
165 );
166 }
167
168 #[test]
169 fn test_tokenize_trailing_comment_after_json() {
170 let input = r#"{
171 "key": "value"
172}
173// trailing comment
174"#;
175 let result = from_str(input).unwrap();
176 assert_eq!(result["key"], "value");
177 }
178
179 #[test]
180 fn test_tokenize_block_comment_multiline() {
181 let input = r#"{
182 /* this is
183 a multiline
184 block comment */
185 "key": "value"
186}"#;
187 let result = from_str(input).unwrap();
188 assert_eq!(result["key"], "value");
189 }
190
191 #[test]
192 fn test_tokenize_slash_in_string_preserved() {
193 let input = r#"{"url": "http://example.com", "path": "a/b/c"}"#;
194 let result = from_str(input).unwrap();
195 assert_eq!(result["url"], "http://example.com");
196 assert_eq!(result["path"], "a/b/c");
197 }
198
199 #[test]
200 fn test_tokenize_escaped_quotes_in_string() {
201 let input = r#"{"text": "say \"hello\" // not a comment"}"#;
202 let result = from_str(input).unwrap();
203 assert_eq!(result["text"], "say \"hello\" // not a comment");
204 }
205
206 #[test]
207 fn test_tokenize_hash_preserves_newlines() {
208 let input = "{\n # comment line 1\n # comment line 2\n \"key\": \"value\"\n}";
209 let result = from_str(input).unwrap();
210 assert_eq!(result["key"], "value");
211 }
212}