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    fn parse_type(&mut self) -> Result<Spanned<SchemaType>, SchemaParseError> {
135        self.skip_whitespace();
136        let start = self.position();
137
138        match self.peek() {
139            Some(b'[') => {
140                // List: consume '[', parse inner type, expect ']'
141                self.advance();
142                self.skip_whitespace();
143                let inner = self.parse_type()?;
144                self.skip_whitespace();
145                self.expect_char(b']')?;
146                let end = self.position();
147                Ok(Spanned {
148                    value: SchemaType::List(Box::new(inner.value)),
149                    span: Span { start, end },
150                })
151            }
152            Some(b'(') => {
153                let struct_def = self.parse_struct()?;
154                let end = self.position();
155                Ok(Spanned {
156                    value: SchemaType::Struct(struct_def),
157                    span: Span { start, end },
158                })
159            }
160            Some(b) if b.is_ascii_alphabetic() => {
161                // Identifier: could be primitive, Option, or EnumRef
162                let id = self.parse_identifier()?;
163                match id.value.as_str() {
164                    "String" => Ok(Spanned { value: SchemaType::String, span: id.span }),
165                    "Integer" => Ok(Spanned { value: SchemaType::Integer, span: id.span }),
166                    "Float" => Ok(Spanned { value: SchemaType::Float, span: id.span }),
167                    "Bool" => Ok(Spanned { value: SchemaType::Bool, span: id.span }),
168                    "Option" => {
169                        // expect '(', parse inner type, expect ')'
170                        self.skip_whitespace();
171                        self.expect_char(b'(')?;
172                        self.skip_whitespace();
173                        let inner = self.parse_type()?;
174                        self.skip_whitespace();
175                        self.expect_char(b')')?;
176                        let end = self.position();
177                        Ok(Spanned {
178                            value: SchemaType::Option(Box::new(inner.value)),
179                            span: Span { start, end },
180                        })
181                    }
182                    _ => Ok(Spanned { value: SchemaType::EnumRef(id.value), span: id.span }),
183                }
184            }
185            Some(b) => {
186                // Error: unexpected character
187                self.advance();
188                let end = self.position();
189                Err(SchemaParseError {
190                    span: Span { start, end },
191                    kind: SchemaErrorKind::UnexpectedToken {
192                        expected: "type".to_string(),
193                        found: format!("'{}'", b as char),
194                    },
195                })
196            }
197            None => {
198                Err(SchemaParseError {
199                    span: Span { start, end: start },
200                    kind: SchemaErrorKind::UnexpectedToken {
201                        expected: "type".to_string(),
202                        found: "end of input".to_string(),
203                    },
204                })
205            }
206        }
207    }
208
209    fn parse_field(&mut self) -> Result<FieldDef, SchemaParseError> {
210        self.skip_whitespace();
211        let name = self.parse_identifier()?;
212        self.skip_whitespace();
213        self.expect_char(b':')?;
214        self.skip_whitespace();
215        let type_ = self.parse_type()?;
216        Ok(FieldDef{
217            name,
218            type_
219        })
220    }
221
222    fn parse_struct(&mut self) -> Result<StructDef, SchemaParseError> {
223        self.skip_whitespace();
224        self.expect_char(b'(')?;
225        let mut fields: Vec<FieldDef> = Vec::new();
226        loop {
227            self.skip_whitespace();
228            if let Some(byte) = self.peek() {
229                if byte == b')' {
230                    break ;
231                } 
232                let field = self.parse_field()?;
233                fields.push(field);
234                self.skip_whitespace();
235                if self.peek() == Some(b',') {
236                    self.advance();
237                }
238            } else {
239                return Err(SchemaParseError {
240                    span: Span { start: self.position(), end: self.position() },
241                    kind: SchemaErrorKind::UnexpectedToken { expected: ")".to_string(), found: "end of file".to_string() }
242                });
243            }
244        }
245        self.expect_char(b')')?;
246        Ok(StructDef { fields })
247    }
248
249    fn parse_enum_def(&mut self) -> Result<EnumDef, SchemaParseError> {
250        self.skip_whitespace();
251        let keyword = self.parse_identifier()?;
252        if keyword.value != "enum" {
253            return Err(SchemaParseError {
254                span: keyword.span,
255                kind: SchemaErrorKind::UnexpectedToken {
256                    expected: "\"enum\"".to_string(),
257                    found: keyword.value,
258                },
259            });
260        }
261        self.skip_whitespace();
262        let name = self.parse_identifier()?;
263        self.skip_whitespace();
264        self.expect_char(b'{')?;
265        let mut variants = HashSet::new();
266        loop {
267            self.skip_whitespace();
268            if let Some(byte) = self.peek() {
269                if byte == b'}' {
270                    break ;
271                } 
272                let variant = self.parse_identifier()?;
273                variants.insert(variant.value);
274                self.skip_whitespace();
275                if self.peek() == Some(b',') {
276                    self.advance();
277                }
278            } else {
279                return Err(SchemaParseError {
280                    span: Span { start: self.position(), end: self.position() },
281                    kind: SchemaErrorKind::UnexpectedToken { expected: "}".to_string(), found: "end of file".to_string() }
282                });
283            }
284        }
285
286        self.expect_char(b'}')?;
287        Ok(EnumDef { name: name.value, variants })
288    }
289
290    /// Parses `type Name = <type>` — assumes the "type" keyword has already been confirmed.
291    fn parse_alias_def(&mut self) -> Result<(String, Spanned<SchemaType>), SchemaParseError> {
292        self.skip_whitespace();
293        self.parse_identifier()?; // consume "type" keyword
294        self.skip_whitespace();
295        let name = self.parse_identifier()?;
296        self.skip_whitespace();
297        self.expect_char(b'=')?;
298        self.skip_whitespace();
299        let type_ = self.parse_type()?;
300        Ok((name.value, type_))
301    }
302}
303
304/// Parses a `.ronschema` source string into a [`Schema`].
305///
306/// # Errors
307///
308/// Returns a [`SchemaParseError`] if the source contains syntax errors,
309/// duplicate definitions, or unresolved enum references.
310pub fn parse_schema(source: &str) -> Result<Schema, SchemaParseError> {
311    let mut parser = Parser::new(source);
312    parser.skip_whitespace();
313
314    let mut root = if parser.peek() == Some(b'(') {
315        parser.parse_struct()?
316    } else {
317        StructDef { fields: Vec::new() }
318    };
319
320    let mut enums: HashMap<String, EnumDef> = HashMap::new();
321    let mut aliases: HashMap<String, Spanned<SchemaType>> = HashMap::new();
322
323    loop {
324        parser.skip_whitespace();
325        if parser.peek().is_none() {
326            break;
327        }
328
329        // Peek ahead to determine if this is "enum" or "type"
330        let start = parser.position();
331        let keyword = parser.parse_identifier()?;
332
333        match keyword.value.as_str() {
334            "enum" => {
335                // Rewind — parse_enum_def expects to consume "enum" itself
336                parser.offset = start.offset;
337                parser.line = start.line;
338                parser.column = start.column;
339
340                let enum_def = parser.parse_enum_def()?;
341                if let Some(old) = enums.insert(enum_def.name.clone(), enum_def) {
342                    return Err(SchemaParseError {
343                        span: Span { start: parser.position(), end: parser.position() },
344                        kind: SchemaErrorKind::DuplicateEnum { name: old.name },
345                    });
346                }
347            }
348            "type" => {
349                // Rewind — parse_alias_def expects to consume "type" itself
350                parser.offset = start.offset;
351                parser.line = start.line;
352                parser.column = start.column;
353
354                let (name, type_) = parser.parse_alias_def()?;
355                if aliases.contains_key(&name) {
356                    return Err(SchemaParseError {
357                        span: type_.span,
358                        kind: SchemaErrorKind::DuplicateAlias { name },
359                    });
360                }
361                aliases.insert(name, type_);
362            }
363            other => {
364                return Err(SchemaParseError {
365                    span: keyword.span,
366                    kind: SchemaErrorKind::UnexpectedToken {
367                        expected: "\"enum\" or \"type\"".to_string(),
368                        found: other.to_string(),
369                    },
370                });
371            }
372        }
373    }
374
375    // Reclassify EnumRefs that are actually aliases — in the root struct and in alias definitions.
376    // Collect alias names into a set to avoid borrow conflicts when mutating alias values.
377    let alias_names: HashSet<String> = aliases.keys().cloned().collect();
378    reclassify_refs_in_struct_by_name(&mut root, &alias_names);
379    for spanned_type in aliases.values_mut() {
380        reclassify_refs_in_type_by_name(&mut spanned_type.value, &alias_names);
381    }
382
383    // Verify all refs resolve to a known enum or alias
384    verify_refs(&root, &enums, &aliases)?;
385
386    // Check for recursive aliases
387    verify_no_recursive_aliases(&aliases)?;
388
389    Ok(Schema { root, enums, aliases })
390}
391
392/// Reclassifies `EnumRef` names that are actually type aliases into `AliasRef`.
393/// Mutates the struct in place.
394fn reclassify_refs_in_struct_by_name(
395    struct_def: &mut StructDef,
396    alias_names: &HashSet<String>,
397) {
398    for field in &mut struct_def.fields {
399        reclassify_refs_in_type_by_name(&mut field.type_.value, alias_names);
400    }
401}
402
403fn reclassify_refs_in_type_by_name(
404    schema_type: &mut SchemaType,
405    alias_names: &HashSet<String>,
406) {
407    match schema_type {
408        SchemaType::EnumRef(name) if alias_names.contains(name.as_str()) => {
409            *schema_type = SchemaType::AliasRef(name.clone());
410        }
411        SchemaType::Option(inner) | SchemaType::List(inner) => {
412            reclassify_refs_in_type_by_name(inner, alias_names);
413        }
414        SchemaType::Struct(struct_def) => {
415            reclassify_refs_in_struct_by_name(struct_def, alias_names);
416        }
417        _ => {}
418    }
419}
420
421/// Verifies all `EnumRef` names resolve to a defined enum.
422/// (`AliasRefs` have already been reclassified, so any remaining `EnumRef` must be an actual enum.)
423fn verify_refs(
424    struct_def: &StructDef,
425    enums: &HashMap<String, EnumDef>,
426    aliases: &HashMap<String, Spanned<SchemaType>>,
427) -> Result<(), SchemaParseError> {
428    for field in &struct_def.fields {
429        check_type_refs(&field.type_.value, field.type_.span, enums, aliases)?;
430    }
431    Ok(())
432}
433
434fn check_type_refs(
435    schema_type: &SchemaType,
436    span: Span,
437    enums: &HashMap<String, EnumDef>,
438    aliases: &HashMap<String, Spanned<SchemaType>>,
439) -> Result<(), SchemaParseError> {
440    match schema_type {
441        SchemaType::EnumRef(name) => {
442            if !enums.contains_key(name) {
443                return Err(SchemaParseError {
444                    span,
445                    kind: SchemaErrorKind::UnresolvedType { name: name.clone() },
446                });
447            }
448        }
449        SchemaType::AliasRef(name) => {
450            if !aliases.contains_key(name) {
451                return Err(SchemaParseError {
452                    span,
453                    kind: SchemaErrorKind::UnresolvedType { name: name.clone() },
454                });
455            }
456        }
457        SchemaType::Option(inner) | SchemaType::List(inner) => {
458            check_type_refs(inner, span, enums, aliases)?;
459        }
460        SchemaType::Struct(struct_def) => {
461            verify_refs(struct_def, enums, aliases)?;
462        }
463        _ => {}
464    }
465    Ok(())
466}
467
468/// Detects recursive type aliases — an alias that references itself directly or indirectly.
469fn verify_no_recursive_aliases(
470    aliases: &HashMap<String, Spanned<SchemaType>>,
471) -> Result<(), SchemaParseError> {
472    for (name, spanned_type) in aliases {
473        let mut visited = HashSet::new();
474        visited.insert(name.as_str());
475        if let Some(cycle_name) = find_alias_cycle(&spanned_type.value, aliases, &mut visited) {
476            return Err(SchemaParseError {
477                span: spanned_type.span,
478                kind: SchemaErrorKind::RecursiveAlias { name: cycle_name },
479            });
480        }
481    }
482    Ok(())
483}
484
485fn find_alias_cycle<'a>(
486    schema_type: &'a SchemaType,
487    aliases: &'a HashMap<String, Spanned<SchemaType>>,
488    visited: &mut HashSet<&'a str>,
489) -> Option<String> {
490    match schema_type {
491        SchemaType::AliasRef(name) => {
492            if visited.contains(name.as_str()) {
493                return Some(name.clone());
494            }
495            visited.insert(name.as_str());
496            if let Some(target) = aliases.get(name) {
497                return find_alias_cycle(&target.value, aliases, visited);
498            }
499            None
500        }
501        SchemaType::Option(inner) | SchemaType::List(inner) => {
502            find_alias_cycle(inner, aliases, visited)
503        }
504        SchemaType::Struct(struct_def) => {
505            for field in &struct_def.fields {
506                if let Some(cycle) = find_alias_cycle(&field.type_.value, aliases, visited) {
507                    return Some(cycle);
508                }
509            }
510            None
511        }
512        _ => None,
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519
520    // ========================================================
521    // Helper: constructs a Parser for direct method testing
522    // ========================================================
523
524    fn parser(source: &str) -> Parser<'_> {
525        Parser::new(source)
526    }
527
528    // ========================================================
529    // peek() tests
530    // ========================================================
531
532    // Returns the current byte without advancing.
533    #[test]
534    fn peek_returns_current_byte() {
535        let p = parser("abc");
536        assert_eq!(p.peek(), Some(b'a'));
537    }
538
539    // Returns None when at end of input.
540    #[test]
541    fn peek_returns_none_at_end() {
542        let p = parser("");
543        assert_eq!(p.peek(), None);
544    }
545
546    // ========================================================
547    // advance() tests
548    // ========================================================
549
550    // Moves to the next byte and increments column.
551    #[test]
552    fn advance_increments_offset_and_column() {
553        let mut p = parser("ab");
554        p.advance();
555        assert_eq!(p.offset, 1);
556        assert_eq!(p.column, 2);
557        assert_eq!(p.peek(), Some(b'b'));
558    }
559
560    // Newline resets column to 1 and increments line.
561    #[test]
562    fn advance_past_newline_increments_line() {
563        let mut p = parser("a\nb");
564        p.advance(); // past 'a'
565        p.advance(); // past '\n'
566        assert_eq!(p.line, 2);
567        assert_eq!(p.column, 1);
568    }
569
570    // Advancing at end of input is a no-op.
571    #[test]
572    fn advance_at_end_is_noop() {
573        let mut p = parser("");
574        p.advance();
575        assert_eq!(p.offset, 0);
576    }
577
578    // ========================================================
579    // position() tests
580    // ========================================================
581
582    // Initial position is offset 0, line 1, column 1.
583    #[test]
584    fn position_initial_state() {
585        let p = parser("abc");
586        let pos = p.position();
587        assert_eq!(pos.offset, 0);
588        assert_eq!(pos.line, 1);
589        assert_eq!(pos.column, 1);
590    }
591
592    // Position tracks correctly after advancing.
593    #[test]
594    fn position_after_advance() {
595        let mut p = parser("ab\nc");
596        p.advance(); // 'a'
597        p.advance(); // 'b'
598        p.advance(); // '\n'
599        let pos = p.position();
600        assert_eq!(pos.offset, 3);
601        assert_eq!(pos.line, 2);
602        assert_eq!(pos.column, 1);
603    }
604
605    // ========================================================
606    // skip_whitespace() tests
607    // ========================================================
608
609    // Skips spaces, tabs, and newlines.
610    #[test]
611    fn skip_whitespace_skips_spaces_tabs_newlines() {
612        let mut p = parser("  \t\nabc");
613        p.skip_whitespace();
614        assert_eq!(p.peek(), Some(b'a'));
615    }
616
617    // Skips line comments.
618    #[test]
619    fn skip_whitespace_skips_line_comment() {
620        let mut p = parser("// comment\nabc");
621        p.skip_whitespace();
622        assert_eq!(p.peek(), Some(b'a'));
623    }
624
625    // Skips whitespace after a comment.
626    #[test]
627    fn skip_whitespace_skips_comment_then_whitespace() {
628        let mut p = parser("// comment\n  abc");
629        p.skip_whitespace();
630        assert_eq!(p.peek(), Some(b'a'));
631    }
632
633    // Does nothing when already on a non-whitespace character.
634    #[test]
635    fn skip_whitespace_noop_on_nonwhitespace() {
636        let mut p = parser("abc");
637        p.skip_whitespace();
638        assert_eq!(p.offset, 0);
639    }
640
641    // ========================================================
642    // expect_char() tests
643    // ========================================================
644
645    // Consumes the expected character and returns Ok.
646    #[test]
647    fn expect_char_consumes_matching_byte() {
648        let mut p = parser("(abc");
649        assert!(p.expect_char(b'(').is_ok());
650        assert_eq!(p.peek(), Some(b'a'));
651    }
652
653    // Returns error when character doesn't match.
654    #[test]
655    fn expect_char_error_on_mismatch() {
656        let mut p = parser("abc");
657        let err = p.expect_char(b'(').unwrap_err();
658        assert!(matches!(err.kind, SchemaErrorKind::UnexpectedToken { .. }));
659    }
660
661    // Returns error at end of input.
662    #[test]
663    fn expect_char_error_at_end_of_input() {
664        let mut p = parser("");
665        let err = p.expect_char(b'(').unwrap_err();
666        match err.kind {
667            SchemaErrorKind::UnexpectedToken { found, .. } => {
668                assert_eq!(found, "end of input");
669            }
670            other => panic!("expected UnexpectedToken, got {:?}", other),
671        }
672    }
673
674    // ========================================================
675    // parse_identifier() tests
676    // ========================================================
677
678    // Reads a simple alphabetic identifier.
679    #[test]
680    fn parse_identifier_reads_alpha() {
681        let mut p = parser("name:");
682        let id = p.parse_identifier().unwrap();
683        assert_eq!(id.value, "name");
684    }
685
686    // Reads an identifier with underscores.
687    #[test]
688    fn parse_identifier_reads_snake_case() {
689        let mut p = parser("field_name:");
690        let id = p.parse_identifier().unwrap();
691        assert_eq!(id.value, "field_name");
692    }
693
694    // Reads an identifier with digits.
695    #[test]
696    fn parse_identifier_reads_alphanumeric() {
697        let mut p = parser("cost2:");
698        let id = p.parse_identifier().unwrap();
699        assert_eq!(id.value, "cost2");
700    }
701
702    // Reads a PascalCase identifier (for types/enums).
703    #[test]
704    fn parse_identifier_reads_pascal_case() {
705        let mut p = parser("CardType ");
706        let id = p.parse_identifier().unwrap();
707        assert_eq!(id.value, "CardType");
708    }
709
710    // Stops at non-identifier characters.
711    #[test]
712    fn parse_identifier_stops_at_delimiter() {
713        let mut p = parser("name: String");
714        let id = p.parse_identifier().unwrap();
715        assert_eq!(id.value, "name");
716        assert_eq!(p.peek(), Some(b':'));
717    }
718
719    // Records correct span for the identifier.
720    #[test]
721    fn parse_identifier_span_is_correct() {
722        let mut p = parser("name:");
723        let id = p.parse_identifier().unwrap();
724        assert_eq!(id.span.start.offset, 0);
725        assert_eq!(id.span.end.offset, 4);
726    }
727
728    // Error when starting with a digit.
729    #[test]
730    fn parse_identifier_error_on_digit_start() {
731        let mut p = parser("42abc");
732        assert!(p.parse_identifier().is_err());
733    }
734
735    // Error at end of input.
736    #[test]
737    fn parse_identifier_error_at_end_of_input() {
738        let mut p = parser("");
739        assert!(p.parse_identifier().is_err());
740    }
741
742    // ========================================================
743    // parse_type() tests
744    // ========================================================
745
746    // Parses "String" as SchemaType::String.
747    #[test]
748    fn parse_type_string() {
749        let mut p = parser("String");
750        let t = p.parse_type().unwrap();
751        assert_eq!(t.value, SchemaType::String);
752    }
753
754    // Parses "Integer" as SchemaType::Integer.
755    #[test]
756    fn parse_type_integer() {
757        let mut p = parser("Integer");
758        let t = p.parse_type().unwrap();
759        assert_eq!(t.value, SchemaType::Integer);
760    }
761
762    // Parses "Float" as SchemaType::Float.
763    #[test]
764    fn parse_type_float() {
765        let mut p = parser("Float");
766        let t = p.parse_type().unwrap();
767        assert_eq!(t.value, SchemaType::Float);
768    }
769
770    // Parses "Bool" as SchemaType::Bool.
771    #[test]
772    fn parse_type_bool() {
773        let mut p = parser("Bool");
774        let t = p.parse_type().unwrap();
775        assert_eq!(t.value, SchemaType::Bool);
776    }
777
778    // Parses "[String]" as a List wrapping String.
779    #[test]
780    fn parse_type_list() {
781        let mut p = parser("[String]");
782        let t = p.parse_type().unwrap();
783        assert_eq!(t.value, SchemaType::List(Box::new(SchemaType::String)));
784    }
785
786    // Parses "Option(Integer)" as an Option wrapping Integer.
787    #[test]
788    fn parse_type_option() {
789        let mut p = parser("Option(Integer)");
790        let t = p.parse_type().unwrap();
791        assert_eq!(t.value, SchemaType::Option(Box::new(SchemaType::Integer)));
792    }
793
794    // Parses an unknown PascalCase name as an EnumRef.
795    #[test]
796    fn parse_type_enum_ref() {
797        let mut p = parser("Faction");
798        let t = p.parse_type().unwrap();
799        assert_eq!(t.value, SchemaType::EnumRef("Faction".to_string()));
800    }
801
802    // Parses nested composites: [Option(String)].
803    #[test]
804    fn parse_type_nested_list_of_option() {
805        let mut p = parser("[Option(String)]");
806        let t = p.parse_type().unwrap();
807        assert_eq!(
808            t.value,
809            SchemaType::List(Box::new(SchemaType::Option(Box::new(SchemaType::String))))
810        );
811    }
812
813    // Parses an inline struct type.
814    #[test]
815    fn parse_type_inline_struct() {
816        let mut p = parser("(\n  x: Integer,\n)");
817        let t = p.parse_type().unwrap();
818        if let SchemaType::Struct(s) = &t.value {
819            assert_eq!(s.fields.len(), 1);
820            assert_eq!(s.fields[0].name.value, "x");
821        } else {
822            panic!("expected SchemaType::Struct");
823        }
824    }
825
826    // Error on unexpected token in type position.
827    #[test]
828    fn parse_type_error_on_unexpected_token() {
829        let mut p = parser("42");
830        let err = p.parse_type().unwrap_err();
831        match err.kind {
832            SchemaErrorKind::UnexpectedToken { expected, .. } => {
833                assert_eq!(expected, "type");
834            }
835            other => panic!("expected UnexpectedToken, got {:?}", other),
836        }
837    }
838
839    // ========================================================
840    // parse_field() tests
841    // ========================================================
842
843    // Parses "name: String" into a FieldDef.
844    #[test]
845    fn parse_field_name_and_type() {
846        let mut p = parser("name: String,");
847        let f = p.parse_field().unwrap();
848        assert_eq!(f.name.value, "name");
849        assert_eq!(f.type_.value, SchemaType::String);
850    }
851
852    // Error when colon is missing.
853    #[test]
854    fn parse_field_error_missing_colon() {
855        let mut p = parser("name String");
856        let err = p.parse_field().unwrap_err();
857        assert!(matches!(err.kind, SchemaErrorKind::UnexpectedToken { .. }));
858    }
859
860    // ========================================================
861    // parse_struct() tests
862    // ========================================================
863
864    // Parses an empty struct.
865    #[test]
866    fn parse_struct_empty() {
867        let mut p = parser("()");
868        let s = p.parse_struct().unwrap();
869        assert!(s.fields.is_empty());
870    }
871
872    // Parses a struct with one field.
873    #[test]
874    fn parse_struct_single_field() {
875        let mut p = parser("(\n  name: String,\n)");
876        let s = p.parse_struct().unwrap();
877        assert_eq!(s.fields.len(), 1);
878        assert_eq!(s.fields[0].name.value, "name");
879    }
880
881    // Parses a struct with multiple fields.
882    #[test]
883    fn parse_struct_multiple_fields() {
884        let mut p = parser("(\n  a: String,\n  b: Integer,\n)");
885        let s = p.parse_struct().unwrap();
886        assert_eq!(s.fields.len(), 2);
887    }
888
889    // Struct without trailing comma is valid.
890    #[test]
891    fn parse_struct_no_trailing_comma() {
892        let mut p = parser("(\n  name: String\n)");
893        let s = p.parse_struct().unwrap();
894        assert_eq!(s.fields.len(), 1);
895    }
896
897    // Error on unclosed struct.
898    #[test]
899    fn parse_struct_error_on_unclosed() {
900        let mut p = parser("(\n  name: String,\n");
901        assert!(p.parse_struct().is_err());
902    }
903
904    // ========================================================
905    // parse_enum_def() tests
906    // ========================================================
907
908    // Parses a simple enum definition.
909    #[test]
910    fn parse_enum_def_simple() {
911        let mut p = parser("enum Dir { North, South }");
912        let e = p.parse_enum_def().unwrap();
913        assert_eq!(e.name, "Dir");
914        assert_eq!(e.variants.len(), 2);
915        assert!(e.variants.contains("North"));
916        assert!(e.variants.contains("South"));
917    }
918
919    // Trailing comma in variant list is allowed.
920    #[test]
921    fn parse_enum_def_trailing_comma() {
922        let mut p = parser("enum Dir { North, South, }");
923        let e = p.parse_enum_def().unwrap();
924        assert_eq!(e.variants.len(), 2);
925    }
926
927    // Single variant enum is valid.
928    #[test]
929    fn parse_enum_def_single_variant() {
930        let mut p = parser("enum Single { Only }");
931        let e = p.parse_enum_def().unwrap();
932        assert_eq!(e.variants.len(), 1);
933    }
934
935    // Error when keyword is not "enum".
936    #[test]
937    fn parse_enum_def_error_wrong_keyword() {
938        let mut p = parser("struct Dir { North }");
939        let err = p.parse_enum_def().unwrap_err();
940        assert!(matches!(err.kind, SchemaErrorKind::UnexpectedToken { .. }));
941    }
942
943    // Error on unclosed enum.
944    #[test]
945    fn parse_enum_def_error_on_unclosed() {
946        let mut p = parser("enum Dir { North, South");
947        assert!(p.parse_enum_def().is_err());
948    }
949
950    // ========================================================
951    // parse_schema() integration tests
952    // ========================================================
953
954    // Empty input produces an empty schema.
955    #[test]
956    fn schema_empty_input() {
957        let schema = parse_schema("").unwrap();
958        assert!(schema.root.fields.is_empty());
959    }
960
961    // Empty input produces no enums.
962    #[test]
963    fn schema_empty_input_no_enums() {
964        let schema = parse_schema("").unwrap();
965        assert!(schema.enums.is_empty());
966    }
967
968    // Root struct with enum ref resolves when enum is defined.
969    #[test]
970    fn schema_enum_ref_resolves() {
971        let source = "(\n  faction: Faction,\n)\nenum Faction { Sentinels, Reavers }";
972        let schema = parse_schema(source).unwrap();
973        assert_eq!(schema.root.fields[0].type_.value, SchemaType::EnumRef("Faction".to_string()));
974    }
975
976    // Multiple enum definitions are all stored.
977    #[test]
978    fn schema_multiple_enums_stored() {
979        let source = "enum A { X }\nenum B { Y }";
980        let schema = parse_schema(source).unwrap();
981        assert_eq!(schema.enums.len(), 2);
982    }
983
984    // Comments before root struct are ignored.
985    #[test]
986    fn schema_comments_before_root() {
987        let source = "// comment\n(\n  name: String,\n)";
988        let schema = parse_schema(source).unwrap();
989        assert_eq!(schema.root.fields.len(), 1);
990    }
991
992    // Inline comment after field is ignored.
993    #[test]
994    fn schema_inline_comment_after_field() {
995        let source = "(\n  name: String, // a name\n)";
996        let schema = parse_schema(source).unwrap();
997        assert_eq!(schema.root.fields[0].name.value, "name");
998    }
999
1000    // Unresolved type ref is an error.
1001    #[test]
1002    fn schema_unresolved_type_ref() {
1003        let err = parse_schema("(\n  f: Faction,\n)").unwrap_err();
1004        assert_eq!(err.kind, SchemaErrorKind::UnresolvedType { name: "Faction".to_string() });
1005    }
1006
1007    // Unresolved type ref inside Option is an error.
1008    #[test]
1009    fn schema_unresolved_type_ref_in_option() {
1010        let err = parse_schema("(\n  t: Option(Timing),\n)").unwrap_err();
1011        assert_eq!(err.kind, SchemaErrorKind::UnresolvedType { name: "Timing".to_string() });
1012    }
1013
1014    // Unresolved type ref inside List is an error.
1015    #[test]
1016    fn schema_unresolved_type_ref_in_list() {
1017        let err = parse_schema("(\n  t: [CardType],\n)").unwrap_err();
1018        assert_eq!(err.kind, SchemaErrorKind::UnresolvedType { name: "CardType".to_string() });
1019    }
1020
1021    // Duplicate enum name is an error.
1022    #[test]
1023    fn schema_duplicate_enum_name() {
1024        let err = parse_schema("enum A { X }\nenum A { Y }").unwrap_err();
1025        assert_eq!(err.kind, SchemaErrorKind::DuplicateEnum { name: "A".to_string() });
1026    }
1027
1028    // ========================================================
1029    // Type alias tests — parsing
1030    // ========================================================
1031
1032    // Basic type alias is stored in schema.aliases.
1033    #[test]
1034    fn alias_stored_in_schema() {
1035        let source = "(\n  cost: Cost,\n)\ntype Cost = (generic: Integer,)";
1036        let schema = parse_schema(source).unwrap();
1037        assert!(schema.aliases.contains_key("Cost"));
1038    }
1039
1040    // Alias field is reclassified from EnumRef to AliasRef.
1041    #[test]
1042    fn alias_ref_reclassified() {
1043        let source = "(\n  cost: Cost,\n)\ntype Cost = (generic: Integer,)";
1044        let schema = parse_schema(source).unwrap();
1045        assert_eq!(schema.root.fields[0].type_.value, SchemaType::AliasRef("Cost".to_string()));
1046    }
1047
1048    // Alias to a primitive type.
1049    #[test]
1050    fn alias_to_primitive() {
1051        let source = "(\n  name: Name,\n)\ntype Name = String";
1052        let schema = parse_schema(source).unwrap();
1053        assert_eq!(schema.aliases["Name"].value, SchemaType::String);
1054    }
1055
1056    // Alias to a list type.
1057    #[test]
1058    fn alias_to_list() {
1059        let source = "(\n  tags: Tags,\n)\ntype Tags = [String]";
1060        let schema = parse_schema(source).unwrap();
1061        assert_eq!(schema.aliases["Tags"].value, SchemaType::List(Box::new(SchemaType::String)));
1062    }
1063
1064    // Alias to an option type.
1065    #[test]
1066    fn alias_to_option() {
1067        let source = "(\n  power: Power,\n)\ntype Power = Option(Integer)";
1068        let schema = parse_schema(source).unwrap();
1069        assert_eq!(schema.aliases["Power"].value, SchemaType::Option(Box::new(SchemaType::Integer)));
1070    }
1071
1072    // Alias inside a list field is reclassified.
1073    #[test]
1074    fn alias_ref_inside_list_reclassified() {
1075        let source = "(\n  costs: [Cost],\n)\ntype Cost = (generic: Integer,)";
1076        let schema = parse_schema(source).unwrap();
1077        assert_eq!(
1078            schema.root.fields[0].type_.value,
1079            SchemaType::List(Box::new(SchemaType::AliasRef("Cost".to_string())))
1080        );
1081    }
1082
1083    // Alias inside an option field is reclassified.
1084    #[test]
1085    fn alias_ref_inside_option_reclassified() {
1086        let source = "(\n  cost: Option(Cost),\n)\ntype Cost = (generic: Integer,)";
1087        let schema = parse_schema(source).unwrap();
1088        assert_eq!(
1089            schema.root.fields[0].type_.value,
1090            SchemaType::Option(Box::new(SchemaType::AliasRef("Cost".to_string())))
1091        );
1092    }
1093
1094    // Enums and aliases can coexist.
1095    #[test]
1096    fn alias_and_enum_coexist() {
1097        let source = "(\n  cost: Cost,\n  kind: Kind,\n)\ntype Cost = (generic: Integer,)\nenum Kind { A, B }";
1098        let schema = parse_schema(source).unwrap();
1099        assert!(schema.aliases.contains_key("Cost"));
1100        assert!(schema.enums.contains_key("Kind"));
1101    }
1102
1103    // ========================================================
1104    // Type alias tests — error cases
1105    // ========================================================
1106
1107    // Duplicate alias name is an error.
1108    #[test]
1109    fn alias_duplicate_name() {
1110        let source = "type A = String\ntype A = Integer";
1111        let err = parse_schema(source).unwrap_err();
1112        assert_eq!(err.kind, SchemaErrorKind::DuplicateAlias { name: "A".to_string() });
1113    }
1114
1115    // Recursive alias is an error.
1116    #[test]
1117    fn alias_recursive_direct() {
1118        let source = "(\n  x: Foo,\n)\ntype Foo = Option(Foo)";
1119        let err = parse_schema(source).unwrap_err();
1120        assert_eq!(err.kind, SchemaErrorKind::RecursiveAlias { name: "Foo".to_string() });
1121    }
1122
1123    // Indirect recursive alias is an error.
1124    #[test]
1125    fn alias_recursive_indirect() {
1126        let source = "(\n  x: Foo,\n)\ntype Foo = Option(Bar)\ntype Bar = [Foo]";
1127        let err = parse_schema(source).unwrap_err();
1128        assert!(matches!(err.kind, SchemaErrorKind::RecursiveAlias { .. }));
1129    }
1130}