1use 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
12const DEFAULT_MAX_DEPTH: usize = 64;
14
15const DEFAULT_MAX_INPUT_BYTES: usize = 16 * 1024 * 1024; #[derive(Debug, Clone)]
21pub struct JsonPathConfig {
22 pub max_input_bytes: Option<usize>,
25 pub max_depth: Option<usize>,
28}
29
30impl JsonPathConfig {
31 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
46pub struct JsonPathLanguage {
48 config: JsonPathConfig,
49}
50
51impl JsonPathLanguage {
52 pub fn new() -> Self {
54 Self {
55 config: JsonPathConfig::default(),
56 }
57 }
58
59 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
81fn 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
108fn 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 check_depth(v, max_depth)?;
120 Ok(v.clone())
121 }
122 Body::Text(s) => {
123 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 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 #[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 #[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 #[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 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 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 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 let config = JsonPathConfig::default();
580 assert_eq!(config.max_input_bytes, Some(16 * 1024 * 1024));
581 assert_eq!(config.max_depth, None);
583 }
584
585 #[tokio::test]
586 async fn default_config_rejects_oversized_text_body() {
587 let lang = JsonPathLanguage::new(); 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 let config = JsonPathConfig {
643 max_input_bytes: Some(10), max_depth: Some(3),
645 };
646 let lang = JsonPathLanguage::with_config(config);
647 let expr = lang.create_expression("$.a").unwrap();
648 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}