Skip to main content

toasty_core/schema/app/relation/
via.rs

1use crate::{
2    schema::app::{Cardinality, Model, ModelId, Name, Schema},
3    stmt,
4};
5
6/// A multi-step relation path.
7///
8/// A `Has` relation declared with `#[has_many(via = a.b)]` or
9/// `#[has_one(via = a.b)]` reaches its
10/// target by following a path of existing relations rather than pairing with a
11/// single `BelongsTo`. The path is resolved at macro-expansion time — the
12/// derive emits a chained call on the model's `Fields` struct
13/// (e.g. `User::fields().comments().article()`) and converts it into a
14/// [`stmt::Path`], so a misspelled or otherwise unresolvable segment is a
15/// Rust compile error rather than a runtime schema validation failure.
16///
17/// The terminal segment of the path is usually another relation (the via
18/// reaches a model). It may also be a **scalar field**, in which case the via
19/// projects that field through the relation path — e.g.
20/// `#[has_many(via = todos.tags.name)] tag_names: Vec<String>` collects the
21/// `name` of every tag reachable through todos. For a scalar terminal,
22/// [`terminal`](Self::terminal) holds the terminal field's index on
23/// [`target`](Self::target) (the model the relation chain reaches), and
24/// [`path`](Self::path) still includes that terminal step as its last element.
25#[derive(Debug, Clone)]
26pub struct Via {
27    /// The [`ModelId`] of the model the relation chain reaches. For a relation
28    /// terminal this is the associated (target) model; for a scalar terminal
29    /// it is the model that owns the projected terminal field.
30    pub target: ModelId,
31
32    /// The expression type this field evaluates to from the application's
33    /// perspective.
34    pub expr_ty: stmt::Type,
35
36    /// Whether this relation is one-to-many or one-to-one.
37    pub cardinality: Cardinality,
38
39    /// The resolved field path, rooted at the model that declares the via
40    /// relation. When [`terminal`](Self::terminal) is `Some`, the last element
41    /// of the path is the scalar terminal field (on [`target`](Self::target))
42    /// and the preceding elements form the relation chain.
43    pub path: stmt::Path,
44
45    /// For a scalar-terminal via, the index of the projected terminal field on
46    /// [`target`](Self::target). `None` when the via reaches a model (the
47    /// terminal segment is itself a relation).
48    pub terminal: Option<usize>,
49}
50
51impl Via {
52    /// Create a `Via` from its fully resolved field path.
53    pub fn new(
54        target: ModelId,
55        expr_ty: stmt::Type,
56        cardinality: Cardinality,
57        path: stmt::Path,
58        terminal: Option<usize>,
59    ) -> Self {
60        Self {
61            target,
62            expr_ty,
63            cardinality,
64            path,
65            terminal,
66        }
67    }
68
69    /// Returns `true` when the via projects a scalar terminal field rather than
70    /// reaching a model.
71    pub fn is_scalar(&self) -> bool {
72        self.terminal.is_some()
73    }
74
75    /// Returns `true` when this is a one-to-many relation.
76    pub fn is_many(&self) -> bool {
77        self.cardinality.is_many()
78    }
79
80    /// Returns `true` when this is a one-to-one relation.
81    pub fn is_one(&self) -> bool {
82        self.cardinality.is_one()
83    }
84
85    /// Returns the singular item name for a one-to-many relation.
86    pub fn singular(&self) -> Option<&Name> {
87        self.cardinality.singular()
88    }
89
90    /// Resolves the target [`Model`] from the given schema.
91    pub fn target<'a>(&self, schema: &'a Schema) -> &'a Model {
92        schema.model(self.target)
93    }
94}