toasty_core/schema/app/
schema.rs

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