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}