toasty_core/stmt/path.rs
1use super::{Expr, Projection};
2use crate::schema::app::{FieldId, ModelId, VariantId};
3
4/// The root of a path traversal.
5///
6/// A path can originate from a top-level model or from a specific variant of
7/// an embedded enum field.
8#[derive(Debug, Clone, PartialEq)]
9pub enum PathRoot {
10 /// The path originates from a top-level model.
11 Model(ModelId),
12
13 /// The path originates from a specific variant of an embedded enum.
14 ///
15 /// `parent` navigates to the enum field; subsequent projection steps index
16 /// into that variant's fields using 0-based local indices.
17 Variant {
18 /// Path that navigates to the enum field containing this variant.
19 parent: Box<Path>,
20 /// Identifies which variant of the enum this path targets.
21 variant_id: VariantId,
22 },
23}
24
25impl PathRoot {
26 /// Returns `true` if this root is an embedded enum variant.
27 pub fn is_variant(&self) -> bool {
28 matches!(self, Self::Variant { .. })
29 }
30
31 /// Returns the `ModelId`, panicking if this root is a `Variant` root.
32 pub fn as_model_unwrap(&self) -> ModelId {
33 match self {
34 PathRoot::Model(id) => *id,
35 PathRoot::Variant { .. } => panic!("expected Model root, got Variant root"),
36 }
37 }
38
39 /// Returns the `ModelId` if this is a `Model` root, or `None` for a
40 /// `Variant` root.
41 pub fn as_model(&self) -> Option<ModelId> {
42 match self {
43 PathRoot::Model(id) => Some(*id),
44 PathRoot::Variant { .. } => None,
45 }
46 }
47}
48
49/// A rooted field traversal path through the application schema.
50///
51/// A `Path` starts at a [`PathRoot`] (a model or an enum variant) and
52/// navigates through fields via a [`Projection`]. It is used by the query
53/// engine to identify which field or nested field is being referenced.
54///
55/// # Examples
56///
57/// ```ignore
58/// use toasty_core::stmt::Path;
59/// use toasty_core::schema::app::ModelId;
60///
61/// // Path pointing to the root of model 0
62/// let p = Path::model(ModelId::from_index(0));
63/// assert!(p.projection.is_empty()); // no field steps
64/// ```
65#[derive(Debug, Clone, PartialEq)]
66pub struct Path {
67 /// Where the path originates from.
68 pub root: PathRoot,
69
70 /// Traversal through the fields.
71 pub projection: Projection,
72}
73
74impl Path {
75 /// Creates a path rooted at a model with an identity projection (no field steps).
76 pub fn model(root: impl Into<ModelId>) -> Self {
77 Self {
78 root: PathRoot::Model(root.into()),
79 projection: Projection::identity(),
80 }
81 }
82
83 /// Creates a path rooted at a model that navigates to a single field by index.
84 pub fn field(root: impl Into<ModelId>, field: usize) -> Self {
85 Self {
86 root: PathRoot::Model(root.into()),
87 projection: Projection::single(field),
88 }
89 }
90
91 /// Creates a path rooted at a model with a single field step (const-compatible).
92 pub const fn from_index(root: ModelId, index: usize) -> Self {
93 Self {
94 root: PathRoot::Model(root),
95 projection: Projection::from_index(index),
96 }
97 }
98
99 /// Creates a path rooted at a specific enum variant.
100 ///
101 /// `parent` is the path that navigates to the enum field. Subsequent
102 /// projection steps (appended via [`chain`][Path::chain]) index into the
103 /// variant's fields using 0-based local indices. [`into_stmt`] renders
104 /// the root as a variant selection ([`Expr::variant`]) over the parent's
105 /// expression.
106 ///
107 /// [`into_stmt`]: Path::into_stmt
108 pub fn from_variant(parent: Path, variant_id: VariantId) -> Self {
109 Self {
110 root: PathRoot::Variant {
111 parent: Box::new(parent),
112 variant_id,
113 },
114 projection: Projection::identity(),
115 }
116 }
117
118 /// Appends `other`'s traversal onto this path.
119 ///
120 /// `other`'s root model is dropped: its steps continue from wherever
121 /// `self` ends. A variant selection in `other` is preserved — the result
122 /// selects the same variant at the same point of the traversal — so a
123 /// variant-rooted path can be chained onto a path reaching its enum from
124 /// another model.
125 pub fn chain(&mut self, other: &Self) {
126 if let PathRoot::Variant { parent, variant_id } = &other.root {
127 self.chain(parent);
128 *self = Self::from_variant(self.clone(), *variant_id);
129 }
130
131 for field in &other.projection[..] {
132 self.projection.push(*field);
133 }
134 }
135
136 /// Converts this path into an [`Expr`] that references the path's field.
137 ///
138 /// A variant root becomes an [`ExprVariant`](super::ExprVariant) over the
139 /// parent path's expression, projected by the variant-local steps. The
140 /// expression carries no variant check; the engine's statement
141 /// normalization adds one per selection to the predicate built over it.
142 pub fn into_stmt(self) -> Expr {
143 self.into_stmt_with_nesting(0)
144 }
145
146 /// Converts this path into an [`Expr`] at the specified query nesting level.
147 ///
148 /// A nesting level of `0` references the current query; `1` references its
149 /// parent. Projections and variant selections preserve this nesting level.
150 pub fn into_stmt_with_nesting(self, nesting: usize) -> Expr {
151 match self.root {
152 PathRoot::Model(model_id) => match self.projection.as_slice() {
153 [] => Expr::ref_ancestor_model(nesting),
154 [field, project @ ..] => {
155 let mut ret = Expr::ref_field(
156 nesting,
157 FieldId {
158 model: model_id,
159 index: *field,
160 },
161 );
162
163 if !project.is_empty() {
164 ret = Expr::project(ret, project);
165 }
166
167 ret
168 }
169 },
170 PathRoot::Variant { parent, variant_id } => {
171 // The selection stands for the variant's payload, so the
172 // steps index the variant's fields by their local positions.
173 let selection = Expr::variant(parent.into_stmt_with_nesting(nesting), variant_id);
174
175 match self.projection.as_slice() {
176 [] => selection,
177 steps => Expr::project(selection, steps),
178 }
179 }
180 }
181 }
182}