1use 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 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 while self.peek().is_some_and(|b| b.is_ascii_alphanumeric() || b == b'_') {
123 self.advance();
124 }
125
126 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 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 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 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 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 fn parse_alias_def(&mut self) -> Result<(String, Spanned<SchemaType>), SchemaParseError> {
292 self.skip_whitespace();
293 self.parse_identifier()?; 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
304pub 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 let start = parser.position();
331 let keyword = parser.parse_identifier()?;
332
333 match keyword.value.as_str() {
334 "enum" => {
335 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 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 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_refs(&root, &enums, &aliases)?;
385
386 verify_no_recursive_aliases(&aliases)?;
388
389 Ok(Schema { root, enums, aliases })
390}
391
392fn 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
421fn 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
468fn 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 fn parser(source: &str) -> Parser<'_> {
525 Parser::new(source)
526 }
527
528 #[test]
534 fn peek_returns_current_byte() {
535 let p = parser("abc");
536 assert_eq!(p.peek(), Some(b'a'));
537 }
538
539 #[test]
541 fn peek_returns_none_at_end() {
542 let p = parser("");
543 assert_eq!(p.peek(), None);
544 }
545
546 #[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 #[test]
562 fn advance_past_newline_increments_line() {
563 let mut p = parser("a\nb");
564 p.advance(); p.advance(); assert_eq!(p.line, 2);
567 assert_eq!(p.column, 1);
568 }
569
570 #[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 #[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 #[test]
594 fn position_after_advance() {
595 let mut p = parser("ab\nc");
596 p.advance(); p.advance(); p.advance(); 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[test]
956 fn schema_empty_input() {
957 let schema = parse_schema("").unwrap();
958 assert!(schema.root.fields.is_empty());
959 }
960
961 #[test]
963 fn schema_empty_input_no_enums() {
964 let schema = parse_schema("").unwrap();
965 assert!(schema.enums.is_empty());
966 }
967
968 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}