Skip to main content

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}