1use async_trait::async_trait;
7use camel_language_api::{Body, Exchange, ExpressionErrorClass, Value};
8use camel_language_api::{Expression, Language, LanguageError, Predicate};
9use jsonpath_rust::parser::model::JpQuery;
10use jsonpath_rust::parser::parse_json_path;
11use jsonpath_rust::query::js_path_process;
12use serde_json::Value as JsonValue;
13#[cfg(test)]
14thread_local! {
15 static COMPILE_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
16}
17
18const DEFAULT_MAX_DEPTH: usize = 64;
20
21const DEFAULT_MAX_INPUT_BYTES: usize = 16 * 1024 * 1024; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub enum JsonPathPredicateMode {
30 #[default]
34 Strict,
35 Matches,
41}
42
43#[derive(Debug, Clone)]
45pub struct JsonPathConfig {
46 pub max_input_bytes: Option<usize>,
49 pub max_depth: Option<usize>,
52 pub predicate_mode: JsonPathPredicateMode,
55}
56
57impl JsonPathConfig {
58 fn effective_max_depth(&self) -> usize {
60 self.max_depth.unwrap_or(DEFAULT_MAX_DEPTH)
61 }
62}
63
64impl Default for JsonPathConfig {
65 fn default() -> Self {
66 Self {
67 max_input_bytes: Some(DEFAULT_MAX_INPUT_BYTES),
68 max_depth: None,
69 predicate_mode: JsonPathPredicateMode::default(),
70 }
71 }
72}
73
74pub struct JsonPathLanguage {
76 config: JsonPathConfig,
77}
78
79impl JsonPathLanguage {
80 pub fn new() -> Self {
82 Self {
83 config: JsonPathConfig::default(),
84 }
85 }
86
87 pub fn with_config(config: JsonPathConfig) -> Self {
89 Self { config }
90 }
91}
92
93impl Default for JsonPathLanguage {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99struct JsonPathExpression {
100 query: JpQuery,
101 config: JsonPathConfig,
102}
103
104struct JsonPathPredicate {
105 query: JpQuery,
106 config: JsonPathConfig,
107}
108
109fn check_depth(value: &JsonValue, max_depth: usize) -> Result<(), LanguageError> {
111 fn recurse(value: &JsonValue, max_depth: usize, current: usize) -> Result<(), LanguageError> {
112 if current > max_depth {
113 return Err(LanguageError::EvalError(format!(
114 "JSON nesting depth {current} exceeds limit of {max_depth}"
115 )));
116 }
117 match value {
118 JsonValue::Object(map) => {
119 for v in map.values() {
120 recurse(v, max_depth, current + 1)?;
121 }
122 Ok(())
123 }
124 JsonValue::Array(arr) => {
125 for v in arr {
126 recurse(v, max_depth, current + 1)?;
127 }
128 Ok(())
129 }
130 _ => Ok(()),
131 }
132 }
133 recurse(value, max_depth, 0)
134}
135
136fn extract_json(exchange: &Exchange, config: &JsonPathConfig) -> Result<JsonValue, LanguageError> {
142 let max_depth = config.effective_max_depth();
143
144 match &exchange.input.body {
145 Body::Json(v) => {
146 check_depth(v, max_depth)?;
148 Ok(v.clone())
149 }
150 Body::Text(s) => {
151 if let Some(limit) = config.max_input_bytes
153 && s.len() > limit
154 {
155 return Err(LanguageError::EvalError(format!(
156 "input size {} bytes exceeds limit of {limit} bytes",
157 s.len()
158 )));
159 }
160 let value: JsonValue =
163 serde_json::from_str(s).map_err(|_| LanguageError::EvalFailure {
164 class: ExpressionErrorClass::Conversion,
165 position: None,
166 detail: Some("body is not valid JSON".to_string()),
167 })?;
168 check_depth(&value, max_depth)?;
169 Ok(value)
170 }
171 other => other
172 .clone()
173 .try_into_json()
174 .map_err(|e| {
175 LanguageError::EvalError(format!("body is not JSON and cannot be coerced: {e}"))
176 })
177 .and_then(|b| match b {
178 Body::Json(v) => {
179 check_depth(&v, max_depth)?;
180 Ok(v)
181 }
182 _ => Err(LanguageError::EvalError(
183 "body coercion did not produce JSON".into(),
184 )),
185 }),
186 }
187}
188
189fn run_query_all(query: &JpQuery, json: &JsonValue) -> Result<Vec<JsonValue>, LanguageError> {
191 let results = js_path_process(query, json)
192 .map_err(|e| LanguageError::EvalError(format!("jsonpath query '{query}' failed: {e}")))?;
193 Ok(results.into_iter().map(|r| r.val.clone()).collect())
194}
195
196fn run_query(query: &JpQuery, json: &JsonValue) -> Result<JsonValue, LanguageError> {
200 let values = run_query_all(query, json)?;
201 Ok(match values.as_slice() {
202 [] => JsonValue::Null,
203 [one] => one.clone(),
204 _ => JsonValue::Array(values),
205 })
206}
207
208#[async_trait]
209impl Expression for JsonPathExpression {
210 async fn evaluate(&self, exchange: &Exchange) -> Result<Value, LanguageError> {
215 let json = extract_json(exchange, &self.config)?;
216 run_query(&self.query, &json)
217 }
218}
219
220#[async_trait]
221impl Predicate for JsonPathPredicate {
222 async fn matches(&self, exchange: &Exchange) -> Result<bool, LanguageError> {
223 let json = extract_json(exchange, &self.config)?;
224 match self.config.predicate_mode {
225 JsonPathPredicateMode::Matches => {
226 let values = run_query_all(&self.query, &json)?;
231 Ok(match values.as_slice() {
232 [] => false,
233 [JsonValue::Bool(b)] => *b,
234 _ => true,
235 })
236 }
237 JsonPathPredicateMode::Strict => {
238 let result = run_query(&self.query, &json)?;
239 match &result {
244 JsonValue::Bool(b) => Ok(*b),
245 other => Err(LanguageError::TypeMismatch {
246 expected: "bool".to_string(),
247 actual: json_type_name(other).to_string(),
248 position: None,
249 }),
250 }
251 }
252 }
253 }
254}
255
256fn json_type_name(value: &JsonValue) -> &'static str {
259 match value {
260 JsonValue::Null => "null",
261 JsonValue::Bool(_) => "bool",
262 JsonValue::Number(_) => "number",
263 JsonValue::String(_) => "string",
264 JsonValue::Array(_) => "array",
265 JsonValue::Object(_) => "object",
266 }
267}
268
269impl Language for JsonPathLanguage {
270 fn name(&self) -> &'static str {
271 "jsonpath"
272 }
273
274 fn create_expression(&self, script: &str) -> Result<Box<dyn Expression>, LanguageError> {
275 if !script.starts_with('$') {
276 return Err(LanguageError::ParseError {
277 expr: script.to_string(),
278 reason: "JsonPath expression must start with '$'".into(),
279 });
280 }
281 let parsed = parse_json_path(script).map_err(|e| LanguageError::ParseError {
282 expr: script.to_string(),
283 reason: e.to_string(),
284 })?;
285 #[cfg(test)]
286 {
287 COMPILE_COUNT.with(|c| c.set(c.get() + 1));
288 }
289 Ok(Box::new(JsonPathExpression {
290 query: parsed,
291 config: self.config.clone(),
292 }))
293 }
294
295 fn create_predicate(&self, script: &str) -> Result<Box<dyn Predicate>, LanguageError> {
296 if !script.starts_with('$') {
297 return Err(LanguageError::ParseError {
298 expr: script.to_string(),
299 reason: "JsonPath expression must start with '$'".into(),
300 });
301 }
302 let parsed = parse_json_path(script).map_err(|e| LanguageError::ParseError {
303 expr: script.to_string(),
304 reason: e.to_string(),
305 })?;
306 #[cfg(test)]
307 {
308 COMPILE_COUNT.with(|c| c.set(c.get() + 1));
309 }
310 Ok(Box::new(JsonPathPredicate {
311 query: parsed,
312 config: self.config.clone(),
313 }))
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use camel_language_api::Message;
321
322 async fn exchange_with_json(json: &str) -> Exchange {
323 let value: JsonValue = serde_json::from_str(json).unwrap();
324 Exchange::new(Message::new(Body::Json(value)))
325 }
326
327 async fn exchange_with_text_body(text: &str) -> Exchange {
328 Exchange::new(Message::new(Body::Text(text.to_string())))
329 }
330
331 async fn empty_exchange() -> Exchange {
332 Exchange::new(Message::default())
333 }
334
335 async fn default_lang() -> JsonPathLanguage {
336 JsonPathLanguage::new()
337 }
338
339 #[tokio::test]
340 async fn expression_simple_path() {
341 let lang = default_lang().await;
342 let expr = lang.create_expression("$.store.name").unwrap();
343 let ex = exchange_with_json(r#"{"store":{"name":"books"}}"#).await;
344 let result = expr.evaluate(&ex).await.unwrap();
345 assert_eq!(result, JsonValue::String("books".to_string()));
346 }
347
348 #[tokio::test]
349 async fn expression_nested_path() {
350 let lang = default_lang().await;
351 let expr = lang.create_expression("$.a.b.c").unwrap();
352 let ex = exchange_with_json(r#"{"a":{"b":{"c":42}}}"#).await;
353 let result = expr.evaluate(&ex).await.unwrap();
354 assert_eq!(result, JsonValue::Number(42.into()));
355 }
356
357 #[tokio::test]
358 async fn expression_array_index() {
359 let lang = default_lang().await;
360 let expr = lang.create_expression("$.items[0]").unwrap();
361 let ex = exchange_with_json(r#"{"items":["a","b","c"]}"#).await;
362 let result = expr.evaluate(&ex).await.unwrap();
363 assert_eq!(result, JsonValue::String("a".to_string()));
364 }
365
366 #[tokio::test]
367 async fn expression_wildcard() {
368 let lang = default_lang().await;
369 let expr = lang.create_expression("$.items[*].name").unwrap();
370 let ex = exchange_with_json(r#"{"items":[{"name":"a"},{"name":"b"}]}"#).await;
371 let result = expr.evaluate(&ex).await.unwrap();
372 assert_eq!(
373 result,
374 JsonValue::Array(vec![
375 JsonValue::String("a".to_string()),
376 JsonValue::String("b".to_string())
377 ])
378 );
379 }
380
381 #[tokio::test]
382 async fn expression_root_path() {
383 let lang = default_lang().await;
384 let expr = lang.create_expression("$").unwrap();
385 let ex = exchange_with_json(r#"{"x":1}"#).await;
386 let result = expr.evaluate(&ex).await.unwrap();
387 assert_eq!(result["x"], JsonValue::Number(1.into()));
388 }
389
390 #[tokio::test]
391 async fn expression_text_body_with_valid_json() {
392 let lang = default_lang().await;
393 let expr = lang.create_expression("$.name").unwrap();
394 let ex = exchange_with_text_body(r#"{"name":"test"}"#).await;
395 let result = expr.evaluate(&ex).await.unwrap();
396 assert_eq!(result, JsonValue::String("test".to_string()));
397 }
398
399 #[tokio::test]
400 async fn expression_empty_body_is_error() {
401 let lang = default_lang().await;
402 let expr = lang.create_expression("$.x").unwrap();
403 let ex = empty_exchange().await;
404 let result = expr.evaluate(&ex).await;
405 assert!(result.is_err());
406 }
407
408 #[tokio::test]
409 async fn expression_invalid_jsonpath_syntax() {
410 let lang = default_lang().await;
411 let result = lang.create_expression("$[invalid");
412 let err = match result {
413 Err(e) => e,
414 Ok(_) => panic!("expected ParseError"),
415 };
416 match err {
417 LanguageError::ParseError { expr, reason } => {
418 assert!(!expr.is_empty());
419 assert!(!reason.is_empty());
420 }
421 other => panic!("expected ParseError, got {other:?}"),
422 }
423 }
424
425 #[tokio::test]
428 async fn expression_without_dollar_prefix_is_rejected() {
429 let lang = default_lang().await;
430 let result = lang.create_expression("store.name");
431 assert!(result.is_err(), "expected error for missing $ prefix");
432 let err = match result {
433 Err(e) => e,
434 Ok(_) => panic!("expected ParseError"),
435 };
436 match err {
437 LanguageError::ParseError { expr, reason } => {
438 assert_eq!(expr, "store.name");
439 assert!(
440 reason.contains("'$'"),
441 "reason should mention '$', got: {reason}"
442 );
443 }
444 other => panic!("expected ParseError, got {other:?}"),
445 }
446 }
447
448 #[tokio::test]
449 async fn predicate_without_dollar_prefix_is_rejected() {
450 let lang = default_lang().await;
451 let result = lang.create_predicate("store.name");
452 assert!(result.is_err(), "expected error for missing $ prefix");
453 let err = match result {
454 Err(e) => e,
455 Ok(_) => panic!("expected ParseError"),
456 };
457 match err {
458 LanguageError::ParseError { reason, .. } => {
459 assert!(
460 reason.contains("'$'"),
461 "reason should mention '$', got: {reason}"
462 );
463 }
464 other => panic!("expected ParseError, got {other:?}"),
465 }
466 }
467
468 #[tokio::test]
471 async fn expression_deeply_nested_path() {
472 let lang = default_lang().await;
473 let expr = lang.create_expression("$.a.b.c.d").unwrap();
474 let ex = exchange_with_json(r#"{"a":{"b":{"c":{"d":"deep"}}}}"#).await;
475 let result = expr.evaluate(&ex).await.unwrap();
476 assert_eq!(result, JsonValue::String("deep".to_string()));
477 }
478
479 #[tokio::test]
480 async fn expression_array_index_nested() {
481 let lang = default_lang().await;
482 let expr = lang.create_expression("$.data.items[1].name").unwrap();
483 let ex = exchange_with_json(
484 r#"{"data":{"items":[{"name":"first"},{"name":"second"},{"name":"third"}]}}"#,
485 )
486 .await;
487 let result = expr.evaluate(&ex).await.unwrap();
488 assert_eq!(result, JsonValue::String("second".to_string()));
489 }
490
491 #[tokio::test]
492 async fn jsonpath_predicate_non_bool_is_type_mismatch() {
493 let lang = default_lang().await;
497 for (json_body, actual) in [
498 (r#"{"val":"false"}"#, "string"),
499 (r#"{"val":"x"}"#, "string"),
500 (r#"{"val":0}"#, "number"),
501 (r#"{"val":1}"#, "number"),
502 (r#"{"val":[]}"#, "array"),
503 (r#"{"val":{}}"#, "object"),
504 (r#"{"other":1}"#, "null"),
505 ] {
506 let pred = lang.create_predicate("$.val").unwrap();
507 let ex = exchange_with_json(json_body).await;
508 match pred.matches(&ex).await {
509 Err(LanguageError::TypeMismatch {
510 expected,
511 actual: got,
512 position: None,
513 }) => {
514 assert_eq!(expected, "bool");
515 assert_eq!(got, actual, "body {json_body}");
516 }
517 other => panic!("expected TypeMismatch for {json_body}, got: {other:?}"),
518 }
519 }
520
521 let pred = lang.create_predicate("$.active").unwrap();
523 assert!(
524 pred.matches(&exchange_with_json(r#"{"active":true}"#).await)
525 .await
526 .unwrap()
527 );
528 let pred = lang.create_predicate("$.active").unwrap();
529 assert!(
530 !pred
531 .matches(&exchange_with_json(r#"{"active":false}"#).await)
532 .await
533 .unwrap()
534 );
535 }
536
537 #[tokio::test]
538 async fn jsonpath_invalid_body_error_is_conversion_class_no_snippet() {
539 let lang = default_lang().await;
540 let expr = lang.create_expression("$.key").unwrap();
541 let mut ex = Exchange::new(Message::default());
542 ex.input.body = Body::Text("SECRETVAL not json".to_string());
543 let err = expr.evaluate(&ex).await.expect_err("must fail");
544 match &err {
545 LanguageError::EvalFailure {
546 class: ExpressionErrorClass::Conversion,
547 position: None,
548 detail: Some(detail),
549 } => {
550 assert_eq!(detail, "body is not valid JSON");
551 }
552 other => panic!("expected Conversion EvalFailure, got: {other:?}"),
553 }
554 assert!(!err.to_string().contains("SECRETVAL"), "body leaked: {err}");
555 }
556
557 #[tokio::test]
560 async fn oversized_input_is_rejected() {
561 let config = JsonPathConfig {
562 max_input_bytes: Some(100),
563 ..Default::default()
564 };
565 let lang = JsonPathLanguage::with_config(config);
566 let expr = lang.create_expression("$.key").unwrap();
567 let big_value = "x".repeat(200);
569 let big_json = format!(r#"{{"key":"{}"}}"#, big_value);
570 assert!(big_json.len() > 100);
571 let ex = exchange_with_text_body(&big_json).await;
572 let result = expr.evaluate(&ex).await;
573 assert!(
574 result.is_err(),
575 "expected error for oversized input, got {result:?}"
576 );
577 }
578
579 #[tokio::test]
580 async fn input_under_limit_is_accepted() {
581 let config = JsonPathConfig {
582 max_input_bytes: Some(1024),
583 ..Default::default()
584 };
585 let lang = JsonPathLanguage::with_config(config);
586 let expr = lang.create_expression("$.key").unwrap();
587 let ex = exchange_with_text_body(r#"{"key":"value"}"#).await;
588 let result = expr.evaluate(&ex).await;
589 assert!(
590 result.is_ok(),
591 "expected success for input under limit, got {result:?}"
592 );
593 }
594
595 #[tokio::test]
596 async fn deeply_nested_input_is_rejected() {
597 let config = JsonPathConfig {
598 max_depth: Some(5),
599 ..Default::default()
600 };
601 let lang = JsonPathLanguage::with_config(config);
602 let expr = lang.create_expression("$.a").unwrap();
603 let mut json = "1".to_string();
605 for _ in 0..10 {
606 json = format!(r#"{{"a":{json}}}"#);
607 }
608 let ex = exchange_with_text_body(&json).await;
609 let result = expr.evaluate(&ex).await;
610 assert!(
611 result.is_err(),
612 "expected error for deeply nested input, got {result:?}"
613 );
614 }
615
616 #[tokio::test]
617 async fn nesting_within_depth_limit_is_accepted() {
618 let config = JsonPathConfig {
619 max_depth: Some(10),
620 ..Default::default()
621 };
622 let lang = JsonPathLanguage::with_config(config);
623 let expr = lang.create_expression("$.a").unwrap();
624 let mut json = "1".to_string();
626 for _ in 0..5 {
627 json = format!(r#"{{"a":{json}}}"#);
628 }
629 let ex = exchange_with_text_body(&json).await;
630 let result = expr.evaluate(&ex).await;
631 assert!(
632 result.is_ok(),
633 "expected success for nesting within limit, got {result:?}"
634 );
635 }
636
637 #[tokio::test]
638 async fn default_config_has_safe_defaults() {
639 let config = JsonPathConfig::default();
641 assert_eq!(config.max_input_bytes, Some(16 * 1024 * 1024));
642 assert_eq!(config.max_depth, None);
644 }
645
646 #[tokio::test]
647 async fn default_config_rejects_oversized_text_body() {
648 let lang = JsonPathLanguage::new(); let expr = lang.create_expression("$.key").unwrap();
651 let big_value = "x".repeat(16 * 1024 * 1024 + 10);
652 let big_json = format!(r#"{{"key":"{}"}}"#, big_value);
653 let ex = exchange_with_text_body(&big_json).await;
654 let result = expr.evaluate(&ex).await;
655 assert!(
656 result.is_err(),
657 "default config must reject oversized input, got {result:?}"
658 );
659 }
660
661 #[tokio::test]
662 async fn oversized_input_also_rejected_for_predicate() {
663 let config = JsonPathConfig {
664 max_input_bytes: Some(100),
665 ..Default::default()
666 };
667 let lang = JsonPathLanguage::with_config(config);
668 let pred = lang.create_predicate("$.key").unwrap();
669 let big_value = "x".repeat(200);
670 let big_json = format!(r#"{{"key":"{}"}}"#, big_value);
671 let ex = exchange_with_text_body(&big_json).await;
672 let result = pred.matches(&ex).await;
673 assert!(
674 result.is_err(),
675 "expected error for oversized input in predicate, got {result:?}"
676 );
677 }
678
679 #[tokio::test]
680 async fn deeply_nested_input_rejected_for_predicate() {
681 let config = JsonPathConfig {
682 max_depth: Some(3),
683 ..Default::default()
684 };
685 let lang = JsonPathLanguage::with_config(config);
686 let pred = lang.create_predicate("$.a").unwrap();
687 let mut json = "1".to_string();
688 for _ in 0..5 {
689 json = format!(r#"{{"a":{json}}}"#);
690 }
691 let ex = exchange_with_text_body(&json).await;
692 let result = pred.matches(&ex).await;
693 assert!(
694 result.is_err(),
695 "expected error for deeply nested input in predicate, got {result:?}"
696 );
697 }
698
699 #[tokio::test]
700 async fn body_json_no_input_size_check_but_depth_checked() {
701 let config = JsonPathConfig {
704 max_input_bytes: Some(10), max_depth: Some(3),
706 predicate_mode: JsonPathPredicateMode::Strict,
707 };
708 let lang = JsonPathLanguage::with_config(config);
709 let expr = lang.create_expression("$.a").unwrap();
710 let mut json_str = "1".to_string();
712 for _ in 0..5 {
713 json_str = format!(r#"{{"a":{json_str}}}"#);
714 }
715 let ex = exchange_with_json(&json_str).await;
716 let result = expr.evaluate(&ex).await;
717 assert!(
718 result.is_err(),
719 "expected depth error for pre-parsed JSON, got {result:?}"
720 );
721 }
722
723 #[test]
729 fn compile_once_types_are_send_sync() {
730 fn assert_send_sync<T: Send + Sync>() {}
731 assert_send_sync::<JsonPathExpression>();
732 assert_send_sync::<JsonPathPredicate>();
733 assert_send_sync::<JpQuery>();
734 }
735
736 #[tokio::test]
745 async fn compilation_happens_only_once_for_expression() {
746 let before = COMPILE_COUNT.with(std::cell::Cell::get);
747 let lang = JsonPathLanguage::new();
748 let expr = lang.create_expression("$.foo.bar").unwrap();
749 let after_create = COMPILE_COUNT.with(std::cell::Cell::get);
750 assert_eq!(
751 after_create,
752 before + 1,
753 "create_expression must compile exactly once"
754 );
755
756 for _ in 0..50 {
758 let ex = exchange_with_json(r#"{"foo":{"bar":"baz"}}"#).await;
759 let result = expr.evaluate(&ex).await.unwrap();
760 assert_eq!(result, JsonValue::String("baz".to_string()));
761 }
762 assert_eq!(
763 COMPILE_COUNT.with(std::cell::Cell::get),
764 after_create,
765 "evaluate must NOT trigger re-compilation"
766 );
767 }
768
769 #[tokio::test]
770 async fn compilation_happens_only_once_for_predicate() {
771 let before = COMPILE_COUNT.with(std::cell::Cell::get);
772 let lang = JsonPathLanguage::new();
773 let pred = lang.create_predicate("$.active").unwrap();
774 let after_create = COMPILE_COUNT.with(std::cell::Cell::get);
775 assert_eq!(
776 after_create,
777 before + 1,
778 "create_predicate must compile exactly once"
779 );
780
781 for _ in 0..50 {
782 let ex = exchange_with_json(r#"{"active":true}"#).await;
783 assert!(pred.matches(&ex).await.unwrap());
784 }
785 assert_eq!(
786 COMPILE_COUNT.with(std::cell::Cell::get),
787 after_create,
788 "matches must NOT trigger re-compilation"
789 );
790 }
791
792 fn matches_mode_lang() -> JsonPathLanguage {
793 JsonPathLanguage::with_config(JsonPathConfig {
794 predicate_mode: JsonPathPredicateMode::Matches,
795 ..JsonPathConfig::default()
796 })
797 }
798
799 #[tokio::test]
800 async fn matches_mode_cardinality_table() {
801 let lang = matches_mode_lang();
807 let table = [
808 (
810 r#"{"items":[{"price":20},{"price":5}]}"#,
811 "$.items[?@.price>10]",
812 true,
813 ),
814 (r#"{"items":[{"price":20}]}"#, "$.items[?@.price>10]", true),
815 (r#"{"items":[{"price":5}]}"#, "$.items[?@.price>10]", false),
816 (r#"{"items":[1,2]}"#, "$.missing", false),
817 (r#"{"val":{}}"#, "$.val", true),
818 (r#"{"val":[]}"#, "$.val", true),
819 (r#"{"val":""}"#, "$.val", true),
820 (r#"{"val":"false"}"#, "$.val", true),
821 (r#"{"val":0}"#, "$.val", true),
822 (r#"{"val":null}"#, "$.val", true),
823 (r#"{"val":true}"#, "$.val", true),
824 (r#"{"val":false}"#, "$.val", false),
825 ];
826 for (body, query, expected) in table {
827 let pred = lang.create_predicate(query).unwrap();
828 let ex = exchange_with_json(body).await;
829 let got = pred.matches(&ex).await.unwrap_or_else(|e| {
830 panic!("matches mode must not error for {query} on {body}: {e:?}")
831 });
832 assert_eq!(got, expected, "query {query} on {body}");
833 }
834 }
835
836 #[tokio::test]
837 async fn strict_stays_default_and_repro_cases_remain_type_errors() {
838 let lang = default_lang().await;
841 let ex = exchange_with_json(r#"{"items":[{"price":20},{"price":5}]}"#).await;
842 let pred = lang.create_predicate("$.items[?@.price>10]").unwrap();
843 match pred.matches(&ex).await {
844 Err(LanguageError::TypeMismatch {
845 actual,
846 position: None,
847 ..
848 }) => {
849 assert_eq!(actual, "object");
852 }
853 other => panic!("strict default must stay a type error, got: {other:?}"),
854 }
855 let pred = lang.create_predicate("$.missing").unwrap();
856 match pred.matches(&ex).await {
857 Err(LanguageError::TypeMismatch {
858 actual,
859 position: None,
860 ..
861 }) => {
862 assert_eq!(actual, "null");
863 }
864 other => panic!("strict default must stay a type error, got: {other:?}"),
865 }
866 }
867
868 #[tokio::test]
869 async fn matches_mode_evaluation_failure_stays_typed_error() {
870 let lang = matches_mode_lang();
874 let pred = lang.create_predicate("$.val").unwrap();
875 let ex = exchange_with_text_body("not json at all").await;
876 assert!(
877 pred.matches(&ex).await.is_err(),
878 "evaluation failure must not coerce to false"
879 );
880 }
881}