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 the `ModelId`, panicking if this root is a `Variant` root.
27 pub fn as_model_unwrap(&self) -> ModelId {
28 match self {
29 PathRoot::Model(id) => *id,
30 PathRoot::Variant { .. } => panic!("expected Model root, got Variant root"),
31 }
32 }
33
34 /// Returns the `ModelId` if this is a `Model` root, or `None` for a
35 /// `Variant` root.
36 pub fn as_model(&self) -> Option<ModelId> {
37 match self {
38 PathRoot::Model(id) => Some(*id),
39 PathRoot::Variant { .. } => None,
40 }
41 }
42}
43
44/// A rooted field traversal path through the application schema.
45///
46/// A `Path` starts at a [`PathRoot`] (a model or an enum variant) and
47/// navigates through fields via a [`Projection`]. It is used by the query
48/// engine to identify which field or nested field is being referenced.
49///
50/// # Examples
51///
52/// ```ignore
53/// use toasty_core::stmt::Path;
54/// use toasty_core::schema::app::ModelId;
55///
56/// // Path pointing to the root of model 0
57/// let p = Path::model(ModelId::from_index(0));
58/// assert!(p.is_empty()); // no field steps
59/// ```
60#[derive(Debug, Clone, PartialEq)]
61pub struct Path {
62 /// Where the path originates from.
63 pub root: PathRoot,
64
65 /// Traversal through the fields.
66 pub projection: Projection,
67}
68
69impl Path {
70 /// Creates a path rooted at a model with an identity projection (no field steps).
71 pub fn model(root: impl Into<ModelId>) -> Self {
72 Self {
73 root: PathRoot::Model(root.into()),
74 projection: Projection::identity(),
75 }
76 }
77
78 /// Creates a path rooted at a model that navigates to a single field by index.
79 pub fn field(root: impl Into<ModelId>, field: usize) -> Self {
80 Self {
81 root: PathRoot::Model(root.into()),
82 projection: Projection::single(field),
83 }
84 }
85
86 /// Creates a path rooted at a model with a single field step (const-compatible).
87 pub const fn from_index(root: ModelId, index: usize) -> Self {
88 Self {
89 root: PathRoot::Model(root),
90 projection: Projection::from_index(index),
91 }
92 }
93
94 /// Creates a path rooted at a specific enum variant.
95 ///
96 /// `parent` is the path that navigates to the enum field. Subsequent
97 /// projection steps (appended via [`chain`][Path::chain]) index into the
98 /// variant's fields using 0-based local indices.
99 pub fn from_variant(parent: Path, variant_id: VariantId) -> Self {
100 Self {
101 root: PathRoot::Variant {
102 parent: Box::new(parent),
103 variant_id,
104 },
105 projection: Projection::identity(),
106 }
107 }
108
109 /// Returns `true` if the path has no field steps (identity projection).
110 pub fn is_empty(&self) -> bool {
111 self.projection.is_empty()
112 }
113
114 /// Returns the number of field steps in the path.
115 pub fn len(&self) -> usize {
116 self.projection.len()
117 }
118
119 /// Appends all field steps from `other` onto this path's projection.
120 pub fn chain(&mut self, other: &Self) {
121 for field in &other.projection[..] {
122 self.projection.push(*field);
123 }
124 }
125
126 /// Converts this path into an [`Expr`] that references the path's field.
127 pub fn into_stmt(self) -> Expr {
128 match self.root {
129 PathRoot::Model(model_id) => match self.projection.as_slice() {
130 [] => Expr::ref_ancestor_model(0),
131 [field, project @ ..] => {
132 let mut ret = Expr::ref_self_field(FieldId {
133 model: model_id,
134 index: *field,
135 });
136
137 if !project.is_empty() {
138 ret = Expr::project(ret, project);
139 }
140
141 ret
142 }
143 },
144 PathRoot::Variant { parent, .. } => {
145 let parent_expr = parent.into_stmt();
146 match self.projection.as_slice() {
147 [] => parent_expr,
148 [local_idx, rest @ ..] => {
149 // Record position 0 is the discriminant; variant fields
150 // start at position 1, so add 1 to the local field index.
151 let mut ret = Expr::project(parent_expr, Projection::single(local_idx + 1));
152
153 if !rest.is_empty() {
154 ret = Expr::project(ret, rest);
155 }
156
157 ret
158 }
159 }
160 }
161 }
162 }
163}