Skip to main content

neco_toml/
lib.rs

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