Skip to main content

seam_core/
schema.rs

1use crate::format::Format;
2use std::collections::BTreeMap;
3
4#[derive(Debug, Clone, Default, PartialEq)]
5pub struct Schema {
6    pub types: BTreeMap<String, ObjectType>,
7    /// Tagged unions, kept beside the object types rather than inside them.
8    /// A union is a choice between objects, not a kind of object, and the two
9    /// share one namespace: `declares` is what answers "is this name taken".
10    pub unions: BTreeMap<String, UnionType>,
11}
12
13impl Schema {
14    pub fn get(&self, name: &str) -> Option<&ObjectType> {
15        self.types.get(name)
16    }
17
18    pub fn union(&self, name: &str) -> Option<&UnionType> {
19        self.unions.get(name)
20    }
21
22    /// Whether the name is declared at all, as either an object or a union.
23    pub fn declares(&self, name: &str) -> bool {
24        self.types.contains_key(name) || self.unions.contains_key(name)
25    }
26}
27
28/// A choice between object types, told apart by the value of one field.
29///
30/// The tag is always written down. A union that inferred which field decides,
31/// or defaulted to a conventional name, would be guessing what the data means
32/// — the same mistake as reading a naive datetime as local time.
33#[derive(Debug, Clone, Default, PartialEq)]
34pub struct UnionType {
35    pub name: String,
36    /// The key carrying the discriminant.
37    pub tag: String,
38    /// Declaration order, because it is the order the variants are listed in
39    /// when a payload names none of them.
40    pub variants: Vec<Variant>,
41}
42
43impl UnionType {
44    pub fn variant(&self, tag: &str) -> Option<&Variant> {
45        self.variants.iter().find(|v| v.tag == tag)
46    }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Variant {
51    /// The value of the tag field that selects this variant.
52    pub tag: String,
53    /// The object type it selects. Always a declared `schema`, never a union:
54    /// a variant that were itself a union would need a second discriminant to
55    /// resolve, and nothing in the payload says which one to read first.
56    pub type_name: String,
57}
58
59#[derive(Debug, Clone, Default, PartialEq)]
60pub struct ObjectType {
61    pub name: String,
62    /// Declaration order, because it is the order errors are reported in.
63    pub fields: Vec<Field>,
64    pub deny_unknown_fields: bool,
65}
66
67impl ObjectType {
68    pub fn field(&self, name: &str) -> Option<&Field> {
69        self.fields.iter().find(|f| f.name == name)
70    }
71}
72
73#[derive(Debug, Clone, PartialEq)]
74pub struct Field {
75    pub name: String,
76    pub ty: Type,
77    pub presence: Presence,
78    pub rules: Vec<Rule>,
79}
80
81/// Two independent axes, not one. An absent key means "don't touch this field";
82/// an explicit null means "clear it".
83///
84/// | `.seam`            | `optional` | `nullable` |
85/// |--------------------|------------|------------|
86/// | `String`           | false      | false      |
87/// | `String?`          | false      | true       |
88/// | `optional String`  | true       | false      |
89/// | `optional String?` | true       | true       |
90#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
91pub struct Presence {
92    pub optional: bool,
93    pub nullable: bool,
94}
95
96impl Presence {
97    pub const fn required() -> Self {
98        Self { optional: false, nullable: false }
99    }
100
101    pub const fn nullable() -> Self {
102        Self { optional: false, nullable: true }
103    }
104
105    pub const fn optional() -> Self {
106        Self { optional: true, nullable: false }
107    }
108
109    pub const fn optional_nullable() -> Self {
110        Self { optional: true, nullable: true }
111    }
112}
113
114#[derive(Debug, Clone, PartialEq)]
115pub enum Type {
116    Bool,
117    Int(IntType),
118    Float,
119    String,
120    /// A calendar date. Never widened into an instant.
121    Date,
122    /// An instant with a mandatory UTC offset.
123    DateTime,
124    Enum(Vec<String>),
125    /// An element has two states, a value or null, so only nullability applies
126    /// to it. Absence is a property of a key, and an array has no keys.
127    Array {
128        item: Box<Type>,
129        item_nullable: bool,
130    },
131    Object(Box<ObjectType>),
132    Ref(String),
133}
134
135impl Type {
136    pub fn kind(&self) -> &'static str {
137        match self {
138            Type::Bool => "bool",
139            Type::Int(_) => "integer",
140            Type::Float => "float",
141            Type::String => "string",
142            Type::Date => "date",
143            Type::DateTime => "datetime",
144            Type::Enum(_) => "enum",
145            Type::Array { .. } => "array",
146            Type::Object(_) | Type::Ref(_) => "object",
147        }
148    }
149}
150
151/// Width is part of the type because it is what makes a cross-language range
152/// check possible at all.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub struct IntType {
155    pub width: IntWidth,
156    pub signed: bool,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum IntWidth {
161    W8,
162    W16,
163    W32,
164    W64,
165}
166
167impl IntType {
168    pub fn range(self) -> (i128, i128) {
169        let bits = match self.width {
170            IntWidth::W8 => 8,
171            IntWidth::W16 => 16,
172            IntWidth::W32 => 32,
173            IntWidth::W64 => 64,
174        };
175        if self.signed {
176            let half = 1_i128 << (bits - 1);
177            (-half, half - 1)
178        } else {
179            (0, (1_i128 << bits) - 1)
180        }
181    }
182
183    pub fn name(self) -> &'static str {
184        match (self.signed, self.width) {
185            (true, IntWidth::W8) => "i8",
186            (true, IntWidth::W16) => "i16",
187            (true, IntWidth::W32) => "i32",
188            (true, IntWidth::W64) => "i64",
189            (false, IntWidth::W8) => "u8",
190            (false, IntWidth::W16) => "u16",
191            (false, IntWidth::W32) => "u32",
192            (false, IntWidth::W64) => "u64",
193        }
194    }
195
196    /// Whether the JS binding can use `number` rather than `bigint`.
197    pub fn fits_js_number(self) -> bool {
198        !matches!(self.width, IntWidth::W64)
199    }
200}
201
202#[derive(Debug, Clone, PartialEq)]
203pub enum Rule {
204    MinLen(usize),
205    MaxLen(usize),
206    Range {
207        min: i128,
208        max: i128,
209    },
210    MinItems(usize),
211    MaxItems(usize),
212    /// A named shape the string must have. Never a regular expression: see
213    /// [`crate::format`] for why that is the design and not an omission.
214    Format(Format),
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn integer_ranges_are_exact_at_the_edges() {
223        let u64t = IntType { width: IntWidth::W64, signed: false };
224        assert_eq!(u64t.range(), (0, i128::from(u64::MAX)));
225
226        let i32t = IntType { width: IntWidth::W32, signed: true };
227        assert_eq!(i32t.range(), (i128::from(i32::MIN), i128::from(i32::MAX)));
228
229        let u8t = IntType { width: IntWidth::W8, signed: false };
230        assert_eq!(u8t.range(), (0, 255));
231    }
232
233    #[test]
234    fn only_64_bit_integers_need_bigint_in_js() {
235        assert!(IntType { width: IntWidth::W32, signed: true }.fits_js_number());
236        assert!(!IntType { width: IntWidth::W64, signed: false }.fits_js_number());
237    }
238
239    #[test]
240    fn presence_axes_are_independent() {
241        assert_eq!(
242            Presence::optional_nullable(),
243            Presence { optional: true, nullable: true }
244        );
245        assert_ne!(Presence::optional(), Presence::nullable());
246    }
247}