Skip to main content

jetro_core/
expr.rs

1//! Typed expression values.
2//!
3//! [`Expr<T>`] wraps a Jetro expression string together with a phantom
4//! output type `T`.  The expression is parse-checked at construction
5//! time so invalid syntax fails fast rather than at first evaluation.
6//!
7//! The phantom type carries the caller's intent about the result
8//! shape.  It is not enforced at compile time (Jetro has no static
9//! schema today), but it drives the typed accessors on [`Bucket`] and
10//! similar helpers so that callers never have to hand-write
11//! `.as_i64().unwrap()` chains.
12//!
13//! # Composition
14//!
15//! `Expr<T>` supports a pipeline operator that mirrors Jetro's own `|`
16//! syntax.  `a | b` produces a new expression whose source is
17//! `"(a) | (b)"`, letting callers build complex queries from reusable
18//! fragments.
19
20use std::marker::PhantomData;
21use std::ops::BitOr;
22
23use serde::de::DeserializeOwned;
24use serde_json::Value;
25
26use crate::parser;
27use crate::vm::VM;
28use crate::Error;
29
30/// A parse-checked Jetro expression with a phantom output type.
31///
32/// The phantom `T` captures the caller's intent about the result
33/// shape and is used by typed accessors (e.g. `Bucket::get_as`) to
34/// drive `serde` deserialisation.  It is *not* statically verified —
35/// constructing an `Expr<u64>` from an expression that yields a
36/// string is possible; the mismatch surfaces at evaluation time as a
37/// deserialisation error.
38#[derive(Debug, Clone)]
39pub struct Expr<T> {
40    src:     String,
41    _marker: PhantomData<fn() -> T>,
42}
43
44impl<T> Expr<T> {
45    /// Parse and wrap `src`.  Returns [`Error::Parse`] if the
46    /// expression is not syntactically valid.
47    pub fn new<S: Into<String>>(src: S) -> Result<Self, Error> {
48        let src = src.into();
49        parser::parse(&src)?;
50        Ok(Self { src, _marker: PhantomData })
51    }
52
53    /// Raw source text — useful for storing in an [`ExprBucket`] or
54    /// for debugging.
55    pub fn as_str(&self) -> &str { &self.src }
56
57    /// Discard the phantom output type.  Rarely needed; `cast::<U>`
58    /// is usually what callers want.
59    pub fn into_string(self) -> String { self.src }
60
61    /// Re-tag the expression with a different phantom output type.
62    /// No reparse; cheap.
63    pub fn cast<U>(self) -> Expr<U> {
64        Expr { src: self.src, _marker: PhantomData }
65    }
66}
67
68impl<T: DeserializeOwned> Expr<T> {
69    /// Evaluate `self` against `doc`, returning a typed value.
70    ///
71    /// Goes through a fresh VM — for repeated evaluations prefer
72    /// [`Expr::eval_with`] so caches accumulate.
73    pub fn eval(&self, doc: &Value) -> Result<T, Error> {
74        let raw = VM::new().run_str(&self.src, doc)?;
75        serde_json::from_value(raw).map_err(|e| Error::Eval(crate::EvalError(e.to_string())))
76    }
77
78    /// Evaluate with a caller-supplied VM so its compile and
79    /// resolution caches are shared across calls.
80    pub fn eval_with(&self, vm: &mut VM, doc: &Value) -> Result<T, Error> {
81        let raw = vm.run_str(&self.src, doc)?;
82        serde_json::from_value(raw).map_err(|e| Error::Eval(crate::EvalError(e.to_string())))
83    }
84}
85
86impl Expr<Value> {
87    /// Evaluate and return the raw [`Value`] without deserialisation.
88    pub fn eval_raw(&self, doc: &Value) -> Result<Value, Error> {
89        Ok(VM::new().run_str(&self.src, doc)?)
90    }
91}
92
93/// `a | b` → expression source `"(a) | (b)"`.  The resulting type is
94/// `Expr<U>` since the right-hand side determines the output shape.
95impl<T, U> BitOr<Expr<U>> for Expr<T> {
96    type Output = Expr<U>;
97    fn bitor(self, rhs: Expr<U>) -> Expr<U> {
98        Expr {
99            src:     format!("({}) | ({})", self.src, rhs.src),
100            _marker: PhantomData,
101        }
102    }
103}
104
105impl<T> AsRef<str> for Expr<T> {
106    fn as_ref(&self) -> &str { &self.src }
107}
108
109impl<T> std::fmt::Display for Expr<T> {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        f.write_str(&self.src)
112    }
113}
114
115// ── Tests ─────────────────────────────────────────────────────────────────────
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use serde_json::json;
121
122    #[test]
123    fn parse_ok() {
124        let e: Expr<i64> = Expr::new("$.x.len()").unwrap();
125        assert_eq!(e.as_str(), "$.x.len()");
126    }
127
128    #[test]
129    fn parse_err() {
130        let e: Result<Expr<i64>, _> = Expr::new("$$$ not valid");
131        assert!(e.is_err());
132    }
133
134    #[test]
135    fn eval_typed() {
136        let e: Expr<i64> = Expr::new("$.xs.len()").unwrap();
137        let n = e.eval(&json!({"xs": [1, 2, 3]})).unwrap();
138        assert_eq!(n, 3);
139    }
140
141    #[test]
142    fn eval_vec() {
143        let e: Expr<Vec<String>> = Expr::new("$.users.map(name)").unwrap();
144        let names = e.eval(&json!({
145            "users": [{"name":"a"}, {"name":"b"}]
146        })).unwrap();
147        assert_eq!(names, vec!["a", "b"]);
148    }
149
150    #[test]
151    fn pipe_compose() {
152        let a: Expr<Value>    = Expr::new("$.books").unwrap();
153        let b: Expr<Vec<Value>> = Expr::new("@.filter(price > 10)").unwrap();
154        let piped = a | b;
155        assert_eq!(piped.as_str(), "($.books) | (@.filter(price > 10))");
156    }
157
158    #[test]
159    fn cast_keeps_src() {
160        let e: Expr<i64> = Expr::new("$.n").unwrap();
161        let s = e.clone().cast::<String>();
162        assert_eq!(s.as_str(), e.as_str());
163    }
164}