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}