Skip to main content

jpx_engine/
eval.rs

1//! JMESPath expression evaluation.
2//!
3//! This module provides the core evaluation methods for the [`JpxEngine`]:
4//! single evaluation, string-based evaluation, batch evaluation, and validation.
5
6use crate::JpxEngine;
7use crate::error::{EngineError, Result};
8use crate::types::{BatchEvaluateResult, BatchExpressionResult, ValidationResult};
9use serde_json::Value;
10
11impl JpxEngine {
12    /// Evaluates a JMESPath expression against JSON input.
13    ///
14    /// This is the primary method for running JMESPath queries. The expression
15    /// is compiled and executed against the provided JSON value.
16    ///
17    /// # Arguments
18    ///
19    /// * `expression` - A JMESPath expression string
20    /// * `input` - JSON data to query
21    ///
22    /// # Errors
23    ///
24    /// Returns [`EngineError::InvalidExpression`] if the expression has syntax errors,
25    /// or [`EngineError::EvaluationFailed`] if evaluation fails (e.g., calling an
26    /// undefined function in strict mode).
27    ///
28    /// # Example
29    ///
30    /// ```rust
31    /// use jpx_engine::JpxEngine;
32    /// use serde_json::json;
33    ///
34    /// let engine = JpxEngine::new();
35    ///
36    /// // Simple field access
37    /// let result = engine.evaluate("name", &json!({"name": "alice"})).unwrap();
38    /// assert_eq!(result, json!("alice"));
39    ///
40    /// // Array projection with function
41    /// let result = engine.evaluate("users[*].name | sort(@)", &json!({
42    ///     "users": [{"name": "charlie"}, {"name": "alice"}, {"name": "bob"}]
43    /// })).unwrap();
44    /// assert_eq!(result, json!(["alice", "bob", "charlie"]));
45    /// ```
46    pub fn evaluate(&self, expression: &str, input: &Value) -> Result<Value> {
47        let expr = self
48            .runtime
49            .compile(expression)
50            .map_err(|e| EngineError::InvalidExpression(e.to_string()))?;
51
52        if self.strict && crate::explain::has_let_nodes(expr.as_ast()) {
53            return Err(EngineError::InvalidExpression(
54                "Let expressions are not available in strict mode (standard JMESPath only). \
55                 Remove --strict to use let expressions."
56                    .to_string(),
57            ));
58        }
59
60        let result = expr
61            .search(input)
62            .map_err(|e| EngineError::evaluation_failed(e.to_string()))?;
63
64        Ok(result)
65    }
66
67    /// Evaluates a JMESPath expression against a JSON string.
68    ///
69    /// Convenience method that parses the JSON string before evaluation.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`EngineError::InvalidJson`] if the input is not valid JSON,
74    /// or evaluation errors as with [`evaluate`](Self::evaluate).
75    ///
76    /// # Example
77    ///
78    /// ```rust
79    /// use jpx_engine::JpxEngine;
80    /// use serde_json::json;
81    ///
82    /// let engine = JpxEngine::new();
83    /// let result = engine.evaluate_str("length(@)", r#"[1, 2, 3, 4, 5]"#).unwrap();
84    /// assert_eq!(result, json!(5));
85    /// ```
86    pub fn evaluate_str(&self, expression: &str, input: &str) -> Result<Value> {
87        let json: Value =
88            serde_json::from_str(input).map_err(|e| EngineError::InvalidJson(e.to_string()))?;
89        self.evaluate(expression, &json)
90    }
91
92    /// Evaluates multiple expressions against the same input.
93    ///
94    /// Useful for extracting multiple values from a document in one call.
95    /// Each expression is evaluated independently; failures don't affect other expressions.
96    ///
97    /// # Example
98    ///
99    /// ```rust
100    /// use jpx_engine::JpxEngine;
101    /// use serde_json::json;
102    ///
103    /// let engine = JpxEngine::new();
104    /// let input = json!({"name": "alice", "age": 30, "active": true});
105    ///
106    /// let exprs = vec![
107    ///     "name".to_string(),
108    ///     "age".to_string(),
109    ///     "missing".to_string(),  // Returns null, not an error
110    /// ];
111    ///
112    /// let results = engine.batch_evaluate(&exprs, &input);
113    /// assert_eq!(results.results[0].result, Some(json!("alice")));
114    /// assert_eq!(results.results[1].result, Some(json!(30)));
115    /// assert_eq!(results.results[2].result, Some(json!(null)));
116    /// ```
117    pub fn batch_evaluate(&self, expressions: &[String], input: &Value) -> BatchEvaluateResult {
118        let results = expressions
119            .iter()
120            .map(|expr| match self.evaluate(expr, input) {
121                Ok(result) => BatchExpressionResult {
122                    expression: expr.clone(),
123                    result: Some(result),
124                    error: None,
125                },
126                Err(e) => BatchExpressionResult {
127                    expression: expr.clone(),
128                    result: None,
129                    error: Some(e.to_string()),
130                },
131            })
132            .collect();
133
134        BatchEvaluateResult { results }
135    }
136
137    /// Validates a JMESPath expression without evaluating it.
138    ///
139    /// Checks if an expression has valid syntax without needing input data.
140    /// Useful for validating user-provided expressions before storing them.
141    ///
142    /// # Example
143    ///
144    /// ```rust
145    /// use jpx_engine::JpxEngine;
146    ///
147    /// let engine = JpxEngine::new();
148    ///
149    /// // Valid expression
150    /// let result = engine.validate("users[*].name | sort(@)");
151    /// assert!(result.valid);
152    /// assert!(result.error.is_none());
153    ///
154    /// // Invalid expression (unclosed bracket)
155    /// let result = engine.validate("users[*.name");
156    /// assert!(!result.valid);
157    /// assert!(result.error.is_some());
158    /// ```
159    pub fn validate(&self, expression: &str) -> ValidationResult {
160        match jpx_core::compile(expression) {
161            Ok(expr) => {
162                if self.strict {
163                    // Reject let expressions in strict mode
164                    if crate::explain::has_let_nodes(expr.as_ast()) {
165                        return ValidationResult {
166                            valid: false,
167                            error: Some(
168                                "Let expressions are not available in strict mode \
169                                 (standard JMESPath only)."
170                                    .to_string(),
171                            ),
172                        };
173                    }
174
175                    // Reject extension functions in strict mode
176                    let func_names = crate::explain::collect_function_names(expr.as_ast());
177                    let extension_fns: Vec<&String> = func_names
178                        .iter()
179                        .filter(|name| {
180                            self.registry
181                                .get_function(name)
182                                .is_some_and(|f| !f.is_standard)
183                        })
184                        .collect();
185                    if !extension_fns.is_empty() {
186                        let names = extension_fns
187                            .iter()
188                            .map(|s| s.as_str())
189                            .collect::<Vec<_>>()
190                            .join(", ");
191                        return ValidationResult {
192                            valid: false,
193                            error: Some(format!(
194                                "Extension function(s) not available in strict mode: {names}. \
195                                 Only standard JMESPath functions are allowed."
196                            )),
197                        };
198                    }
199                }
200
201                ValidationResult {
202                    valid: true,
203                    error: None,
204                }
205            }
206            Err(e) => ValidationResult {
207                valid: false,
208                error: Some(e.to_string()),
209            },
210        }
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use serde_json::json;
218
219    #[test]
220    fn test_evaluate() {
221        let engine = JpxEngine::new();
222        let input = json!({"users": [{"name": "alice"}, {"name": "bob"}]});
223        let result = engine.evaluate("users[*].name", &input).unwrap();
224        assert_eq!(result, json!(["alice", "bob"]));
225    }
226
227    #[test]
228    fn test_evaluate_str() {
229        let engine = JpxEngine::new();
230        let result = engine.evaluate_str("length(@)", r#"[1, 2, 3]"#).unwrap();
231        assert_eq!(result, json!(3));
232    }
233
234    #[test]
235    fn test_batch_evaluate() {
236        let engine = JpxEngine::new();
237        let input = json!({"a": 1, "b": 2});
238        let exprs = vec!["a".to_string(), "b".to_string(), "c".to_string()];
239        let result = engine.batch_evaluate(&exprs, &input);
240
241        assert_eq!(result.results.len(), 3);
242        assert_eq!(result.results[0].result, Some(json!(1)));
243        assert_eq!(result.results[1].result, Some(json!(2)));
244        assert_eq!(result.results[2].result, Some(json!(null)));
245    }
246
247    #[test]
248    fn test_validate() {
249        let engine = JpxEngine::new();
250
251        let valid = engine.validate("users[*].name");
252        assert!(valid.valid);
253        assert!(valid.error.is_none());
254
255        let invalid = engine.validate("users[*.name");
256        assert!(!invalid.valid);
257        assert!(invalid.error.is_some());
258    }
259
260    #[test]
261    fn test_evaluate_extension_function() {
262        let engine = JpxEngine::new();
263        let result = engine
264            .evaluate("upper(name)", &json!({"name": "alice"}))
265            .unwrap();
266        assert_eq!(result, json!("ALICE"));
267    }
268
269    #[test]
270    fn test_evaluate_strict_rejects_extensions() {
271        let engine = JpxEngine::strict();
272        let result = engine.evaluate("upper(name)", &json!({"name": "alice"}));
273        assert!(result.is_err());
274        assert!(matches!(
275            result,
276            Err(crate::EngineError::EvaluationFailed { .. })
277        ));
278    }
279
280    #[test]
281    fn test_evaluate_invalid_expression() {
282        let engine = JpxEngine::new();
283        let result = engine.evaluate("users[*.name", &json!({}));
284        assert!(matches!(
285            result,
286            Err(crate::EngineError::InvalidExpression(_))
287        ));
288    }
289
290    #[test]
291    fn test_evaluate_str_invalid_json() {
292        let engine = JpxEngine::new();
293        let result = engine.evaluate_str("@", "not json");
294        assert!(matches!(result, Err(crate::EngineError::InvalidJson(_))));
295    }
296
297    #[test]
298    fn test_batch_evaluate_with_errors() {
299        let engine = JpxEngine::new();
300        let exprs = vec!["a".to_string(), "invalid[".to_string()];
301        let result = engine.batch_evaluate(&exprs, &json!({"a": 1}));
302
303        assert_eq!(result.results.len(), 2);
304        assert!(result.results[0].result.is_some());
305        assert!(result.results[0].error.is_none());
306        assert!(result.results[1].result.is_none());
307        assert!(result.results[1].error.is_some());
308    }
309
310    #[test]
311    fn test_evaluate_unicode() {
312        let engine = JpxEngine::new();
313        let input = json!({"name": "ñoño", "greeting": "こんにちは"});
314
315        let result = engine.evaluate("name", &input).unwrap();
316        assert_eq!(result, json!("ñoño"));
317
318        let result = engine.evaluate("greeting", &input).unwrap();
319        assert_eq!(result, json!("こんにちは"));
320    }
321
322    #[test]
323    fn test_evaluate_deeply_nested() {
324        let engine = JpxEngine::new();
325        let input = json!({"a": {"b": {"c": {"d": {"e": "deep"}}}}});
326
327        let result = engine.evaluate("a.b.c.d.e", &input).unwrap();
328        assert_eq!(result, json!("deep"));
329
330        let result = engine.evaluate("a.b.c.d", &input).unwrap();
331        assert_eq!(result, json!({"e": "deep"}));
332    }
333
334    #[test]
335    fn test_evaluate_null_result() {
336        let engine = JpxEngine::new();
337        let input = json!({"a": 1, "b": "hello"});
338
339        let result = engine.evaluate("missing", &input).unwrap();
340        assert_eq!(result, json!(null));
341
342        let result = engine.evaluate("a.b.c", &input).unwrap();
343        assert_eq!(result, json!(null));
344    }
345
346    #[test]
347    fn test_batch_evaluate_large() {
348        let engine = JpxEngine::new();
349        let input = json!({"value": 42});
350        let exprs: Vec<String> = (0..50).map(|_| "value".to_string()).collect();
351
352        let result = engine.batch_evaluate(&exprs, &input);
353        assert_eq!(result.results.len(), 50);
354        for r in &result.results {
355            assert_eq!(r.result, Some(json!(42)));
356            assert!(r.error.is_none());
357        }
358    }
359
360    #[test]
361    fn test_strict_rejects_let_expression() {
362        let engine = JpxEngine::strict();
363        let result = engine.evaluate("let $x = name in $x", &json!({"name": "alice"}));
364        assert!(result.is_err());
365        let err = result.unwrap_err().to_string();
366        assert!(err.contains("strict mode"), "error was: {err}");
367    }
368
369    #[test]
370    fn test_non_strict_allows_let_expression() {
371        let engine = JpxEngine::new();
372        let result = engine
373            .evaluate("let $x = name in $x", &json!({"name": "alice"}))
374            .unwrap();
375        assert_eq!(result, json!("alice"));
376    }
377
378    #[test]
379    fn test_batch_evaluate_empty() {
380        let engine = JpxEngine::new();
381        let input = json!({"a": 1});
382        let exprs: Vec<String> = vec![];
383
384        let result = engine.batch_evaluate(&exprs, &input);
385        assert!(result.results.is_empty());
386    }
387
388    #[test]
389    fn test_validate_complex_valid() {
390        let engine = JpxEngine::new();
391
392        let result = engine.validate("users[?age > `30`].name | sort(@) | join(', ', @)");
393        assert!(result.valid);
394        assert!(result.error.is_none());
395
396        let result = engine.validate("items[*].{id: id, name: name} | [?id > `5`]");
397        assert!(result.valid);
398        assert!(result.error.is_none());
399    }
400
401    #[test]
402    fn test_validate_strict_rejects_let_expression() {
403        let engine = JpxEngine::strict();
404        let result = engine.validate("let $x = name in $x");
405        assert!(!result.valid);
406        let err = result.error.unwrap();
407        assert!(err.contains("strict mode"), "error was: {err}");
408        assert!(err.contains("Let expression"), "error was: {err}");
409    }
410
411    #[test]
412    fn test_validate_strict_rejects_extension_function() {
413        let engine = JpxEngine::strict();
414        let result = engine.validate("upper(name)");
415        assert!(!result.valid);
416        let err = result.error.unwrap();
417        assert!(err.contains("strict mode"), "error was: {err}");
418        assert!(err.contains("upper"), "error was: {err}");
419    }
420
421    #[test]
422    fn test_validate_strict_allows_standard_functions() {
423        let engine = JpxEngine::strict();
424        let result = engine.validate("length(sort(@))");
425        assert!(result.valid, "error: {:?}", result.error);
426    }
427
428    #[test]
429    fn test_validate_non_strict_allows_all() {
430        let engine = JpxEngine::new();
431
432        let result = engine.validate("upper(name)");
433        assert!(result.valid, "error: {:?}", result.error);
434
435        let result = engine.validate("let $x = name in $x");
436        assert!(result.valid, "error: {:?}", result.error);
437    }
438}