1use crate::ast::{
2 ComponentProtocol, ComponentReference, CompositeIndex, Constraint, ConstraintParam, EnumDef,
3 Field, FieldType, Model, Projection, RelationInclusion, RelationType, Schema, Struct,
4};
5use crate::lexer::{Lexer, Token, TokenWithPos};
6use forgedb_validation::{Position, ValidationError};
7
8#[derive(Debug, Clone)]
15pub struct ParsedSchema {
16 pub schema: Schema,
17 pub diagnostics: Vec<ValidationError>,
18}
19
20pub struct Parser {
21 tokens: Vec<Token>,
22 tokens_with_pos: Vec<TokenWithPos>,
23 position: usize,
24 use_validation: bool,
25 recovering: bool,
30 recovery_diagnostics: Vec<ValidationError>,
32}
33
34impl Parser {
35 pub fn new(input: &str) -> Result<Self, String> {
36 Self::new_with_validation(input, true)
37 }
38
39 pub fn new_with_validation(input: &str, use_validation: bool) -> Result<Self, String> {
40 let mut lexer = Lexer::new(input);
41 let tokens = lexer.tokenize()?;
42
43 let mut lexer = Lexer::new(input);
44 let tokens_with_pos = lexer.tokenize_with_pos()?;
45
46 Ok(Parser {
47 tokens,
48 tokens_with_pos,
49 position: 0,
50 use_validation,
51 recovering: false,
52 recovery_diagnostics: Vec::new(),
53 })
54 }
55
56 fn get_current_position(&self) -> Option<Position> {
57 if self.position < self.tokens_with_pos.len() {
58 Some(self.tokens_with_pos[self.position].position)
59 } else {
60 None
61 }
62 }
63
64 fn position_at(&self, idx: usize) -> Option<Position> {
68 self.tokens_with_pos.get(idx).map(|t| t.position)
69 }
70
71 fn diag(message: String, position: Option<Position>) -> ValidationError {
73 let err = ValidationError::new(message);
74 match position {
75 Some(p) => err.with_position(p),
76 None => err,
77 }
78 }
79
80 fn recover_to_member_boundary(&mut self) {
85 while !matches!(
86 self.current_token(),
87 Token::Newline | Token::RBrace | Token::Eof
88 ) {
89 self.advance();
90 }
91 self.skip_newlines();
92 }
93
94 fn next_significant_is_lbrace(&self, idx: usize) -> bool {
98 let mut i = idx;
99 while matches!(self.tokens.get(i), Some(Token::Newline)) {
100 i += 1;
101 }
102 matches!(self.tokens.get(i), Some(Token::LBrace))
103 }
104
105 fn synchronize_from(&mut self, start: usize) {
113 let n = self.tokens.len();
114 let mut i = start;
115
116 while i < n {
117 match &self.tokens[i] {
118 Token::LBrace => {
119 let mut depth = 0i32;
121 let mut j = i;
122 while j < n {
123 match self.tokens[j] {
124 Token::LBrace => depth += 1,
125 Token::RBrace => {
126 depth -= 1;
127 if depth == 0 {
128 self.position = j + 1;
129 return;
130 }
131 }
132 Token::Eof => {
133 self.position = j;
134 return;
135 }
136 _ => {}
137 }
138 j += 1;
139 }
140 self.position = n; return;
142 }
143 Token::Eof => {
144 self.position = i;
145 return;
146 }
147 Token::KwStruct | Token::KwEnum if i > start => {
150 self.position = i;
151 return;
152 }
153 Token::Ident(_) if i > start && self.next_significant_is_lbrace(i + 1) => {
154 self.position = i;
155 return;
156 }
157 _ => i += 1,
158 }
159 }
160 self.position = n;
161 }
162
163 fn recover_diag(&mut self, message: String, at: usize) {
165 let pos = self.position_at(at);
166 self.recovery_diagnostics.push(Self::diag(message, pos));
167 }
168
169 fn current_token(&self) -> &Token {
170 if self.position < self.tokens.len() {
171 &self.tokens[self.position]
172 } else {
173 &Token::Eof
174 }
175 }
176
177 fn advance(&mut self) {
178 if self.position < self.tokens.len() {
179 self.position += 1;
180 }
181 }
182
183 fn skip_newlines(&mut self) {
184 while matches!(self.current_token(), Token::Newline) {
185 self.advance();
186 }
187 }
188
189 fn read_component_path(&mut self) -> Result<String, String> {
190 let mut path = String::new();
193
194 loop {
195 match self.current_token() {
196 Token::Ident(name) => {
197 path.push_str(name);
198 self.advance();
199
200 if matches!(self.current_token(), Token::Slash) {
202 path.push('/');
203 self.advance();
204 } else {
206 break;
208 }
209 }
210 _ => {
211 return Err(format!(
212 "Expected path component (identifier), found {:?}",
213 self.current_token()
214 ))
215 }
216 }
217 }
218
219 if path.is_empty() {
220 return Err("Component path cannot be empty".to_string());
221 }
222
223 Ok(path)
224 }
225
226 fn expect(&mut self, expected: Token) -> Result<(), String> {
227 if self.current_token() == &expected {
228 self.advance();
229 Ok(())
230 } else {
231 Err(format!(
232 "Expected {:?}, found {:?}",
233 expected,
234 self.current_token()
235 ))
236 }
237 }
238
239 fn parse_constraint(&mut self) -> Result<Constraint, String> {
240 self.expect(Token::At)?;
242
243 let name = match self.current_token() {
245 Token::Ident(s) => s.clone(),
246 _ => {
247 return Err(format!(
248 "Expected constraint name after '@', found {:?}",
249 self.current_token()
250 ))
251 }
252 };
253 self.advance();
254
255 let mut constraint = Constraint::new(name);
256
257 if matches!(self.current_token(), Token::LParen) {
259 self.advance();
260
261 loop {
263 match self.current_token() {
264 Token::Number(n) => {
265 constraint = constraint.with_param(ConstraintParam::Number(*n));
266 self.advance();
267 }
268 Token::Ident(s) => {
269 constraint = constraint.with_param(ConstraintParam::String(s.clone()));
270 self.advance();
271 }
272 Token::Str(s) => {
273 constraint = constraint.with_param(ConstraintParam::String(s.clone()));
274 self.advance();
275 }
276 Token::Asterisk => {
277 constraint =
279 constraint.with_param(ConstraintParam::String("*".to_string()));
280 self.advance();
281 }
282 _ => {
283 return Err(format!(
284 "Expected constraint parameter, found {:?}",
285 self.current_token()
286 ))
287 }
288 }
289
290 match self.current_token() {
292 Token::Comma => {
293 self.advance();
294 continue;
295 }
296 Token::RParen => {
297 self.advance();
298 break;
299 }
300 _ => {
301 return Err(format!(
302 "Expected ',' or ')' in constraint parameters, found {:?}",
303 self.current_token()
304 ))
305 }
306 }
307 }
308 }
309
310 Ok(constraint)
311 }
312
313 fn parse_type(&mut self) -> Result<FieldType, String> {
314 match self.current_token() {
316 Token::LBracket => {
318 self.advance();
319
320 let first_token = self.current_token().clone();
322
323 match first_token {
324 Token::Ident(name) => {
325 self.advance();
326
327 match self.current_token() {
329 Token::Semicolon => {
330 self.advance();
332 let count = match self.current_token() {
333 Token::Number(n) => *n as usize,
334 _ => {
335 return Err(format!(
336 "Expected array count after ';', found {:?}",
337 self.current_token()
338 ))
339 }
340 };
341 self.advance();
342 self.expect(Token::RBracket)?;
343 return Ok(FieldType::FixedArray(
345 Box::new(FieldType::StructType(name)),
346 count,
347 ));
348 }
349 Token::RBracket => {
350 self.advance();
352 return Ok(FieldType::Relation(RelationType::OneToMany(name)));
353 }
354 _ => {
355 return Err(format!(
356 "Expected ';' or ']' after type name, found {:?}",
357 self.current_token()
358 ))
359 }
360 }
361 }
362 _ => {
363 let inner_type = self.parse_primitive_type()?;
365 self.expect(Token::Semicolon)?;
366 let count = match self.current_token() {
367 Token::Number(n) => *n as usize,
368 _ => {
369 return Err(format!(
370 "Expected array count, found {:?}",
371 self.current_token()
372 ))
373 }
374 };
375 self.advance();
376 self.expect(Token::RBracket)?;
377 return Ok(FieldType::FixedArray(Box::new(inner_type), count));
378 }
379 }
380 }
381 Token::Asterisk => {
383 self.advance();
384 let model_name = match self.current_token() {
385 Token::Ident(name) => name.clone(),
386 _ => {
387 return Err(format!(
388 "Expected model name after '*', found {:?}",
389 self.current_token()
390 ))
391 }
392 };
393 self.advance();
394 return Ok(FieldType::Relation(RelationType::RequiredReference(
395 model_name,
396 )));
397 }
398 Token::Question => {
400 self.advance();
401 match self.current_token().clone() {
402 Token::Ident(name) => {
403 self.advance();
404 return Ok(FieldType::Relation(RelationType::OptionalReference(name)));
405 }
406 Token::TypeU32
408 | Token::TypeU64
409 | Token::TypeI32
410 | Token::TypeI64
411 | Token::TypeF64
412 | Token::TypeBool
413 | Token::TypeString
414 | Token::TypeUuid
415 | Token::TypeTimestamp
416 | Token::TypeJson
417 | Token::TypeDecimal
418 | Token::TypeChar => {
419 let inner = self.parse_primitive_type()?;
420 return Ok(FieldType::Nullable(Box::new(inner)));
421 }
422 _ => {
423 return Err(format!(
424 "Expected model name or primitive type after '?', found {:?}",
425 self.current_token()
426 ))
427 }
428 }
429 }
430 Token::Ident(name) => {
432 let type_name = name.clone();
433 self.advance();
434
435 if matches!(type_name.as_str(), "tsx" | "jsx" | "api")
437 && matches!(self.current_token(), Token::Colon)
438 {
439 self.advance(); self.skip_newlines(); self.expect(Token::Slash)?;
445 self.expect(Token::Slash)?;
446
447 let path = self.read_component_path()?;
449
450 let protocol = match type_name.as_str() {
452 "tsx" => ComponentProtocol::Tsx,
453 "jsx" => ComponentProtocol::Jsx,
454 "api" => ComponentProtocol::Api,
455 _ => unreachable!(),
456 };
457
458 return Ok(FieldType::Component(ComponentReference {
459 protocol,
460 path,
461 relations: RelationInclusion::None,
462 }));
463 }
464
465 return Ok(FieldType::StructType(type_name));
467 }
468 _ => {}
469 }
470
471 self.parse_primitive_type()
473 }
474
475 fn parse_primitive_type(&mut self) -> Result<FieldType, String> {
476 let field_type = match self.current_token() {
477 Token::TypeU32 => FieldType::U32,
478 Token::TypeU64 => FieldType::U64,
479 Token::TypeI32 => FieldType::I32,
480 Token::TypeI64 => FieldType::I64,
481 Token::TypeF64 => FieldType::F64,
482 Token::TypeBool => FieldType::Bool,
483 Token::TypeString => FieldType::String,
484 Token::TypeJson => FieldType::Json,
485 Token::TypeDecimal => FieldType::Decimal,
486 Token::TypeUuid => FieldType::Uuid,
487 Token::TypeTimestamp => FieldType::Timestamp,
488 Token::TypeChar => {
489 self.advance();
491 self.expect(Token::LParen)?;
492 let size = match self.current_token() {
493 Token::Number(n) => *n as usize,
494 _ => {
495 return Err(format!(
496 "Expected size after 'char(', found {:?}",
497 self.current_token()
498 ))
499 }
500 };
501 self.advance();
502 self.expect(Token::RParen)?;
503 return Ok(FieldType::Char(size));
504 }
505 _ => return Err(format!("Expected type, found {:?}", self.current_token())),
506 };
507 self.advance();
508 Ok(field_type)
509 }
510
511 fn parse_directive(&mut self) -> Result<CompositeIndex, String> {
512 self.expect(Token::At)?;
514
515 let directive_name = match self.current_token() {
517 Token::Ident(s) => s.clone(),
518 _ => {
519 return Err(format!(
520 "Expected directive name after '@', found {:?}",
521 self.current_token()
522 ))
523 }
524 };
525 self.advance();
526
527 match directive_name.as_str() {
528 "index" => self.parse_index_directive(),
529 _ => Err(format!("Unknown directive: @{}", directive_name)),
530 }
531 }
532
533 fn parse_index_directive(&mut self) -> Result<CompositeIndex, String> {
534 self.expect(Token::LParen)?;
536
537 let mut fields = Vec::new();
539 loop {
540 let field_name = match self.current_token() {
542 Token::Ident(s) => s.clone(),
543 _ => {
544 return Err(format!(
545 "Expected field name in @index directive, found {:?}",
546 self.current_token()
547 ))
548 }
549 };
550 self.advance();
551 fields.push(field_name);
552
553 match self.current_token() {
555 Token::Comma => {
556 self.advance();
557 }
559 Token::RParen => {
560 self.advance();
561 break;
562 }
563 _ => {
564 return Err(format!(
565 "Expected ',' or ')' in @index directive, found {:?}",
566 self.current_token()
567 ))
568 }
569 }
570 }
571
572 if fields.len() < 2 {
573 return Err("Composite index must include at least 2 fields".to_string());
574 }
575
576 Ok(CompositeIndex { fields })
577 }
578
579 fn parse_projection_directive(&mut self) -> Result<Projection, String> {
582 self.expect(Token::LParen)?;
583
584 let name = match self.current_token() {
586 Token::Ident(s) => s.clone(),
587 _ => {
588 return Err(format!(
589 "Expected projection name in @projection directive, found {:?}",
590 self.current_token()
591 ))
592 }
593 };
594 self.advance();
595
596 self.expect(Token::Colon)?;
598
599 let mut fields = Vec::new();
601 loop {
602 let field_name = match self.current_token() {
603 Token::Ident(s) => s.clone(),
604 _ => {
605 return Err(format!(
606 "Expected field name in @projection directive, found {:?}",
607 self.current_token()
608 ))
609 }
610 };
611 self.advance();
612 fields.push(field_name);
613
614 match self.current_token() {
615 Token::Comma => {
616 self.advance();
617 }
618 Token::RParen => {
619 self.advance();
620 break;
621 }
622 _ => {
623 return Err(format!(
624 "Expected ',' or ')' in @projection directive, found {:?}",
625 self.current_token()
626 ))
627 }
628 }
629 }
630
631 if fields.is_empty() {
632 return Err(format!(
633 "@projection '{}' must name at least one field",
634 name
635 ));
636 }
637
638 Ok(Projection { name, fields })
639 }
640
641 fn parse_field(&mut self) -> Result<Field, String> {
642 self.skip_newlines();
643
644 let field_pos = self.get_current_position();
646 let name = match self.current_token() {
647 Token::Ident(s) => s.clone(),
648 _ => {
649 return Err(format!(
650 "Expected field name, found {:?}",
651 self.current_token()
652 ))
653 }
654 };
655 self.advance();
656
657 self.expect(Token::Colon)?;
664
665 let mut auto_generate = false;
667 let mut unique = false;
668 let mut indexed = false;
669
670 loop {
671 match self.current_token() {
672 Token::Plus => {
673 auto_generate = true;
674 self.advance();
675 }
676 Token::Ampersand => {
677 unique = true;
678 self.advance();
679 }
680 Token::Caret => {
681 indexed = true;
682 self.advance();
683 }
684 _ => break,
685 }
686 }
687
688 let mut field_type = self.parse_type()?;
690
691 if matches!(self.current_token(), Token::Question) {
693 match field_type {
694 FieldType::StructType(ref name) => {
695 let name = name.clone();
696 self.advance();
697 field_type = FieldType::OptionalStructType(name);
698 }
699 FieldType::U32
701 | FieldType::U64
702 | FieldType::I32
703 | FieldType::I64
704 | FieldType::F64
705 | FieldType::Bool
706 | FieldType::String
707 | FieldType::Json
708 | FieldType::Decimal
709 | FieldType::Uuid
710 | FieldType::Timestamp
711 | FieldType::Char(_)
712 | FieldType::FixedArray(_, _) => {
713 let inner = field_type.clone();
714 self.advance();
715 field_type = FieldType::Nullable(Box::new(inner));
716 }
717 _ => {} }
719 }
720
721 if auto_generate && !field_type.is_auto_generatable() {
723 return Err(format!(
724 "Auto-generate symbol '+' cannot be used with type {:?}. Only u32, u64, uuid, and timestamp support auto-generation",
725 field_type
726 ));
727 }
728
729 let mut constraints = Vec::new();
731 let mut is_computed = false;
732 let mut fulltext_indexed = false;
733 let mut is_materialized = false;
734 while matches!(self.current_token(), Token::At) {
735 let constraint = self.parse_constraint()?;
736 if constraint.name == "relations" {
738 if let FieldType::Component(ref mut comp_ref) = field_type {
739 if constraint.params.is_empty() {
741 return Err("@relations directive requires parameters".to_string());
742 }
743
744 if constraint.params.len() == 1 {
746 if let ConstraintParam::String(s) = &constraint.params[0] {
747 if s == "*" {
748 comp_ref.relations = RelationInclusion::All;
749 } else {
750 comp_ref.relations = RelationInclusion::Specific(vec![s.clone()]);
752 }
753 } else {
754 return Err(
755 "@relations parameter must be a field name or *".to_string()
756 );
757 }
758 } else {
759 let mut fields = Vec::new();
761 for param in &constraint.params {
762 if let ConstraintParam::String(field_name) = param {
763 fields.push(field_name.clone());
764 } else {
765 return Err("@relations parameters must be field names".to_string());
766 }
767 }
768 comp_ref.relations = RelationInclusion::Specific(fields);
769 }
770 continue;
772 } else {
773 return Err(
774 "@relations directive can only be used with component fields".to_string(),
775 );
776 }
777 }
778 if constraint.name == "computed" {
780 is_computed = true;
781 }
782 if constraint.name == "fulltext" {
784 fulltext_indexed = true;
785 }
786 if constraint.name == "materialized" {
788 is_materialized = true;
789 }
790 constraints.push(constraint);
791 }
792
793 let index_type = field_type.default_index_type();
795
796 Ok(Field {
797 name,
798 field_type,
799 auto_generate,
800 unique,
801 indexed,
802 constraints,
803 index_type,
804 is_computed,
805 fulltext_indexed,
806 is_materialized,
807 position: field_pos,
808 })
809 }
810
811 fn parse_struct(&mut self) -> Result<Struct, String> {
812 self.skip_newlines();
813
814 self.expect(Token::KwStruct)?;
816
817 let struct_pos = self.get_current_position();
819 let name = match self.current_token() {
820 Token::Ident(s) => s.clone(),
821 _ => {
822 return Err(format!(
823 "Expected struct name, found {:?}",
824 self.current_token()
825 ))
826 }
827 };
828 self.advance();
829
830 self.skip_newlines();
834 self.expect(Token::LBrace)?;
835
836 let mut fields = Vec::new();
838 self.skip_newlines();
839
840 while !matches!(self.current_token(), Token::RBrace | Token::Eof) {
841 let member_start = self.position;
842 match self.parse_field() {
843 Ok(field) => {
844 fields.push(field);
845 self.skip_newlines();
846 }
847 Err(e) if self.recovering => {
848 self.recover_diag(e, member_start);
849 self.recover_to_member_boundary();
850 if self.position == member_start {
851 self.advance();
852 }
853 }
854 Err(e) => return Err(e),
855 }
856 }
857
858 if let Err(e) = self.expect(Token::RBrace) {
860 if self.recovering {
861 self.recover_diag(e, self.position);
862 } else {
863 return Err(e);
864 }
865 }
866
867 if fields.is_empty() {
868 let e = format!("Struct '{}' has no fields", name);
869 if self.recovering {
870 self.recovery_diagnostics.push(Self::diag(e, struct_pos));
871 } else {
872 return Err(e);
873 }
874 }
875
876 Ok(Struct {
877 name,
878 fields,
879 position: struct_pos,
880 })
881 }
882
883 fn parse_enum(&mut self) -> Result<EnumDef, String> {
887 self.skip_newlines();
888
889 self.expect(Token::KwEnum)?;
891
892 let enum_pos = self.get_current_position();
894 let name = match self.current_token() {
895 Token::Ident(s) => s.clone(),
896 _ => {
897 return Err(format!(
898 "Expected enum name, found {:?}",
899 self.current_token()
900 ))
901 }
902 };
903 self.advance();
904
905 self.skip_newlines();
909 self.expect(Token::LBrace)?;
910 self.skip_newlines();
911
912 let mut variants = Vec::new();
914 while !matches!(self.current_token(), Token::RBrace | Token::Eof) {
915 let variant = match self.current_token() {
916 Token::Ident(s) => s.clone(),
917 _ => {
918 return Err(format!(
919 "Expected enum variant name in enum '{}', found {:?}",
920 name,
921 self.current_token()
922 ))
923 }
924 };
925 self.advance();
926
927 variants.push(variant);
929
930 match self.current_token() {
932 Token::Comma => {
933 self.advance();
934 self.skip_newlines();
935 }
936 _ => {
937 self.skip_newlines();
938 }
939 }
940 }
941
942 self.expect(Token::RBrace)?;
943
944 if variants.is_empty() {
945 return Err(format!("Enum '{}' has no variants", name));
946 }
947
948 Ok(EnumDef {
949 name,
950 variants,
951 position: enum_pos,
952 })
953 }
954
955 fn parse_model(&mut self) -> Result<Model, String> {
956 self.skip_newlines();
957
958 let model_pos = self.get_current_position();
960 let name = match self.current_token() {
961 Token::Ident(s) => s.clone(),
962 _ => {
963 return Err(format!(
964 "Expected model name, found {:?}",
965 self.current_token()
966 ))
967 }
968 };
969 self.advance();
970
971 self.skip_newlines();
975 self.expect(Token::LBrace)?;
976
977 let mut fields = Vec::new();
979 let mut composite_indexes = Vec::new();
980 let mut projections = Vec::new();
981 let mut soft_delete = false;
982 self.skip_newlines();
983
984 while !matches!(self.current_token(), Token::RBrace | Token::Eof) {
985 let member_start = self.position;
986 match self.parse_model_member(
987 &mut fields,
988 &mut composite_indexes,
989 &mut projections,
990 &mut soft_delete,
991 ) {
992 Ok(()) => {}
993 Err(e) if self.recovering => {
994 self.recover_diag(e, member_start);
998 self.recover_to_member_boundary();
999 if self.position == member_start {
1000 self.advance();
1001 }
1002 }
1003 Err(e) => return Err(e),
1004 }
1005 }
1006
1007 if let Err(e) = self.expect(Token::RBrace) {
1009 if self.recovering {
1010 self.recover_diag(e, self.position);
1011 } else {
1012 return Err(e);
1013 }
1014 }
1015
1016 if fields.is_empty() {
1017 let e = format!("Model '{}' has no fields", name);
1018 if self.recovering {
1019 self.recovery_diagnostics
1021 .push(Self::diag(e, model_pos));
1022 } else {
1023 return Err(e);
1024 }
1025 }
1026
1027 Ok(Model {
1032 name,
1033 fields,
1034 composite_indexes,
1035 projections,
1036 soft_delete,
1037 position: model_pos,
1038 })
1039 }
1040
1041 fn parse_model_member(
1047 &mut self,
1048 fields: &mut Vec<Field>,
1049 composite_indexes: &mut Vec<CompositeIndex>,
1050 projections: &mut Vec<Projection>,
1051 soft_delete: &mut bool,
1052 ) -> Result<(), String> {
1053 if matches!(self.current_token(), Token::At) {
1054 let start_pos = self.position;
1056 match self.parse_directive() {
1057 Ok(composite_index) => {
1058 composite_indexes.push(composite_index);
1059 self.skip_newlines();
1060 }
1061 Err(_) => {
1062 self.position = start_pos;
1064 self.advance(); let directive_name = match self.current_token() {
1066 Token::Ident(s) => s.clone(),
1067 _ => {
1068 return Err(format!(
1069 "Expected directive name after '@', found {:?}",
1070 self.current_token()
1071 ))
1072 }
1073 };
1074 self.advance();
1075
1076 match directive_name.as_str() {
1077 "soft_delete" => {
1078 *soft_delete = true;
1079 self.skip_newlines();
1080 }
1081 "projection" => {
1082 let projection = self.parse_projection_directive()?;
1083 projections.push(projection);
1084 self.skip_newlines();
1085 }
1086 _ => {
1087 return Err(format!("Unknown model directive: @{}", directive_name));
1088 }
1089 }
1090 }
1091 }
1092 } else {
1093 let field = self.parse_field()?;
1094 fields.push(field);
1095 self.skip_newlines();
1096 }
1097 Ok(())
1098 }
1099
1100 pub fn parse(&mut self) -> Result<Schema, String> {
1107 let schema = self.parse_unvalidated()?;
1108
1109 let mut errors = Vec::new();
1110 if self.use_validation {
1111 crate::validate::collect_naming_errors(&schema, &mut errors);
1112 }
1113 crate::validate::collect_structure_errors(&schema, &mut errors);
1114
1115 if let Some(first) = errors.first() {
1116 return Err(first.to_string());
1117 }
1118
1119 Ok(schema)
1120 }
1121
1122 pub fn parse_unvalidated(&mut self) -> Result<Schema, String> {
1135 let mut structs = Vec::new();
1136 let mut enums = Vec::new();
1137 let mut models = Vec::new();
1138 let mut enum_names = std::collections::HashSet::new();
1141 self.skip_newlines();
1142
1143 while !matches!(self.current_token(), Token::Eof) {
1144 if matches!(self.current_token(), Token::KwStruct) {
1146 structs.push(self.parse_struct()?);
1147 } else if matches!(self.current_token(), Token::KwEnum) {
1148 let enum_def = self.parse_enum()?;
1149 enum_names.insert(enum_def.name.clone());
1150 enums.push(enum_def);
1151 } else {
1152 models.push(self.parse_model()?);
1153 }
1154
1155 self.skip_newlines();
1156 }
1157
1158 if models.is_empty() && structs.is_empty() && enums.is_empty() {
1159 return Err("Schema is empty".to_string());
1160 }
1161
1162 for model in &mut models {
1169 for field in &mut model.fields {
1170 Self::resolve_enum_field_type(&mut field.field_type, &enum_names);
1171 }
1172 }
1173
1174 Ok(Schema {
1175 structs,
1176 enums,
1177 models,
1178 })
1179 }
1180
1181 pub fn parse_recover(&mut self) -> ParsedSchema {
1200 enum Decl {
1201 Struct(Struct),
1202 Enum(EnumDef),
1203 Model(Model),
1204 }
1205
1206 let prev_recovering = self.recovering;
1207 self.recovering = true;
1208 self.recovery_diagnostics.clear();
1209
1210 let mut structs = Vec::new();
1211 let mut enums = Vec::new();
1212 let mut models = Vec::new();
1213 let mut enum_names = std::collections::HashSet::new();
1214
1215 self.skip_newlines();
1216 while !matches!(self.current_token(), Token::Eof) {
1217 let decl_start = self.position;
1218 let result = if matches!(self.current_token(), Token::KwStruct) {
1219 self.parse_struct().map(Decl::Struct)
1220 } else if matches!(self.current_token(), Token::KwEnum) {
1221 self.parse_enum().map(Decl::Enum)
1222 } else {
1223 self.parse_model().map(Decl::Model)
1224 };
1225
1226 match result {
1227 Ok(Decl::Struct(s)) => structs.push(s),
1228 Ok(Decl::Enum(e)) => {
1229 enum_names.insert(e.name.clone());
1230 enums.push(e);
1231 }
1232 Ok(Decl::Model(m)) => models.push(m),
1233 Err(e) => {
1234 self.recover_diag(e, decl_start);
1235 self.synchronize_from(decl_start);
1236 }
1237 }
1238
1239 if self.position == decl_start {
1242 self.advance();
1243 }
1244 self.skip_newlines();
1245 }
1246
1247 for model in &mut models {
1249 for field in &mut model.fields {
1250 Self::resolve_enum_field_type(&mut field.field_type, &enum_names);
1251 }
1252 }
1253
1254 let schema = Schema {
1255 structs,
1256 enums,
1257 models,
1258 };
1259
1260 let mut diagnostics = std::mem::take(&mut self.recovery_diagnostics);
1261 diagnostics.extend(crate::validate::validate_schema(&schema));
1262 diagnostics.sort_by_key(|d| {
1264 d.position
1265 .map(|p| (p.line, p.column))
1266 .unwrap_or((usize::MAX, usize::MAX))
1267 });
1268
1269 self.recovering = prev_recovering;
1270 ParsedSchema { schema, diagnostics }
1271 }
1272
1273 fn resolve_enum_field_type(
1277 field_type: &mut FieldType,
1278 enum_names: &std::collections::HashSet<String>,
1279 ) {
1280 match field_type {
1281 FieldType::StructType(name) if enum_names.contains(name) => {
1282 *field_type = FieldType::Enum(name.clone());
1283 }
1284 FieldType::OptionalStructType(name) if enum_names.contains(name) => {
1285 *field_type = FieldType::Nullable(Box::new(FieldType::Enum(name.clone())));
1286 }
1287 _ => {}
1288 }
1289 }
1290}
1291
1292#[cfg(test)]
1293mod tests {
1294 use super::*;
1295
1296 fn field_constraints<'a>(schema: &'a Schema, model: &str, field: &str) -> &'a [Constraint] {
1297 let m = schema
1298 .models
1299 .iter()
1300 .find(|m| m.name == model)
1301 .expect("model not found");
1302 let f = m
1303 .fields
1304 .iter()
1305 .find(|f| f.name == field)
1306 .expect("field not found");
1307 &f.constraints
1308 }
1309
1310 #[test]
1314 fn parsed_nodes_carry_source_positions() {
1315 let src = "User {\n id: +uuid\n email: &string\n}\n\nstruct Point {\n x: i32\n}\n\nenum Status {\n Active\n Inactive\n}\n";
1320 let schema = Parser::new(src).unwrap().parse().unwrap();
1321
1322 let user = schema.find_model("User").unwrap();
1323 let upos = user.position.expect("model carries a position");
1324 assert_eq!((upos.line, upos.column), (1, 1), "User model name");
1325
1326 let id = user.fields.iter().find(|f| f.name == "id").unwrap();
1327 let idpos = id.position.expect("field carries a position");
1328 assert_eq!((idpos.line, idpos.column), (2, 3), "id field name");
1329
1330 let email = user.fields.iter().find(|f| f.name == "email").unwrap();
1331 assert_eq!(email.position.unwrap().line, 3, "email field line");
1332
1333 let point = schema.find_struct("Point").unwrap();
1334 assert_eq!(point.position.unwrap().line, 6, "Point struct line");
1335
1336 let status = schema.find_enum("Status").unwrap();
1337 assert_eq!(status.position.unwrap().line, 10, "Status enum line");
1338 }
1339
1340 #[test]
1341 fn parses_projection_directive() {
1342 let src = r#"
1343 Post {
1344 id: +uuid
1345 title: string
1346 slug: ^string
1347 content: string
1348
1349 @projection(card: title, slug)
1350 @projection(list_row: title)
1351 }
1352 "#;
1353 let schema = Parser::new(src).unwrap().parse().unwrap();
1354 let post = schema.models.iter().find(|m| m.name == "Post").unwrap();
1355 assert_eq!(post.projections.len(), 2);
1356 assert_eq!(post.projections[0].name, "card");
1357 assert_eq!(post.projections[0].fields, vec!["title", "slug"]);
1358 assert_eq!(post.projections[1].name, "list_row");
1359 assert_eq!(post.projections[1].fields, vec!["title"]);
1360 }
1361
1362 #[test]
1363 fn projection_rejects_unknown_field() {
1364 let src = r#"
1365 Post {
1366 id: +uuid
1367 title: string
1368 @projection(card: title, nope)
1369 }
1370 "#;
1371 let err = Parser::new(src).unwrap().parse().unwrap_err();
1372 assert!(err.contains("undefined field 'nope'"), "got: {err}");
1373 }
1374
1375 #[test]
1376 fn projection_rejects_duplicate_name() {
1377 let src = r#"
1378 Post {
1379 id: +uuid
1380 title: string
1381 slug: string
1382 @projection(card: title)
1383 @projection(card: slug)
1384 }
1385 "#;
1386 let err = Parser::new(src).unwrap().parse().unwrap_err();
1387 assert!(err.contains("Duplicate @projection name 'card'"), "got: {err}");
1388 }
1389
1390 #[test]
1391 fn parses_json_and_nullable_json_types() {
1392 let src = r#"
1393 Event {
1394 id: +uuid
1395 payload: json
1396 meta: json?
1397 }
1398 "#;
1399 let schema = Parser::new(src).unwrap().parse().unwrap();
1400 let m = schema.models.iter().find(|m| m.name == "Event").unwrap();
1401 let payload = m.fields.iter().find(|f| f.name == "payload").unwrap();
1402 let meta = m.fields.iter().find(|f| f.name == "meta").unwrap();
1403 assert_eq!(payload.field_type, FieldType::Json);
1404 assert_eq!(
1405 meta.field_type,
1406 FieldType::Nullable(Box::new(FieldType::Json))
1407 );
1408 assert!(!payload.field_type.is_fixed_size());
1410 assert!(!meta.field_type.is_fixed_size());
1411 }
1412
1413 #[test]
1414 fn parses_decimal_and_nullable_decimal_types() {
1415 let src = r#"
1416 Product {
1417 id: +uuid
1418 price: decimal
1419 discount: decimal?
1420 }
1421 "#;
1422 let schema = Parser::new(src).unwrap().parse().unwrap();
1423 let m = schema.models.iter().find(|m| m.name == "Product").unwrap();
1424 let price = m.fields.iter().find(|f| f.name == "price").unwrap();
1425 let discount = m.fields.iter().find(|f| f.name == "discount").unwrap();
1426 assert_eq!(price.field_type, FieldType::Decimal);
1427 assert_eq!(
1428 discount.field_type,
1429 FieldType::Nullable(Box::new(FieldType::Decimal))
1430 );
1431 assert!(price.field_type.is_fixed_size());
1433 assert!(discount.field_type.is_fixed_size());
1434 assert_eq!(price.field_type.to_rust_type(), "rust_decimal::Decimal");
1435 }
1436
1437 #[test]
1438 fn parses_string_literal_directive_arg() {
1439 let src = r#"
1440 User {
1441 id: +uuid
1442 phone: string @pattern("^[0-9]+$")
1443 }
1444 "#;
1445 let schema = Parser::new(src).unwrap().parse().unwrap();
1446 let cons = field_constraints(&schema, "User", "phone");
1447 assert_eq!(cons.len(), 1);
1448 assert_eq!(cons[0].name, "pattern");
1449 assert_eq!(
1450 cons[0].params,
1451 vec![ConstraintParam::String("^[0-9]+$".to_string())]
1452 );
1453 }
1454
1455 #[test]
1456 fn parses_string_default_on_nullable_string() {
1457 let src = r#"
1458 Ticket {
1459 id: +uuid
1460 status: string? @default("pending")
1461 }
1462 "#;
1463 let schema = Parser::new(src).unwrap().parse().unwrap();
1464 let cons = field_constraints(&schema, "Ticket", "status");
1465 assert_eq!(cons[0].name, "default");
1466 assert_eq!(
1467 cons[0].params,
1468 vec![ConstraintParam::String("pending".to_string())]
1469 );
1470 }
1471
1472 #[test]
1473 fn bare_identifier_default_still_parses() {
1474 let src = r#"
1476 Ticket {
1477 id: +uuid
1478 status: string @default(pending)
1479 }
1480 "#;
1481 let schema = Parser::new(src).unwrap().parse().unwrap();
1482 let cons = field_constraints(&schema, "Ticket", "status");
1483 assert_eq!(
1484 cons[0].params,
1485 vec![ConstraintParam::String("pending".to_string())]
1486 );
1487 }
1488
1489 #[test]
1490 fn string_and_number_args_mix() {
1491 let src = r#"
1492 Thing {
1493 id: +uuid
1494 code: string @pattern("[A-Z]{3}") @length(3, 3)
1495 }
1496 "#;
1497 let schema = Parser::new(src).unwrap().parse().unwrap();
1498 let cons = field_constraints(&schema, "Thing", "code");
1499 assert_eq!(cons.len(), 2);
1500 assert_eq!(cons[0].name, "pattern");
1501 assert_eq!(
1502 cons[0].params,
1503 vec![ConstraintParam::String("[A-Z]{3}".to_string())]
1504 );
1505 assert_eq!(cons[1].name, "length");
1506 assert_eq!(
1507 cons[1].params,
1508 vec![ConstraintParam::Number(3), ConstraintParam::Number(3)]
1509 );
1510 }
1511
1512 #[test]
1513 fn parses_enum_and_enum_typed_fields() {
1514 let src = r#"
1518 Order {
1519 id: +uuid
1520 status: Status
1521 prev_status: Status?
1522 }
1523 enum Status { Active, Inactive, Pending }
1524 "#;
1525 let schema = Parser::new(src).unwrap().parse().unwrap();
1526 assert_eq!(schema.enums.len(), 1);
1527 let status_enum = schema.find_enum("Status").unwrap();
1528 assert_eq!(status_enum.variants, vec!["Active", "Inactive", "Pending"]);
1529
1530 let order = schema.models.iter().find(|m| m.name == "Order").unwrap();
1531 let status = order.fields.iter().find(|f| f.name == "status").unwrap();
1532 assert_eq!(status.field_type, FieldType::Enum("Status".to_string()));
1533 let prev = order.fields.iter().find(|f| f.name == "prev_status").unwrap();
1534 assert_eq!(
1535 prev.field_type,
1536 FieldType::Nullable(Box::new(FieldType::Enum("Status".to_string())))
1537 );
1538 assert!(status.field_type.is_fixed_size());
1540 }
1541
1542 #[test]
1543 fn enum_trailing_comma_optional() {
1544 let with = Parser::new("enum A { X, Y, }\nM { id: +uuid }")
1546 .unwrap()
1547 .parse()
1548 .unwrap();
1549 assert_eq!(with.find_enum("A").unwrap().variants, vec!["X", "Y"]);
1550 let without = Parser::new("enum A { X, Y }\nM { id: +uuid }")
1551 .unwrap()
1552 .parse()
1553 .unwrap();
1554 assert_eq!(without.find_enum("A").unwrap().variants, vec!["X", "Y"]);
1555 }
1556
1557 #[test]
1558 fn enum_rejects_duplicate_variant() {
1559 let err = Parser::new("enum Status { Active, Active }\nM { id: +uuid }")
1560 .unwrap()
1561 .parse()
1562 .unwrap_err();
1563 assert!(err.contains("Duplicate variant 'Active'"), "got: {err}");
1564 }
1565
1566 #[test]
1567 fn enum_rejects_lowercase_variant() {
1568 let err = Parser::new("enum Status { active }\nM { id: +uuid }")
1570 .unwrap()
1571 .parse()
1572 .unwrap_err();
1573 assert!(err.contains("PascalCase"), "got: {err}");
1574 }
1575
1576 #[test]
1577 fn enum_rejects_empty() {
1578 let err = Parser::new("enum Status { }\nM { id: +uuid }")
1579 .unwrap()
1580 .parse()
1581 .unwrap_err();
1582 assert!(err.contains("no variants"), "got: {err}");
1583 }
1584
1585 #[test]
1586 fn field_referencing_undefined_named_type_errors() {
1587 let err = Parser::new("Order { id: +uuid\n status: Nope }")
1590 .unwrap()
1591 .parse()
1592 .unwrap_err();
1593 assert!(err.contains("unknown type 'Nope'"), "got: {err}");
1594 }
1595
1596 #[test]
1597 fn enum_rejects_duplicate_declaration() {
1598 let err = Parser::new("enum S { A }\nenum S { B }\nM { id: +uuid }")
1599 .unwrap()
1600 .parse()
1601 .unwrap_err();
1602 assert!(err.contains("Duplicate enum name 'S'"), "got: {err}");
1603 }
1604
1605 fn recover(input: &str) -> ParsedSchema {
1608 Parser::new(input).unwrap().parse_recover()
1609 }
1610
1611 #[test]
1614 fn recover_valid_schema_matches_parse() {
1615 let input = "User {\n id: +uuid\n email: string\n}\nPost {\n id: +uuid\n}\n";
1616 let recovered = recover(input);
1617 assert!(recovered.diagnostics.is_empty(), "no diagnostics: {:?}", recovered.diagnostics);
1618 assert_eq!(recovered.schema, Parser::new(input).unwrap().parse().unwrap());
1619 }
1620
1621 #[test]
1624 fn recover_empty_input_is_not_an_error() {
1625 let recovered = recover("\n \n");
1626 assert!(recovered.diagnostics.is_empty());
1627 assert!(recovered.schema.models.is_empty());
1628 }
1629
1630 #[test]
1633 fn recover_skips_a_broken_declaration_and_keeps_the_rest() {
1634 let input = "\
1635User {
1636 id: +uuid
1637}
1638
1639Broken
1640 id: +uuid
1641
1642Post {
1643 id: +uuid
1644}
1645";
1646 let recovered = recover(input);
1647 let names: Vec<&str> = recovered.schema.models.iter().map(|m| m.name.as_str()).collect();
1648 assert!(names.contains(&"User"), "User survived: {names:?}");
1649 assert!(names.contains(&"Post"), "Post survived after recovery: {names:?}");
1650 assert!(
1651 recovered.diagnostics.iter().any(|d| d.position.is_some()),
1652 "the broken declaration produced a positioned diagnostic"
1653 );
1654 }
1655
1656 #[test]
1659 fn recover_skips_a_broken_field_and_keeps_the_model() {
1660 let input = "User {\n id: +uuid\n @@@ : broken\n email: string\n}\n";
1661 let recovered = recover(input);
1662 let user = recovered
1663 .schema
1664 .find_model("User")
1665 .expect("User model still produced despite a bad field");
1666 let fields: Vec<&str> = user.fields.iter().map(|f| f.name.as_str()).collect();
1667 assert!(fields.contains(&"id") && fields.contains(&"email"), "good fields survived: {fields:?}");
1668 assert!(!recovered.diagnostics.is_empty(), "the bad field was reported");
1669 }
1670
1671 #[test]
1674 fn recover_merges_semantic_diagnostics() {
1675 let input = "User {\n id: +uuid\n pet: *Ghost\n}\n";
1676 let recovered = recover(input);
1677 assert!(
1678 recovered
1679 .diagnostics
1680 .iter()
1681 .any(|d| d.message.contains("references undefined model 'Ghost'")),
1682 "semantic diagnostics included: {:?}",
1683 recovered.diagnostics
1684 );
1685 }
1686
1687 #[test]
1689 fn recover_diagnostics_are_sorted_by_position() {
1690 let input = "User {\n id: +uuid\n BadName: string\n a: *Nope\n}\n";
1692 let recovered = recover(input);
1693 let lines: Vec<usize> = recovered
1694 .diagnostics
1695 .iter()
1696 .filter_map(|d| d.position.map(|p| p.line))
1697 .collect();
1698 let mut sorted = lines.clone();
1699 sorted.sort();
1700 assert_eq!(lines, sorted, "diagnostics sorted by line: {lines:?}");
1701 }
1702
1703 #[test]
1706 fn recover_handles_unterminated_block() {
1707 let recovered = recover("User {\n id: +uuid\n email:");
1708 assert!(!recovered.diagnostics.is_empty(), "unterminated buffer is diagnosed");
1709 }
1710
1711 #[test]
1714 fn recover_reports_multiple_broken_declarations() {
1715 let input = "\
1716A
1717 x: u32
1718
1719Good {
1720 id: +uuid
1721}
1722
1723B
1724 y: u32
1725";
1726 let recovered = recover(input);
1727 assert!(recovered.schema.find_model("Good").is_some(), "middle valid model survived");
1728 assert!(
1729 recovered.diagnostics.len() >= 2,
1730 "both broken declarations reported: {:?}",
1731 recovered.diagnostics
1732 );
1733 }
1734}