Skip to main content

jpx_core/
lib.rs

1//! jpx-core: A complete JMESPath implementation using `serde_json::Value`.
2//!
3//! This crate provides a JMESPath parser, interpreter, and runtime that works
4//! directly with `serde_json::Value`, eliminating the need for a separate
5//! `Variable` type and the conversion overhead that comes with it.
6//!
7//! # Quick Start
8//!
9//! ```
10//! use jpx_core::compile;
11//! use serde_json::json;
12//!
13//! let expr = compile("foo.bar").unwrap();
14//! let data = json!({"foo": {"bar": true}});
15//! let result = expr.search(&data).unwrap();
16//! assert_eq!(result, json!(true));
17//! ```
18
19pub mod ast;
20#[cfg(feature = "extensions")]
21pub mod extensions;
22pub mod functions;
23pub mod query_library;
24pub mod registry;
25pub mod value_ext;
26
27pub use crate::error::{ErrorReason, JmespathError, RuntimeError};
28pub use crate::interpreter::SearchResult;
29pub use crate::parser::{ParseResult, parse};
30pub use crate::registry::{Category, Feature, FunctionInfo, FunctionRegistry};
31pub use crate::runtime::{Runtime, RuntimeBuilder};
32pub use crate::value_ext::{JmespathType, ValueExt};
33
34mod error;
35pub mod interpreter;
36mod lexer;
37mod parser;
38mod runtime;
39
40#[cfg(feature = "let-expr")]
41use std::collections::HashMap;
42use std::fmt;
43use std::sync::LazyLock;
44
45use serde_json::Value;
46
47use crate::ast::Ast;
48use crate::interpreter::interpret;
49
50/// The default runtime with all 26 built-in JMESPath functions registered.
51pub static DEFAULT_RUNTIME: LazyLock<Runtime> = LazyLock::new(|| {
52    let mut runtime = Runtime::new();
53    runtime.register_builtin_functions();
54    runtime
55});
56
57/// Compiles a JMESPath expression using the default Runtime.
58#[inline]
59pub fn compile(expression: &str) -> Result<Expression<'static>, JmespathError> {
60    DEFAULT_RUNTIME.compile(expression)
61}
62
63/// A compiled JMESPath expression.
64///
65/// The compiled expression can be used multiple times without incurring
66/// the cost of re-parsing the expression each time.
67#[derive(Clone)]
68pub struct Expression<'a> {
69    ast: Ast,
70    expression: String,
71    runtime: &'a Runtime,
72}
73
74impl<'a> Expression<'a> {
75    /// Creates a new JMESPath expression.
76    #[inline]
77    pub fn new<S>(expression: S, ast: Ast, runtime: &'a Runtime) -> Expression<'a>
78    where
79        S: Into<String>,
80    {
81        Expression {
82            expression: expression.into(),
83            ast,
84            runtime,
85        }
86    }
87
88    /// Searches data with the compiled expression.
89    ///
90    /// Takes a `&Value` and returns a `Value` directly -- no conversion needed.
91    pub fn search(&self, data: &Value) -> SearchResult {
92        let mut ctx = Context::new(&self.expression, self.runtime);
93        let result = interpret(data, &self.ast, &mut ctx)?;
94        // Strip expref sentinels from top-level results
95        Ok(strip_expref_sentinels(result))
96    }
97
98    /// Returns the JMESPath expression string.
99    pub fn as_str(&self) -> &str {
100        &self.expression
101    }
102
103    /// Returns the AST of the parsed JMESPath expression.
104    pub fn as_ast(&self) -> &Ast {
105        &self.ast
106    }
107}
108
109impl<'a> fmt::Display for Expression<'a> {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(f, "{}", self.as_str())
112    }
113}
114
115impl<'a> fmt::Debug for Expression<'a> {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        fmt::Display::fmt(self, f)
118    }
119}
120
121impl<'a> PartialEq for Expression<'a> {
122    fn eq(&self, other: &Expression<'_>) -> bool {
123        self.as_str() == other.as_str()
124    }
125}
126
127/// Context object used during expression evaluation.
128///
129/// The Context struct carries state needed by the interpreter and functions,
130/// including the expression string (for error messages), runtime (for function
131/// lookup), current AST offset, and expref side-channel table.
132pub struct Context<'a> {
133    /// Expression string being interpreted.
134    pub expression: &'a str,
135    /// JMESPath runtime used for function lookup.
136    pub runtime: &'a Runtime,
137    /// Current AST offset (for error reporting).
138    pub offset: usize,
139    /// Side-channel table for expression references.
140    /// Exprefs are stored here and referenced by index in sentinel values.
141    pub(crate) expref_table: Vec<Ast>,
142    /// Variable scopes for let expressions (JEP-18).
143    #[cfg(feature = "let-expr")]
144    scopes: Vec<HashMap<String, Value>>,
145}
146
147impl<'a> Context<'a> {
148    /// Creates a new context.
149    #[inline]
150    pub fn new(expression: &'a str, runtime: &'a Runtime) -> Context<'a> {
151        Context {
152            expression,
153            runtime,
154            offset: 0,
155            expref_table: Vec::new(),
156            #[cfg(feature = "let-expr")]
157            scopes: Vec::new(),
158        }
159    }
160
161    /// Stores an expref AST and returns its index in the table.
162    pub(crate) fn store_expref(&mut self, ast: Ast) -> usize {
163        let id = self.expref_table.len();
164        self.expref_table.push(ast);
165        id
166    }
167
168    /// Retrieves an expref AST by index.
169    pub fn get_expref(&self, id: usize) -> Option<&Ast> {
170        self.expref_table.get(id)
171    }
172
173    /// Push a new scope onto the scope stack.
174    #[cfg(feature = "let-expr")]
175    #[inline]
176    pub fn push_scope(&mut self, bindings: HashMap<String, Value>) {
177        self.scopes.push(bindings);
178    }
179
180    /// Pop the innermost scope from the scope stack.
181    #[cfg(feature = "let-expr")]
182    #[inline]
183    pub fn pop_scope(&mut self) {
184        self.scopes.pop();
185    }
186
187    /// Look up a variable in the scope stack.
188    #[cfg(feature = "let-expr")]
189    #[inline]
190    pub fn get_variable(&self, name: &str) -> Option<Value> {
191        for scope in self.scopes.iter().rev() {
192            if let Some(value) = scope.get(name) {
193                return Some(value.clone());
194            }
195        }
196        None
197    }
198}
199
200/// Creates an expref sentinel value from a table index.
201pub(crate) fn make_expref_sentinel(id: usize) -> Value {
202    let mut map = serde_json::Map::new();
203    map.insert(
204        "__jpx_expref__".to_string(),
205        Value::Number(serde_json::Number::from(id)),
206    );
207    Value::Object(map)
208}
209
210/// Extracts the expref ID from a sentinel value.
211pub fn get_expref_id(value: &Value) -> Option<usize> {
212    value
213        .as_object()
214        .and_then(|m| m.get("__jpx_expref__"))
215        .and_then(|v| v.as_u64())
216        .map(|v| v as usize)
217}
218
219/// Strips expref sentinels from a value (recursive for arrays/objects).
220fn strip_expref_sentinels(value: Value) -> Value {
221    match value {
222        Value::Object(map) if map.contains_key("__jpx_expref__") => Value::Null,
223        Value::Array(arr) => Value::Array(arr.into_iter().map(strip_expref_sentinels).collect()),
224        Value::Object(map) => Value::Object(
225            map.into_iter()
226                .map(|(k, v)| (k, strip_expref_sentinels(v)))
227                .collect(),
228        ),
229        other => other,
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use serde_json::json;
237
238    #[test]
239    fn formats_expression_as_string_or_debug() {
240        let expr = compile("foo | baz").unwrap();
241        assert_eq!("foo | baz/foo | baz", format!("{expr}/{expr:?}"));
242    }
243
244    #[test]
245    fn implements_partial_eq() {
246        let a = compile("@").unwrap();
247        let b = compile("@").unwrap();
248        assert!(a == b);
249    }
250
251    #[test]
252    fn can_evaluate_jmespath_expression() {
253        let expr = compile("foo.bar").unwrap();
254        let data = json!({"foo": {"bar": true}});
255        assert_eq!(json!(true), expr.search(&data).unwrap());
256    }
257
258    #[test]
259    fn can_get_expression_ast() {
260        let expr = compile("foo").unwrap();
261        assert_eq!(
262            &Ast::Field {
263                offset: 0,
264                name: "foo".to_string(),
265            },
266            expr.as_ast()
267        );
268    }
269
270    #[test]
271    fn expression_clone() {
272        let expr = compile("foo").unwrap();
273        let _ = expr.clone();
274    }
275
276    #[test]
277    fn test_invalid_number() {
278        let _ = compile("6455555524");
279    }
280}
281
282#[cfg(all(test, feature = "let-expr"))]
283mod let_tests {
284    use super::*;
285    use serde_json::json;
286
287    #[test]
288    fn test_simple_let_expression() {
289        let expr = compile("let $x = `1` in $x").unwrap();
290        let data = json!({});
291        let result = expr.search(&data).unwrap();
292        assert_eq!(result, json!(1));
293    }
294
295    #[test]
296    fn test_let_with_data_reference() {
297        let expr = compile("let $name = name in $name").unwrap();
298        let data = json!({"name": "Alice"});
299        let result = expr.search(&data).unwrap();
300        assert_eq!(result, json!("Alice"));
301    }
302
303    #[test]
304    fn test_let_multiple_bindings() {
305        let expr = compile("let $a = `1`, $b = `2` in [$a, $b]").unwrap();
306        let data = json!({});
307        let result = expr.search(&data).unwrap();
308        assert_eq!(result, json!([1, 2]));
309    }
310
311    #[test]
312    fn test_let_with_expression_body() {
313        let expr = compile("let $items = items in $items[0].name").unwrap();
314        let data = json!({"items": [{"name": "first"}, {"name": "second"}]});
315        let result = expr.search(&data).unwrap();
316        assert_eq!(result, json!("first"));
317    }
318
319    #[test]
320    fn test_nested_let() {
321        let expr = compile("let $x = `1` in let $y = `2` in [$x, $y]").unwrap();
322        let data = json!({});
323        let result = expr.search(&data).unwrap();
324        assert_eq!(result, json!([1, 2]));
325    }
326
327    #[test]
328    fn test_let_variable_shadowing() {
329        let expr = compile("let $x = `1` in let $x = `2` in $x").unwrap();
330        let data = json!({});
331        let result = expr.search(&data).unwrap();
332        assert_eq!(result, json!(2));
333    }
334
335    #[test]
336    fn test_undefined_variable_error() {
337        let expr = compile("$undefined").unwrap();
338        let data = json!({});
339        let result = expr.search(&data);
340        assert!(result.is_err());
341    }
342
343    #[test]
344    fn test_let_in_projection() {
345        let expr = compile("let $threshold = `50` in numbers[? @ > $threshold]").unwrap();
346        let data = json!({"numbers": [10, 30, 50, 70, 90]});
347        let result = expr.search(&data).unwrap();
348        assert_eq!(result, json!([70, 90]));
349    }
350
351    #[test]
352    fn test_let_variable_used_multiple_times() {
353        let expr = compile("let $foo = foo.bar in [$foo, $foo]").unwrap();
354        let data = json!({"foo": {"bar": "baz"}});
355        let result = expr.search(&data).unwrap();
356        assert_eq!(result, json!(["baz", "baz"]));
357    }
358
359    #[test]
360    fn test_let_shadowing_in_projection() {
361        let expr = compile("let $a = a in b[*].[a, $a, let $a = 'shadow' in $a]").unwrap();
362        let data = json!({"a": "topval", "b": [{"a": "inner1"}, {"a": "inner2"}]});
363        let result = expr.search(&data).unwrap();
364        assert_eq!(
365            result,
366            json!([
367                ["inner1", "topval", "shadow"],
368                ["inner2", "topval", "shadow"]
369            ])
370        );
371    }
372
373    #[test]
374    fn test_let_bindings_evaluated_in_outer_scope() {
375        let expr = compile("let $a = 'top-a' in let $a = 'in-a', $b = $a in $b").unwrap();
376        let data = json!({});
377        let result = expr.search(&data).unwrap();
378        assert_eq!(result, json!("top-a"));
379    }
380
381    #[test]
382    fn test_let_projection_stopping() {
383        let expr = compile("let $foo = foo[*] in $foo[0]").unwrap();
384        let data = json!({"foo": [[0, 1], [2, 3], [4, 5]]});
385        let result = expr.search(&data).unwrap();
386        assert_eq!(result, json!([0, 1]));
387    }
388
389    #[test]
390    fn test_let_shadow_and_restore() {
391        let expr = compile("let $x = 'outer' in [let $x = 'inner' in $x, $x]").unwrap();
392        let data = json!({});
393        let result = expr.search(&data).unwrap();
394        assert_eq!(result, json!(["inner", "outer"]));
395    }
396
397    #[test]
398    fn test_let_with_functions() {
399        let expr = compile("let $arr = numbers in length($arr)").unwrap();
400        let data = json!({"numbers": [1, 2, 3, 4, 5]});
401        let result = expr.search(&data).unwrap();
402        assert_eq!(result, json!(5));
403    }
404
405    #[test]
406    fn test_let_deeply_nested_scopes() {
407        let expr = compile("let $a = `1` in let $b = `2` in let $c = `3` in [$a, $b, $c]").unwrap();
408        let data = json!({});
409        let result = expr.search(&data).unwrap();
410        assert_eq!(result, json!([1, 2, 3]));
411    }
412
413    #[test]
414    fn test_let_with_flatten() {
415        let expr = compile("let $data = nested in $data[].items[]").unwrap();
416        let data = json!({"nested": [{"items": [1, 2]}, {"items": [3, 4]}]});
417        let result = expr.search(&data).unwrap();
418        assert_eq!(result, json!([1, 2, 3, 4]));
419    }
420
421    #[test]
422    fn test_let_with_slice() {
423        let expr = compile("let $arr = numbers in $arr[1:3]").unwrap();
424        let data = json!({"numbers": [0, 1, 2, 3, 4]});
425        let result = expr.search(&data).unwrap();
426        assert_eq!(result, json!([1, 2]));
427    }
428
429    #[test]
430    fn test_let_with_or_expression() {
431        let expr = compile("let $default = 'N/A' in name || $default").unwrap();
432        let data = json!({});
433        let result = expr.search(&data).unwrap();
434        assert_eq!(result, json!("N/A"));
435    }
436
437    #[test]
438    fn test_let_with_not_expression() {
439        let expr = compile("let $val = `false` in !$val").unwrap();
440        let data = json!({});
441        let result = expr.search(&data).unwrap();
442        assert_eq!(result, json!(true));
443    }
444
445    #[test]
446    fn test_let_binding_to_literal() {
447        let expr = compile(
448            "let $str = 'hello', $num = `42`, $bool = `true`, $null = `null` in [$str, $num, $bool, $null]",
449        )
450        .unwrap();
451        let data = json!({});
452        let result = expr.search(&data).unwrap();
453        assert_eq!(result, json!(["hello", 42, true, null]));
454    }
455}