Skip to main content

bern_protocol/
types.rs

1use crate::{LocalizedText, SchemaError};
2use serde::{Deserialize, Serialize};
3
4/// Types that may cross a Bern boundary. The first version deliberately
5/// limits itself to booleans, numbers, text, enums, optionals, records and
6/// lists, each with a defined mapping in every target language.
7#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(tag = "kind", rename_all = "snake_case")]
9pub enum Type {
10    Unit,
11    Bool,
12    Int,
13    Float,
14    Text,
15    Enum { name: String },
16    Record { name: String },
17    Optional { inner: Box<Type> },
18    List { inner: Box<Type> },
19}
20
21impl Type {
22    pub fn optional(inner: Type) -> Self {
23        Type::Optional {
24            inner: Box::new(inner),
25        }
26    }
27
28    pub fn list(inner: Type) -> Self {
29        Type::List {
30            inner: Box::new(inner),
31        }
32    }
33
34    /// Short human readable spelling used in diagnostics and help output.
35    pub fn display(&self) -> String {
36        match self {
37            Type::Unit => "()".into(),
38            Type::Bool => "bool".into(),
39            Type::Int => "int".into(),
40            Type::Float => "float".into(),
41            Type::Text => "text".into(),
42            Type::Enum { name } | Type::Record { name } => name.clone(),
43            Type::Optional { inner } => format!("{}?", inner.display()),
44            Type::List { inner } => format!("[{}]", inner.display()),
45        }
46    }
47}
48
49#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
50pub struct FieldDef {
51    pub name: String,
52    #[serde(rename = "type")]
53    pub ty: Type,
54    #[serde(default, skip_serializing_if = "String::is_empty")]
55    pub doc: String,
56    /// `#[content]`: text that is data, such as a title or a name, shown
57    /// as it is; other text the engine gives is not shown untranslated.
58    #[serde(default, skip_serializing_if = "is_false")]
59    pub content: bool,
60}
61
62fn is_false(value: &bool) -> bool {
63    !*value
64}
65
66#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
67pub struct VariantDef {
68    pub name: String,
69    /// Named payload fields. Unit variants have none.
70    #[serde(default, skip_serializing_if = "Vec::is_empty")]
71    pub fields: Vec<FieldDef>,
72    /// User-visible label, required when the enum is shown in a UI.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub label: Option<LocalizedText>,
75}
76
77#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
78pub struct EnumDef {
79    pub name: String,
80    pub variants: Vec<VariantDef>,
81}
82
83impl EnumDef {
84    pub fn is_unit_only(&self) -> bool {
85        self.variants
86            .iter()
87            .all(|variant| variant.fields.is_empty())
88    }
89
90    pub fn variant_index(&self, name: &str) -> Option<usize> {
91        self.variants
92            .iter()
93            .position(|variant| variant.name == name)
94    }
95}
96
97#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
98pub struct RecordDef {
99    pub name: String,
100    pub fields: Vec<FieldDef>,
101}
102
103impl RecordDef {
104    pub fn field_index(&self, name: &str) -> Option<usize> {
105        self.fields.iter().position(|field| field.name == name)
106    }
107}
108
109/// Named enum and record definitions referenced by [`Type`].
110#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
111pub struct TypeTable {
112    #[serde(default)]
113    pub enums: Vec<EnumDef>,
114    #[serde(default)]
115    pub records: Vec<RecordDef>,
116}
117
118impl TypeTable {
119    pub fn enum_def(&self, name: &str) -> Option<&EnumDef> {
120        self.enums.iter().find(|def| def.name == name)
121    }
122
123    pub fn record_def(&self, name: &str) -> Option<&RecordDef> {
124        self.records.iter().find(|def| def.name == name)
125    }
126
127    /// Merge another table, rejecting different definitions with one name.
128    pub fn merge(&mut self, other: &TypeTable) -> Result<(), SchemaError> {
129        for def in &other.enums {
130            match self.enum_def(&def.name) {
131                Some(existing) if existing != def => {
132                    return Err(SchemaError::new(format!(
133                        "enum `{}` is defined twice with different shapes",
134                        def.name
135                    )));
136                }
137                Some(_) => {}
138                None => self.enums.push(def.clone()),
139            }
140        }
141        for def in &other.records {
142            match self.record_def(&def.name) {
143                Some(existing) if existing != def => {
144                    return Err(SchemaError::new(format!(
145                        "record `{}` is defined twice with different shapes",
146                        def.name
147                    )));
148                }
149                Some(_) => {}
150                None => self.records.push(def.clone()),
151            }
152        }
153        Ok(())
154    }
155
156    /// Check that every referenced name exists and that records do not
157    /// contain themselves, which the first version cannot represent.
158    pub fn validate(&self) -> Result<(), SchemaError> {
159        let mut names = std::collections::BTreeSet::new();
160        for name in self
161            .enums
162            .iter()
163            .map(|def| &def.name)
164            .chain(self.records.iter().map(|def| &def.name))
165        {
166            if !names.insert(name) {
167                return Err(SchemaError::new(format!("type `{name}` is declared twice")));
168            }
169        }
170        for def in &self.enums {
171            if def.variants.is_empty() {
172                return Err(SchemaError::new(format!(
173                    "enum `{}` has no variants",
174                    def.name
175                )));
176            }
177            let mut seen = std::collections::BTreeSet::new();
178            for variant in &def.variants {
179                if !seen.insert(&variant.name) {
180                    return Err(SchemaError::new(format!(
181                        "enum `{}` repeats variant `{}`",
182                        def.name, variant.name
183                    )));
184                }
185                for field in &variant.fields {
186                    self.check_type(&field.ty, &mut Vec::new())?;
187                }
188                // A label shows an enum field by that enum's labels.
189                for param in variant.label.iter().flat_map(|label| &label.params) {
190                    let Some(Type::Enum { name }) = variant
191                        .fields
192                        .iter()
193                        .find(|field| field.name == *param)
194                        .map(|field| &field.ty)
195                    else {
196                        continue;
197                    };
198                    if self
199                        .enum_def(name)
200                        .is_some_and(|shown| shown.variants.iter().any(|v| v.label.is_none()))
201                    {
202                        return Err(SchemaError::new(format!(
203                            "the label of `{}::{}` shows `{{{param}}}`, a `{name}`; give every variant of `{name}` a label",
204                            def.name, variant.name
205                        )));
206                    }
207                }
208            }
209        }
210        for def in &self.records {
211            self.check_type(
212                &Type::Record {
213                    name: def.name.clone(),
214                },
215                &mut Vec::new(),
216            )?;
217        }
218        Ok(())
219    }
220
221    pub fn check_type(&self, ty: &Type, stack: &mut Vec<String>) -> Result<(), SchemaError> {
222        match ty {
223            Type::Unit | Type::Bool | Type::Int | Type::Float | Type::Text => Ok(()),
224            Type::Enum { name } => self
225                .enum_def(name)
226                .map(|_| ())
227                .ok_or_else(|| SchemaError::new(format!("unknown enum `{name}`"))),
228            Type::Record { name } => {
229                let def = self
230                    .record_def(name)
231                    .ok_or_else(|| SchemaError::new(format!("unknown record `{name}`")))?;
232                if stack.contains(name) {
233                    return Err(SchemaError::new(format!(
234                        "record `{name}` contains itself; recursive data needs a dedicated design"
235                    )));
236                }
237                stack.push(name.clone());
238                for field in &def.fields {
239                    self.check_type(&field.ty, stack)?;
240                }
241                stack.pop();
242                Ok(())
243            }
244            Type::Optional { inner } => {
245                if matches!(**inner, Type::Optional { .. }) {
246                    return Err(SchemaError::new("nested optionals are not supported"));
247                }
248                self.check_type(inner, stack)
249            }
250            Type::List { inner } => self.check_type(inner, stack),
251        }
252    }
253}