1use base64::Engine;
2use chrono::{Duration, NaiveDate, NaiveTime};
3use indexmap::IndexMap;
4use nom::{
5 IResult, Parser,
6 branch::alt,
7 bytes::complete::{escaped_transform, is_not, tag, take_till1, take_until, take_while1},
8 character::complete::{alphanumeric1, char, multispace0, one_of},
9 combinator::{opt, recognize, value},
10 error::ParseError,
11 multi::{many0, separated_list0},
12 sequence::delimited,
13};
14use regex::Regex;
15use rust_decimal::Decimal;
16use serde::{Deserialize, Serialize};
17use std::collections::HashSet;
18use std::path::{Path, PathBuf};
19use thiserror::Error;
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22#[serde(untagged)]
23pub enum Value {
24 String(String),
25 Integer(i64),
26 Float(f64),
27 Decimal(Decimal),
28 Boolean(bool),
29 Array(Vec<Value>),
30 Object(IndexMap<String, Value>),
31 Null,
32 Date(NaiveDate),
33 Time(NaiveTime),
34 Duration(Duration),
35 Binary(Vec<u8>),
36}
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct ZokoFile {
40 pub entries: IndexMap<String, Value>,
41}
42
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44#[serde(tag = "type")]
45pub enum SchemaType {
46 String,
47 Integer,
48 Float,
49 Decimal,
50 Boolean,
51 Null,
52 Array {
53 items: Box<SchemaType>,
54 },
55 Object {
56 properties: IndexMap<String, SchemaType>,
57 },
58 Date,
59 Time,
60 Duration,
61 Binary,
62 Any,
63}
64
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub struct Schema {
67 pub properties: IndexMap<String, SchemaType>,
68 pub required: Vec<String>,
69}
70
71#[derive(Debug, Error)]
72pub enum ValidationError {
73 #[error("Missing required field: {field}")]
74 MissingRequiredField { field: String },
75 #[error("Type mismatch for field '{field}': expected {expected}, found {found}")]
76 TypeMismatch {
77 field: String,
78 expected: String,
79 found: String,
80 },
81 #[error("Invalid value for field '{field}': {message}")]
82 InvalidValue { field: String, message: String },
83}
84
85pub fn validate_schema(zoko: &ZokoFile, schema: &Schema) -> Result<(), ValidationError> {
86 for field in &schema.required {
87 if !zoko.entries.contains_key(field) {
88 return Err(ValidationError::MissingRequiredField {
89 field: field.clone(),
90 });
91 }
92 }
93
94 for (field, value) in &zoko.entries {
95 if let Some(expected_type) = schema.properties.get(field) {
96 validate_value_type(field, value, expected_type)?;
97 }
98 }
99
100 Ok(())
101}
102
103fn validate_value_type(
104 field: &str,
105 value: &Value,
106 expected_type: &SchemaType,
107) -> Result<(), ValidationError> {
108 match (value, expected_type) {
109 (Value::String(_), SchemaType::String) => Ok(()),
110 (Value::Integer(_), SchemaType::Integer) => Ok(()),
111 (Value::Float(_), SchemaType::Float) => Ok(()),
112 (Value::Decimal(_), SchemaType::Decimal) => Ok(()),
113 (Value::Boolean(_), SchemaType::Boolean) => Ok(()),
114 (Value::Null, SchemaType::Null) => Ok(()),
115 (Value::Array(arr), SchemaType::Array { items }) => {
116 for (i, item) in arr.iter().enumerate() {
117 validate_value_type(&format!("{}[{}]", field, i), item, items)?;
118 }
119 Ok(())
120 }
121 (Value::Object(obj), SchemaType::Object { properties }) => {
122 for (prop, val) in obj {
123 if let Some(prop_type) = properties.get(prop) {
124 validate_value_type(&format!("{}.{}", field, prop), val, prop_type)?;
125 }
126 }
127 Ok(())
128 }
129 (Value::Date(_), SchemaType::Date) => Ok(()),
130 (Value::Time(_), SchemaType::Time) => Ok(()),
131 (Value::Duration(_), SchemaType::Duration) => Ok(()),
132 (Value::Binary(_), SchemaType::Binary) => Ok(()),
133 (_, SchemaType::Any) => Ok(()),
134 (value, expected) => Err(ValidationError::TypeMismatch {
135 field: field.to_string(),
136 expected: format!("{:?}", expected),
137 found: format!("{:?}", value),
138 }),
139 }
140}
141
142#[derive(Debug, Error)]
143pub enum ParseErrorKind {
144 #[error("Unexpected character: {0}")]
145 UnexpectedChar(char),
146 #[error("Unexpected end of input")]
147 UnexpectedEof,
148 #[error("Invalid number format: {0}")]
149 InvalidNumber(String),
150 #[error("Invalid escape sequence: {0}")]
151 InvalidEscape(String),
152 #[error("Expected {expected}, found {found}")]
153 Expected { expected: String, found: String },
154 #[error("Invalid date format: {0}")]
155 InvalidDate(String),
156 #[error("Invalid time format: {0}")]
157 InvalidTime(String),
158 #[error("Invalid duration format: {0}")]
159 InvalidDuration(String),
160 #[error("Invalid decimal format: {0}")]
161 InvalidDecimal(String),
162 #[error("Invalid binary data: {0}")]
163 InvalidBinary(String),
164 #[error("Environment variable not found: {0}")]
165 EnvVarNotFound(String),
166 #[error("Failed to read include file: {0}")]
167 IncludeFileError(String),
168 #[error("Circular include detected: {0}")]
169 CircularInclude(String),
170}
171
172impl<I> ParseError<I> for ParseErrorKind {
173 fn from_error_kind(_input: I, kind: nom::error::ErrorKind) -> Self {
174 ParseErrorKind::Expected {
175 expected: format!("{:?}", kind),
176 found: "unknown".to_string(),
177 }
178 }
179
180 fn append(_input: I, _kind: nom::error::ErrorKind, other: Self) -> Self {
181 other
182 }
183}
184
185pub type ParseResult<'a, T> = IResult<&'a str, T, ParseErrorKind>;
186
187fn ws<'a, F, O, E: ParseError<&'a str>>(inner: F) -> impl Parser<&'a str, Output = O, Error = E>
188where
189 F: Parser<&'a str, Output = O, Error = E>,
190{
191 delimited(multispace0, inner, multispace0)
192}
193
194fn parse_identifier(input: &str) -> ParseResult<'_, String> {
195 let (input, ident) = recognize((
196 alt((alphanumeric1, tag("_"), tag("-"), tag("@"), tag("/"))),
197 many0(alt((
198 alphanumeric1,
199 tag("_"),
200 tag("-"),
201 tag("@"),
202 tag("/"),
203 tag("."),
204 ))),
205 ))
206 .parse(input)?;
207
208 Ok((input, ident.to_string()))
209}
210
211fn parse_string_single_quoted(input: &str) -> ParseResult<'_, String> {
212 let (input, _) = char('\'')(input)?;
213 let (input, content) = take_until("'")(input)?;
214 let (input, _) = char('\'')(input)?;
215 Ok((input, content.to_string()))
216}
217
218fn parse_string_double_quoted(input: &str) -> ParseResult<'_, String> {
219 let (input, _) = char('"')(input)?;
220 let (input, content) = escaped_transform(
221 is_not("\"\\"),
222 '\\',
223 alt((
224 value("\n", char('n')),
225 value("\r", char('r')),
226 value("\t", char('t')),
227 value("\\", char('\\')),
228 value("\"", char('"')),
229 value("'", char('\'')),
230 )),
231 )(input)?;
232 let (input, _) = char('"')(input)?;
233 Ok((input, content))
234}
235
236fn parse_string_backtick(input: &str) -> ParseResult<'_, String> {
237 let (input, _) = char('`')(input)?;
238 let (input, content) = take_until("`")(input)?;
239 let (input, _) = char('`')(input)?;
240 let lines: Vec<&str> = content.lines().collect();
241 if lines.is_empty() {
242 Ok((input, String::new()))
243 } else {
244 let min_whitespace = lines
245 .iter()
246 .filter(|line| !line.is_empty())
247 .map(|line| line.len() - line.trim_start().len())
248 .min()
249 .unwrap_or(0);
250
251 let stripped: Vec<String> = lines
252 .iter()
253 .map(|line| {
254 if line.is_empty() {
255 String::new()
256 } else {
257 line[min_whitespace..].to_string()
258 }
259 })
260 .collect();
261
262 let content = stripped.join("\n");
263 let substituted = substitute_env_vars(&content).map_err(nom::Err::Error)?;
264 Ok((input, substituted))
265 }
266}
267
268fn parse_string(input: &str) -> ParseResult<'_, String> {
269 alt((
270 parse_string_double_quoted,
271 parse_string_single_quoted,
272 parse_string_backtick,
273 ))
274 .parse(input)
275}
276
277fn parse_integer(input: &str) -> ParseResult<'_, Value> {
278 let (input, num_str) =
279 recognize((opt(char('-')), take_while1(|c: char| c.is_ascii_digit()))).parse(input)?;
280
281 let num = num_str
282 .parse::<i64>()
283 .map_err(|_| nom::Err::Error(ParseErrorKind::InvalidNumber(num_str.to_string())))?;
284 Ok((input, Value::Integer(num)))
285}
286
287fn parse_float(input: &str) -> ParseResult<'_, Value> {
288 let (input, num_str) = recognize((
289 opt(char('-')),
290 take_while1(|c: char| c.is_ascii_digit()),
291 opt((char('.'), take_while1(|c: char| c.is_ascii_digit()))),
292 opt((
293 one_of("eE"),
294 opt(one_of("+-")),
295 take_while1(|c: char| c.is_ascii_digit()),
296 )),
297 ))
298 .parse(input)?;
299
300 if !num_str.contains('.') && !num_str.contains('e') && !num_str.contains('E') {
301 return Err(nom::Err::Error(ParseErrorKind::InvalidNumber(
302 num_str.to_string(),
303 )));
304 }
305
306 let num = num_str
307 .parse::<f64>()
308 .map_err(|_| nom::Err::Error(ParseErrorKind::InvalidNumber(num_str.to_string())))?;
309 Ok((input, Value::Float(num)))
310}
311
312fn parse_boolean(input: &str) -> ParseResult<'_, Value> {
313 alt((
314 value(Value::Boolean(true), tag("true")),
315 value(Value::Boolean(false), tag("false")),
316 ))
317 .parse(input)
318}
319
320fn parse_null(input: &str) -> ParseResult<'_, Value> {
321 value(Value::Null, tag("null")).parse(input)
322}
323
324fn parse_array(input: &str) -> ParseResult<'_, Value> {
325 let (input, values) = delimited(
326 ws(char('[')),
327 (
328 separated_list0(ws(char(',')), ws(parse_value)),
329 opt(ws(char(','))),
330 ),
331 ws(char(']')),
332 )
333 .parse(input)?;
334 Ok((input, Value::Array(values.0)))
335}
336
337fn parse_object_entry(input: &str) -> ParseResult<'_, (String, Value)> {
338 let (input, key) = ws(parse_identifier).parse(input)?;
339 let (input, _) = ws(char(':')).parse(input)?;
340 let (input, value) = ws(parse_value).parse(input)?;
341 Ok((input, (key, value)))
342}
343
344fn parse_object(input: &str) -> ParseResult<'_, Value> {
345 let (input, entries) = delimited(
346 ws(char('{')),
347 (
348 separated_list0(ws(char(',')), parse_object_entry),
349 opt(ws(char(','))),
350 ),
351 ws(char('}')),
352 )
353 .parse(input)?;
354
355 let mut map = IndexMap::new();
356 for (k, v) in entries.0 {
357 map.insert(k, v);
358 }
359 Ok((input, Value::Object(map)))
360}
361
362fn parse_value(input: &str) -> ParseResult<'_, Value> {
363 ws(alt((
364 parse_function_call,
365 parse_string.map(Value::String),
366 parse_float,
367 parse_integer,
368 parse_boolean,
369 parse_null,
370 parse_array,
371 parse_object,
372 )))
373 .parse(input)
374}
375
376fn parse_function_call(input: &str) -> ParseResult<'_, Value> {
377 alt((
378 parse_date_call,
379 parse_time_call,
380 parse_duration_call,
381 parse_decimal_call,
382 parse_binary_call,
383 ))
384 .parse(input)
385}
386
387fn parse_date_call(input: &str) -> ParseResult<'_, Value> {
388 let (input, _) = tag("date")(input)?;
389 let (input, _) = ws(char('(')).parse(input)?;
390 let (input, date_str) = ws(parse_string).parse(input)?;
391 let (input, _) = ws(char(')')).parse(input)?;
392
393 let date = NaiveDate::parse_from_str(&date_str, "%Y-%m-%d")
394 .map_err(|_| nom::Err::Error(ParseErrorKind::InvalidDate(date_str.to_string())))?;
395 Ok((input, Value::Date(date)))
396}
397
398fn parse_time_call(input: &str) -> ParseResult<'_, Value> {
399 let (input, _) = tag("time")(input)?;
400 let (input, _) = ws(char('(')).parse(input)?;
401 let (input, time_str) = ws(parse_string).parse(input)?;
402 let (input, _) = ws(char(')')).parse(input)?;
403
404 let time = NaiveTime::parse_from_str(&time_str, "%H:%M:%S")
405 .map_err(|_| nom::Err::Error(ParseErrorKind::InvalidTime(time_str.to_string())))?;
406 Ok((input, Value::Time(time)))
407}
408
409fn parse_duration_call(input: &str) -> ParseResult<'_, Value> {
410 let (input, _) = tag("duration")(input)?;
411 let (input, _) = ws(char('(')).parse(input)?;
412 let (input, duration_str) = ws(parse_string).parse(input)?;
413 let (input, _) = ws(char(')')).parse(input)?;
414
415 let duration =
416 if duration_str.ends_with('h') {
417 let hours: f64 = duration_str.trim_end_matches('h').parse().map_err(|_| {
418 nom::Err::Error(ParseErrorKind::InvalidDuration(duration_str.clone()))
419 })?;
420 Duration::hours(hours as i64)
421 } else if duration_str.ends_with('m') {
422 let minutes: f64 = duration_str.trim_end_matches('m').parse().map_err(|_| {
423 nom::Err::Error(ParseErrorKind::InvalidDuration(duration_str.clone()))
424 })?;
425 Duration::minutes(minutes as i64)
426 } else if duration_str.ends_with('s') {
427 let seconds: f64 = duration_str.trim_end_matches('s').parse().map_err(|_| {
428 nom::Err::Error(ParseErrorKind::InvalidDuration(duration_str.clone()))
429 })?;
430 Duration::seconds(seconds as i64)
431 } else {
432 return Err(nom::Err::Error(ParseErrorKind::InvalidDuration(
433 duration_str,
434 )));
435 };
436
437 Ok((input, Value::Duration(duration)))
438}
439
440fn parse_decimal_call(input: &str) -> ParseResult<'_, Value> {
441 let (input, _) = tag("decimal")(input)?;
442 let (input, _) = ws(char('(')).parse(input)?;
443 let (input, decimal_str) = ws(parse_string).parse(input)?;
444 let (input, _) = ws(char(')')).parse(input)?;
445
446 let decimal = decimal_str
447 .parse::<Decimal>()
448 .map_err(|_| nom::Err::Error(ParseErrorKind::InvalidDecimal(decimal_str)))?;
449 Ok((input, Value::Decimal(decimal)))
450}
451
452fn parse_binary_call(input: &str) -> ParseResult<'_, Value> {
453 let (input, _) = tag("binary")(input)?;
454 let (input, _) = ws(char('(')).parse(input)?;
455 let (input, binary_str) = ws(parse_string).parse(input)?;
456 let (input, _) = ws(char(')')).parse(input)?;
457
458 let binary = base64::engine::general_purpose::STANDARD
459 .decode(&binary_str)
460 .map_err(|_| nom::Err::Error(ParseErrorKind::InvalidBinary(binary_str)))?;
461 Ok((input, Value::Binary(binary)))
462}
463
464fn substitute_env_vars(value: &str) -> Result<String, ParseErrorKind> {
465 let re = Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}").unwrap();
466 let mut result = value.to_string();
467
468 for cap in re.captures_iter(value) {
469 let var_name = &cap[1];
470 match std::env::var(var_name) {
471 Ok(val) => {
472 result = result.replace(&cap[0], &val);
473 }
474 Err(_) => {
475 return Err(ParseErrorKind::EnvVarNotFound(var_name.to_string()));
476 }
477 }
478 }
479
480 Ok(result)
481}
482
483fn parse_zoko_with_context(
484 input: &str,
485 base_path: &Path,
486 included_files: &mut HashSet<PathBuf>,
487) -> Result<ZokoFile, ParseErrorKind> {
488 let (remaining, _) = parse_comments
489 .parse(input)
490 .map_err(|e| ParseErrorKind::Expected {
491 expected: "valid zoko input".to_string(),
492 found: format!("{:?}", e),
493 })?;
494
495 let (remaining, entries) =
496 many0(|input| parse_entry_or_include_flat(input, base_path, included_files))
497 .parse(remaining)
498 .map_err(|e| ParseErrorKind::Expected {
499 expected: "valid zoko entries".to_string(),
500 found: format!("{:?}", e),
501 })?;
502
503 let (_, _) = parse_comments
504 .parse(remaining)
505 .map_err(|e| ParseErrorKind::Expected {
506 expected: "end of input".to_string(),
507 found: format!("{:?}", e),
508 })?;
509
510 let mut map = IndexMap::new();
511 for entry_vec in entries {
512 for (k, v) in entry_vec {
513 map.insert(k, v);
514 }
515 }
516
517 Ok(ZokoFile { entries: map })
518}
519
520fn parse_entry_or_include_flat<'a>(
521 input: &'a str,
522 base_path: &Path,
523 included_files: &mut HashSet<PathBuf>,
524) -> ParseResult<'a, Vec<(String, Value)>> {
525 let (input, _) = parse_comments(input)?;
526
527 if let Ok((input_after_tag, _)) = tag::<&str, &str, ParseErrorKind>("@include")(input) {
528 let (input, _) = ws(char('(')).parse(input_after_tag)?;
529 let (input, file_path) = ws(parse_string).parse(input)?;
530 let (input, _) = ws(char(')')).parse(input)?;
531 let (input, _) = opt(ws(char(','))).parse(input)?;
532
533 let full_path = if Path::new(&file_path).is_absolute() {
534 PathBuf::from(file_path)
535 } else {
536 base_path.join(&file_path)
537 };
538
539 if included_files.contains(&full_path) {
540 return Err(nom::Err::Error(ParseErrorKind::CircularInclude(
541 full_path.display().to_string(),
542 )));
543 }
544
545 included_files.insert(full_path.clone());
546
547 let content = std::fs::read_to_string(&full_path).map_err(|_| {
548 nom::Err::Error(ParseErrorKind::IncludeFileError(
549 full_path.display().to_string(),
550 ))
551 })?;
552
553 let zoko_file = parse_zoko_with_context(
554 &content,
555 full_path.parent().unwrap_or(base_path),
556 included_files,
557 )
558 .map_err(|e| nom::Err::Error(ParseErrorKind::IncludeFileError(e.to_string())))?;
559
560 let entries: Vec<(String, Value)> = zoko_file.entries.into_iter().collect();
561 return Ok((input, entries));
562 }
563
564 let (input, key) = ws(parse_identifier).parse(input)?;
565 let (input, _) = ws(char(':')).parse(input)?;
566 let (input, value) = parse_value_with_context(input, base_path, included_files)?;
567 let (input, _) = parse_comments(input)?;
568 let (input, _) = opt(ws(char(','))).parse(input)?;
569
570 Ok((input, vec![(key, value)]))
571}
572
573fn parse_value_with_context<'a>(
574 input: &'a str,
575 _base_path: &Path,
576 _included_files: &mut HashSet<PathBuf>,
577) -> ParseResult<'a, Value> {
578 parse_value(input)
579}
580
581fn parse_comment(input: &str) -> ParseResult<'_, ()> {
582 alt((
583 value((), (tag("//"), take_till1(|c| c == '\n'), multispace0)),
584 value((), (tag("/*"), take_until("*/"), tag("*/"), multispace0)),
585 ))
586 .parse(input)
587}
588
589fn parse_comments(input: &str) -> ParseResult<'_, ()> {
590 let (input, _) = multispace0(input)?;
591 many0(parse_comment).parse(input).map(|(i, _)| (i, ()))
592}
593
594fn parse_entry(input: &str) -> ParseResult<'_, (String, Value)> {
595 let (input, _) = parse_comments(input)?;
596 let (input, key) = ws(parse_identifier).parse(input)?;
597 let (input, _) = ws(char(':')).parse(input)?;
598 let (input, value) = ws(parse_value).parse(input)?;
599 let (input, _) = parse_comments(input)?;
600 let (input, _) = opt(ws(char(','))).parse(input)?;
601 Ok((input, (key, value)))
602}
603
604pub fn parse_zoko(input: &str) -> Result<ZokoFile, ParseErrorKind> {
605 let (remaining, _) = parse_comments
606 .parse(input)
607 .map_err(|e| ParseErrorKind::Expected {
608 expected: "valid zoko input".to_string(),
609 found: format!("{:?}", e),
610 })?;
611
612 let (remaining, entries) =
613 many0(parse_entry)
614 .parse(remaining)
615 .map_err(|e| ParseErrorKind::Expected {
616 expected: "valid zoko entries".to_string(),
617 found: format!("{:?}", e),
618 })?;
619
620 let (_, _) = parse_comments
621 .parse(remaining)
622 .map_err(|e| ParseErrorKind::Expected {
623 expected: "end of input".to_string(),
624 found: format!("{:?}", e),
625 })?;
626
627 let mut map = IndexMap::new();
628 for (k, v) in entries {
629 map.insert(k, v);
630 }
631
632 Ok(ZokoFile { entries: map })
633}
634
635pub fn parse_zoko_with_includes(input: &str, base_path: &Path) -> Result<ZokoFile, ParseErrorKind> {
636 let mut included_files = HashSet::new();
637 parse_zoko_with_context(input, base_path, &mut included_files)
638}
639
640pub fn parse_zoko_to_json(input: &str) -> Result<String, ParseErrorKind> {
641 let zoko = parse_zoko(input)?;
642 serde_json::to_string_pretty(&zoko).map_err(|e| ParseErrorKind::Expected {
643 expected: "valid JSON serialization".to_string(),
644 found: e.to_string(),
645 })
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651
652 #[test]
653 fn test_parse_simple_object() {
654 let input = r#"name: "value""#;
655 let result = parse_zoko(input).unwrap();
656 assert_eq!(
657 result.entries.get("name"),
658 Some(&Value::String("value".to_string()))
659 );
660 }
661
662 #[test]
663 fn test_parse_map() {
664 let input = r#"
665 map: {
666 id: "value",
667 id2: "value2",
668 }
669 "#;
670 let result = parse_zoko(input).unwrap();
671 let map = result.entries.get("map").unwrap();
672 if let Value::Object(obj) = map {
673 assert_eq!(obj.get("id"), Some(&Value::String("value".to_string())));
674 assert_eq!(obj.get("id2"), Some(&Value::String("value2".to_string())));
675 } else {
676 panic!("Expected object");
677 }
678 }
679
680 #[test]
681 fn test_parse_array() {
682 let input = r#"tags: ["Hello", "Zoil"]"#;
683 let result = parse_zoko(input).unwrap();
684 let arr = result.entries.get("tags").unwrap();
685 if let Value::Array(vec) = arr {
686 assert_eq!(vec.len(), 2);
687 assert_eq!(vec[0], Value::String("Hello".to_string()));
688 assert_eq!(vec[1], Value::String("Zoil".to_string()));
689 } else {
690 panic!("Expected array");
691 }
692 }
693
694 #[test]
695 fn test_parse_comments() {
696 let input = r#"
697 // Single line comment
698 name: "value"
699 /* Multi line
700 Comment */
701 "#;
702 let result = parse_zoko(input).unwrap();
703 assert_eq!(
704 result.entries.get("name"),
705 Some(&Value::String("value".to_string()))
706 );
707 }
708
709 #[test]
710 fn test_parse_complex() {
711 let input = r#"
712 name: "@Main/Hello",
713 channel: "main",
714 branch: "Production",
715 status: "Release",
716 version: 1.0.0,
717 description: "Hello package for Zoil",
718 tags: [
719 "Hello",
720 "Zoil",
721 ],
722 website: "https://hello.nel.co",
723 dependencies: [
724 "Hola": 1.0.2,
725 "@German/Hallo": {
726 channel: "main",
727 version: "latest",
728 },
729 ],
730 "#;
731 let result = parse_zoko(input).unwrap();
732 assert_eq!(
733 result.entries.get("name"),
734 Some(&Value::String("@Main/Hello".to_string()))
735 );
736 assert_eq!(
737 result.entries.get("channel"),
738 Some(&Value::String("main".to_string()))
739 );
740 assert_eq!(result.entries.get("version"), Some(&Value::Float(1.0)));
741 }
742
743 #[test]
744 fn test_parse_number_formats() {
745 let input = r#"
746 int: 42,
747 float: 3.14,
748 negative: -10,
749 scientific: 1.5e10,
750 "#;
751 let result = parse_zoko(input).unwrap();
752 assert_eq!(result.entries.get("int"), Some(&Value::Integer(42)));
753 assert_eq!(result.entries.get("float"), Some(&Value::Float(3.14)));
754 assert_eq!(result.entries.get("negative"), Some(&Value::Integer(-10)));
755 assert_eq!(
756 result.entries.get("scientific"),
757 Some(&Value::Float(1.5e10))
758 );
759 }
760
761 #[test]
762 fn test_parse_boolean() {
763 let input = r#"
764 yes: true,
765 no: false,
766 "#;
767 let result = parse_zoko(input).unwrap();
768 assert_eq!(result.entries.get("yes"), Some(&Value::Boolean(true)));
769 assert_eq!(result.entries.get("no"), Some(&Value::Boolean(false)));
770 }
771
772 #[test]
773 fn test_parse_null() {
774 let input = r#"value: null"#;
775 let result = parse_zoko(input).unwrap();
776 assert_eq!(result.entries.get("value"), Some(&Value::Null));
777 }
778
779 #[test]
780 fn test_parse_different_string_types() {
781 let input = r#"
782 double: "hello",
783 single: 'world',
784 backtick: `multiline
785string`,
786 "#;
787 let result = parse_zoko(input).unwrap();
788 assert_eq!(
789 result.entries.get("double"),
790 Some(&Value::String("hello".to_string()))
791 );
792 assert_eq!(
793 result.entries.get("single"),
794 Some(&Value::String("world".to_string()))
795 );
796 assert_eq!(
797 result.entries.get("backtick"),
798 Some(&Value::String("multiline\nstring".to_string()))
799 );
800 }
801
802 #[test]
803 fn test_trailing_comma() {
804 let input = r#"
805 a: 1,
806 b: 2,
807 "#;
808 let result = parse_zoko(input).unwrap();
809 assert_eq!(result.entries.len(), 2);
810 }
811
812 #[test]
813 fn test_to_json() {
814 let input = r#"name: "test", value: 42"#;
815 let json = parse_zoko_to_json(input).unwrap();
816 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
817 assert_eq!(parsed["entries"]["name"], "test");
818 assert_eq!(parsed["entries"]["value"], 42);
819 }
820
821 #[test]
822 fn test_array_with_objects() {
823 let input = r#"dependencies: [{name: "hola", version: "1.1.0"}, {name: "@german/hallo", version: "latest"}]"#;
824 let result = parse_zoko(input).unwrap();
825 let deps = result.entries.get("dependencies").unwrap();
826 if let Value::Array(vec) = deps {
827 assert_eq!(vec.len(), 2);
828 if let Value::Object(obj) = &vec[0] {
829 assert_eq!(obj.get("name"), Some(&Value::String("hola".to_string())));
830 assert_eq!(
831 obj.get("version"),
832 Some(&Value::String("1.1.0".to_string()))
833 );
834 } else {
835 panic!("Expected object for first dependency");
836 }
837 if let Value::Object(obj) = &vec[1] {
838 assert_eq!(
839 obj.get("name"),
840 Some(&Value::String("@german/hallo".to_string()))
841 );
842 assert_eq!(
843 obj.get("version"),
844 Some(&Value::String("latest".to_string()))
845 );
846 } else {
847 panic!("Expected object for second dependency");
848 }
849 } else {
850 panic!("Expected array");
851 }
852 }
853
854 #[test]
855 fn test_json_compatibility() {
856 let zoko_input = r#"dependencies: [{name: "hola", version: "1.1.0"}, {name: "@german/hallo", version: "latest"}]"#;
857 let json_output = parse_zoko_to_json(zoko_input).unwrap();
858
859 let json_value: serde_json::Value = serde_json::from_str(&json_output).unwrap();
860
861 assert!(json_value["entries"]["dependencies"].is_array());
862 assert_eq!(
863 json_value["entries"]["dependencies"]
864 .as_array()
865 .unwrap()
866 .len(),
867 2
868 );
869 }
870
871 #[test]
872 fn test_entry_order_preservation() {
873 let input = r#"first: "value1", second: "value2", third: "value3""#;
874 let result = parse_zoko(input).unwrap();
875
876 let keys: Vec<&String> = result.entries.keys().collect();
877 assert_eq!(keys, vec!["first", "second", "third"]);
878 }
879
880 #[test]
881 fn test_complex_file_parsing() {
882 let input = r#"name: "@Main/Hello",
883channel: "main",
884branch: "Production",
885status: "Release",
886version: "1.0.0",
887description: "Hello package for Zoil",
888tags: ["Hello", "Zoil"],
889website: "https://hello.nel.co",
890dependencies: [
891 {name: "Hola", version: "1.0.2"},
892 {name: "@German/Hallo", channel: "main", version: "latest"},
893],
894"#;
895 let result = parse_zoko(input).unwrap();
896
897 assert_eq!(result.entries.len(), 9);
898 assert!(result.entries.contains_key("name"));
899 assert!(result.entries.contains_key("channel"));
900 assert!(result.entries.contains_key("branch"));
901 assert!(result.entries.contains_key("status"));
902 assert!(result.entries.contains_key("version"));
903 assert!(result.entries.contains_key("description"));
904 assert!(result.entries.contains_key("tags"));
905 assert!(result.entries.contains_key("website"));
906 assert!(result.entries.contains_key("dependencies"));
907 }
908
909 #[test]
910 fn test_parse_date() {
911 let input = r#"release_date: date("2024-06-21")"#;
912 let result = parse_zoko(input).unwrap();
913 assert!(result.entries.get("release_date").is_some());
914 }
915
916 #[test]
917 fn test_parse_time() {
918 let input = r#"meeting_time: time("14:30:00")"#;
919 let result = parse_zoko(input).unwrap();
920 assert!(result.entries.get("meeting_time").is_some());
921 }
922
923 #[test]
924 fn test_parse_duration() {
925 let input = r#"timeout: duration("30s")"#;
926 let result = parse_zoko(input).unwrap();
927 assert!(result.entries.get("timeout").is_some());
928 }
929
930 #[test]
931 fn test_parse_decimal() {
932 let input = r#"price: decimal("19.99")"#;
933 let result = parse_zoko(input).unwrap();
934 assert!(result.entries.get("price").is_some());
935 }
936
937 #[test]
938 fn test_parse_binary() {
939 let input = r#"data: binary("SGVsbG8gV29ybGQ=")"#;
940 let result = parse_zoko(input).unwrap();
941 assert!(result.entries.get("data").is_some());
942 }
943
944 #[test]
945 fn test_parse_include() {
946 use std::fs;
947
948 let temp_dir = std::env::temp_dir();
949 let include_file = temp_dir.join("test_include.zo");
950 fs::write(&include_file, r#"included_key: "included_value""#).unwrap();
951
952 let base_path = temp_dir.as_path();
953 let input = r#"@include("test_include.zo")"#;
954 let result = parse_zoko_with_includes(input, base_path).unwrap();
955
956 assert!(result.entries.contains_key("included_key"));
957 fs::remove_file(&include_file).unwrap();
958 }
959
960 #[test]
961 fn test_env_var_substitution() {
962 unsafe {
963 std::env::set_var("TEST_VAR", "hello");
964 }
965 let input = r#"message: `${TEST_VAR}`"#;
966 let result = parse_zoko(input).unwrap();
967 if let Some(Value::String(msg)) = result.entries.get("message") {
968 assert_eq!(msg, "hello");
969 } else {
970 panic!("Expected string value");
971 }
972 unsafe {
973 std::env::remove_var("TEST_VAR");
974 }
975 }
976
977 #[test]
978 fn test_schema_validation() {
979 let input = r#"name: "test", age: 25, active: true"#;
980 let zoko = parse_zoko(input).unwrap();
981
982 let mut properties = IndexMap::new();
983 properties.insert("name".to_string(), SchemaType::String);
984 properties.insert("age".to_string(), SchemaType::Integer);
985 properties.insert("active".to_string(), SchemaType::Boolean);
986
987 let schema = Schema {
988 properties,
989 required: vec!["name".to_string(), "age".to_string()],
990 };
991
992 let result = validate_schema(&zoko, &schema);
993 assert!(result.is_ok());
994 }
995
996 #[test]
997 fn test_schema_validation_type_mismatch() {
998 let input = r#"name: "test", age: "25", active: true"#;
999 let zoko = parse_zoko(input).unwrap();
1000
1001 let mut properties = IndexMap::new();
1002 properties.insert("name".to_string(), SchemaType::String);
1003 properties.insert("age".to_string(), SchemaType::Integer);
1004 properties.insert("active".to_string(), SchemaType::Boolean);
1005
1006 let schema = Schema {
1007 properties,
1008 required: vec!["name".to_string(), "age".to_string()],
1009 };
1010
1011 let result = validate_schema(&zoko, &schema);
1012 assert!(result.is_err());
1013 }
1014
1015 #[test]
1016 fn test_schema_validation_missing_required() {
1017 let input = r#"name: "test", active: true"#;
1018 let zoko = parse_zoko(input).unwrap();
1019
1020 let mut properties = IndexMap::new();
1021 properties.insert("name".to_string(), SchemaType::String);
1022 properties.insert("age".to_string(), SchemaType::Integer);
1023 properties.insert("active".to_string(), SchemaType::Boolean);
1024
1025 let schema = Schema {
1026 properties,
1027 required: vec!["name".to_string(), "age".to_string()],
1028 };
1029
1030 let result = validate_schema(&zoko, &schema);
1031 assert!(result.is_err());
1032 }
1033}