Skip to main content

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 = if has_many.pair.is_placeholder() {
253                        self.find_has_many_pair(src, target, &field_name)?
254                    } else {
255                        self.validate_pair(src, target, &field_name, has_many.pair)?;
256                        has_many.pair
257                    };
258                    self.models[curr].as_root_mut_unwrap().fields[index]
259                        .ty
260                        .as_has_many_mut_unwrap()
261                        .pair = pair;
262                }
263            }
264        }
265
266        // Link HasOne relations and compute BelongsTo foreign keys
267        for curr in 0..self.models.len() {
268            if self.models[curr].is_embedded() {
269                continue;
270            }
271            for index in 0..self.models[curr].as_root_unwrap().fields.len() {
272                let model = &self.models[curr];
273                let src = model.id();
274                let field = &model.as_root_unwrap().fields[index];
275
276                match &field.ty {
277                    FieldTy::HasOne(has_one) => {
278                        let target = has_one.target;
279                        let field_name = field.name.app_unwrap().to_string();
280                        let pair = if has_one.pair.is_placeholder() {
281                            match self.find_belongs_to_pair(src, target, &field_name)? {
282                                Some(pair) => pair,
283                                None => {
284                                    return Err(crate::Error::invalid_schema(format!(
285                                        "field `{}::{}` has no matching `BelongsTo` relation on the target model",
286                                        self.models[curr].name().upper_camel_case(),
287                                        field_name,
288                                    )));
289                                }
290                            }
291                        } else {
292                            self.validate_pair(src, target, &field_name, has_one.pair)?;
293                            has_one.pair
294                        };
295
296                        self.models[curr].as_root_mut_unwrap().fields[index]
297                            .ty
298                            .as_has_one_mut_unwrap()
299                            .pair = pair;
300                    }
301                    FieldTy::BelongsTo(belongs_to) => {
302                        assert!(!belongs_to.foreign_key.is_placeholder());
303                        continue;
304                    }
305                    _ => {}
306                }
307            }
308        }
309
310        // Finally, link BelongsTo relations with their pairs
311        for curr in 0..self.models.len() {
312            if self.models[curr].is_embedded() {
313                continue;
314            }
315            for index in 0..self.models[curr].as_root_unwrap().fields.len() {
316                let model = &self.models[curr];
317                let field_id = model.as_root_unwrap().fields[index].id;
318
319                let pair = match &self.models[curr].as_root_unwrap().fields[index].ty {
320                    FieldTy::BelongsTo(belongs_to) => {
321                        let mut pair = None;
322                        let target = match self.models.get_index_of(&belongs_to.target) {
323                            Some(target) => target,
324                            None => {
325                                let model = &self.models[curr];
326                                return Err(crate::Error::invalid_schema(format!(
327                                    "field `{}::{}` references a model that was not registered \
328                                     with the schema; did you forget to register it with `Db::builder()`?",
329                                    model.name().upper_camel_case(),
330                                    model.as_root_unwrap().fields[index].name(),
331                                )));
332                            }
333                        };
334
335                        for target_index in 0..self.models[target].as_root_unwrap().fields.len() {
336                            pair = match &self.models[target].as_root_unwrap().fields[target_index]
337                                .ty
338                            {
339                                FieldTy::HasMany(has_many) if has_many.pair == field_id => {
340                                    assert!(pair.is_none());
341                                    Some(
342                                        self.models[target].as_root_unwrap().fields[target_index]
343                                            .id,
344                                    )
345                                }
346                                FieldTy::HasOne(has_one) if has_one.pair == field_id => {
347                                    assert!(pair.is_none());
348                                    Some(
349                                        self.models[target].as_root_unwrap().fields[target_index]
350                                            .id,
351                                    )
352                                }
353                                _ => continue,
354                            }
355                        }
356
357                        if pair.is_none() {
358                            continue;
359                        }
360
361                        pair
362                    }
363                    _ => continue,
364                };
365
366                self.models[curr].as_root_mut_unwrap().fields[index]
367                    .ty
368                    .as_belongs_to_mut_unwrap()
369                    .pair = pair;
370            }
371        }
372
373        Ok(())
374    }
375
376    fn find_belongs_to_pair(
377        &self,
378        src: ModelId,
379        target: ModelId,
380        field_name: &str,
381    ) -> crate::Result<Option<FieldId>> {
382        let src_model = &self.models[&src];
383
384        let target = match self.models.get(&target) {
385            Some(target) => target,
386            None => {
387                return Err(crate::Error::invalid_schema(format!(
388                    "field `{}::{}` references a model that was not registered with the schema; \
389                     did you forget to register it with `Db::builder()`?",
390                    src_model.name().upper_camel_case(),
391                    field_name,
392                )));
393            }
394        };
395
396        // Find all BelongsTo relations that reference the model
397        let belongs_to: Vec<_> = target
398            .as_root_unwrap()
399            .fields
400            .iter()
401            .filter(|field| match &field.ty {
402                FieldTy::BelongsTo(rel) => rel.target == src,
403                _ => false,
404            })
405            .collect();
406
407        match &belongs_to[..] {
408            [field] => Ok(Some(field.id)),
409            [] => Ok(None),
410            _ => Err(crate::Error::invalid_schema(format!(
411                "model `{}` has more than one `BelongsTo` relation targeting `{}`; \
412                 disambiguate by adding `pair = <field>` on the paired `has_many`/`has_one` \
413                 field",
414                target.name().upper_camel_case(),
415                src_model.name().upper_camel_case(),
416            ))),
417        }
418    }
419
420    fn find_has_many_pair(
421        &mut self,
422        src: ModelId,
423        target: ModelId,
424        field_name: &str,
425    ) -> crate::Result<FieldId> {
426        if let Some(field_id) = self.find_belongs_to_pair(src, target, field_name)? {
427            return Ok(field_id);
428        }
429
430        Err(crate::Error::invalid_schema(format!(
431            "field `{}::{}` has no matching `BelongsTo` relation on the target model",
432            self.models[&src].name().upper_camel_case(),
433            field_name,
434        )))
435    }
436
437    /// Verify that `pair` — resolved from `#[has_many(pair = <field>)]` or
438    /// `#[has_one(pair = <field>)]` via `field_name_to_id` on the target —
439    /// names a `BelongsTo` field on `target` that points back at `src`.
440    fn validate_pair(
441        &self,
442        src: ModelId,
443        target: ModelId,
444        field_name: &str,
445        pair: FieldId,
446    ) -> crate::Result<()> {
447        let src_model = &self.models[&src];
448
449        let target_model = match self.models.get(&target) {
450            Some(target) => target,
451            None => {
452                return Err(crate::Error::invalid_schema(format!(
453                    "field `{}::{}` references a model that was not registered with the schema; \
454                     did you forget to register it with `Db::builder()`?",
455                    src_model.name().upper_camel_case(),
456                    field_name,
457                )));
458            }
459        };
460
461        if pair.model != target {
462            return Err(crate::Error::invalid_schema(format!(
463                "field `{}::{}` specifies a `pair` on a model other than its target `{}`",
464                src_model.name().upper_camel_case(),
465                field_name,
466                target_model.name().upper_camel_case(),
467            )));
468        }
469
470        let paired = &target_model.as_root_unwrap().fields[pair.index];
471        match &paired.ty {
472            FieldTy::BelongsTo(rel) if rel.target == src => Ok(()),
473            _ => Err(crate::Error::invalid_schema(format!(
474                "field `{}::{}` specifies `pair = {}`, but `{}::{}` is not a `BelongsTo` \
475                 targeting `{}`",
476                src_model.name().upper_camel_case(),
477                field_name,
478                paired.name.app_unwrap(),
479                target_model.name().upper_camel_case(),
480                paired.name.app_unwrap(),
481                src_model.name().upper_camel_case(),
482            ))),
483        }
484    }
485}