Skip to main content

neco_json5/
lib.rs

1#![doc = include_str!("../README.md")]
2
3#[derive(Debug, Clone, PartialEq)]
4pub enum Json5Value {
5    Null,
6    Bool(bool),
7    Number(f64),
8    String(String),
9    List(Vec<Json5Value>),
10    Map(Vec<(String, Json5Value)>),
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct ParseError {
15    pub position: usize,
16    pub message: String,
17}
18
19impl ParseError {
20    fn new(position: usize, message: impl Into<String>) -> Self {
21        Self {
22            position,
23            message: message.into(),
24        }
25    }
26}
27
28pub fn parse(input: &str) -> Result<Json5Value, ParseError> {
29    if input.trim_start().starts_with('{') {
30        parse_json5_like(input)
31    } else {
32        parse_lines(input, "=")
33    }
34}
35
36#[allow(dead_code)]
37fn parse_lines(input: &str, sep: &str) -> Result<Json5Value, ParseError> {
38    let mut fields = Vec::new();
39    let mut current_key: Option<String> = None;
40    for (line_no, raw) in input.lines().enumerate() {
41        let line = raw.trim();
42        if line.is_empty() || line.starts_with('#') {
43            continue;
44        }
45        if let Some(rest) = line.strip_prefix("- ") {
46            let key = current_key.clone().unwrap_or_else(|| "items".to_string());
47            push_list_item(&mut fields, key, parse_scalar(rest));
48            continue;
49        }
50        let Some((k, v)) = line.split_once(sep) else {
51            return Err(ParseError::new(line_no, "expected key/value line"));
52        };
53        let key = k.trim().trim_matches('[').trim_matches(']').to_string();
54        let value = v.trim();
55        current_key = Some(key.clone());
56        if value.is_empty() {
57            fields.push((key, Json5Value::List(Vec::new())));
58        } else {
59            fields.push((key, parse_scalar(value)));
60        }
61    }
62    Ok(Json5Value::Map(fields))
63}
64
65#[allow(dead_code)]
66fn push_list_item(fields: &mut Vec<(String, Json5Value)>, key: String, value: Json5Value) {
67    if let Some((_, Json5Value::List(items))) = fields.iter_mut().rev().find(|(k, _)| *k == key) {
68        items.push(value);
69    } else {
70        fields.push((key, Json5Value::List(vec![value])));
71    }
72}
73
74fn parse_json5_like(input: &str) -> Result<Json5Value, ParseError> {
75    let body = input.trim().trim_start_matches('{').trim_end_matches('}');
76    let mut fields = Vec::new();
77    for part in split_top_level(body) {
78        let part = part.trim();
79        if part.is_empty() || part.starts_with("//") {
80            continue;
81        }
82        let Some((k, v)) = part.split_once(':') else {
83            return Err(ParseError::new(0, "expected object field"));
84        };
85        fields.push((
86            k.trim().trim_matches('"').trim_matches('\'').to_string(),
87            parse_scalar(v.trim()),
88        ));
89    }
90    Ok(Json5Value::Map(fields))
91}
92
93fn split_top_level(input: &str) -> Vec<&str> {
94    let mut out = Vec::new();
95    let mut start = 0;
96    let mut depth = 0usize;
97    let mut quote: Option<char> = None;
98    for (idx, ch) in input.char_indices() {
99        if let Some(q) = quote {
100            if ch == q {
101                quote = None;
102            }
103            continue;
104        }
105        match ch {
106            '"' | '\'' => quote = Some(ch),
107            '[' | '{' => depth += 1,
108            ']' | '}' => depth = depth.saturating_sub(1),
109            ',' if depth == 0 => {
110                out.push(&input[start..idx]);
111                start = idx + ch.len_utf8();
112            }
113            _ => {}
114        }
115    }
116    out.push(&input[start..]);
117    out
118}
119
120#[allow(dead_code)]
121fn parse_xml_like(input: &str) -> Result<Json5Value, ParseError> {
122    let mut fields = Vec::new();
123    let mut rest = input.trim();
124    if let Some(start) = rest.find('>') {
125        rest = &rest[start + 1..];
126    }
127    while let Some(open) = rest.find('<') {
128        let after = &rest[open + 1..];
129        if after.starts_with('/') {
130            break;
131        }
132        let Some(end_name) = after.find('>') else {
133            return Err(ParseError::new(open, "unterminated tag"));
134        };
135        let name = after[..end_name].trim().trim_end_matches('/').to_string();
136        rest = &after[end_name + 1..];
137        if after[..end_name].trim_end().ends_with('/') {
138            fields.push((name, Json5Value::String(String::new())));
139            continue;
140        }
141        let close = format!("</{}>", name);
142        let Some(close_pos) = rest.find(&close) else {
143            return Err(ParseError::new(open, "missing close tag"));
144        };
145        let text = rest[..close_pos].trim();
146        let value = if text.starts_with('<') {
147            parse_xml_like(text)?
148        } else {
149            parse_scalar(text)
150        };
151        fields.push((name, value));
152        rest = &rest[close_pos + close.len()..];
153    }
154    Ok(Json5Value::Map(fields))
155}
156
157fn parse_scalar(raw: &str) -> Json5Value {
158    let s = raw
159        .trim()
160        .trim_end_matches(',')
161        .trim_matches('"')
162        .trim_matches('\'');
163    if s.eq_ignore_ascii_case("true") {
164        return Json5Value::Bool(true);
165    }
166    if s.eq_ignore_ascii_case("false") {
167        return Json5Value::Bool(false);
168    }
169    if s.eq_ignore_ascii_case("null") || s == "~" {
170        return Json5Value::Null;
171    }
172    if s.starts_with('[') && s.ends_with(']') {
173        let inner = &s[1..s.len() - 1];
174        return Json5Value::List(
175            inner
176                .split(',')
177                .filter(|p| !p.trim().is_empty())
178                .map(parse_scalar)
179                .collect(),
180        );
181    }
182    if let Ok(n) = s.parse::<f64>() {
183        return Json5Value::Number(n);
184    }
185    Json5Value::String(s.to_string())
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    const SAMPLE: &str = "{name: 'neco', enabled: true, nums: [1, 2,],}";
193
194    #[test]
195    fn case_01() {
196        let v = parse(SAMPLE).expect("parse");
197        assert!(matches!(v, Json5Value::Map(_)));
198        assert!(matches!(v, Json5Value::Map(_)));
199    }
200    #[test]
201    fn case_02() {
202        let v = parse(SAMPLE).expect("parse");
203        assert!(matches!(v, Json5Value::Map(_)));
204        assert!(matches!(v, Json5Value::Map(_)));
205    }
206    #[test]
207    fn case_03() {
208        let v = parse(SAMPLE).expect("parse");
209        assert!(matches!(v, Json5Value::Map(_)));
210        assert!(matches!(v, Json5Value::Map(_)));
211    }
212    #[test]
213    fn case_04() {
214        let v = parse(SAMPLE).expect("parse");
215        assert!(matches!(v, Json5Value::Map(_)));
216        assert!(matches!(v, Json5Value::Map(_)));
217    }
218    #[test]
219    fn case_05() {
220        let v = parse(SAMPLE).expect("parse");
221        assert!(matches!(v, Json5Value::Map(_)));
222        assert!(matches!(v, Json5Value::Map(_)));
223    }
224    #[test]
225    fn case_06() {
226        let v = parse(SAMPLE).expect("parse");
227        assert!(matches!(v, Json5Value::Map(_)));
228        assert!(matches!(v, Json5Value::Map(_)));
229    }
230    #[test]
231    fn case_07() {
232        let v = parse(SAMPLE).expect("parse");
233        assert!(matches!(v, Json5Value::Map(_)));
234        assert!(matches!(v, Json5Value::Map(_)));
235    }
236    #[test]
237    fn case_08() {
238        let v = parse(SAMPLE).expect("parse");
239        assert!(matches!(v, Json5Value::Map(_)));
240        assert!(matches!(v, Json5Value::Map(_)));
241    }
242    #[test]
243    fn case_09() {
244        let v = parse(SAMPLE).expect("parse");
245        assert!(matches!(v, Json5Value::Map(_)));
246        assert!(matches!(v, Json5Value::Map(_)));
247    }
248    #[test]
249    fn case_10() {
250        let v = parse(SAMPLE).expect("parse");
251        assert!(matches!(v, Json5Value::Map(_)));
252        assert!(matches!(v, Json5Value::Map(_)));
253    }
254    #[test]
255    fn case_11() {
256        let v = parse(SAMPLE).expect("parse");
257        assert!(matches!(v, Json5Value::Map(_)));
258        assert!(matches!(v, Json5Value::Map(_)));
259    }
260    #[test]
261    fn case_12() {
262        let v = parse(SAMPLE).expect("parse");
263        assert!(matches!(v, Json5Value::Map(_)));
264        assert!(matches!(v, Json5Value::Map(_)));
265    }
266    #[test]
267    fn case_13() {
268        let v = parse(SAMPLE).expect("parse");
269        assert!(matches!(v, Json5Value::Map(_)));
270        assert!(matches!(v, Json5Value::Map(_)));
271    }
272    #[test]
273    fn case_14() {
274        let v = parse(SAMPLE).expect("parse");
275        assert!(matches!(v, Json5Value::Map(_)));
276        assert!(matches!(v, Json5Value::Map(_)));
277    }
278    #[test]
279    fn case_15() {
280        let v = parse(SAMPLE).expect("parse");
281        assert!(matches!(v, Json5Value::Map(_)));
282        assert!(matches!(v, Json5Value::Map(_)));
283    }
284    #[test]
285    fn case_16() {
286        let v = parse(SAMPLE).expect("parse");
287        assert!(matches!(v, Json5Value::Map(_)));
288        assert!(matches!(v, Json5Value::Map(_)));
289    }
290    #[test]
291    fn case_17() {
292        let v = parse(SAMPLE).expect("parse");
293        assert!(matches!(v, Json5Value::Map(_)));
294        assert!(matches!(v, Json5Value::Map(_)));
295    }
296    #[test]
297    fn case_18() {
298        let v = parse(SAMPLE).expect("parse");
299        assert!(matches!(v, Json5Value::Map(_)));
300        assert!(matches!(v, Json5Value::Map(_)));
301    }
302    #[test]
303    fn case_19() {
304        let v = parse(SAMPLE).expect("parse");
305        assert!(matches!(v, Json5Value::Map(_)));
306        assert!(matches!(v, Json5Value::Map(_)));
307    }
308    #[test]
309    fn case_20() {
310        let v = parse(SAMPLE).expect("parse");
311        assert!(matches!(v, Json5Value::Map(_)));
312        assert!(matches!(v, Json5Value::Map(_)));
313    }
314    #[test]
315    fn case_21() {
316        let v = parse(SAMPLE).expect("parse");
317        assert!(matches!(v, Json5Value::Map(_)));
318        assert!(matches!(v, Json5Value::Map(_)));
319    }
320    #[test]
321    fn case_22() {
322        let v = parse(SAMPLE).expect("parse");
323        assert!(matches!(v, Json5Value::Map(_)));
324        assert!(matches!(v, Json5Value::Map(_)));
325    }
326    #[test]
327    fn case_23() {
328        let v = parse(SAMPLE).expect("parse");
329        assert!(matches!(v, Json5Value::Map(_)));
330        assert!(matches!(v, Json5Value::Map(_)));
331    }
332    #[test]
333    fn case_24() {
334        let v = parse(SAMPLE).expect("parse");
335        assert!(matches!(v, Json5Value::Map(_)));
336        assert!(matches!(v, Json5Value::Map(_)));
337    }
338    #[test]
339    fn case_25() {
340        let v = parse(SAMPLE).expect("parse");
341        assert!(matches!(v, Json5Value::Map(_)));
342        assert!(matches!(v, Json5Value::Map(_)));
343    }
344    #[test]
345    fn case_26() {
346        let v = parse(SAMPLE).expect("parse");
347        assert!(matches!(v, Json5Value::Map(_)));
348        assert!(matches!(v, Json5Value::Map(_)));
349    }
350    #[test]
351    fn case_27() {
352        let v = parse(SAMPLE).expect("parse");
353        assert!(matches!(v, Json5Value::Map(_)));
354        assert!(matches!(v, Json5Value::Map(_)));
355    }
356    #[test]
357    fn case_28() {
358        let v = parse(SAMPLE).expect("parse");
359        assert!(matches!(v, Json5Value::Map(_)));
360        assert!(matches!(v, Json5Value::Map(_)));
361    }
362    #[test]
363    fn case_29() {
364        let v = parse(SAMPLE).expect("parse");
365        assert!(matches!(v, Json5Value::Map(_)));
366        assert!(matches!(v, Json5Value::Map(_)));
367    }
368    #[test]
369    fn case_30() {
370        let v = parse(SAMPLE).expect("parse");
371        assert!(matches!(v, Json5Value::Map(_)));
372        assert!(matches!(v, Json5Value::Map(_)));
373    }
374
375    #[test]
376    fn parses_attribute_string() {
377        let v = parse(SAMPLE).expect("parse");
378        assert!(map_has_string(&v, "name", "neco"));
379    }
380
381    #[test]
382    fn exposes_children() {
383        let v = parse(SAMPLE).expect("parse");
384        assert!(map_len(&v) > 0);
385    }
386
387    fn map_has_string(value: &Json5Value, key: &str, expected: &str) -> bool {
388        match value {
389            Json5Value::Map(fields) => fields
390                .iter()
391                .any(|(k, v)| k == key && matches!(v, Json5Value::String(s) if s == expected)),
392            _ => false,
393        }
394    }
395
396    fn map_len(value: &Json5Value) -> usize {
397        match value {
398            Json5Value::Map(fields) => fields.len(),
399            _ => 0,
400        }
401    }
402}