Skip to main content

toasty_core/schema/app/relation/
via.rs

1use crate::{
2    schema::app::{Cardinality, Model, ModelId, 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///
26/// A common model-terminal use is many-to-many traversal through a join model.
27/// If `User` has many `Membership` records and each membership belongs to a
28/// `Group`, `#[has_many(via = memberships.group)]` exposes the distinct groups
29/// reachable from a user. The join model owns the foreign keys and any fields
30/// that describe the connection.
31#[derive(Debug, Clone)]
32pub struct Via {
33    /// The [`ModelId`] of the model the relation chain reaches. For a relation
34    /// terminal this is the associated (target) model; for a scalar terminal
35    /// it is the model that owns the projected terminal field.
36    pub target: ModelId,
37
38    /// The expression type this field evaluates to from the application's
39    /// perspective.
40    pub expr_ty: stmt::Type,
41
42    /// Whether this relation is one-to-many or one-to-one.
43    pub cardinality: Cardinality,
44
45    /// The resolved field path, rooted at the model that declares the via
46    /// relation. When [`terminal`](Self::terminal) is `Some`, the last element
47    /// of the path is the scalar terminal field (on [`target`](Self::target))
48    /// and the preceding elements form the relation chain.
49    pub path: stmt::Path,
50
51    /// For a scalar-terminal via, the index of the projected terminal field on
52    /// [`target`](Self::target). `None` when the via reaches a model (the
53    /// terminal segment is itself a relation).
54    pub terminal: Option<usize>,
55}
56
57impl Via {
58    /// Create a `Via` from its fully resolved field path.
59    pub fn new(
60        target: ModelId,
61        expr_ty: stmt::Type,
62        cardinality: Cardinality,
63        path: stmt::Path,
64        terminal: Option<usize>,
65    ) -> Self {
66        Self {
67            target,
68            expr_ty,
69            cardinality,
70            path,
71            terminal,
72        }
73    }
74
75    /// Returns `true` when the via projects a scalar terminal field rather than
76    /// reaching a model.
77    pub fn is_scalar(&self) -> bool {
78        self.terminal.is_some()
79    }
80
81    /// Returns `true` when this is a one-to-many relation.
82    pub fn is_many(&self) -> bool {
83        self.cardinality.is_many()
84    }
85
86    /// Returns `true` when this is a one-to-one relation.
87    pub fn is_one(&self) -> bool {
88        self.cardinality.is_one()
89    }
90
91    /// Resolves the target [`Model`] from the given schema.
92    pub(crate) fn target<'a>(&self, schema: &'a Schema) -> &'a Model {
93        schema.model(self.target)
94    }
95}