1use crate::format::Format;
19use crate::schema::{
20 Field, IntType, IntWidth, ObjectType, Presence, Rule, Schema, Type, UnionType, Variant,
21};
22use std::fmt;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct ParseError {
26 pub line: usize,
27 pub column: usize,
28 pub message: String,
29}
30
31impl fmt::Display for ParseError {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 write!(f, "{}:{}: {}", self.line, self.column, self.message)
34 }
35}
36
37impl std::error::Error for ParseError {}
38
39pub fn parse(source: &str) -> Result<Schema, ParseError> {
40 let tokens = lex(source)?;
41 let mut p = Parser { toks: tokens, pos: 0, refs: Vec::new(), variant_refs: Vec::new() };
42 let schema = p.file()?;
43 p.resolve(&schema)?;
44 Ok(schema)
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
50enum Tok {
51 Ident(String),
52 Str(String),
53 Int(i128),
54 LBrace,
55 RBrace,
56 LBracket,
57 RBracket,
58 LParen,
59 RParen,
60 Colon,
61 Question,
62 At,
63 Comma,
64 RangeIncl,
65 Eof,
66}
67
68impl Tok {
69 fn describe(&self) -> String {
70 match self {
71 Tok::Ident(s) => format!("`{s}`"),
72 Tok::Str(s) => format!("string \"{s}\""),
73 Tok::Int(n) => format!("`{n}`"),
74 Tok::LBrace => "`{`".into(),
75 Tok::RBrace => "`}`".into(),
76 Tok::LBracket => "`[`".into(),
77 Tok::RBracket => "`]`".into(),
78 Tok::LParen => "`(`".into(),
79 Tok::RParen => "`)`".into(),
80 Tok::Colon => "`:`".into(),
81 Tok::Question => "`?`".into(),
82 Tok::At => "`@`".into(),
83 Tok::Comma => "`,`".into(),
84 Tok::RangeIncl => "`..=`".into(),
85 Tok::Eof => "end of input".into(),
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
91struct Token {
92 tok: Tok,
93 line: usize,
94 column: usize,
95}
96
97fn lex(src: &str) -> Result<Vec<Token>, ParseError> {
98 let chars: Vec<char> = src.chars().collect();
99 let mut out = Vec::new();
100 let mut i = 0;
101 let mut line = 1;
102 let mut col = 1;
103
104 while let Some(&c) = chars.get(i) {
105 if c == '\n' {
106 i += 1;
107 line += 1;
108 col = 1;
109 continue;
110 }
111 if c.is_whitespace() {
112 i += 1;
113 col += 1;
114 continue;
115 }
116 if c == '/' && chars.get(i + 1) == Some(&'/') {
117 while matches!(chars.get(i), Some(&ch) if ch != '\n') {
118 i += 1;
119 col += 1;
120 }
121 continue;
122 }
123
124 let (tl, tc) = (line, col);
125
126 if c == '.' && chars.get(i + 1) == Some(&'.') && chars.get(i + 2) == Some(&'=') {
127 out.push(Token { tok: Tok::RangeIncl, line: tl, column: tc });
128 i += 3;
129 col += 3;
130 continue;
131 }
132
133 let symbol = match c {
134 '{' => Some(Tok::LBrace),
135 '}' => Some(Tok::RBrace),
136 '[' => Some(Tok::LBracket),
137 ']' => Some(Tok::RBracket),
138 '(' => Some(Tok::LParen),
139 ')' => Some(Tok::RParen),
140 ':' => Some(Tok::Colon),
141 '?' => Some(Tok::Question),
142 '@' => Some(Tok::At),
143 ',' => Some(Tok::Comma),
144 _ => None,
145 };
146 if let Some(tok) = symbol {
147 out.push(Token { tok, line: tl, column: tc });
148 i += 1;
149 col += 1;
150 continue;
151 }
152
153 if c == '"' {
154 i += 1;
155 col += 1;
156 let mut s = String::new();
157 loop {
158 match chars.get(i) {
159 None | Some('\n') => {
160 return Err(ParseError {
161 line: tl,
162 column: tc,
163 message: "unterminated string".into(),
164 })
165 }
166 Some('"') => {
167 i += 1;
168 col += 1;
169 break;
170 }
171 Some(&ch) => {
172 s.push(ch);
173 i += 1;
174 col += 1;
175 }
176 }
177 }
178 out.push(Token { tok: Tok::Str(s), line: tl, column: tc });
179 continue;
180 }
181
182 let negative = c == '-' && matches!(chars.get(i + 1), Some(ch) if ch.is_ascii_digit());
183 if c.is_ascii_digit() || negative {
184 let mut s = String::new();
185 if negative {
186 s.push('-');
187 i += 1;
188 col += 1;
189 }
190 while let Some(&ch) = chars.get(i) {
191 if ch.is_ascii_digit() {
192 s.push(ch);
193 } else if ch != '_' {
194 break;
195 }
196 i += 1;
197 col += 1;
198 }
199 let n = s.parse::<i128>().map_err(|_| ParseError {
200 line: tl,
201 column: tc,
202 message: format!("`{s}` does not fit a 128-bit integer"),
203 })?;
204 out.push(Token { tok: Tok::Int(n), line: tl, column: tc });
205 continue;
206 }
207
208 if c.is_alphabetic() || c == '_' {
209 let mut s = String::new();
210 while let Some(&ch) = chars.get(i) {
211 if ch.is_alphanumeric() || ch == '_' {
212 s.push(ch);
213 i += 1;
214 col += 1;
215 } else {
216 break;
217 }
218 }
219 out.push(Token { tok: Tok::Ident(s), line: tl, column: tc });
220 continue;
221 }
222
223 return Err(ParseError {
224 line: tl,
225 column: tc,
226 message: format!("unexpected character `{c}`"),
227 });
228 }
229
230 out.push(Token { tok: Tok::Eof, line, column: col });
231 Ok(out)
232}
233
234struct Parser {
237 toks: Vec<Token>,
238 pos: usize,
239 refs: Vec<(String, usize, usize)>,
242 variant_refs: Vec<(String, String, String, usize, usize)>,
245}
246
247impl Parser {
248 fn cur(&self) -> &Token {
249 match self.toks.get(self.pos) {
250 Some(t) => t,
251 None => match self.toks.last() {
253 Some(t) => t,
254 None => &EOF,
255 },
256 }
257 }
258
259 fn at(&self, t: &Tok) -> bool {
260 &self.cur().tok == t
261 }
262
263 fn at_keyword(&self, kw: &str) -> bool {
264 matches!(&self.cur().tok, Tok::Ident(s) if s == kw)
265 }
266
267 fn bump(&mut self) -> Token {
268 let t = self.cur().clone();
269 if !matches!(t.tok, Tok::Eof) {
270 self.pos += 1;
271 }
272 t
273 }
274
275 fn eat(&mut self, t: &Tok) -> bool {
276 if self.at(t) {
277 self.bump();
278 true
279 } else {
280 false
281 }
282 }
283
284 fn eat_keyword(&mut self, kw: &str) -> bool {
285 if self.at_keyword(kw) {
286 self.bump();
287 true
288 } else {
289 false
290 }
291 }
292
293 fn error<T>(&self, message: String) -> Result<T, ParseError> {
294 let t = self.cur();
295 Err(ParseError { line: t.line, column: t.column, message })
296 }
297
298 fn expect(&mut self, t: &Tok) -> Result<(), ParseError> {
299 if self.eat(t) {
300 Ok(())
301 } else {
302 let found = self.cur().tok.describe();
303 self.error(format!("expected {}, found {found}", t.describe()))
304 }
305 }
306
307 fn expect_ident(&mut self, what: &str) -> Result<String, ParseError> {
308 match &self.cur().tok {
309 Tok::Ident(s) => {
310 let s = s.clone();
311 self.bump();
312 Ok(s)
313 }
314 other => {
315 let found = other.describe();
316 self.error(format!("expected {what}, found {found}"))
317 }
318 }
319 }
320
321 fn expect_int(&mut self) -> Result<i128, ParseError> {
322 match self.cur().tok {
323 Tok::Int(n) => {
324 self.bump();
325 Ok(n)
326 }
327 ref other => {
328 let found = other.describe();
329 self.error(format!("expected a number, found {found}"))
330 }
331 }
332 }
333
334 fn file(&mut self) -> Result<Schema, ParseError> {
335 let mut schema = Schema::default();
336 while !self.at(&Tok::Eof) {
337 if self.at_keyword("union") {
338 let (name, union) = self.union_declaration()?;
339 if schema.declares(&name) {
342 return self.error(format!("`{name}` is declared more than once"));
343 }
344 schema.unions.insert(name, union);
345 continue;
346 }
347 if !self.at_keyword("schema") {
348 let found = self.cur().tok.describe();
349 return self.error(format!("expected `schema` or `union`, found {found}"));
350 }
351 let (name, ty) = self.declaration()?;
352 if schema.declares(&name) {
353 return self.error(format!("`{name}` is declared more than once"));
354 }
355 schema.types.insert(name, ty);
356 }
357 Ok(schema)
358 }
359
360 fn union_declaration(&mut self) -> Result<(String, UnionType), ParseError> {
361 self.bump(); let name = self.expect_ident("a union name")?;
363
364 let at = self.cur().clone();
368 if !self.eat(&Tok::At) {
369 let found = at.tok.describe();
370 return self.error(format!(
371 "`{name}` needs `@tag(\"...\")` naming the field that decides the variant, found {found}"
372 ));
373 }
374 let attribute = self.expect_ident("`tag`")?;
375 if attribute != "tag" {
376 return Err(ParseError {
377 line: at.line,
378 column: at.column,
379 message: format!("unknown union attribute `@{attribute}`, expected `@tag`"),
380 });
381 }
382 self.expect(&Tok::LParen)?;
383 let tag = match &self.cur().tok {
384 Tok::Str(s) | Tok::Ident(s) => s.clone(),
385 other => {
386 let found = other.describe();
387 return self.error(format!("expected the tag field's name, found {found}"));
388 }
389 };
390 self.bump();
391 if tag.is_empty() {
392 return self.error("the tag field's name cannot be empty".into());
393 }
394 self.expect(&Tok::RParen)?;
395
396 self.expect(&Tok::LBrace)?;
397 let mut variants: Vec<Variant> = Vec::new();
398 while !self.at(&Tok::RBrace) && !self.at(&Tok::Eof) {
399 let value = match &self.cur().tok {
400 Tok::Ident(s) | Tok::Str(s) => s.clone(),
401 other => {
402 let found = other.describe();
403 return self.error(format!("expected a variant tag value, found {found}"));
404 }
405 };
406 self.bump();
407 self.expect(&Tok::Colon)?;
408
409 let t = self.cur().clone();
410 let type_name = self.expect_ident("a schema name")?;
411 if builtin(&type_name).is_some() {
412 return Err(ParseError {
413 line: t.line,
414 column: t.column,
415 message: format!(
416 "a variant must be a declared `schema`, and `{type_name}` is a built-in type"
417 ),
418 });
419 }
420 if variants.iter().any(|v| v.tag == value) {
421 return self.error(format!("`{name}` lists the tag `{value}` twice"));
422 }
423 self.variant_refs.push((
426 name.clone(),
427 tag.clone(),
428 type_name.clone(),
429 t.line,
430 t.column,
431 ));
432 variants.push(Variant { tag: value, type_name });
433 }
434 self.expect(&Tok::RBrace)?;
435
436 if variants.is_empty() {
437 return self.error(format!("`{name}` needs at least one variant"));
438 }
439
440 Ok((name.clone(), UnionType { name, tag, variants }))
441 }
442
443 fn declaration(&mut self) -> Result<(String, ObjectType), ParseError> {
444 self.bump(); let name = self.expect_ident("a schema name")?;
446 self.expect(&Tok::LBrace)?;
447
448 let mut fields: Vec<Field> = Vec::new();
449 while !self.at(&Tok::RBrace) && !self.at(&Tok::Eof) {
450 let field = self.field()?;
451 if fields.iter().any(|f| f.name == field.name) {
452 return self.error(format!("`{name}` declares `{}` more than once", field.name));
453 }
454 fields.push(field);
455 }
456 self.expect(&Tok::RBrace)?;
457
458 Ok((
459 name.clone(),
460 ObjectType { name, fields, deny_unknown_fields: true },
461 ))
462 }
463
464 fn field(&mut self) -> Result<Field, ParseError> {
465 let name = self.expect_ident("a field name")?;
466 self.expect(&Tok::Colon)?;
467 let optional = self.eat_keyword("optional");
468 let (ty, nullable) = self.ty()?;
469
470 let mut rules = Vec::new();
471 while self.at(&Tok::At) {
472 rules.push(self.rule()?);
473 }
474
475 Ok(Field { name, ty, presence: Presence { optional, nullable }, rules })
476 }
477
478 fn ty(&mut self) -> Result<(Type, bool), ParseError> {
480 let base = if self.eat(&Tok::LBracket) {
481 let (item, item_nullable) = self.ty()?;
482 self.expect(&Tok::RBracket)?;
483 Type::Array { item: Box::new(item), item_nullable }
484 } else if self.at_keyword("enum") {
485 self.enumeration()?
486 } else {
487 let t = self.cur().clone();
488 let name = self.expect_ident("a type")?;
489 match builtin(&name) {
490 Some(ty) => ty,
491 None => {
492 self.refs.push((name.clone(), t.line, t.column));
493 Type::Ref(name)
494 }
495 }
496 };
497
498 let nullable = self.eat(&Tok::Question);
499 Ok((base, nullable))
500 }
501
502 fn enumeration(&mut self) -> Result<Type, ParseError> {
503 self.bump(); self.expect(&Tok::LBrace)?;
505
506 let mut values: Vec<String> = Vec::new();
507 loop {
508 if self.at(&Tok::RBrace) {
509 break;
510 }
511 let value = match &self.cur().tok {
512 Tok::Ident(s) | Tok::Str(s) => s.clone(),
513 other => {
514 let found = other.describe();
515 return self.error(format!("expected an enum value, found {found}"));
516 }
517 };
518 self.bump();
519 if values.contains(&value) {
520 return self.error(format!("`{value}` is listed twice"));
521 }
522 values.push(value);
523 if !self.eat(&Tok::Comma) {
524 break;
525 }
526 }
527 self.expect(&Tok::RBrace)?;
528
529 if values.is_empty() {
530 return self.error("an enum needs at least one value".into());
531 }
532 Ok(Type::Enum(values))
533 }
534
535 fn rule(&mut self) -> Result<Rule, ParseError> {
536 self.expect(&Tok::At)?;
537 let at = self.cur().clone();
538 let name = self.expect_ident("a rule name")?;
539 self.expect(&Tok::LParen)?;
540
541 let rule = match name.as_str() {
542 "min_len" => Rule::MinLen(self.count(&name)?),
543 "max_len" => Rule::MaxLen(self.count(&name)?),
544 "min_items" => Rule::MinItems(self.count(&name)?),
545 "max_items" => Rule::MaxItems(self.count(&name)?),
546 "format" => {
547 let at = self.cur().clone();
548 let value = match &self.cur().tok {
549 Tok::Ident(s) | Tok::Str(s) => s.clone(),
550 other => {
551 let found = other.describe();
552 return self.error(format!("expected a format name, found {found}"));
553 }
554 };
555 self.bump();
556 match Format::parse(&value) {
557 Some(f) => Rule::Format(f),
558 None => {
559 let names: Vec<&str> = Format::ALL.iter().map(|f| f.name()).collect();
563 return Err(ParseError {
564 line: at.line,
565 column: at.column,
566 message: format!(
567 "unknown format `{value}`, expected one of: {}",
568 names.join(", ")
569 ),
570 });
571 }
572 }
573 }
574 "range" => {
575 let min = self.expect_int()?;
576 self.expect(&Tok::RangeIncl)?;
577 let max = self.expect_int()?;
578 if min > max {
579 return self.error(format!("`range({min}..={max})` is empty"));
580 }
581 Rule::Range { min, max }
582 }
583 _ => {
584 return Err(ParseError {
585 line: at.line,
586 column: at.column,
587 message: format!("unknown rule `@{name}`"),
588 })
589 }
590 };
591
592 self.expect(&Tok::RParen)?;
593 Ok(rule)
594 }
595
596 fn count(&mut self, rule: &str) -> Result<usize, ParseError> {
597 let at = self.cur().clone();
598 let n = self.expect_int()?;
599 usize::try_from(n).map_err(|_| ParseError {
600 line: at.line,
601 column: at.column,
602 message: format!("`@{rule}` needs a non-negative number, found {n}"),
603 })
604 }
605
606 fn resolve(&self, schema: &Schema) -> Result<(), ParseError> {
607 for (name, line, column) in &self.refs {
608 if !schema.declares(name) {
609 return Err(ParseError {
610 line: *line,
611 column: *column,
612 message: format!("unknown type `{name}`"),
613 });
614 }
615 }
616
617 for (union, tag, type_name, line, column) in &self.variant_refs {
618 let err = |message: String| ParseError { line: *line, column: *column, message };
619
620 let Some(object) = schema.types.get(type_name) else {
621 return Err(err(if schema.unions.contains_key(type_name) {
622 format!(
625 "`{type_name}` is a union, and a variant of `{union}` must be a `schema`"
626 )
627 } else {
628 format!("unknown type `{type_name}`")
629 }));
630 };
631
632 if object.field(tag).is_some() {
636 return Err(err(format!(
637 "`{type_name}` declares `{tag}`, which is the tag `{union}` uses; \
638 the tag belongs to the union, not to its variants"
639 )));
640 }
641 }
642
643 Ok(())
644 }
645}
646
647static EOF: Token = Token { tok: Tok::Eof, line: 1, column: 1 };
648
649fn builtin(name: &str) -> Option<Type> {
650 let int = |width, signed| Some(Type::Int(IntType { width, signed }));
651 match name {
652 "String" => Some(Type::String),
653 "bool" => Some(Type::Bool),
654 "f64" => Some(Type::Float),
655 "Date" => Some(Type::Date),
656 "DateTime" => Some(Type::DateTime),
657 "i8" => int(IntWidth::W8, true),
658 "i16" => int(IntWidth::W16, true),
659 "i32" => int(IntWidth::W32, true),
660 "i64" => int(IntWidth::W64, true),
661 "u8" => int(IntWidth::W8, false),
662 "u16" => int(IntWidth::W16, false),
663 "u32" => int(IntWidth::W32, false),
664 "u64" => int(IntWidth::W64, false),
665 _ => None,
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 const USER: &str = r#"
674schema User {
675 id: u64
676 name: String @min_len(3) @max_len(64)
677 age: u32 @range(18..=120)
678 plan: enum { free, pro, enterprise }
679 tags: [String] @max_items(10)
680
681 nickname: String? // present, may be null
682 bio: optional String // may be absent
683 avatar: optional String? // may be absent OR null
684}
685"#;
686
687 fn user() -> ObjectType {
688 let schema = parse(USER).expect("USER should parse");
689 schema.get("User").cloned().expect("User should exist")
690 }
691
692 fn field(name: &str) -> Field {
693 user().field(name).cloned().expect("field should exist")
694 }
695
696 #[test]
697 fn parses_the_readme_schema() {
698 let u = user();
699 assert_eq!(u.name, "User");
700 assert_eq!(u.fields.len(), 8);
701 assert!(u.deny_unknown_fields);
702 }
703
704 #[test]
705 fn fields_keep_declaration_order() {
706 let names: Vec<_> = user().fields.iter().map(|f| f.name.clone()).collect();
707 assert_eq!(
708 names,
709 ["id", "name", "age", "plan", "tags", "nickname", "bio", "avatar"]
710 );
711 }
712
713 #[test]
714 fn the_four_presence_states_round_trip() {
715 assert_eq!(field("id").presence, Presence::required());
716 assert_eq!(field("nickname").presence, Presence::nullable());
717 assert_eq!(field("bio").presence, Presence::optional());
718 assert_eq!(field("avatar").presence, Presence::optional_nullable());
719 }
720
721 #[test]
722 fn integer_width_and_signedness_survive() {
723 assert_eq!(
724 field("id").ty,
725 Type::Int(IntType { width: IntWidth::W64, signed: false })
726 );
727 assert_eq!(
728 field("age").ty,
729 Type::Int(IntType { width: IntWidth::W32, signed: false })
730 );
731 }
732
733 #[test]
734 fn enums_arrays_and_rules_parse() {
735 assert_eq!(
736 field("plan").ty,
737 Type::Enum(vec!["free".into(), "pro".into(), "enterprise".into()])
738 );
739 assert_eq!(
740 field("tags").ty,
741 Type::Array { item: Box::new(Type::String), item_nullable: false }
742 );
743 assert_eq!(field("name").rules, vec![Rule::MinLen(3), Rule::MaxLen(64)]);
744 assert_eq!(field("age").rules, vec![Rule::Range { min: 18, max: 120 }]);
745 assert_eq!(field("tags").rules, vec![Rule::MaxItems(10)]);
746 }
747
748 #[test]
749 fn comments_are_ignored() {
750 let s = parse("// leading\nschema A { x: u8 } // trailing\n").expect("should parse");
751 assert!(s.get("A").is_some());
752 }
753
754 #[test]
755 fn a_type_may_refer_to_one_declared_later() {
756 let s = parse("schema A { b: B }\nschema B { x: u8 }").expect("should parse");
757 assert_eq!(
758 s.get("A").and_then(|a| a.field("b")).map(|f| f.ty.clone()),
759 Some(Type::Ref("B".into()))
760 );
761 }
762
763 fn err(src: &str) -> ParseError {
764 parse(src).expect_err("should not parse")
765 }
766
767 #[test]
768 fn an_unknown_type_is_reported_where_it_is_used() {
769 let e = err("schema A { b: Nope }");
770 assert_eq!(e.message, "unknown type `Nope`");
771 assert_eq!((e.line, e.column), (1, 15));
772 }
773
774 #[test]
775 fn unknown_rules_are_rejected_rather_than_ignored() {
776 assert_eq!(
777 err("schema A { x: u8 @nope(1) }").message,
778 "unknown rule `@nope`"
779 );
780 }
781
782 #[test]
783 fn structural_mistakes_point_at_the_right_token() {
784 assert_eq!(err("schema A { x u8 }").message, "expected `:`, found `u8`");
785 assert_eq!(
786 err("schema A {").message,
787 "expected `}`, found end of input"
788 );
789 assert_eq!(
790 err("A { }").message,
791 "expected `schema` or `union`, found `A`"
792 );
793 }
794
795 #[test]
796 fn duplicates_are_caught() {
797 assert!(err("schema A { x: u8\n x: u8 }")
798 .message
799 .contains("more than once"));
800 assert!(err("schema A { x: u8 }\nschema A { y: u8 }")
801 .message
802 .contains("more than once"));
803 assert!(err("schema A { x: enum { a, a } }")
804 .message
805 .contains("twice"));
806 }
807
808 #[test]
809 fn array_items_carry_their_own_nullability() {
810 let s = parse("schema A { x: [String?]\n y: optional [u8]? }").expect("should parse");
811 let a = s.get("A").expect("A should exist");
812
813 let x = a.field("x").expect("x should exist");
814 assert_eq!(
815 x.ty,
816 Type::Array { item: Box::new(Type::String), item_nullable: true }
817 );
818 assert_eq!(x.presence, Presence::required());
820
821 let y = a.field("y").expect("y should exist");
823 assert_eq!(y.presence, Presence::optional_nullable());
824 assert!(matches!(y.ty, Type::Array { item_nullable: false, .. }));
825 }
826
827 #[test]
828 fn rule_arguments_are_checked() {
829 assert!(err("schema A { x: String @min_len(-1) }")
830 .message
831 .contains("non-negative"));
832 assert!(err("schema A { x: u8 @range(10..=1) }")
833 .message
834 .contains("empty"));
835 }
836
837 #[test]
838 fn quoted_enum_values_allow_characters_idents_cannot_hold() {
839 let s =
840 parse(r#"schema A { r: enum { "us-east-1", "eu-west-2" } }"#).expect("should parse");
841 assert_eq!(
842 s.get("A").and_then(|a| a.field("r")).map(|f| f.ty.clone()),
843 Some(Type::Enum(vec!["us-east-1".into(), "eu-west-2".into()]))
844 );
845 }
846}