Skip to main content

ron_schema/schema/
parser.rs

1/*************************
2 * Author: Bradley Hunter
3 */
4
5use crate::span::{Position, Span, Spanned};
6use crate::error::{SchemaParseError, SchemaErrorKind};
7use super::{SchemaType, FieldDef, StructDef, EnumDef, HashSet, Schema, HashMap};
8
9#[derive(Debug)]
10struct Parser<'a> {
11    source: &'a str,
12    bytes: &'a [u8],
13    offset: usize,
14    line: usize,
15    column: usize,
16}
17
18impl<'a> Parser<'a> {
19    fn new(source: &'a str) -> Self {
20        Self { source, bytes: source.as_bytes(), offset: 0, line: 1, column: 1 }
21    }
22
23    fn position(&self) -> Position {
24        Position { offset: self.offset, line: self.line, column: self.column }
25    }
26
27    fn peek(&self) -> Option<u8> {
28        self.bytes.get(self.offset).copied()
29    }
30
31    fn advance(&mut self) {
32        if let Some(byte) = self.peek() {
33            if byte == b'\n'{
34                self.column = 1;
35                self.line += 1;
36            } else {
37                self.column += 1;
38            }
39            self.offset += 1;
40        } 
41    }
42
43    fn skip_whitespace(&mut self) {
44        loop {
45            match self.peek() {
46                Some(b' ' | b'\t' | b'\n' | b'\r') => self.advance(),
47                Some(b'/') if self.bytes.get(self.offset + 1) == Some(&b'/') => {
48                    while self.peek().is_some_and(|b| b != b'\n') {
49                        self.advance();
50                    }
51                }
52                _ => break,
53            }
54        }
55    }
56
57    fn expect_char(&mut self, expected: u8) -> Result<(), SchemaParseError> {
58        let start = self.position();
59        match self.peek() {
60            Some(b) if b == expected => {
61                self.advance();
62                Ok(())
63            },
64            Some(b) => {
65                self.advance();
66                let end = self.position();
67                Err(SchemaParseError { 
68                    span: Span { 
69                        start, 
70                        end 
71                    }, 
72                    kind: SchemaErrorKind::UnexpectedToken { 
73                        expected: format!("'{}'", expected as char), 
74                        found: format!("'{}'", b as char) 
75                    } 
76                })
77            },
78            None => {
79                Err(SchemaParseError { 
80                    span: Span { 
81                        start, 
82                        end: start 
83                    }, 
84                    kind: SchemaErrorKind::UnexpectedToken { 
85                        expected: format!("'{}'", expected as char), 
86                        found: "end of input".to_string() 
87                    } 
88                })
89            }
90        }
91    }
92
93    fn parse_identifier(&mut self) -> Result<Spanned<String>, SchemaParseError> {
94        let start = self.position();
95
96        // Check for valid identifier start
97        match self.peek() {
98            Some(b) if b.is_ascii_alphabetic() || b == b'_' => {},
99            Some(b) => {
100                self.advance();
101                let end = self.position();
102                return Err(SchemaParseError {
103                    span: Span { start, end },
104                    kind: SchemaErrorKind::UnexpectedToken {
105                        expected: "identifier".to_string(),
106                        found: format!("'{}'", b as char),
107                    },
108                });
109            },
110            None => {
111                return Err(SchemaParseError {
112                    span: Span { start, end: start },
113                    kind: SchemaErrorKind::UnexpectedToken {
114                        expected: "identifier".to_string(),
115                        found: "end of input".to_string(),
116                    },
117                });
118            },
119        }
120
121        // Consume all identifier continuation characters
122        while self.peek().is_some_and(|b| b.is_ascii_alphanumeric() || b == b'_') {
123            self.advance();
124        }
125
126        // Slice out the identifier text
127        let end = self.position();
128        Ok(Spanned {
129            value: self.source[start.offset..end.offset].to_string(),
130            span: Span { start, end },
131        })
132    }
133
134    #[allow(clippy::too_many_lines)]
135    fn parse_type(&mut self) -> Result<Spanned<SchemaType>, SchemaParseError> {
136        self.skip_whitespace();
137        let start = self.position();
138
139        match self.peek() {
140            Some(b'[') => {
141                // List: consume '[', parse inner type, expect ']'
142                self.advance();
143                self.skip_whitespace();
144                let inner = self.parse_type()?;
145                self.skip_whitespace();
146                self.expect_char(b']')?;
147                let end = self.position();
148                Ok(Spanned {
149                    value: SchemaType::List(Box::new(inner.value)),
150                    span: Span { start, end },
151                })
152            }
153            Some(b'{') => {
154                // Map: consume '{', parse key type, expect ':', parse value type, expect '}'
155                self.advance();
156                self.skip_whitespace();
157                let key_type = self.parse_type()?;
158                // Validate key type is String, Integer, or EnumRef
159                match &key_type.value {
160                    SchemaType::String | SchemaType::Integer | SchemaType::EnumRef(_) => {}
161                    _ => {
162                        return Err(SchemaParseError {
163                            span: key_type.span,
164                            kind: SchemaErrorKind::InvalidMapKeyType {
165                                found: format!("{:?}", key_type.value),
166                            },
167                        });
168                    }
169                }
170                self.skip_whitespace();
171                self.expect_char(b':')?;
172                self.skip_whitespace();
173                let value_type = self.parse_type()?;
174                self.skip_whitespace();
175                self.expect_char(b'}')?;
176                let end = self.position();
177                Ok(Spanned {
178                    value: SchemaType::Map(Box::new(key_type.value), Box::new(value_type.value)),
179                    span: Span { start, end },
180                })
181            }
182            Some(b'(') => {
183                // Disambiguate struct vs tuple:
184                // Save position, consume '(', skip whitespace.
185                // If ')' → empty struct. If identifier followed by ':' → struct.
186                // Otherwise → tuple (comma-separated types).
187                let saved = (self.offset, self.line, self.column);
188                self.advance(); // consume '('
189                self.skip_whitespace();
190
191                let is_struct = if self.peek() == Some(b')') {
192                    true // empty parens → treat as empty struct
193                } else {
194                    // Try to determine if this is name: Type (struct) or Type, Type (tuple)
195                    let probe_pos = (self.offset, self.line, self.column);
196                    let is_field = if let Ok(_id) = self.parse_identifier() {
197                        self.skip_whitespace();
198                        
199                        self.peek() == Some(b':')
200                    } else {
201                        false
202                    };
203                    // Rewind to after '('
204                    self.offset = probe_pos.0;
205                    self.line = probe_pos.1;
206                    self.column = probe_pos.2;
207                    is_field
208                };
209
210                // Rewind to before '(' and parse as struct or tuple
211                self.offset = saved.0;
212                self.line = saved.1;
213                self.column = saved.2;
214
215                if is_struct {
216                    let struct_def = self.parse_struct()?;
217                    let end = self.position();
218                    Ok(Spanned {
219                        value: SchemaType::Struct(struct_def),
220                        span: Span { start, end },
221                    })
222                } else {
223                    let types = self.parse_tuple_type()?;
224                    let end = self.position();
225                    Ok(Spanned {
226                        value: SchemaType::Tuple(types),
227                        span: Span { start, end },
228                    })
229                }
230            }
231            Some(b) if b.is_ascii_alphabetic() => {
232                // Identifier: could be primitive, Option, or EnumRef
233                let id = self.parse_identifier()?;
234                match id.value.as_str() {
235                    "String" => Ok(Spanned { value: SchemaType::String, span: id.span }),
236                    "Integer" => Ok(Spanned { value: SchemaType::Integer, span: id.span }),
237                    "Float" => Ok(Spanned { value: SchemaType::Float, span: id.span }),
238                    "Bool" => Ok(Spanned { value: SchemaType::Bool, span: id.span }),
239                    "Option" => {
240                        // expect '(', parse inner type, expect ')'
241                        self.skip_whitespace();
242                        self.expect_char(b'(')?;
243                        self.skip_whitespace();
244                        let inner = self.parse_type()?;
245                        self.skip_whitespace();
246                        self.expect_char(b')')?;
247                        let end = self.position();
248                        Ok(Spanned {
249                            value: SchemaType::Option(Box::new(inner.value)),
250                            span: Span { start, end },
251                        })
252                    }
253                    _ => Ok(Spanned { value: SchemaType::EnumRef(id.value), span: id.span }),
254                }
255            }
256            Some(b) => {
257                // Error: unexpected character
258                self.advance();
259                let end = self.position();
260                Err(SchemaParseError {
261                    span: Span { start, end },
262                    kind: SchemaErrorKind::UnexpectedToken {
263                        expected: "type".to_string(),
264                        found: format!("'{}'", b as char),
265                    },
266                })
267            }
268            None => {
269                Err(SchemaParseError {
270                    span: Span { start, end: start },
271                    kind: SchemaErrorKind::UnexpectedToken {
272                        expected: "type".to_string(),
273                        found: "end of input".to_string(),
274                    },
275                })
276            }
277        }
278    }
279
280    fn parse_field(&mut self) -> Result<FieldDef, SchemaParseError> {
281        self.skip_whitespace();
282        let name = self.parse_identifier()?;
283        self.skip_whitespace();
284        self.expect_char(b':')?;
285        self.skip_whitespace();
286        let type_ = self.parse_type()?;
287        Ok(FieldDef{
288            name,
289            type_
290        })
291    }
292
293    fn parse_struct(&mut self) -> Result<StructDef, SchemaParseError> {
294        self.skip_whitespace();
295        self.expect_char(b'(')?;
296        let mut fields: Vec<FieldDef> = Vec::new();
297        loop {
298            self.skip_whitespace();
299            if let Some(byte) = self.peek() {
300                if byte == b')' {
301                    break ;
302                } 
303                let field = self.parse_field()?;
304                fields.push(field);
305                self.skip_whitespace();
306                if self.peek() == Some(b',') {
307                    self.advance();
308                }
309            } else {
310                return Err(SchemaParseError {
311                    span: Span { start: self.position(), end: self.position() },
312                    kind: SchemaErrorKind::UnexpectedToken { expected: ")".to_string(), found: "end of file".to_string() }
313                });
314            }
315        }
316        self.expect_char(b')')?;
317        Ok(StructDef { fields })
318    }
319
320    /// Parses `(Type, Type, ...)` as a tuple type.
321    fn parse_tuple_type(&mut self) -> Result<Vec<SchemaType>, SchemaParseError> {
322        self.skip_whitespace();
323        self.expect_char(b'(')?;
324        let mut types = Vec::new();
325        loop {
326            self.skip_whitespace();
327            if self.peek() == Some(b')') {
328                break;
329            }
330            let t = self.parse_type()?;
331            types.push(t.value);
332            self.skip_whitespace();
333            if self.peek() == Some(b',') {
334                self.advance();
335            }
336        }
337        self.expect_char(b')')?;
338        Ok(types)
339    }
340
341    fn parse_enum_def(&mut self) -> Result<EnumDef, SchemaParseError> {
342        self.skip_whitespace();
343        let keyword = self.parse_identifier()?;
344        if keyword.value != "enum" {
345            return Err(SchemaParseError {
346                span: keyword.span,
347                kind: SchemaErrorKind::UnexpectedToken {
348                    expected: "\"enum\"".to_string(),
349                    found: keyword.value,
350                },
351            });
352        }
353        self.skip_whitespace();
354        let name = self.parse_identifier()?;
355        self.skip_whitespace();
356        self.expect_char(b'{')?;
357        let mut variants = HashSet::new();
358        loop {
359            self.skip_whitespace();
360            if let Some(byte) = self.peek() {
361                if byte == b'}' {
362                    break ;
363                } 
364                let variant = self.parse_identifier()?;
365                variants.insert(variant.value);
366                self.skip_whitespace();
367                if self.peek() == Some(b',') {
368                    self.advance();
369                }
370            } else {
371                return Err(SchemaParseError {
372                    span: Span { start: self.position(), end: self.position() },
373                    kind: SchemaErrorKind::UnexpectedToken { expected: "}".to_string(), found: "end of file".to_string() }
374                });
375            }
376        }
377
378        self.expect_char(b'}')?;
379        Ok(EnumDef { name: name.value, variants })
380    }
381
382    /// Parses `type Name = <type>` — assumes the "type" keyword has already been confirmed.
383    fn parse_alias_def(&mut self) -> Result<(String, Spanned<SchemaType>), SchemaParseError> {
384        self.skip_whitespace();
385        self.parse_identifier()?; // consume "type" keyword
386        self.skip_whitespace();
387        let name = self.parse_identifier()?;
388        self.skip_whitespace();
389        self.expect_char(b'=')?;
390        self.skip_whitespace();
391        let type_ = self.parse_type()?;
392        Ok((name.value, type_))
393    }
394}
395
396/// Parses a `.ronschema` source string into a [`Schema`].
397///
398/// # Errors
399///
400/// Returns a [`SchemaParseError`] if the source contains syntax errors,
401/// duplicate definitions, or unresolved enum references.
402pub fn parse_schema(source: &str) -> Result<Schema, SchemaParseError> {
403    let mut parser = Parser::new(source);
404    parser.skip_whitespace();
405
406    let mut root = if parser.peek() == Some(b'(') {
407        parser.parse_struct()?
408    } else {
409        StructDef { fields: Vec::new() }
410    };
411
412    let mut enums: HashMap<String, EnumDef> = HashMap::new();
413    let mut aliases: HashMap<String, Spanned<SchemaType>> = HashMap::new();
414
415    loop {
416        parser.skip_whitespace();
417        if parser.peek().is_none() {
418            break;
419        }
420
421        // Peek ahead to determine if this is "enum" or "type"
422        let start = parser.position();
423        let keyword = parser.parse_identifier()?;
424
425        match keyword.value.as_str() {
426            "enum" => {
427                // Rewind — parse_enum_def expects to consume "enum" itself
428                parser.offset = start.offset;
429                parser.line = start.line;
430                parser.column = start.column;
431
432                let enum_def = parser.parse_enum_def()?;
433                if let Some(old) = enums.insert(enum_def.name.clone(), enum_def) {
434                    return Err(SchemaParseError {
435                        span: Span { start: parser.position(), end: parser.position() },
436                        kind: SchemaErrorKind::DuplicateEnum { name: old.name },
437                    });
438                }
439            }
440            "type" => {
441                // Rewind — parse_alias_def expects to consume "type" itself
442                parser.offset = start.offset;
443                parser.line = start.line;
444                parser.column = start.column;
445
446                let (name, type_) = parser.parse_alias_def()?;
447                if aliases.contains_key(&name) {
448                    return Err(SchemaParseError {
449                        span: type_.span,
450                        kind: SchemaErrorKind::DuplicateAlias { name },
451                    });
452                }
453                aliases.insert(name, type_);
454            }
455            other => {
456                return Err(SchemaParseError {
457                    span: keyword.span,
458                    kind: SchemaErrorKind::UnexpectedToken {
459                        expected: "\"enum\" or \"type\"".to_string(),
460                        found: other.to_string(),
461                    },
462                });
463            }
464        }
465    }
466
467    // Reclassify EnumRefs that are actually aliases — in the root struct and in alias definitions.
468    // Collect alias names into a set to avoid borrow conflicts when mutating alias values.
469    let alias_names: HashSet<String> = aliases.keys().cloned().collect();
470    reclassify_refs_in_struct_by_name(&mut root, &alias_names);
471    for spanned_type in aliases.values_mut() {
472        reclassify_refs_in_type_by_name(&mut spanned_type.value, &alias_names);
473    }
474
475    // Verify all refs resolve to a known enum or alias
476    verify_refs(&root, &enums, &aliases)?;
477
478    // Check for recursive aliases
479    verify_no_recursive_aliases(&aliases)?;
480
481    Ok(Schema { root, enums, aliases })
482}
483
484/// Reclassifies `EnumRef` names that are actually type aliases into `AliasRef`.
485/// Mutates the struct in place.
486fn reclassify_refs_in_struct_by_name(
487    struct_def: &mut StructDef,
488    alias_names: &HashSet<String>,
489) {
490    for field in &mut struct_def.fields {
491        reclassify_refs_in_type_by_name(&mut field.type_.value, alias_names);
492    }
493}
494
495fn reclassify_refs_in_type_by_name(
496    schema_type: &mut SchemaType,
497    alias_names: &HashSet<String>,
498) {
499    match schema_type {
500        SchemaType::EnumRef(name) if alias_names.contains(name.as_str()) => {
501            *schema_type = SchemaType::AliasRef(name.clone());
502        }
503        SchemaType::Option(inner) | SchemaType::List(inner) => {
504            reclassify_refs_in_type_by_name(inner, alias_names);
505        }
506        SchemaType::Map(key, value) => {
507            reclassify_refs_in_type_by_name(key, alias_names);
508            reclassify_refs_in_type_by_name(value, alias_names);
509        }
510        SchemaType::Tuple(types) => {
511            for t in types {
512                reclassify_refs_in_type_by_name(t, alias_names);
513            }
514        }
515        SchemaType::Struct(struct_def) => {
516            reclassify_refs_in_struct_by_name(struct_def, alias_names);
517        }
518        _ => {}
519    }
520}
521
522/// Verifies all `EnumRef` names resolve to a defined enum.
523/// (`AliasRefs` have already been reclassified, so any remaining `EnumRef` must be an actual enum.)
524fn verify_refs(
525    struct_def: &StructDef,
526    enums: &HashMap<String, EnumDef>,
527    aliases: &HashMap<String, Spanned<SchemaType>>,
528) -> Result<(), SchemaParseError> {
529    for field in &struct_def.fields {
530        check_type_refs(&field.type_.value, field.type_.span, enums, aliases)?;
531    }
532    Ok(())
533}
534
535fn check_type_refs(
536    schema_type: &SchemaType,
537    span: Span,
538    enums: &HashMap<String, EnumDef>,
539    aliases: &HashMap<String, Spanned<SchemaType>>,
540) -> Result<(), SchemaParseError> {
541    match schema_type {
542        SchemaType::EnumRef(name) => {
543            if !enums.contains_key(name) {
544                return Err(SchemaParseError {
545                    span,
546                    kind: SchemaErrorKind::UnresolvedType { name: name.clone() },
547                });
548            }
549        }
550        SchemaType::AliasRef(name) => {
551            if !aliases.contains_key(name) {
552                return Err(SchemaParseError {
553                    span,
554                    kind: SchemaErrorKind::UnresolvedType { name: name.clone() },
555                });
556            }
557        }
558        SchemaType::Option(inner) | SchemaType::List(inner) => {
559            check_type_refs(inner, span, enums, aliases)?;
560        }
561        SchemaType::Map(key, value) => {
562            check_type_refs(key, span, enums, aliases)?;
563            check_type_refs(value, span, enums, aliases)?;
564        }
565        SchemaType::Tuple(types) => {
566            for t in types {
567                check_type_refs(t, span, enums, aliases)?;
568            }
569        }
570        SchemaType::Struct(struct_def) => {
571            verify_refs(struct_def, enums, aliases)?;
572        }
573        _ => {}
574    }
575    Ok(())
576}
577
578/// Detects recursive type aliases — an alias that references itself directly or indirectly.
579fn verify_no_recursive_aliases(
580    aliases: &HashMap<String, Spanned<SchemaType>>,
581) -> Result<(), SchemaParseError> {
582    for (name, spanned_type) in aliases {
583        let mut visited = HashSet::new();
584        visited.insert(name.as_str());
585        if let Some(cycle_name) = find_alias_cycle(&spanned_type.value, aliases, &mut visited) {
586            return Err(SchemaParseError {
587                span: spanned_type.span,
588                kind: SchemaErrorKind::RecursiveAlias { name: cycle_name },
589            });
590        }
591    }
592    Ok(())
593}
594
595fn find_alias_cycle<'a>(
596    schema_type: &'a SchemaType,
597    aliases: &'a HashMap<String, Spanned<SchemaType>>,
598    visited: &mut HashSet<&'a str>,
599) -> Option<String> {
600    match schema_type {
601        SchemaType::AliasRef(name) => {
602            if visited.contains(name.as_str()) {
603                return Some(name.clone());
604            }
605            visited.insert(name.as_str());
606            if let Some(target) = aliases.get(name) {
607                return find_alias_cycle(&target.value, aliases, visited);
608            }
609            None
610        }
611        SchemaType::Option(inner) | SchemaType::List(inner) => {
612            find_alias_cycle(inner, aliases, visited)
613        }
614        SchemaType::Map(key, value) => {
615            if let Some(cycle) = find_alias_cycle(key, aliases, visited) {
616                return Some(cycle);
617            }
618            find_alias_cycle(value, aliases, visited)
619        }
620        SchemaType::Tuple(types) => {
621            for t in types {
622                if let Some(cycle) = find_alias_cycle(t, aliases, visited) {
623                    return Some(cycle);
624                }
625            }
626            None
627        }
628        SchemaType::Struct(struct_def) => {
629            for field in &struct_def.fields {
630                if let Some(cycle) = find_alias_cycle(&field.type_.value, aliases, visited) {
631                    return Some(cycle);
632                }
633            }
634            None
635        }
636        _ => None,
637    }
638}
639
640#[cfg(test)]
641mod tests {
642    use super::*;
643
644    // ========================================================
645    // Helper: constructs a Parser for direct method testing
646    // ========================================================
647
648    fn parser(source: &str) -> Parser<'_> {
649        Parser::new(source)
650    }
651
652    // ========================================================
653    // peek() tests
654    // ========================================================
655
656    // Returns the current byte without advancing.
657    #[test]
658    fn peek_returns_current_byte() {
659        let p = parser("abc");
660        assert_eq!(p.peek(), Some(b'a'));
661    }
662
663    // Returns None when at end of input.
664    #[test]
665    fn peek_returns_none_at_end() {
666        let p = parser("");
667        assert_eq!(p.peek(), None);
668    }
669
670    // ========================================================
671    // advance() tests
672    // ========================================================
673
674    // Moves to the next byte and increments column.
675    #[test]
676    fn advance_increments_offset_and_column() {
677        let mut p = parser("ab");
678        p.advance();
679        assert_eq!(p.offset, 1);
680        assert_eq!(p.column, 2);
681        assert_eq!(p.peek(), Some(b'b'));
682    }
683
684    // Newline resets column to 1 and increments line.
685    #[test]
686    fn advance_past_newline_increments_line() {
687        let mut p = parser("a\nb");
688        p.advance(); // past 'a'
689        p.advance(); // past '\n'
690        assert_eq!(p.line, 2);
691        assert_eq!(p.column, 1);
692    }
693
694    // Advancing at end of input is a no-op.
695    #[test]
696    fn advance_at_end_is_noop() {
697        let mut p = parser("");
698        p.advance();
699        assert_eq!(p.offset, 0);
700    }
701
702    // ========================================================
703    // position() tests
704    // ========================================================
705
706    // Initial position is offset 0, line 1, column 1.
707    #[test]
708    fn position_initial_state() {
709        let p = parser("abc");
710        let pos = p.position();
711        assert_eq!(pos.offset, 0);
712        assert_eq!(pos.line, 1);
713        assert_eq!(pos.column, 1);
714    }
715
716    // Position tracks correctly after advancing.
717    #[test]
718    fn position_after_advance() {
719        let mut p = parser("ab\nc");
720        p.advance(); // 'a'
721        p.advance(); // 'b'
722        p.advance(); // '\n'
723        let pos = p.position();
724        assert_eq!(pos.offset, 3);
725        assert_eq!(pos.line, 2);
726        assert_eq!(pos.column, 1);
727    }
728
729    // ========================================================
730    // skip_whitespace() tests
731    // ========================================================
732
733    // Skips spaces, tabs, and newlines.
734    #[test]
735    fn skip_whitespace_skips_spaces_tabs_newlines() {
736        let mut p = parser("  \t\nabc");
737        p.skip_whitespace();
738        assert_eq!(p.peek(), Some(b'a'));
739    }
740
741    // Skips line comments.
742    #[test]
743    fn skip_whitespace_skips_line_comment() {
744        let mut p = parser("// comment\nabc");
745        p.skip_whitespace();
746        assert_eq!(p.peek(), Some(b'a'));
747    }
748
749    // Skips whitespace after a comment.
750    #[test]
751    fn skip_whitespace_skips_comment_then_whitespace() {
752        let mut p = parser("// comment\n  abc");
753        p.skip_whitespace();
754        assert_eq!(p.peek(), Some(b'a'));
755    }
756
757    // Does nothing when already on a non-whitespace character.
758    #[test]
759    fn skip_whitespace_noop_on_nonwhitespace() {
760        let mut p = parser("abc");
761        p.skip_whitespace();
762        assert_eq!(p.offset, 0);
763    }
764
765    // ========================================================
766    // expect_char() tests
767    // ========================================================
768
769    // Consumes the expected character and returns Ok.
770    #[test]
771    fn expect_char_consumes_matching_byte() {
772        let mut p = parser("(abc");
773        assert!(p.expect_char(b'(').is_ok());
774        assert_eq!(p.peek(), Some(b'a'));
775    }
776
777    // Returns error when character doesn't match.
778    #[test]
779    fn expect_char_error_on_mismatch() {
780        let mut p = parser("abc");
781        let err = p.expect_char(b'(').unwrap_err();
782        assert!(matches!(err.kind, SchemaErrorKind::UnexpectedToken { .. }));
783    }
784
785    // Returns error at end of input.
786    #[test]
787    fn expect_char_error_at_end_of_input() {
788        let mut p = parser("");
789        let err = p.expect_char(b'(').unwrap_err();
790        match err.kind {
791            SchemaErrorKind::UnexpectedToken { found, .. } => {
792                assert_eq!(found, "end of input");
793            }
794            other => panic!("expected UnexpectedToken, got {:?}", other),
795        }
796    }
797
798    // ========================================================
799    // parse_identifier() tests
800    // ========================================================
801
802    // Reads a simple alphabetic identifier.
803    #[test]
804    fn parse_identifier_reads_alpha() {
805        let mut p = parser("name:");
806        let id = p.parse_identifier().unwrap();
807        assert_eq!(id.value, "name");
808    }
809
810    // Reads an identifier with underscores.
811    #[test]
812    fn parse_identifier_reads_snake_case() {
813        let mut p = parser("field_name:");
814        let id = p.parse_identifier().unwrap();
815        assert_eq!(id.value, "field_name");
816    }
817
818    // Reads an identifier with digits.
819    #[test]
820    fn parse_identifier_reads_alphanumeric() {
821        let mut p = parser("cost2:");
822        let id = p.parse_identifier().unwrap();
823        assert_eq!(id.value, "cost2");
824    }
825
826    // Reads a PascalCase identifier (for types/enums).
827    #[test]
828    fn parse_identifier_reads_pascal_case() {
829        let mut p = parser("CardType ");
830        let id = p.parse_identifier().unwrap();
831        assert_eq!(id.value, "CardType");
832    }
833
834    // Stops at non-identifier characters.
835    #[test]
836    fn parse_identifier_stops_at_delimiter() {
837        let mut p = parser("name: String");
838        let id = p.parse_identifier().unwrap();
839        assert_eq!(id.value, "name");
840        assert_eq!(p.peek(), Some(b':'));
841    }
842
843    // Records correct span for the identifier.
844    #[test]
845    fn parse_identifier_span_is_correct() {
846        let mut p = parser("name:");
847        let id = p.parse_identifier().unwrap();
848        assert_eq!(id.span.start.offset, 0);
849        assert_eq!(id.span.end.offset, 4);
850    }
851
852    // Error when starting with a digit.
853    #[test]
854    fn parse_identifier_error_on_digit_start() {
855        let mut p = parser("42abc");
856        assert!(p.parse_identifier().is_err());
857    }
858
859    // Error at end of input.
860    #[test]
861    fn parse_identifier_error_at_end_of_input() {
862        let mut p = parser("");
863        assert!(p.parse_identifier().is_err());
864    }
865
866    // ========================================================
867    // parse_type() tests
868    // ========================================================
869
870    // Parses "String" as SchemaType::String.
871    #[test]
872    fn parse_type_string() {
873        let mut p = parser("String");
874        let t = p.parse_type().unwrap();
875        assert_eq!(t.value, SchemaType::String);
876    }
877
878    // Parses "Integer" as SchemaType::Integer.
879    #[test]
880    fn parse_type_integer() {
881        let mut p = parser("Integer");
882        let t = p.parse_type().unwrap();
883        assert_eq!(t.value, SchemaType::Integer);
884    }
885
886    // Parses "Float" as SchemaType::Float.
887    #[test]
888    fn parse_type_float() {
889        let mut p = parser("Float");
890        let t = p.parse_type().unwrap();
891        assert_eq!(t.value, SchemaType::Float);
892    }
893
894    // Parses "Bool" as SchemaType::Bool.
895    #[test]
896    fn parse_type_bool() {
897        let mut p = parser("Bool");
898        let t = p.parse_type().unwrap();
899        assert_eq!(t.value, SchemaType::Bool);
900    }
901
902    // Parses "[String]" as a List wrapping String.
903    #[test]
904    fn parse_type_list() {
905        let mut p = parser("[String]");
906        let t = p.parse_type().unwrap();
907        assert_eq!(t.value, SchemaType::List(Box::new(SchemaType::String)));
908    }
909
910    // Parses "Option(Integer)" as an Option wrapping Integer.
911    #[test]
912    fn parse_type_option() {
913        let mut p = parser("Option(Integer)");
914        let t = p.parse_type().unwrap();
915        assert_eq!(t.value, SchemaType::Option(Box::new(SchemaType::Integer)));
916    }
917
918    // Parses an unknown PascalCase name as an EnumRef.
919    #[test]
920    fn parse_type_enum_ref() {
921        let mut p = parser("Faction");
922        let t = p.parse_type().unwrap();
923        assert_eq!(t.value, SchemaType::EnumRef("Faction".to_string()));
924    }
925
926    // Parses nested composites: [Option(String)].
927    #[test]
928    fn parse_type_nested_list_of_option() {
929        let mut p = parser("[Option(String)]");
930        let t = p.parse_type().unwrap();
931        assert_eq!(
932            t.value,
933            SchemaType::List(Box::new(SchemaType::Option(Box::new(SchemaType::String))))
934        );
935    }
936
937    // Parses an inline struct type.
938    #[test]
939    fn parse_type_inline_struct() {
940        let mut p = parser("(\n  x: Integer,\n)");
941        let t = p.parse_type().unwrap();
942        if let SchemaType::Struct(s) = &t.value {
943            assert_eq!(s.fields.len(), 1);
944            assert_eq!(s.fields[0].name.value, "x");
945        } else {
946            panic!("expected SchemaType::Struct");
947        }
948    }
949
950    // Error on unexpected token in type position.
951    #[test]
952    fn parse_type_error_on_unexpected_token() {
953        let mut p = parser("42");
954        let err = p.parse_type().unwrap_err();
955        match err.kind {
956            SchemaErrorKind::UnexpectedToken { expected, .. } => {
957                assert_eq!(expected, "type");
958            }
959            other => panic!("expected UnexpectedToken, got {:?}", other),
960        }
961    }
962
963    // ========================================================
964    // parse_field() tests
965    // ========================================================
966
967    // Parses "name: String" into a FieldDef.
968    #[test]
969    fn parse_field_name_and_type() {
970        let mut p = parser("name: String,");
971        let f = p.parse_field().unwrap();
972        assert_eq!(f.name.value, "name");
973        assert_eq!(f.type_.value, SchemaType::String);
974    }
975
976    // Error when colon is missing.
977    #[test]
978    fn parse_field_error_missing_colon() {
979        let mut p = parser("name String");
980        let err = p.parse_field().unwrap_err();
981        assert!(matches!(err.kind, SchemaErrorKind::UnexpectedToken { .. }));
982    }
983
984    // ========================================================
985    // parse_struct() tests
986    // ========================================================
987
988    // Parses an empty struct.
989    #[test]
990    fn parse_struct_empty() {
991        let mut p = parser("()");
992        let s = p.parse_struct().unwrap();
993        assert!(s.fields.is_empty());
994    }
995
996    // Parses a struct with one field.
997    #[test]
998    fn parse_struct_single_field() {
999        let mut p = parser("(\n  name: String,\n)");
1000        let s = p.parse_struct().unwrap();
1001        assert_eq!(s.fields.len(), 1);
1002        assert_eq!(s.fields[0].name.value, "name");
1003    }
1004
1005    // Parses a struct with multiple fields.
1006    #[test]
1007    fn parse_struct_multiple_fields() {
1008        let mut p = parser("(\n  a: String,\n  b: Integer,\n)");
1009        let s = p.parse_struct().unwrap();
1010        assert_eq!(s.fields.len(), 2);
1011    }
1012
1013    // Struct without trailing comma is valid.
1014    #[test]
1015    fn parse_struct_no_trailing_comma() {
1016        let mut p = parser("(\n  name: String\n)");
1017        let s = p.parse_struct().unwrap();
1018        assert_eq!(s.fields.len(), 1);
1019    }
1020
1021    // Error on unclosed struct.
1022    #[test]
1023    fn parse_struct_error_on_unclosed() {
1024        let mut p = parser("(\n  name: String,\n");
1025        assert!(p.parse_struct().is_err());
1026    }
1027
1028    // ========================================================
1029    // parse_enum_def() tests
1030    // ========================================================
1031
1032    // Parses a simple enum definition.
1033    #[test]
1034    fn parse_enum_def_simple() {
1035        let mut p = parser("enum Dir { North, South }");
1036        let e = p.parse_enum_def().unwrap();
1037        assert_eq!(e.name, "Dir");
1038        assert_eq!(e.variants.len(), 2);
1039        assert!(e.variants.contains("North"));
1040        assert!(e.variants.contains("South"));
1041    }
1042
1043    // Trailing comma in variant list is allowed.
1044    #[test]
1045    fn parse_enum_def_trailing_comma() {
1046        let mut p = parser("enum Dir { North, South, }");
1047        let e = p.parse_enum_def().unwrap();
1048        assert_eq!(e.variants.len(), 2);
1049    }
1050
1051    // Single variant enum is valid.
1052    #[test]
1053    fn parse_enum_def_single_variant() {
1054        let mut p = parser("enum Single { Only }");
1055        let e = p.parse_enum_def().unwrap();
1056        assert_eq!(e.variants.len(), 1);
1057    }
1058
1059    // Error when keyword is not "enum".
1060    #[test]
1061    fn parse_enum_def_error_wrong_keyword() {
1062        let mut p = parser("struct Dir { North }");
1063        let err = p.parse_enum_def().unwrap_err();
1064        assert!(matches!(err.kind, SchemaErrorKind::UnexpectedToken { .. }));
1065    }
1066
1067    // Error on unclosed enum.
1068    #[test]
1069    fn parse_enum_def_error_on_unclosed() {
1070        let mut p = parser("enum Dir { North, South");
1071        assert!(p.parse_enum_def().is_err());
1072    }
1073
1074    // ========================================================
1075    // parse_schema() integration tests
1076    // ========================================================
1077
1078    // Empty input produces an empty schema.
1079    #[test]
1080    fn schema_empty_input() {
1081        let schema = parse_schema("").unwrap();
1082        assert!(schema.root.fields.is_empty());
1083    }
1084
1085    // Empty input produces no enums.
1086    #[test]
1087    fn schema_empty_input_no_enums() {
1088        let schema = parse_schema("").unwrap();
1089        assert!(schema.enums.is_empty());
1090    }
1091
1092    // Root struct with enum ref resolves when enum is defined.
1093    #[test]
1094    fn schema_enum_ref_resolves() {
1095        let source = "(\n  faction: Faction,\n)\nenum Faction { Sentinels, Reavers }";
1096        let schema = parse_schema(source).unwrap();
1097        assert_eq!(schema.root.fields[0].type_.value, SchemaType::EnumRef("Faction".to_string()));
1098    }
1099
1100    // Multiple enum definitions are all stored.
1101    #[test]
1102    fn schema_multiple_enums_stored() {
1103        let source = "enum A { X }\nenum B { Y }";
1104        let schema = parse_schema(source).unwrap();
1105        assert_eq!(schema.enums.len(), 2);
1106    }
1107
1108    // Comments before root struct are ignored.
1109    #[test]
1110    fn schema_comments_before_root() {
1111        let source = "// comment\n(\n  name: String,\n)";
1112        let schema = parse_schema(source).unwrap();
1113        assert_eq!(schema.root.fields.len(), 1);
1114    }
1115
1116    // Inline comment after field is ignored.
1117    #[test]
1118    fn schema_inline_comment_after_field() {
1119        let source = "(\n  name: String, // a name\n)";
1120        let schema = parse_schema(source).unwrap();
1121        assert_eq!(schema.root.fields[0].name.value, "name");
1122    }
1123
1124    // Unresolved type ref is an error.
1125    #[test]
1126    fn schema_unresolved_type_ref() {
1127        let err = parse_schema("(\n  f: Faction,\n)").unwrap_err();
1128        assert_eq!(err.kind, SchemaErrorKind::UnresolvedType { name: "Faction".to_string() });
1129    }
1130
1131    // Unresolved type ref inside Option is an error.
1132    #[test]
1133    fn schema_unresolved_type_ref_in_option() {
1134        let err = parse_schema("(\n  t: Option(Timing),\n)").unwrap_err();
1135        assert_eq!(err.kind, SchemaErrorKind::UnresolvedType { name: "Timing".to_string() });
1136    }
1137
1138    // Unresolved type ref inside List is an error.
1139    #[test]
1140    fn schema_unresolved_type_ref_in_list() {
1141        let err = parse_schema("(\n  t: [CardType],\n)").unwrap_err();
1142        assert_eq!(err.kind, SchemaErrorKind::UnresolvedType { name: "CardType".to_string() });
1143    }
1144
1145    // Duplicate enum name is an error.
1146    #[test]
1147    fn schema_duplicate_enum_name() {
1148        let err = parse_schema("enum A { X }\nenum A { Y }").unwrap_err();
1149        assert_eq!(err.kind, SchemaErrorKind::DuplicateEnum { name: "A".to_string() });
1150    }
1151
1152    // ========================================================
1153    // Type alias tests — parsing
1154    // ========================================================
1155
1156    // Basic type alias is stored in schema.aliases.
1157    #[test]
1158    fn alias_stored_in_schema() {
1159        let source = "(\n  cost: Cost,\n)\ntype Cost = (generic: Integer,)";
1160        let schema = parse_schema(source).unwrap();
1161        assert!(schema.aliases.contains_key("Cost"));
1162    }
1163
1164    // Alias field is reclassified from EnumRef to AliasRef.
1165    #[test]
1166    fn alias_ref_reclassified() {
1167        let source = "(\n  cost: Cost,\n)\ntype Cost = (generic: Integer,)";
1168        let schema = parse_schema(source).unwrap();
1169        assert_eq!(schema.root.fields[0].type_.value, SchemaType::AliasRef("Cost".to_string()));
1170    }
1171
1172    // Alias to a primitive type.
1173    #[test]
1174    fn alias_to_primitive() {
1175        let source = "(\n  name: Name,\n)\ntype Name = String";
1176        let schema = parse_schema(source).unwrap();
1177        assert_eq!(schema.aliases["Name"].value, SchemaType::String);
1178    }
1179
1180    // Alias to a list type.
1181    #[test]
1182    fn alias_to_list() {
1183        let source = "(\n  tags: Tags,\n)\ntype Tags = [String]";
1184        let schema = parse_schema(source).unwrap();
1185        assert_eq!(schema.aliases["Tags"].value, SchemaType::List(Box::new(SchemaType::String)));
1186    }
1187
1188    // Alias to an option type.
1189    #[test]
1190    fn alias_to_option() {
1191        let source = "(\n  power: Power,\n)\ntype Power = Option(Integer)";
1192        let schema = parse_schema(source).unwrap();
1193        assert_eq!(schema.aliases["Power"].value, SchemaType::Option(Box::new(SchemaType::Integer)));
1194    }
1195
1196    // Alias inside a list field is reclassified.
1197    #[test]
1198    fn alias_ref_inside_list_reclassified() {
1199        let source = "(\n  costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
1200        let schema = parse_schema(source).unwrap();
1201        assert_eq!(
1202            schema.root.fields[0].type_.value,
1203            SchemaType::List(Box::new(SchemaType::AliasRef("Cost".to_string())))
1204        );
1205    }
1206
1207    // Alias inside an option field is reclassified.
1208    #[test]
1209    fn alias_ref_inside_option_reclassified() {
1210        let source = "(\n  cost: Option(Cost),\n)\ntype Cost = (generic: Integer,)";
1211        let schema = parse_schema(source).unwrap();
1212        assert_eq!(
1213            schema.root.fields[0].type_.value,
1214            SchemaType::Option(Box::new(SchemaType::AliasRef("Cost".to_string())))
1215        );
1216    }
1217
1218    // Enums and aliases can coexist.
1219    #[test]
1220    fn alias_and_enum_coexist() {
1221        let source = "(\n  cost: Cost,\n  kind: Kind,\n)\ntype Cost = (generic: Integer,)\nenum Kind { A, B }";
1222        let schema = parse_schema(source).unwrap();
1223        assert!(schema.aliases.contains_key("Cost"));
1224        assert!(schema.enums.contains_key("Kind"));
1225    }
1226
1227    // ========================================================
1228    // Type alias tests — error cases
1229    // ========================================================
1230
1231    // Duplicate alias name is an error.
1232    #[test]
1233    fn alias_duplicate_name() {
1234        let source = "type A = String\ntype A = Integer";
1235        let err = parse_schema(source).unwrap_err();
1236        assert_eq!(err.kind, SchemaErrorKind::DuplicateAlias { name: "A".to_string() });
1237    }
1238
1239    // Recursive alias is an error.
1240    #[test]
1241    fn alias_recursive_direct() {
1242        let source = "(\n  x: Foo,\n)\ntype Foo = Option(Foo)";
1243        let err = parse_schema(source).unwrap_err();
1244        assert_eq!(err.kind, SchemaErrorKind::RecursiveAlias { name: "Foo".to_string() });
1245    }
1246
1247    // Indirect recursive alias is an error.
1248    #[test]
1249    fn alias_recursive_indirect() {
1250        let source = "(\n  x: Foo,\n)\ntype Foo = Option(Bar)\ntype Bar = [Foo]";
1251        let err = parse_schema(source).unwrap_err();
1252        assert!(matches!(err.kind, SchemaErrorKind::RecursiveAlias { .. }));
1253    }
1254
1255    // ========================================================
1256    // Map type tests — parsing
1257    // ========================================================
1258
1259    // Parses a map type with String keys and Integer values.
1260    #[test]
1261    fn parse_type_map_string_to_integer() {
1262        let mut p = parser("{String: Integer}");
1263        let t = p.parse_type().unwrap();
1264        assert_eq!(
1265            t.value,
1266            SchemaType::Map(Box::new(SchemaType::String), Box::new(SchemaType::Integer))
1267        );
1268    }
1269
1270    // Parses a map type with Integer keys.
1271    #[test]
1272    fn parse_type_map_integer_keys() {
1273        let mut p = parser("{Integer: String}");
1274        let t = p.parse_type().unwrap();
1275        assert_eq!(
1276            t.value,
1277            SchemaType::Map(Box::new(SchemaType::Integer), Box::new(SchemaType::String))
1278        );
1279    }
1280
1281    // Map type field in a schema.
1282    #[test]
1283    fn schema_map_field() {
1284        let source = "(\n  attrs: {String: Integer},\n)";
1285        let schema = parse_schema(source).unwrap();
1286        assert_eq!(
1287            schema.root.fields[0].type_.value,
1288            SchemaType::Map(Box::new(SchemaType::String), Box::new(SchemaType::Integer))
1289        );
1290    }
1291
1292    // Map with enum key type is allowed.
1293    #[test]
1294    fn schema_map_enum_key() {
1295        let source = "(\n  scores: {Stat: Integer},\n)\nenum Stat { Str, Dex, Con }";
1296        let schema = parse_schema(source).unwrap();
1297        assert_eq!(
1298            schema.root.fields[0].type_.value,
1299            SchemaType::Map(Box::new(SchemaType::EnumRef("Stat".to_string())), Box::new(SchemaType::Integer))
1300        );
1301    }
1302
1303    // Map with Float key type is rejected.
1304    #[test]
1305    fn schema_map_float_key_rejected() {
1306        let source = "(\n  bad: {Float: String},\n)";
1307        let err = parse_schema(source).unwrap_err();
1308        assert!(matches!(err.kind, SchemaErrorKind::InvalidMapKeyType { .. }));
1309    }
1310
1311    // Map with Bool key type is rejected.
1312    #[test]
1313    fn schema_map_bool_key_rejected() {
1314        let source = "(\n  bad: {Bool: String},\n)";
1315        let err = parse_schema(source).unwrap_err();
1316        assert!(matches!(err.kind, SchemaErrorKind::InvalidMapKeyType { .. }));
1317    }
1318
1319    // ========================================================
1320    // Tuple type tests — parsing
1321    // ========================================================
1322
1323    // Parses a tuple type with two elements.
1324    #[test]
1325    fn parse_type_tuple() {
1326        let mut p = parser("(Float, Float)");
1327        let t = p.parse_type().unwrap();
1328        assert_eq!(t.value, SchemaType::Tuple(vec![SchemaType::Float, SchemaType::Float]));
1329    }
1330
1331    // Parses a tuple type with mixed types.
1332    #[test]
1333    fn parse_type_tuple_mixed() {
1334        let mut p = parser("(String, Integer, Bool)");
1335        let t = p.parse_type().unwrap();
1336        assert_eq!(
1337            t.value,
1338            SchemaType::Tuple(vec![SchemaType::String, SchemaType::Integer, SchemaType::Bool])
1339        );
1340    }
1341
1342    // Tuple type in a schema field.
1343    #[test]
1344    fn schema_tuple_field() {
1345        let source = "(\n  pos: (Float, Float),\n)";
1346        let schema = parse_schema(source).unwrap();
1347        assert_eq!(
1348            schema.root.fields[0].type_.value,
1349            SchemaType::Tuple(vec![SchemaType::Float, SchemaType::Float])
1350        );
1351    }
1352
1353    // Inline struct still works after tuple disambiguation.
1354    #[test]
1355    fn schema_struct_still_works() {
1356        let source = "(\n  cost: (generic: Integer,),\n)";
1357        let schema = parse_schema(source).unwrap();
1358        if let SchemaType::Struct(s) = &schema.root.fields[0].type_.value {
1359            assert_eq!(s.fields[0].name.value, "generic");
1360        } else {
1361            panic!("expected Struct");
1362        }
1363    }
1364
1365    // Empty parens still parse as empty struct.
1366    #[test]
1367    fn schema_empty_parens_is_struct() {
1368        let source = "(\n  empty: (),\n)";
1369        let schema = parse_schema(source).unwrap();
1370        assert!(matches!(schema.root.fields[0].type_.value, SchemaType::Struct(_)));
1371    }
1372}