Skip to main content

camel_language_jsonpath/
lib.rs

1//! JSONPath language for rust-camel — evaluates `jsonpath_rust` queries against exchange bodies.
2//!
3//! Main types: `JsonPathLanguage`, `JsonPathConfig`, `JsonPathExpression`, `JsonPathPredicate`.
4//! Provides expressions and predicates for JSON-based routing and transformation.
5
6use async_trait::async_trait;
7use camel_language_api::{Body, Exchange, Value};
8use camel_language_api::{Expression, Language, LanguageError, Predicate};
9use jsonpath_rust::JsonPath;
10use serde_json::Value as JsonValue;
11
12/// Default maximum nesting depth for JSON values.
13const DEFAULT_MAX_DEPTH: usize = 64;
14
15/// Default maximum input size in bytes for text-to-JSON conversion (M-L2).
16/// Matches the spirit of the XPath language's input cap.
17const DEFAULT_MAX_INPUT_BYTES: usize = 16 * 1024 * 1024; // 16 MiB
18
19/// Configuration for [`JsonPathLanguage`] resource limits.
20#[derive(Debug, Clone)]
21pub struct JsonPathConfig {
22    /// Maximum allowed input size in bytes for text-to-JSON conversion.
23    /// When `None`, no size limit is enforced.
24    pub max_input_bytes: Option<usize>,
25    /// Maximum allowed nesting depth for JSON values.
26    /// Defaults to [`DEFAULT_MAX_DEPTH`] (64).
27    pub max_depth: Option<usize>,
28}
29
30impl JsonPathConfig {
31    /// Returns the effective max depth, applying the default when not explicitly set.
32    fn effective_max_depth(&self) -> usize {
33        self.max_depth.unwrap_or(DEFAULT_MAX_DEPTH)
34    }
35}
36
37impl Default for JsonPathConfig {
38    fn default() -> Self {
39        Self {
40            max_input_bytes: Some(DEFAULT_MAX_INPUT_BYTES),
41            max_depth: None,
42        }
43    }
44}
45
46/// JSONPath language implementation for rust-camel.
47pub struct JsonPathLanguage {
48    config: JsonPathConfig,
49}
50
51impl JsonPathLanguage {
52    /// Create a new instance with default configuration.
53    pub fn new() -> Self {
54        Self {
55            config: JsonPathConfig::default(),
56        }
57    }
58
59    /// Create a new instance with the given configuration.
60    pub fn with_config(config: JsonPathConfig) -> Self {
61        Self { config }
62    }
63}
64
65impl Default for JsonPathLanguage {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71struct JsonPathExpression {
72    query: String,
73    config: JsonPathConfig,
74}
75
76struct JsonPathPredicate {
77    query: String,
78    config: JsonPathConfig,
79}
80
81/// Check that a [`JsonValue`] does not exceed the given nesting depth.
82fn check_depth(value: &JsonValue, max_depth: usize) -> Result<(), LanguageError> {
83    fn recurse(value: &JsonValue, max_depth: usize, current: usize) -> Result<(), LanguageError> {
84        if current > max_depth {
85            return Err(LanguageError::EvalError(format!(
86                "JSON nesting depth {current} exceeds limit of {max_depth}"
87            )));
88        }
89        match value {
90            JsonValue::Object(map) => {
91                for v in map.values() {
92                    recurse(v, max_depth, current + 1)?;
93                }
94                Ok(())
95            }
96            JsonValue::Array(arr) => {
97                for v in arr {
98                    recurse(v, max_depth, current + 1)?;
99                }
100                Ok(())
101            }
102            _ => Ok(()),
103        }
104    }
105    recurse(value, max_depth, 0)
106}
107
108/// Extract and validate JSON from an exchange body.
109///
110/// Applies resource limits from `config`:
111/// - `max_input_bytes` is checked against text body length before parsing.
112/// - `max_depth` is checked against the resulting JSON value (both pre-parsed and newly-parsed).
113fn extract_json(exchange: &Exchange, config: &JsonPathConfig) -> Result<JsonValue, LanguageError> {
114    let max_depth = config.effective_max_depth();
115
116    match &exchange.input.body {
117        Body::Json(v) => {
118            // Already-parsed JSON: check depth only (no input_bytes limit applies).
119            check_depth(v, max_depth)?;
120            Ok(v.clone())
121        }
122        Body::Text(s) => {
123            // Check input size before parsing.
124            if let Some(limit) = config.max_input_bytes
125                && s.len() > limit
126            {
127                return Err(LanguageError::EvalError(format!(
128                    "input size {} bytes exceeds limit of {limit} bytes",
129                    s.len()
130                )));
131            }
132            let value: JsonValue = serde_json::from_str(s)
133                .map_err(|e| LanguageError::EvalError(format!("body is not valid JSON: {e}")))?;
134            check_depth(&value, max_depth)?;
135            Ok(value)
136        }
137        other => other
138            .clone()
139            .try_into_json()
140            .map_err(|e| {
141                LanguageError::EvalError(format!("body is not JSON and cannot be coerced: {e}"))
142            })
143            .and_then(|b| match b {
144                Body::Json(v) => {
145                    check_depth(&v, max_depth)?;
146                    Ok(v)
147                }
148                _ => Err(LanguageError::EvalError(
149                    "body coercion did not produce JSON".into(),
150                )),
151            }),
152    }
153}
154
155fn run_query(query: &str, json: &JsonValue) -> Result<JsonValue, LanguageError> {
156    json.query(query)
157        .map_err(|e| LanguageError::EvalError(format!("jsonpath query '{query}' failed: {e}")))
158        .map(|results| match results.len() {
159            0 => JsonValue::Null,
160            1 => results[0].clone(),
161            _ => JsonValue::Array(results.into_iter().cloned().collect()),
162        })
163}
164
165#[async_trait]
166impl Expression for JsonPathExpression {
167    // TODO(JPT-004): The return type is currently raw serde_json::Value. For better
168    // interoperability with Camel routing (e.g. header assignments, simple expressions),
169    // the result should be coerced: single scalars unwrapped to their native types
170    // (string, number, bool), arrays preserved, and Null mapped to a clear sentinel.
171    async fn evaluate(&self, exchange: &Exchange) -> Result<Value, LanguageError> {
172        let json = extract_json(exchange, &self.config)?;
173        run_query(&self.query, &json)
174    }
175}
176
177#[async_trait]
178impl Predicate for JsonPathPredicate {
179    async fn matches(&self, exchange: &Exchange) -> Result<bool, LanguageError> {
180        let json = extract_json(exchange, &self.config)?;
181        let result = run_query(&self.query, &json)?;
182        Ok(is_truthy(&result))
183    }
184}
185
186fn is_truthy(value: &JsonValue) -> bool {
187    match value {
188        JsonValue::Null => false,
189        JsonValue::Bool(b) => *b,
190        JsonValue::Number(n) => {
191            if let Some(v) = n.as_i64() {
192                return v != 0;
193            }
194            if let Some(v) = n.as_u64() {
195                return v != 0;
196            }
197            if let Some(v) = n.as_f64() {
198                return v != 0.0;
199            }
200            true
201        }
202        JsonValue::String(s) => !s.is_empty(),
203        JsonValue::Array(arr) => !arr.is_empty(),
204        JsonValue::Object(_) => true,
205    }
206}
207
208impl Language for JsonPathLanguage {
209    fn name(&self) -> &'static str {
210        "jsonpath"
211    }
212
213    fn create_expression(&self, script: &str) -> Result<Box<dyn Expression>, LanguageError> {
214        if !script.starts_with('$') {
215            return Err(LanguageError::ParseError {
216                expr: script.to_string(),
217                reason: "JsonPath expression must start with '$'".into(),
218            });
219        }
220        let empty = JsonValue::Object(serde_json::Map::new());
221        empty.query(script).map_err(|e| LanguageError::ParseError {
222            expr: script.to_string(),
223            reason: e.to_string(),
224        })?;
225        Ok(Box::new(JsonPathExpression {
226            query: script.to_string(),
227            config: self.config.clone(),
228        }))
229    }
230
231    fn create_predicate(&self, script: &str) -> Result<Box<dyn Predicate>, LanguageError> {
232        if !script.starts_with('$') {
233            return Err(LanguageError::ParseError {
234                expr: script.to_string(),
235                reason: "JsonPath expression must start with '$'".into(),
236            });
237        }
238        let empty = JsonValue::Object(serde_json::Map::new());
239        empty.query(script).map_err(|e| LanguageError::ParseError {
240            expr: script.to_string(),
241            reason: e.to_string(),
242        })?;
243        Ok(Box::new(JsonPathPredicate {
244            query: script.to_string(),
245            config: self.config.clone(),
246        }))
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use camel_language_api::Message;
254
255    async fn exchange_with_json(json: &str) -> Exchange {
256        let value: JsonValue = serde_json::from_str(json).unwrap();
257        Exchange::new(Message::new(Body::Json(value)))
258    }
259
260    async fn exchange_with_text_body(text: &str) -> Exchange {
261        Exchange::new(Message::new(Body::Text(text.to_string())))
262    }
263
264    async fn empty_exchange() -> Exchange {
265        Exchange::new(Message::default())
266    }
267
268    async fn default_lang() -> JsonPathLanguage {
269        JsonPathLanguage::new()
270    }
271
272    #[tokio::test]
273    async fn expression_simple_path() {
274        let lang = default_lang().await;
275        let expr = lang.create_expression("$.store.name").unwrap();
276        let ex = exchange_with_json(r#"{"store":{"name":"books"}}"#).await;
277        let result = expr.evaluate(&ex).await.unwrap();
278        assert_eq!(result, JsonValue::String("books".to_string()));
279    }
280
281    #[tokio::test]
282    async fn expression_nested_path() {
283        let lang = default_lang().await;
284        let expr = lang.create_expression("$.a.b.c").unwrap();
285        let ex = exchange_with_json(r#"{"a":{"b":{"c":42}}}"#).await;
286        let result = expr.evaluate(&ex).await.unwrap();
287        assert_eq!(result, JsonValue::Number(42.into()));
288    }
289
290    #[tokio::test]
291    async fn expression_array_index() {
292        let lang = default_lang().await;
293        let expr = lang.create_expression("$.items[0]").unwrap();
294        let ex = exchange_with_json(r#"{"items":["a","b","c"]}"#).await;
295        let result = expr.evaluate(&ex).await.unwrap();
296        assert_eq!(result, JsonValue::String("a".to_string()));
297    }
298
299    #[tokio::test]
300    async fn expression_wildcard() {
301        let lang = default_lang().await;
302        let expr = lang.create_expression("$.items[*].name").unwrap();
303        let ex = exchange_with_json(r#"{"items":[{"name":"a"},{"name":"b"}]}"#).await;
304        let result = expr.evaluate(&ex).await.unwrap();
305        assert_eq!(
306            result,
307            JsonValue::Array(vec![
308                JsonValue::String("a".to_string()),
309                JsonValue::String("b".to_string())
310            ])
311        );
312    }
313
314    #[tokio::test]
315    async fn expression_root_path() {
316        let lang = default_lang().await;
317        let expr = lang.create_expression("$").unwrap();
318        let ex = exchange_with_json(r#"{"x":1}"#).await;
319        let result = expr.evaluate(&ex).await.unwrap();
320        assert_eq!(result["x"], JsonValue::Number(1.into()));
321    }
322
323    #[tokio::test]
324    async fn expression_text_body_with_valid_json() {
325        let lang = default_lang().await;
326        let expr = lang.create_expression("$.name").unwrap();
327        let ex = exchange_with_text_body(r#"{"name":"test"}"#).await;
328        let result = expr.evaluate(&ex).await.unwrap();
329        assert_eq!(result, JsonValue::String("test".to_string()));
330    }
331
332    #[tokio::test]
333    async fn expression_empty_body_is_error() {
334        let lang = default_lang().await;
335        let expr = lang.create_expression("$.x").unwrap();
336        let ex = empty_exchange().await;
337        let result = expr.evaluate(&ex).await;
338        assert!(result.is_err());
339    }
340
341    #[tokio::test]
342    async fn expression_invalid_jsonpath_syntax() {
343        let lang = default_lang().await;
344        let result = lang.create_expression("$[invalid");
345        let err = match result {
346            Err(e) => e,
347            Ok(_) => panic!("expected ParseError"),
348        };
349        match err {
350            LanguageError::ParseError { expr, reason } => {
351                assert!(!expr.is_empty());
352                assert!(!reason.is_empty());
353            }
354            other => panic!("expected ParseError, got {other:?}"),
355        }
356    }
357
358    // --- JPT-005: $ prefix validation ---
359
360    #[tokio::test]
361    async fn expression_without_dollar_prefix_is_rejected() {
362        let lang = default_lang().await;
363        let result = lang.create_expression("store.name");
364        assert!(result.is_err(), "expected error for missing $ prefix");
365        let err = match result {
366            Err(e) => e,
367            Ok(_) => panic!("expected ParseError"),
368        };
369        match err {
370            LanguageError::ParseError { expr, reason } => {
371                assert_eq!(expr, "store.name");
372                assert!(
373                    reason.contains("'$'"),
374                    "reason should mention '$', got: {reason}"
375                );
376            }
377            other => panic!("expected ParseError, got {other:?}"),
378        }
379    }
380
381    #[tokio::test]
382    async fn predicate_without_dollar_prefix_is_rejected() {
383        let lang = default_lang().await;
384        let result = lang.create_predicate("store.name");
385        assert!(result.is_err(), "expected error for missing $ prefix");
386        let err = match result {
387            Err(e) => e,
388            Ok(_) => panic!("expected ParseError"),
389        };
390        match err {
391            LanguageError::ParseError { reason, .. } => {
392                assert!(
393                    reason.contains("'$'"),
394                    "reason should mention '$', got: {reason}"
395                );
396            }
397            other => panic!("expected ParseError, got {other:?}"),
398        }
399    }
400
401    // --- JPT-006: Nested path and array index tests ---
402
403    #[tokio::test]
404    async fn expression_deeply_nested_path() {
405        let lang = default_lang().await;
406        let expr = lang.create_expression("$.a.b.c.d").unwrap();
407        let ex = exchange_with_json(r#"{"a":{"b":{"c":{"d":"deep"}}}}"#).await;
408        let result = expr.evaluate(&ex).await.unwrap();
409        assert_eq!(result, JsonValue::String("deep".to_string()));
410    }
411
412    #[tokio::test]
413    async fn expression_array_index_nested() {
414        let lang = default_lang().await;
415        let expr = lang.create_expression("$.data.items[1].name").unwrap();
416        let ex = exchange_with_json(
417            r#"{"data":{"items":[{"name":"first"},{"name":"second"},{"name":"third"}]}}"#,
418        )
419        .await;
420        let result = expr.evaluate(&ex).await.unwrap();
421        assert_eq!(result, JsonValue::String("second".to_string()));
422    }
423
424    #[tokio::test]
425    async fn predicate_non_empty_array_is_true() {
426        let lang = default_lang().await;
427        let pred = lang.create_predicate("$.items[*]").unwrap();
428        let ex = exchange_with_json(r#"{"items":[1,2,3]}"#).await;
429        assert!(pred.matches(&ex).await.unwrap());
430    }
431
432    #[tokio::test]
433    async fn predicate_empty_result_is_false() {
434        let lang = default_lang().await;
435        let pred = lang.create_predicate("$.missing").unwrap();
436        let ex = exchange_with_json(r#"{"other":1}"#).await;
437        assert!(!pred.matches(&ex).await.unwrap());
438    }
439
440    #[tokio::test]
441    async fn predicate_boolean_true() {
442        let lang = default_lang().await;
443        let pred = lang.create_predicate("$.active").unwrap();
444        let ex = exchange_with_json(r#"{"active":true}"#).await;
445        assert!(pred.matches(&ex).await.unwrap());
446    }
447
448    #[tokio::test]
449    async fn predicate_boolean_false() {
450        let lang = default_lang().await;
451        let pred = lang.create_predicate("$.active").unwrap();
452        let ex = exchange_with_json(r#"{"active":false}"#).await;
453        assert!(!pred.matches(&ex).await.unwrap());
454    }
455
456    #[tokio::test]
457    async fn predicate_found_value_is_true() {
458        let lang = default_lang().await;
459        let pred = lang.create_predicate("$.name").unwrap();
460        let ex = exchange_with_json(r#"{"name":"test"}"#).await;
461        assert!(pred.matches(&ex).await.unwrap());
462    }
463
464    #[tokio::test]
465    async fn predicate_zero_is_false() {
466        let lang = default_lang().await;
467        let pred = lang.create_predicate("$.val").unwrap();
468        let ex = exchange_with_json(r#"{"val":0}"#).await;
469        assert!(!pred.matches(&ex).await.unwrap());
470    }
471
472    #[tokio::test]
473    async fn predicate_non_zero_is_true() {
474        let lang = default_lang().await;
475        let pred = lang.create_predicate("$.val").unwrap();
476        let ex = exchange_with_json(r#"{"val":1}"#).await;
477        assert!(pred.matches(&ex).await.unwrap());
478    }
479
480    #[tokio::test]
481    async fn predicate_empty_string_is_false() {
482        let lang = default_lang().await;
483        let pred = lang.create_predicate("$.val").unwrap();
484        let ex = exchange_with_json(r#"{"val":""}"#).await;
485        assert!(!pred.matches(&ex).await.unwrap());
486    }
487
488    #[tokio::test]
489    async fn predicate_non_empty_string_is_true() {
490        let lang = default_lang().await;
491        let pred = lang.create_predicate("$.val").unwrap();
492        let ex = exchange_with_json(r#"{"val":"x"}"#).await;
493        assert!(pred.matches(&ex).await.unwrap());
494    }
495
496    // --- Resource limit tests (A-26) ---
497
498    #[tokio::test]
499    async fn oversized_input_is_rejected() {
500        let config = JsonPathConfig {
501            max_input_bytes: Some(100),
502            ..Default::default()
503        };
504        let lang = JsonPathLanguage::with_config(config);
505        let expr = lang.create_expression("$.key").unwrap();
506        // Build a valid JSON string that exceeds 100 bytes
507        let big_value = "x".repeat(200);
508        let big_json = format!(r#"{{"key":"{}"}}"#, big_value);
509        assert!(big_json.len() > 100);
510        let ex = exchange_with_text_body(&big_json).await;
511        let result = expr.evaluate(&ex).await;
512        assert!(
513            result.is_err(),
514            "expected error for oversized input, got {result:?}"
515        );
516    }
517
518    #[tokio::test]
519    async fn input_under_limit_is_accepted() {
520        let config = JsonPathConfig {
521            max_input_bytes: Some(1024),
522            ..Default::default()
523        };
524        let lang = JsonPathLanguage::with_config(config);
525        let expr = lang.create_expression("$.key").unwrap();
526        let ex = exchange_with_text_body(r#"{"key":"value"}"#).await;
527        let result = expr.evaluate(&ex).await;
528        assert!(
529            result.is_ok(),
530            "expected success for input under limit, got {result:?}"
531        );
532    }
533
534    #[tokio::test]
535    async fn deeply_nested_input_is_rejected() {
536        let config = JsonPathConfig {
537            max_depth: Some(5),
538            ..Default::default()
539        };
540        let lang = JsonPathLanguage::with_config(config);
541        let expr = lang.create_expression("$.a").unwrap();
542        // Build nesting of depth 10: {"a":{"a":{"a":...}}}
543        let mut json = "1".to_string();
544        for _ in 0..10 {
545            json = format!(r#"{{"a":{json}}}"#);
546        }
547        let ex = exchange_with_text_body(&json).await;
548        let result = expr.evaluate(&ex).await;
549        assert!(
550            result.is_err(),
551            "expected error for deeply nested input, got {result:?}"
552        );
553    }
554
555    #[tokio::test]
556    async fn nesting_within_depth_limit_is_accepted() {
557        let config = JsonPathConfig {
558            max_depth: Some(10),
559            ..Default::default()
560        };
561        let lang = JsonPathLanguage::with_config(config);
562        let expr = lang.create_expression("$.a").unwrap();
563        // Build nesting of depth 5
564        let mut json = "1".to_string();
565        for _ in 0..5 {
566            json = format!(r#"{{"a":{json}}}"#);
567        }
568        let ex = exchange_with_text_body(&json).await;
569        let result = expr.evaluate(&ex).await;
570        assert!(
571            result.is_ok(),
572            "expected success for nesting within limit, got {result:?}"
573        );
574    }
575
576    #[tokio::test]
577    async fn default_config_has_safe_defaults() {
578        // M-L2: max_input_bytes defaults to 16 MiB (was None).
579        let config = JsonPathConfig::default();
580        assert_eq!(config.max_input_bytes, Some(16 * 1024 * 1024));
581        // max_depth still defaults to DEFAULT_MAX_DEPTH via effective_max_depth().
582        assert_eq!(config.max_depth, None);
583    }
584
585    #[tokio::test]
586    async fn default_config_rejects_oversized_text_body() {
587        // Default config must reject a >16 MiB text body without an explicit cap.
588        let lang = JsonPathLanguage::new(); // default config
589        let expr = lang.create_expression("$.key").unwrap();
590        let big_value = "x".repeat(16 * 1024 * 1024 + 10);
591        let big_json = format!(r#"{{"key":"{}"}}"#, big_value);
592        let ex = exchange_with_text_body(&big_json).await;
593        let result = expr.evaluate(&ex).await;
594        assert!(
595            result.is_err(),
596            "default config must reject oversized input, got {result:?}"
597        );
598    }
599
600    #[tokio::test]
601    async fn oversized_input_also_rejected_for_predicate() {
602        let config = JsonPathConfig {
603            max_input_bytes: Some(100),
604            ..Default::default()
605        };
606        let lang = JsonPathLanguage::with_config(config);
607        let pred = lang.create_predicate("$.key").unwrap();
608        let big_value = "x".repeat(200);
609        let big_json = format!(r#"{{"key":"{}"}}"#, big_value);
610        let ex = exchange_with_text_body(&big_json).await;
611        let result = pred.matches(&ex).await;
612        assert!(
613            result.is_err(),
614            "expected error for oversized input in predicate, got {result:?}"
615        );
616    }
617
618    #[tokio::test]
619    async fn deeply_nested_input_rejected_for_predicate() {
620        let config = JsonPathConfig {
621            max_depth: Some(3),
622            ..Default::default()
623        };
624        let lang = JsonPathLanguage::with_config(config);
625        let pred = lang.create_predicate("$.a").unwrap();
626        let mut json = "1".to_string();
627        for _ in 0..5 {
628            json = format!(r#"{{"a":{json}}}"#);
629        }
630        let ex = exchange_with_text_body(&json).await;
631        let result = pred.matches(&ex).await;
632        assert!(
633            result.is_err(),
634            "expected error for deeply nested input in predicate, got {result:?}"
635        );
636    }
637
638    #[tokio::test]
639    async fn body_json_no_input_size_check_but_depth_checked() {
640        // When body is already Body::Json (already parsed), skip input_bytes check
641        // but still enforce depth limit
642        let config = JsonPathConfig {
643            max_input_bytes: Some(10), // very small — but body is already JSON
644            max_depth: Some(3),
645        };
646        let lang = JsonPathLanguage::with_config(config);
647        let expr = lang.create_expression("$.a").unwrap();
648        // Build a JSON value with nesting depth 5
649        let mut json_str = "1".to_string();
650        for _ in 0..5 {
651            json_str = format!(r#"{{"a":{json_str}}}"#);
652        }
653        let ex = exchange_with_json(&json_str).await;
654        let result = expr.evaluate(&ex).await;
655        assert!(
656            result.is_err(),
657            "expected depth error for pre-parsed JSON, got {result:?}"
658        );
659    }
660}