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