Skip to main content

helix_ast/
projection.rs

1use serde::{Deserialize, Serialize};
2
3use crate::expr::Expr;
4/// A property projection with optional rename.
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub struct PropertyProjection {
7    /// Source property.
8    pub source: String,
9    /// Output name.
10    pub alias: String,
11}
12
13impl PropertyProjection {
14    /// Project without rename.
15    pub fn new(name: impl Into<String>) -> Self {
16        let name = name.into();
17        Self {
18            source: name.clone(),
19            alias: name,
20        }
21    }
22
23    /// Project with rename.
24    pub fn renamed(source: impl Into<String>, alias: impl Into<String>) -> Self {
25        Self {
26            source: source.into(),
27            alias: alias.into(),
28        }
29    }
30}
31
32/// Expression-backed projection.
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct ExprProjection {
35    /// Output name.
36    pub alias: String,
37    /// Expression.
38    pub expr: Expr,
39}
40
41impl ExprProjection {
42    /// Create an expression projection.
43    pub fn new(alias: impl Into<String>, expr: Expr) -> Self {
44        Self {
45            alias: alias.into(),
46            expr,
47        }
48    }
49}
50
51/// Projection entry.
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum Projection {
55    /// Property projection.
56    Property(PropertyProjection),
57    /// Expression projection.
58    Expr(ExprProjection),
59}
60
61impl Projection {
62    /// Project a property.
63    pub fn property(source: impl Into<String>, alias: impl Into<String>) -> Self {
64        Self::Property(PropertyProjection::renamed(source, alias))
65    }
66
67    /// Project from the source endpoint of an edge.
68    pub fn from_endpoint(source: impl Into<String>, alias: impl Into<String>) -> Self {
69        Self::property(format!("$from.{}", source.into()), alias)
70    }
71
72    /// Project from the target endpoint of an edge.
73    pub fn to_endpoint(source: impl Into<String>, alias: impl Into<String>) -> Self {
74        Self::property(format!("$to.{}", source.into()), alias)
75    }
76
77    /// Project an expression.
78    pub fn expr(alias: impl Into<String>, expr: Expr) -> Self {
79        Self::Expr(ExprProjection::new(alias, expr))
80    }
81}
82
83impl From<PropertyProjection> for Projection {
84    fn from(value: PropertyProjection) -> Self {
85        Self::Property(value)
86    }
87}
88
89impl From<ExprProjection> for Projection {
90    fn from(value: ExprProjection) -> Self {
91        Self::Expr(value)
92    }
93}
94
95/// Target for row-binding projections.
96///
97/// ```
98/// use helix_ast::projection::BindingTarget;
99///
100/// assert_eq!(sonic_rs::to_string(&BindingTarget::current()).unwrap(), r#""current""#);
101/// assert_eq!(
102///     sonic_rs::to_string(&BindingTarget::binding("service")).unwrap(),
103///     r#"{"binding":"service"}"#
104/// );
105/// ```
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "snake_case")]
108pub enum BindingTarget {
109    /// Current traverser element.
110    Current,
111    /// Named row binding.
112    Binding(String),
113}
114
115impl BindingTarget {
116    /// Current traverser.
117    pub fn current() -> Self {
118        Self::Current
119    }
120
121    /// Named row binding.
122    pub fn binding(name: impl Into<String>) -> Self {
123        Self::Binding(non_empty_string(name, "binding name"))
124    }
125}
126
127/// Reference used by binding projections.
128///
129/// ```
130/// use helix_ast::projection::{BindingTarget, BindingValueRef};
131///
132/// let value_ref = BindingValueRef::new(BindingTarget::binding("service"), "$id");
133/// assert_eq!(
134///     sonic_rs::to_string(&value_ref).unwrap(),
135///     r#"{"target":{"binding":"service"},"source":"$id"}"#
136/// );
137/// ```
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139pub struct BindingValueRef {
140    /// Target element.
141    pub target: BindingTarget,
142    /// Property or virtual field.
143    pub source: String,
144}
145
146impl BindingValueRef {
147    /// Create a reference.
148    pub fn new(target: BindingTarget, source: impl Into<String>) -> Self {
149        Self {
150            target,
151            source: non_empty_string(source, "binding projection source"),
152        }
153    }
154
155    /// Reference current traverser.
156    pub fn current(source: impl Into<String>) -> Self {
157        Self::new(BindingTarget::Current, source)
158    }
159
160    /// Reference named binding.
161    pub fn binding(name: impl Into<String>, source: impl Into<String>) -> Self {
162        Self::new(BindingTarget::binding(name), source)
163    }
164}
165
166/// Projection from row-local bindings.
167///
168/// ```
169/// use helix_ast::projection::{BindingProjection, BindingValueRef};
170///
171/// let projection = BindingProjection::coalesce(
172///     vec![
173///         BindingValueRef::binding("deployment", "$id"),
174///         BindingValueRef::binding("owner", "$id"),
175///     ],
176///     "workload_id",
177/// );
178/// assert_eq!(
179///     sonic_rs::to_string(&projection).unwrap(),
180///     r#"{"coalesce":{"refs":[{"target":{"binding":"deployment"},"source":"$id"},{"target":{"binding":"owner"},"source":"$id"}],"alias":"workload_id"}}"#
181/// );
182/// ```
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "snake_case")]
185pub enum BindingProjection {
186    /// Project a single property.
187    Property {
188        /// Current or named binding.
189        target: BindingTarget,
190        /// Source property.
191        source: String,
192        /// Output name.
193        alias: String,
194    },
195    /// Project first present non-null reference.
196    Coalesce {
197        /// Candidate references.
198        refs: Vec<BindingValueRef>,
199        /// Output name.
200        alias: String,
201    },
202}
203
204impl BindingProjection {
205    /// Project a property.
206    pub fn property(
207        target: BindingTarget,
208        source: impl Into<String>,
209        alias: impl Into<String>,
210    ) -> Self {
211        Self::Property {
212            target,
213            source: non_empty_string(source, "binding projection source"),
214            alias: non_empty_string(alias, "binding projection alias"),
215        }
216    }
217
218    /// Project from current traverser.
219    pub fn current(source: impl Into<String>, alias: impl Into<String>) -> Self {
220        Self::property(BindingTarget::Current, source, alias)
221    }
222
223    /// Project from named binding.
224    pub fn binding(
225        name: impl Into<String>,
226        source: impl Into<String>,
227        alias: impl Into<String>,
228    ) -> Self {
229        Self::property(BindingTarget::binding(name), source, alias)
230    }
231
232    /// Project first present non-null reference.
233    pub fn coalesce(refs: Vec<BindingValueRef>, alias: impl Into<String>) -> Self {
234        assert!(!refs.is_empty(), "binding coalesce refs must not be empty");
235        Self::Coalesce {
236            refs,
237            alias: non_empty_string(alias, "binding projection alias"),
238        }
239    }
240}
241
242pub(crate) fn validate_binding_name(name: impl Into<String>) -> String {
243    non_empty_string(name, "binding name")
244}
245
246pub(crate) fn validate_binding_projections(
247    projections: Vec<BindingProjection>,
248) -> Vec<BindingProjection> {
249    assert!(
250        !projections.is_empty(),
251        "binding projections must not be empty"
252    );
253    projections
254}
255
256fn non_empty_string(value: impl Into<String>, field: &str) -> String {
257    let value = value.into();
258    assert!(!value.is_empty(), "{field} must not be empty");
259    value
260}