Skip to main content

seam_core/
parser.rs

1//! The `.seam` front end.
2//!
3//! ```text
4//! file        := declaration*
5//! declaration := object | union
6//! object      := "schema" ident "{" field* "}"
7//! union       := "union" ident "@tag" "(" string ")" "{" variant* "}"
8//! variant     := value ":" ident
9//! field       := ident ":" "optional"? type rule*
10//! type        := base "?"?
11//! base        := ident | "[" type "]" | enum
12//! enum        := "enum" "{" value ("," value)* ","? "}"
13//! value       := ident | string
14//! rule        := "@" ident "(" args ")"
15//! format      := "@format" "(" name ")"      // a closed set, never a regex
16//! ```
17
18use 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// ---------------------------------------------------------------- lexer
48
49#[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
234// --------------------------------------------------------------- parser
235
236struct Parser {
237    toks: Vec<Token>,
238    pos: usize,
239    /// Type references, checked once the whole file is known so that a schema
240    /// may refer to one declared later.
241    refs: Vec<(String, usize, usize)>,
242    /// `(union, tag field, variant type, line, column)`, checked in the same
243    /// pass and for the same reason.
244    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            // `lex` always appends Eof, and `bump` never moves past it.
252            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                // One namespace: an object and a union cannot share a name,
340                // because a reference names one thing.
341                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(); // `union`
362        let name = self.expect_ident("a union name")?;
363
364        // `@tag` is required. A union that defaulted to a conventional field
365        // name would be guessing which value decides what the payload means,
366        // and the whole point of the file is that nothing is guessed.
367        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            // Recorded so the second pass can check it, and check that the
424            // object it names does not itself declare the tag field.
425            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(); // `schema`
445        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    /// Returns the type and whether a `?` suffix marked it nullable.
479    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(); // `enum`
504        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                        // The set is closed on purpose. Naming what is
560                        // available beats leaving the author to guess, and
561                        // guessing is what a regex would have invited.
562                        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                    // Allowing this would need a second discriminant, and
623                    // nothing in the payload says which one to read first.
624                    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            // The tag is supplied by the union and consumed while validating,
633            // so a variant declaring it too would create two sources of truth
634            // for one value, which could then disagree.
635            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        // The list itself is required and non-null; only its elements may be null.
819        assert_eq!(x.presence, Presence::required());
820
821        // Element nullability and field nullability are independent.
822        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}