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}