Skip to main content

zoko_parser/
lib.rs

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}