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, tuple},
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 FnMut(&'a str) -> IResult<&'a str, O, E>
61where
62    F: FnMut(&'a str) -> IResult<&'a str, O, E>,
63{
64    delimited(multispace0, inner, multispace0)
65}
66
67fn parse_identifier(input: &str) -> ParseResult<'_, String> {
68    let (input, ident) = recognize(tuple((
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    ))(input)
145}
146
147fn parse_number(input: &str) -> ParseResult<'_, Value> {
148    let (input, num_str) = recognize(tuple((
149        opt(char('-')),
150        take_while1(|c: char| c.is_ascii_digit()),
151        opt(tuple((
152            char('.'),
153            take_while1(|c: char| c.is_ascii_digit()),
154        ))),
155        opt(tuple((
156            one_of("eE"),
157            opt(one_of("+-")),
158            take_while1(|c: char| c.is_ascii_digit()),
159        ))),
160    )))(input)?;
161
162    let num = num_str
163        .parse::<f64>()
164        .map_err(|_| nom::Err::Error(ParseErrorKind::InvalidNumber(num_str.to_string())))?;
165    Ok((input, Value::Number(num)))
166}
167
168fn parse_boolean(input: &str) -> ParseResult<'_, Value> {
169    alt((
170        value(Value::Boolean(true), tag("true")),
171        value(Value::Boolean(false), tag("false")),
172    ))(input)
173}
174
175fn parse_null(input: &str) -> ParseResult<'_, Value> {
176    value(Value::Null, tag("null"))(input)
177}
178
179fn parse_array(input: &str) -> ParseResult<'_, Value> {
180    let (input, values) = delimited(
181        ws(char('[')),
182        tuple((
183            separated_list0(ws(char(',')), ws(parse_value)),
184            opt(ws(char(','))),
185        )),
186        ws(char(']')),
187    )(input)?;
188    Ok((input, Value::Array(values.0)))
189}
190
191fn parse_object_entry(input: &str) -> ParseResult<'_, (String, Value)> {
192    let (input, key) = ws(parse_identifier)(input)?;
193    let (input, _) = ws(char(':'))(input)?;
194    let (input, value) = ws(parse_value)(input)?;
195    Ok((input, (key, value)))
196}
197
198fn parse_object(input: &str) -> ParseResult<'_, Value> {
199    let (input, entries) = delimited(
200        ws(char('{')),
201        tuple((
202            separated_list0(ws(char(',')), parse_object_entry),
203            opt(ws(char(','))),
204        )),
205        ws(char('}')),
206    )(input)?;
207
208    let mut map = IndexMap::new();
209    for (k, v) in entries.0 {
210        map.insert(k, v);
211    }
212    Ok((input, Value::Object(map)))
213}
214
215fn parse_value(input: &str) -> ParseResult<'_, Value> {
216    ws(alt((
217        parse_string.map(Value::String),
218        parse_number,
219        parse_boolean,
220        parse_null,
221        parse_array,
222        parse_object,
223    )))(input)
224}
225
226fn parse_comment(input: &str) -> ParseResult<'_, ()> {
227    alt((
228        value(
229            (),
230            tuple((tag("//"), take_till1(|c| c == '\n'), multispace0)),
231        ),
232        value(
233            (),
234            tuple((tag("/*"), take_until("*/"), tag("*/"), multispace0)),
235        ),
236    ))(input)
237}
238
239fn parse_comments(input: &str) -> ParseResult<'_, ()> {
240    let (input, _) = multispace0(input)?;
241    many0(parse_comment)(input).map(|(i, _)| (i, ()))
242}
243
244fn parse_entry(input: &str) -> ParseResult<'_, (String, Value)> {
245    let (input, _) = parse_comments(input)?;
246    let (input, key) = ws(parse_identifier)(input)?;
247    let (input, _) = ws(char(':'))(input)?;
248    let (input, value) = ws(parse_value)(input)?;
249    let (input, _) = parse_comments(input)?;
250    let (input, _) = opt(ws(char(',')))(input)?;
251    Ok((input, (key, value)))
252}
253
254pub fn parse_zoko(input: &str) -> Result<ZokoFile, ParseErrorKind> {
255    let (remaining, _) = parse_comments(input).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)(remaining).map_err(|e| ParseErrorKind::Expected {
262            expected: "valid zoko entries".to_string(),
263            found: format!("{:?}", e),
264        })?;
265
266    let (_, _) = parse_comments(remaining).map_err(|e| ParseErrorKind::Expected {
267        expected: "end of input".to_string(),
268        found: format!("{:?}", e),
269    })?;
270
271    let mut map = IndexMap::new();
272    for (k, v) in entries {
273        map.insert(k, v);
274    }
275
276    Ok(ZokoFile { entries: map })
277}
278
279pub fn parse_zoko_to_json(input: &str) -> Result<String, ParseErrorKind> {
280    let zoko = parse_zoko(input)?;
281    serde_json::to_string_pretty(&zoko).map_err(|e| ParseErrorKind::Expected {
282        expected: "valid JSON serialization".to_string(),
283        found: e.to_string(),
284    })
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_parse_simple_object() {
293        let input = r#"name: "value""#;
294        let result = parse_zoko(input).unwrap();
295        assert_eq!(
296            result.entries.get("name"),
297            Some(&Value::String("value".to_string()))
298        );
299    }
300
301    #[test]
302    fn test_parse_map() {
303        let input = r#"
304        map: {
305            id: "value",
306            id2: "value2",
307        }
308        "#;
309        let result = parse_zoko(input).unwrap();
310        let map = result.entries.get("map").unwrap();
311        if let Value::Object(obj) = map {
312            assert_eq!(obj.get("id"), Some(&Value::String("value".to_string())));
313            assert_eq!(obj.get("id2"), Some(&Value::String("value2".to_string())));
314        } else {
315            panic!("Expected object");
316        }
317    }
318
319    #[test]
320    fn test_parse_array() {
321        let input = r#"tags: ["Hello", "Zoil"]"#;
322        let result = parse_zoko(input).unwrap();
323        let arr = result.entries.get("tags").unwrap();
324        if let Value::Array(vec) = arr {
325            assert_eq!(vec.len(), 2);
326            assert_eq!(vec[0], Value::String("Hello".to_string()));
327            assert_eq!(vec[1], Value::String("Zoil".to_string()));
328        } else {
329            panic!("Expected array");
330        }
331    }
332
333    #[test]
334    fn test_parse_comments() {
335        let input = r#"
336        // Single line comment
337        name: "value"
338        /* Multi line
339        Comment */
340        "#;
341        let result = parse_zoko(input).unwrap();
342        assert_eq!(
343            result.entries.get("name"),
344            Some(&Value::String("value".to_string()))
345        );
346    }
347
348    #[test]
349    fn test_parse_complex() {
350        let input = r#"
351        name: "@Main/Hello",
352        channel: "main",
353        branch: "Production",
354        status: "Release",
355        version: 1.0.0,
356        description: "Hello package for Zoil",
357        tags: [
358            "Hello",
359            "Zoil",
360        ],
361        website: "https://hello.nel.co",
362        dependencies: [
363            "Hola": 1.0.2,
364            "@German/Hallo": {
365                channel: "main",
366                version: "latest",
367            },
368        ],
369        "#;
370        let result = parse_zoko(input).unwrap();
371        assert_eq!(
372            result.entries.get("name"),
373            Some(&Value::String("@Main/Hello".to_string()))
374        );
375        assert_eq!(
376            result.entries.get("channel"),
377            Some(&Value::String("main".to_string()))
378        );
379        assert_eq!(result.entries.get("version"), Some(&Value::Number(1.0)));
380    }
381
382    #[test]
383    fn test_parse_number_formats() {
384        let input = r#"
385        int: 42,
386        float: 3.14,
387        negative: -10,
388        scientific: 1.5e10,
389        "#;
390        let result = parse_zoko(input).unwrap();
391        assert_eq!(result.entries.get("int"), Some(&Value::Number(42.0)));
392        assert_eq!(result.entries.get("float"), Some(&Value::Number(3.14)));
393        assert_eq!(result.entries.get("negative"), Some(&Value::Number(-10.0)));
394        assert_eq!(
395            result.entries.get("scientific"),
396            Some(&Value::Number(1.5e10))
397        );
398    }
399
400    #[test]
401    fn test_parse_boolean() {
402        let input = r#"
403        yes: true,
404        no: false,
405        "#;
406        let result = parse_zoko(input).unwrap();
407        assert_eq!(result.entries.get("yes"), Some(&Value::Boolean(true)));
408        assert_eq!(result.entries.get("no"), Some(&Value::Boolean(false)));
409    }
410
411    #[test]
412    fn test_parse_null() {
413        let input = r#"value: null"#;
414        let result = parse_zoko(input).unwrap();
415        assert_eq!(result.entries.get("value"), Some(&Value::Null));
416    }
417
418    #[test]
419    fn test_parse_different_string_types() {
420        let input = r#"
421        double: "hello",
422        single: 'world',
423        backtick: `multiline
424string`,
425        "#;
426        let result = parse_zoko(input).unwrap();
427        assert_eq!(
428            result.entries.get("double"),
429            Some(&Value::String("hello".to_string()))
430        );
431        assert_eq!(
432            result.entries.get("single"),
433            Some(&Value::String("world".to_string()))
434        );
435        assert_eq!(
436            result.entries.get("backtick"),
437            Some(&Value::String("multiline\nstring".to_string()))
438        );
439    }
440
441    #[test]
442    fn test_trailing_comma() {
443        let input = r#"
444        a: 1,
445        b: 2,
446        "#;
447        let result = parse_zoko(input).unwrap();
448        assert_eq!(result.entries.len(), 2);
449    }
450
451    #[test]
452    fn test_to_json() {
453        let input = r#"name: "test", value: 42"#;
454        let json = parse_zoko_to_json(input).unwrap();
455        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
456        assert_eq!(parsed["entries"]["name"], "test");
457        assert_eq!(parsed["entries"]["value"], 42.0);
458    }
459
460    #[test]
461    fn test_array_with_objects() {
462        let input = r#"dependencies: [{name: "hola", version: "1.1.0"}, {name: "@german/hallo", version: "latest"}]"#;
463        let result = parse_zoko(input).unwrap();
464        let deps = result.entries.get("dependencies").unwrap();
465        if let Value::Array(vec) = deps {
466            assert_eq!(vec.len(), 2);
467            if let Value::Object(obj) = &vec[0] {
468                assert_eq!(obj.get("name"), Some(&Value::String("hola".to_string())));
469                assert_eq!(
470                    obj.get("version"),
471                    Some(&Value::String("1.1.0".to_string()))
472                );
473            } else {
474                panic!("Expected object for first dependency");
475            }
476            if let Value::Object(obj) = &vec[1] {
477                assert_eq!(
478                    obj.get("name"),
479                    Some(&Value::String("@german/hallo".to_string()))
480                );
481                assert_eq!(
482                    obj.get("version"),
483                    Some(&Value::String("latest".to_string()))
484                );
485            } else {
486                panic!("Expected object for second dependency");
487            }
488        } else {
489            panic!("Expected array");
490        }
491    }
492
493    #[test]
494    fn test_json_compatibility() {
495        let zoko_input = r#"dependencies: [{name: "hola", version: "1.1.0"}, {name: "@german/hallo", version: "latest"}]"#;
496        let json_output = parse_zoko_to_json(zoko_input).unwrap();
497
498        let json_value: serde_json::Value = serde_json::from_str(&json_output).unwrap();
499
500        assert!(json_value["entries"]["dependencies"].is_array());
501        assert_eq!(
502            json_value["entries"]["dependencies"]
503                .as_array()
504                .unwrap()
505                .len(),
506            2
507        );
508    }
509
510    #[test]
511    fn test_entry_order_preservation() {
512        let input = r#"first: "value1", second: "value2", third: "value3""#;
513        let result = parse_zoko(input).unwrap();
514
515        let keys: Vec<&String> = result.entries.keys().collect();
516        assert_eq!(keys, vec!["first", "second", "third"]);
517    }
518
519    #[test]
520    fn test_complex_file_parsing() {
521        let input = r#"name: "@Main/Hello",
522channel: "main",
523branch: "Production",
524status: "Release",
525version: "1.0.0",
526description: "Hello package for Zoil",
527tags: ["Hello", "Zoil"],
528website: "https://hello.nel.co",
529dependencies: [
530  {name: "Hola", version: "1.0.2"},
531  {name: "@German/Hallo", channel: "main", version: "latest"},
532],
533"#;
534        let result = parse_zoko(input).unwrap();
535
536        assert_eq!(result.entries.len(), 9);
537        assert!(result.entries.contains_key("name"));
538        assert!(result.entries.contains_key("channel"));
539        assert!(result.entries.contains_key("branch"));
540        assert!(result.entries.contains_key("status"));
541        assert!(result.entries.contains_key("version"));
542        assert!(result.entries.contains_key("description"));
543        assert!(result.entries.contains_key("tags"));
544        assert!(result.entries.contains_key("website"));
545        assert!(result.entries.contains_key("dependencies"));
546    }
547}