Skip to main content

zoko_parser/
lib.rs

1use indexmap::IndexMap;
2use nom::{
3    IResult, Parser,
4    branch::alt,
5    bytes::complete::{escaped_transform, is_not, tag, take_till1, take_until, take_while1},
6    character::complete::{alphanumeric1, char, multispace0, one_of},
7    combinator::{opt, recognize, value},
8    error::ParseError,
9    multi::{many0, separated_list0},
10    sequence::delimited,
11};
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16#[serde(untagged)]
17pub enum Value {
18    String(String),
19    Number(f64),
20    Boolean(bool),
21    Array(Vec<Value>),
22    Object(IndexMap<String, Value>),
23    Null,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27pub struct ZokoFile {
28    pub entries: IndexMap<String, Value>,
29}
30
31#[derive(Debug, Error)]
32pub enum ParseErrorKind {
33    #[error("Unexpected character: {0}")]
34    UnexpectedChar(char),
35    #[error("Unexpected end of input")]
36    UnexpectedEof,
37    #[error("Invalid number format: {0}")]
38    InvalidNumber(String),
39    #[error("Invalid escape sequence: {0}")]
40    InvalidEscape(String),
41    #[error("Expected {expected}, found {found}")]
42    Expected { expected: String, found: String },
43}
44
45impl<I> ParseError<I> for ParseErrorKind {
46    fn from_error_kind(_input: I, kind: nom::error::ErrorKind) -> Self {
47        ParseErrorKind::Expected {
48            expected: format!("{:?}", kind),
49            found: "unknown".to_string(),
50        }
51    }
52
53    fn append(_input: I, _kind: nom::error::ErrorKind, other: Self) -> Self {
54        other
55    }
56}
57
58pub type ParseResult<'a, T> = IResult<&'a str, T, ParseErrorKind>;
59
60fn ws<'a, F, O, E: ParseError<&'a str>>(inner: F) -> impl Parser<&'a str, Output = O, Error = E>
61where
62    F: Parser<&'a str, Output = O, Error = E>,
63{
64    delimited(multispace0, inner, multispace0)
65}
66
67fn parse_identifier(input: &str) -> ParseResult<'_, String> {
68    let (input, ident) = recognize((
69        alt((alphanumeric1, tag("_"), tag("-"), tag("@"), tag("/"))),
70        many0(alt((
71            alphanumeric1,
72            tag("_"),
73            tag("-"),
74            tag("@"),
75            tag("/"),
76            tag("."),
77        ))),
78    ))
79    .parse(input)?;
80
81    Ok((input, ident.to_string()))
82}
83
84fn parse_string_single_quoted(input: &str) -> ParseResult<'_, String> {
85    let (input, _) = char('\'')(input)?;
86    let (input, content) = take_until("'")(input)?;
87    let (input, _) = char('\'')(input)?;
88    Ok((input, content.to_string()))
89}
90
91fn parse_string_double_quoted(input: &str) -> ParseResult<'_, String> {
92    let (input, _) = char('"')(input)?;
93    let (input, content) = escaped_transform(
94        is_not("\"\\"),
95        '\\',
96        alt((
97            value("\n", char('n')),
98            value("\r", char('r')),
99            value("\t", char('t')),
100            value("\\", char('\\')),
101            value("\"", char('"')),
102            value("'", char('\'')),
103        )),
104    )(input)?;
105    let (input, _) = char('"')(input)?;
106    Ok((input, content))
107}
108
109fn parse_string_backtick(input: &str) -> ParseResult<'_, String> {
110    let (input, _) = char('`')(input)?;
111    let (input, content) = take_until("`")(input)?;
112    let (input, _) = char('`')(input)?;
113    let lines: Vec<&str> = content.lines().collect();
114    if lines.is_empty() {
115        Ok((input, String::new()))
116    } else {
117        let min_whitespace = lines
118            .iter()
119            .filter(|line| !line.is_empty())
120            .map(|line| line.len() - line.trim_start().len())
121            .min()
122            .unwrap_or(0);
123
124        let stripped: Vec<String> = lines
125            .iter()
126            .map(|line| {
127                if line.is_empty() {
128                    String::new()
129                } else {
130                    line[min_whitespace..].to_string()
131                }
132            })
133            .collect();
134
135        Ok((input, stripped.join("\n")))
136    }
137}
138
139fn parse_string(input: &str) -> ParseResult<'_, String> {
140    alt((
141        parse_string_double_quoted,
142        parse_string_single_quoted,
143        parse_string_backtick,
144    ))
145    .parse(input)
146}
147
148fn parse_number(input: &str) -> ParseResult<'_, Value> {
149    let (input, num_str) = recognize((
150        opt(char('-')),
151        take_while1(|c: char| c.is_ascii_digit()),
152        opt((char('.'), take_while1(|c: char| c.is_ascii_digit()))),
153        opt((
154            one_of("eE"),
155            opt(one_of("+-")),
156            take_while1(|c: char| c.is_ascii_digit()),
157        )),
158    ))
159    .parse(input)?;
160
161    let num = num_str
162        .parse::<f64>()
163        .map_err(|_| nom::Err::Error(ParseErrorKind::InvalidNumber(num_str.to_string())))?;
164    Ok((input, Value::Number(num)))
165}
166
167fn parse_boolean(input: &str) -> ParseResult<'_, Value> {
168    alt((
169        value(Value::Boolean(true), tag("true")),
170        value(Value::Boolean(false), tag("false")),
171    ))
172    .parse(input)
173}
174
175fn parse_null(input: &str) -> ParseResult<'_, Value> {
176    value(Value::Null, tag("null")).parse(input)
177}
178
179fn parse_array(input: &str) -> ParseResult<'_, Value> {
180    let (input, values) = delimited(
181        ws(char('[')),
182        (
183            separated_list0(ws(char(',')), ws(parse_value)),
184            opt(ws(char(','))),
185        ),
186        ws(char(']')),
187    )
188    .parse(input)?;
189    Ok((input, Value::Array(values.0)))
190}
191
192fn parse_object_entry(input: &str) -> ParseResult<'_, (String, Value)> {
193    let (input, key) = ws(parse_identifier).parse(input)?;
194    let (input, _) = ws(char(':')).parse(input)?;
195    let (input, value) = ws(parse_value).parse(input)?;
196    Ok((input, (key, value)))
197}
198
199fn parse_object(input: &str) -> ParseResult<'_, Value> {
200    let (input, entries) = delimited(
201        ws(char('{')),
202        (
203            separated_list0(ws(char(',')), parse_object_entry),
204            opt(ws(char(','))),
205        ),
206        ws(char('}')),
207    )
208    .parse(input)?;
209
210    let mut map = IndexMap::new();
211    for (k, v) in entries.0 {
212        map.insert(k, v);
213    }
214    Ok((input, Value::Object(map)))
215}
216
217fn parse_value(input: &str) -> ParseResult<'_, Value> {
218    ws(alt((
219        parse_string.map(Value::String),
220        parse_number,
221        parse_boolean,
222        parse_null,
223        parse_array,
224        parse_object,
225    )))
226    .parse(input)
227}
228
229fn parse_comment(input: &str) -> ParseResult<'_, ()> {
230    alt((
231        value((), (tag("//"), take_till1(|c| c == '\n'), multispace0)),
232        value((), (tag("/*"), take_until("*/"), tag("*/"), multispace0)),
233    ))
234    .parse(input)
235}
236
237fn parse_comments(input: &str) -> ParseResult<'_, ()> {
238    let (input, _) = multispace0(input)?;
239    many0(parse_comment).parse(input).map(|(i, _)| (i, ()))
240}
241
242fn parse_entry(input: &str) -> ParseResult<'_, (String, Value)> {
243    let (input, _) = parse_comments(input)?;
244    let (input, key) = ws(parse_identifier).parse(input)?;
245    let (input, _) = ws(char(':')).parse(input)?;
246    let (input, value) = ws(parse_value).parse(input)?;
247    let (input, _) = parse_comments(input)?;
248    let (input, _) = opt(ws(char(','))).parse(input)?;
249    Ok((input, (key, value)))
250}
251
252pub fn parse_zoko(input: &str) -> Result<ZokoFile, ParseErrorKind> {
253    let (remaining, _) = parse_comments
254        .parse(input)
255        .map_err(|e| ParseErrorKind::Expected {
256            expected: "valid zoko input".to_string(),
257            found: format!("{:?}", e),
258        })?;
259
260    let (remaining, entries) =
261        many0(parse_entry)
262            .parse(remaining)
263            .map_err(|e| ParseErrorKind::Expected {
264                expected: "valid zoko entries".to_string(),
265                found: format!("{:?}", e),
266            })?;
267
268    let (_, _) = parse_comments
269        .parse(remaining)
270        .map_err(|e| ParseErrorKind::Expected {
271            expected: "end of input".to_string(),
272            found: format!("{:?}", e),
273        })?;
274
275    let mut map = IndexMap::new();
276    for (k, v) in entries {
277        map.insert(k, v);
278    }
279
280    Ok(ZokoFile { entries: map })
281}
282
283pub fn parse_zoko_to_json(input: &str) -> Result<String, ParseErrorKind> {
284    let zoko = parse_zoko(input)?;
285    serde_json::to_string_pretty(&zoko).map_err(|e| ParseErrorKind::Expected {
286        expected: "valid JSON serialization".to_string(),
287        found: e.to_string(),
288    })
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn test_parse_simple_object() {
297        let input = r#"name: "value""#;
298        let result = parse_zoko(input).unwrap();
299        assert_eq!(
300            result.entries.get("name"),
301            Some(&Value::String("value".to_string()))
302        );
303    }
304
305    #[test]
306    fn test_parse_map() {
307        let input = r#"
308        map: {
309            id: "value",
310            id2: "value2",
311        }
312        "#;
313        let result = parse_zoko(input).unwrap();
314        let map = result.entries.get("map").unwrap();
315        if let Value::Object(obj) = map {
316            assert_eq!(obj.get("id"), Some(&Value::String("value".to_string())));
317            assert_eq!(obj.get("id2"), Some(&Value::String("value2".to_string())));
318        } else {
319            panic!("Expected object");
320        }
321    }
322
323    #[test]
324    fn test_parse_array() {
325        let input = r#"tags: ["Hello", "Zoil"]"#;
326        let result = parse_zoko(input).unwrap();
327        let arr = result.entries.get("tags").unwrap();
328        if let Value::Array(vec) = arr {
329            assert_eq!(vec.len(), 2);
330            assert_eq!(vec[0], Value::String("Hello".to_string()));
331            assert_eq!(vec[1], Value::String("Zoil".to_string()));
332        } else {
333            panic!("Expected array");
334        }
335    }
336
337    #[test]
338    fn test_parse_comments() {
339        let input = r#"
340        // Single line comment
341        name: "value"
342        /* Multi line
343        Comment */
344        "#;
345        let result = parse_zoko(input).unwrap();
346        assert_eq!(
347            result.entries.get("name"),
348            Some(&Value::String("value".to_string()))
349        );
350    }
351
352    #[test]
353    fn test_parse_complex() {
354        let input = r#"
355        name: "@Main/Hello",
356        channel: "main",
357        branch: "Production",
358        status: "Release",
359        version: 1.0.0,
360        description: "Hello package for Zoil",
361        tags: [
362            "Hello",
363            "Zoil",
364        ],
365        website: "https://hello.nel.co",
366        dependencies: [
367            "Hola": 1.0.2,
368            "@German/Hallo": {
369                channel: "main",
370                version: "latest",
371            },
372        ],
373        "#;
374        let result = parse_zoko(input).unwrap();
375        assert_eq!(
376            result.entries.get("name"),
377            Some(&Value::String("@Main/Hello".to_string()))
378        );
379        assert_eq!(
380            result.entries.get("channel"),
381            Some(&Value::String("main".to_string()))
382        );
383        assert_eq!(result.entries.get("version"), Some(&Value::Number(1.0)));
384    }
385
386    #[test]
387    fn test_parse_number_formats() {
388        let input = r#"
389        int: 42,
390        float: 3.14,
391        negative: -10,
392        scientific: 1.5e10,
393        "#;
394        let result = parse_zoko(input).unwrap();
395        assert_eq!(result.entries.get("int"), Some(&Value::Number(42.0)));
396        assert_eq!(result.entries.get("float"), Some(&Value::Number(3.14)));
397        assert_eq!(result.entries.get("negative"), Some(&Value::Number(-10.0)));
398        assert_eq!(
399            result.entries.get("scientific"),
400            Some(&Value::Number(1.5e10))
401        );
402    }
403
404    #[test]
405    fn test_parse_boolean() {
406        let input = r#"
407        yes: true,
408        no: false,
409        "#;
410        let result = parse_zoko(input).unwrap();
411        assert_eq!(result.entries.get("yes"), Some(&Value::Boolean(true)));
412        assert_eq!(result.entries.get("no"), Some(&Value::Boolean(false)));
413    }
414
415    #[test]
416    fn test_parse_null() {
417        let input = r#"value: null"#;
418        let result = parse_zoko(input).unwrap();
419        assert_eq!(result.entries.get("value"), Some(&Value::Null));
420    }
421
422    #[test]
423    fn test_parse_different_string_types() {
424        let input = r#"
425        double: "hello",
426        single: 'world',
427        backtick: `multiline
428string`,
429        "#;
430        let result = parse_zoko(input).unwrap();
431        assert_eq!(
432            result.entries.get("double"),
433            Some(&Value::String("hello".to_string()))
434        );
435        assert_eq!(
436            result.entries.get("single"),
437            Some(&Value::String("world".to_string()))
438        );
439        assert_eq!(
440            result.entries.get("backtick"),
441            Some(&Value::String("multiline\nstring".to_string()))
442        );
443    }
444
445    #[test]
446    fn test_trailing_comma() {
447        let input = r#"
448        a: 1,
449        b: 2,
450        "#;
451        let result = parse_zoko(input).unwrap();
452        assert_eq!(result.entries.len(), 2);
453    }
454
455    #[test]
456    fn test_to_json() {
457        let input = r#"name: "test", value: 42"#;
458        let json = parse_zoko_to_json(input).unwrap();
459        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
460        assert_eq!(parsed["entries"]["name"], "test");
461        assert_eq!(parsed["entries"]["value"], 42.0);
462    }
463
464    #[test]
465    fn test_array_with_objects() {
466        let input = r#"dependencies: [{name: "hola", version: "1.1.0"}, {name: "@german/hallo", version: "latest"}]"#;
467        let result = parse_zoko(input).unwrap();
468        let deps = result.entries.get("dependencies").unwrap();
469        if let Value::Array(vec) = deps {
470            assert_eq!(vec.len(), 2);
471            if let Value::Object(obj) = &vec[0] {
472                assert_eq!(obj.get("name"), Some(&Value::String("hola".to_string())));
473                assert_eq!(
474                    obj.get("version"),
475                    Some(&Value::String("1.1.0".to_string()))
476                );
477            } else {
478                panic!("Expected object for first dependency");
479            }
480            if let Value::Object(obj) = &vec[1] {
481                assert_eq!(
482                    obj.get("name"),
483                    Some(&Value::String("@german/hallo".to_string()))
484                );
485                assert_eq!(
486                    obj.get("version"),
487                    Some(&Value::String("latest".to_string()))
488                );
489            } else {
490                panic!("Expected object for second dependency");
491            }
492        } else {
493            panic!("Expected array");
494        }
495    }
496
497    #[test]
498    fn test_json_compatibility() {
499        let zoko_input = r#"dependencies: [{name: "hola", version: "1.1.0"}, {name: "@german/hallo", version: "latest"}]"#;
500        let json_output = parse_zoko_to_json(zoko_input).unwrap();
501
502        let json_value: serde_json::Value = serde_json::from_str(&json_output).unwrap();
503
504        assert!(json_value["entries"]["dependencies"].is_array());
505        assert_eq!(
506            json_value["entries"]["dependencies"]
507                .as_array()
508                .unwrap()
509                .len(),
510            2
511        );
512    }
513
514    #[test]
515    fn test_entry_order_preservation() {
516        let input = r#"first: "value1", second: "value2", third: "value3""#;
517        let result = parse_zoko(input).unwrap();
518
519        let keys: Vec<&String> = result.entries.keys().collect();
520        assert_eq!(keys, vec!["first", "second", "third"]);
521    }
522
523    #[test]
524    fn test_complex_file_parsing() {
525        let input = r#"name: "@Main/Hello",
526channel: "main",
527branch: "Production",
528status: "Release",
529version: "1.0.0",
530description: "Hello package for Zoil",
531tags: ["Hello", "Zoil"],
532website: "https://hello.nel.co",
533dependencies: [
534  {name: "Hola", version: "1.0.2"},
535  {name: "@German/Hallo", channel: "main", version: "latest"},
536],
537"#;
538        let result = parse_zoko(input).unwrap();
539
540        assert_eq!(result.entries.len(), 9);
541        assert!(result.entries.contains_key("name"));
542        assert!(result.entries.contains_key("channel"));
543        assert!(result.entries.contains_key("branch"));
544        assert!(result.entries.contains_key("status"));
545        assert!(result.entries.contains_key("version"));
546        assert!(result.entries.contains_key("description"));
547        assert!(result.entries.contains_key("tags"));
548        assert!(result.entries.contains_key("website"));
549        assert!(result.entries.contains_key("dependencies"));
550    }
551}