toasty_core/schema/app/
schema.rs

1use super::{EnumVariant, Field, FieldId, FieldTy, Model, ModelId, VariantId};
2
3use crate::{stmt, Result};
4use indexmap::IndexMap;
5
6/// Result of resolving a projection through the application schema.
7///
8/// A projection can resolve to either a concrete field or an enum variant
9/// (when the projection stops at a variant discriminant without descending
10/// into a variant's fields).
11#[derive(Debug)]
12pub enum Resolved<'a> {
13    /// The projection resolved to a concrete field.
14    Field(&'a Field),
15    /// The projection resolved to an enum variant (discriminant-only access).
16    Variant(&'a EnumVariant),
17}
18
19#[derive(Debug, Default)]
20pub struct Schema {
21    pub models: IndexMap<ModelId, Model>,
22}
23
24#[derive(Default)]
25struct Builder {
26    models: IndexMap<ModelId, Model>,
27}
28
29impl Schema {
30    pub fn from_macro(models: &[Model]) -> Result<Self> {
31        Builder::from_macro(models)
32    }
33
34    /// Get a field by ID
35    pub fn field(&self, id: FieldId) -> &Field {
36        let fields = match self.model(id.model) {
37            Model::Root(root) => &root.fields,
38            Model::EmbeddedStruct(embedded) => &embedded.fields,
39            Model::EmbeddedEnum(e) => &e.fields,
40        };
41        fields.get(id.index).expect("invalid field ID")
42    }
43
44    /// Get a variant by ID
45    pub fn variant(&self, id: VariantId) -> &EnumVariant {
46        let Model::EmbeddedEnum(e) = self.model(id.model) else {
47            panic!("VariantId references a non-enum model");
48        };
49        e.variants.get(id.index).expect("invalid variant index")
50    }
51
52    pub fn models(&self) -> impl Iterator<Item = &Model> {
53        self.models.values()
54    }
55
56    /// Try to get a model by ID, returning `None` if not found.
57    pub fn get_model(&self, id: impl Into<ModelId>) -> Option<&Model> {
58        self.models.get(&id.into())
59    }
60
61    /// Get a model by ID
62    pub fn model(&self, id: impl Into<ModelId>) -> &Model {
63        self.models.get(&id.into()).expect("invalid model ID")
64    }
65
66    /// Resolve a projection through the schema, returning either a field or
67    /// an enum variant.
68    ///
69    /// Starting from the root model, walks through each step of the projection,
70    /// resolving fields, following relations/embedded types, and recognizing
71    /// enum variant discriminant access.
72    ///
73    /// Returns `None` if:
74    /// - The projection is empty
75    /// - Any step references an invalid field/variant index
76    /// - A step tries to project through a primitive type
77    pub fn resolve<'a>(
78        &'a self,
79        root: &'a Model,
80        projection: &stmt::Projection,
81    ) -> Option<Resolved<'a>> {
82        let [first, rest @ ..] = projection.as_slice() else {
83            return None;
84        };
85
86        // Get the first field from the root model
87        let mut current_field = root.expect_root().fields.get(*first)?;
88
89        // Walk through remaining steps. Uses a manual iterator because
90        // embedded enums consume two steps (variant discriminant + field index).
91        let mut steps = rest.iter();
92        while let Some(step) = steps.next() {
93            match &current_field.ty {
94                FieldTy::Primitive(..) => {
95                    // Cannot project through primitive fields
96                    return None;
97                }
98                FieldTy::Embedded(embedded) => {
99                    let target = self.model(embedded.target);
100                    match target {
101                        Model::EmbeddedStruct(s) => {
102                            current_field = s.fields.get(*step)?;
103                        }
104                        Model::EmbeddedEnum(e) => {
105                            let variant = e
106                                .variants
107                                .iter()
108                                .find(|v| v.discriminant as usize == *step)?;
109
110                            // Check if there's a field index step after the variant
111                            if let Some(field_step) = steps.next() {
112                                // Two steps: variant disc + field index → field
113                                current_field = e.fields.get(*field_step)?;
114                            } else {
115                                // Single step: variant discriminant only → variant
116                                return Some(Resolved::Variant(variant));
117                            }
118                        }
119                        _ => return None,
120                    }
121                }
122                FieldTy::BelongsTo(belongs_to) => {
123                    current_field = belongs_to.target(self).expect_root().fields.get(*step)?;
124                }
125                FieldTy::HasMany(has_many) => {
126                    current_field = has_many.target(self).expect_root().fields.get(*step)?;
127                }
128                FieldTy::HasOne(has_one) => {
129                    current_field = has_one.target(self).expect_root().fields.get(*step)?;
130                }
131            };
132        }
133
134        Some(Resolved::Field(current_field))
135    }
136
137    /// Resolve a projection to a field, walking through the schema.
138    ///
139    /// Returns `None` if the projection is empty, invalid, or resolves to an
140    /// enum variant rather than a field.
141    pub fn resolve_field<'a>(
142        &'a self,
143        root: &'a Model,
144        projection: &stmt::Projection,
145    ) -> Option<&'a Field> {
146        match self.resolve(root, projection) {
147            Some(Resolved::Field(field)) => Some(field),
148            _ => None,
149        }
150    }
151
152    pub fn resolve_field_path<'a>(&'a self, path: &stmt::Path) -> Option<&'a Field> {
153        let model = self.model(path.root.expect_model());
154        self.resolve_field(model, &path.projection)
155    }
156}
157
158impl Builder {
159    pub(crate) fn from_macro(models: &[Model]) -> Result<Schema> {
160        let mut builder = Self { ..Self::default() };
161
162        for model in models {
163            builder.models.insert(model.id(), model.clone());
164        }
165
166        builder.process_models()?;
167        builder.into_schema()
168    }
169
170    fn into_schema(self) -> Result<Schema> {
171        Ok(Schema {
172            models: self.models,
173        })
174    }
175
176    fn process_models(&mut self) -> Result<()> {
177        // All models have been discovered and initialized at some level, now do
178        // the relation linking.
179        self.link_relations()?;
180
181        Ok(())
182    }
183
184    /// Go through all relations and link them to their pairs
185    fn link_relations(&mut self) -> crate::Result<()> {
186        // Because arbitrary models will be mutated throughout the linking
187        // process, models cannot be iterated as that would hold a reference to
188        // `self`. Instead, we use index based iteration.
189
190        // First, link all HasMany relations. HasManys are linked first because
191        // linking them may result in converting HasOne relations to BelongTo.
192        // We need this conversion to happen before any of the other processing.
193        for curr in 0..self.models.len() {
194            if self.models[curr].is_embedded() {
195                continue;
196            }
197            for index in 0..self.models[curr].expect_root().fields.len() {
198                let model = &self.models[curr];
199                let src = model.id();
200                let field = &model.expect_root().fields[index];
201
202                if let FieldTy::HasMany(has_many) = &field.ty {
203                    let target = has_many.target;
204                    let field_name = field.name.app_name.clone();
205                    let pair = self.find_has_many_pair(src, target, &field_name)?;
206                    self.models[curr].expect_root_mut().fields[index]
207                        .ty
208                        .expect_has_many_mut()
209                        .pair = pair;
210                }
211            }
212        }
213
214        // Link HasOne relations and compute BelongsTo foreign keys
215        for curr in 0..self.models.len() {
216            if self.models[curr].is_embedded() {
217                continue;
218            }
219            for index in 0..self.models[curr].expect_root().fields.len() {
220                let model = &self.models[curr];
221                let src = model.id();
222                let field = &model.expect_root().fields[index];
223
224                match &field.ty {
225                    FieldTy::HasOne(has_one) => {
226                        let target = has_one.target;
227                        let field_name = field.name.app_name.clone();
228                        let pair = match self.find_belongs_to_pair(src, target, &field_name)? {
229                            Some(pair) => pair,
230                            None => {
231                                return Err(crate::Error::invalid_schema(format!(
232                                    "field `{}::{}` has no matching `BelongsTo` relation on the target model",
233                                    self.models[curr].name().upper_camel_case(),
234                                    field_name,
235                                )));
236                            }
237                        };
238
239                        self.models[curr].expect_root_mut().fields[index]
240                            .ty
241                            .expect_has_one_mut()
242                            .pair = pair;
243                    }
244                    FieldTy::BelongsTo(belongs_to) => {
245                        assert!(!belongs_to.foreign_key.is_placeholder());
246                        continue;
247                    }
248                    _ => {}
249                }
250            }
251        }
252
253        // Finally, link BelongsTo relations with their pairs
254        for curr in 0..self.models.len() {
255            if self.models[curr].is_embedded() {
256                continue;
257            }
258            for index in 0..self.models[curr].expect_root().fields.len() {
259                let model = &self.models[curr];
260                let field_id = model.expect_root().fields[index].id;
261
262                let pair = match &self.models[curr].expect_root().fields[index].ty {
263                    FieldTy::BelongsTo(belongs_to) => {
264                        let mut pair = None;
265                        let target = match self.models.get_index_of(&belongs_to.target) {
266                            Some(target) => target,
267                            None => {
268                                let model = &self.models[curr];
269                                return Err(crate::Error::invalid_schema(format!(
270                                    "field `{}::{}` references a model that was not registered \
271                                     with the schema; did you forget to register it with `Db::builder()`?",
272                                    model.name().upper_camel_case(),
273                                    model.expect_root().fields[index].name.app_name,
274                                )));
275                            }
276                        };
277
278                        for target_index in 0..self.models[target].expect_root().fields.len() {
279                            pair = match &self.models[target].expect_root().fields[target_index].ty
280                            {
281                                FieldTy::HasMany(has_many) if has_many.pair == field_id => {
282                                    assert!(pair.is_none());
283                                    Some(self.models[target].expect_root().fields[target_index].id)
284                                }
285                                FieldTy::HasOne(has_one) if has_one.pair == field_id => {
286                                    assert!(pair.is_none());
287                                    Some(self.models[target].expect_root().fields[target_index].id)
288                                }
289                                _ => continue,
290                            }
291                        }
292
293                        if pair.is_none() {
294                            continue;
295                        }
296
297                        pair
298                    }
299                    _ => continue,
300                };
301
302                self.models[curr].expect_root_mut().fields[index]
303                    .ty
304                    .expect_belongs_to_mut()
305                    .pair = pair;
306            }
307        }
308
309        Ok(())
310    }
311
312    fn find_belongs_to_pair(
313        &self,
314        src: ModelId,
315        target: ModelId,
316        field_name: &str,
317    ) -> crate::Result<Option<FieldId>> {
318        let src_model = &self.models[&src];
319
320        let target = match self.models.get(&target) {
321            Some(target) => target,
322            None => {
323                return Err(crate::Error::invalid_schema(format!(
324                    "field `{}::{}` references a model that was not registered with the schema; \
325                     did you forget to register it with `Db::builder()`?",
326                    src_model.name().upper_camel_case(),
327                    field_name,
328                )));
329            }
330        };
331
332        // Find all BelongsTo relations that reference the model
333        let belongs_to: Vec<_> = target
334            .expect_root()
335            .fields
336            .iter()
337            .filter(|field| match &field.ty {
338                FieldTy::BelongsTo(rel) => rel.target == src,
339                _ => false,
340            })
341            .collect();
342
343        match &belongs_to[..] {
344            [field] => Ok(Some(field.id)),
345            [] => Ok(None),
346            _ => Err(crate::Error::invalid_schema(format!(
347                "model `{}` has more than one `BelongsTo` relation targeting `{}`",
348                target.name().upper_camel_case(),
349                src_model.name().upper_camel_case(),
350            ))),
351        }
352    }
353
354    fn find_has_many_pair(
355        &mut self,
356        src: ModelId,
357        target: ModelId,
358        field_name: &str,
359    ) -> crate::Result<FieldId> {
360        if let Some(field_id) = self.find_belongs_to_pair(src, target, field_name)? {
361            return Ok(field_id);
362        }
363
364        Err(crate::Error::invalid_schema(format!(
365            "field `{}::{}` has no matching `BelongsTo` relation on the target model",
366            self.models[&src].name().upper_camel_case(),
367            field_name,
368        )))
369    }
370}