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`, `JsonPathExpression`, `JsonPathPredicate`.
4//! Provides expressions and predicates for JSON-based routing and transformation.
5
6use camel_language_api::{Body, Exchange, Value};
7use camel_language_api::{Expression, Language, LanguageError, Predicate};
8use jsonpath_rust::JsonPath;
9use serde_json::Value as JsonValue;
10
11pub struct JsonPathLanguage;
12
13struct JsonPathExpression {
14    query: String,
15}
16
17struct JsonPathPredicate {
18    query: String,
19}
20
21fn extract_json(exchange: &Exchange) -> Result<JsonValue, LanguageError> {
22    match &exchange.input.body {
23        Body::Json(v) => Ok(v.clone()),
24        other => other
25            .clone()
26            .try_into_json()
27            .map_err(|e| {
28                LanguageError::EvalError(format!("body is not JSON and cannot be coerced: {e}"))
29            })
30            .and_then(|b| match b {
31                Body::Json(v) => Ok(v),
32                _ => Err(LanguageError::EvalError(
33                    "body coercion did not produce JSON".into(),
34                )),
35            }),
36    }
37}
38
39fn run_query(query: &str, json: &JsonValue) -> Result<JsonValue, LanguageError> {
40    json.query(query)
41        .map_err(|e| LanguageError::EvalError(format!("jsonpath query '{query}' failed: {e}")))
42        .map(|results| match results.len() {
43            0 => JsonValue::Null,
44            1 => results[0].clone(),
45            _ => JsonValue::Array(results.into_iter().cloned().collect()),
46        })
47}
48
49impl Expression for JsonPathExpression {
50    fn evaluate(&self, exchange: &Exchange) -> Result<Value, LanguageError> {
51        let json = extract_json(exchange)?;
52        run_query(&self.query, &json)
53    }
54}
55
56impl Predicate for JsonPathPredicate {
57    fn matches(&self, exchange: &Exchange) -> Result<bool, LanguageError> {
58        let json = extract_json(exchange)?;
59        let result = run_query(&self.query, &json)?;
60        Ok(match &result {
61            JsonValue::Null => false,
62            JsonValue::Bool(b) => *b,
63            JsonValue::Array(arr) => !arr.is_empty(),
64            _ => true,
65        })
66    }
67}
68
69impl Language for JsonPathLanguage {
70    fn name(&self) -> &'static str {
71        "jsonpath"
72    }
73
74    fn create_expression(&self, script: &str) -> Result<Box<dyn Expression>, LanguageError> {
75        let empty = JsonValue::Object(serde_json::Map::new());
76        empty.query(script).map_err(|e| LanguageError::ParseError {
77            expr: script.to_string(),
78            reason: e.to_string(),
79        })?;
80        Ok(Box::new(JsonPathExpression {
81            query: script.to_string(),
82        }))
83    }
84
85    fn create_predicate(&self, script: &str) -> Result<Box<dyn Predicate>, LanguageError> {
86        let empty = JsonValue::Object(serde_json::Map::new());
87        empty.query(script).map_err(|e| LanguageError::ParseError {
88            expr: script.to_string(),
89            reason: e.to_string(),
90        })?;
91        Ok(Box::new(JsonPathPredicate {
92            query: script.to_string(),
93        }))
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use camel_language_api::Message;
101
102    fn exchange_with_json(json: &str) -> Exchange {
103        let value: JsonValue = serde_json::from_str(json).unwrap();
104        Exchange::new(Message::new(Body::Json(value)))
105    }
106
107    fn exchange_with_text_body(text: &str) -> Exchange {
108        Exchange::new(Message::new(Body::Text(text.to_string())))
109    }
110
111    fn empty_exchange() -> Exchange {
112        Exchange::new(Message::default())
113    }
114
115    #[test]
116    fn expression_simple_path() {
117        let lang = JsonPathLanguage;
118        let expr = lang.create_expression("$.store.name").unwrap();
119        let ex = exchange_with_json(r#"{"store":{"name":"books"}}"#);
120        let result = expr.evaluate(&ex).unwrap();
121        assert_eq!(result, JsonValue::String("books".to_string()));
122    }
123
124    #[test]
125    fn expression_nested_path() {
126        let lang = JsonPathLanguage;
127        let expr = lang.create_expression("$.a.b.c").unwrap();
128        let ex = exchange_with_json(r#"{"a":{"b":{"c":42}}}"#);
129        let result = expr.evaluate(&ex).unwrap();
130        assert_eq!(result, JsonValue::Number(42.into()));
131    }
132
133    #[test]
134    fn expression_array_index() {
135        let lang = JsonPathLanguage;
136        let expr = lang.create_expression("$.items[0]").unwrap();
137        let ex = exchange_with_json(r#"{"items":["a","b","c"]}"#);
138        let result = expr.evaluate(&ex).unwrap();
139        assert_eq!(result, JsonValue::String("a".to_string()));
140    }
141
142    #[test]
143    fn expression_wildcard() {
144        let lang = JsonPathLanguage;
145        let expr = lang.create_expression("$.items[*].name").unwrap();
146        let ex = exchange_with_json(r#"{"items":[{"name":"a"},{"name":"b"}]}"#);
147        let result = expr.evaluate(&ex).unwrap();
148        assert_eq!(
149            result,
150            JsonValue::Array(vec![
151                JsonValue::String("a".to_string()),
152                JsonValue::String("b".to_string())
153            ])
154        );
155    }
156
157    #[test]
158    fn expression_root_path() {
159        let lang = JsonPathLanguage;
160        let expr = lang.create_expression("$").unwrap();
161        let ex = exchange_with_json(r#"{"x":1}"#);
162        let result = expr.evaluate(&ex).unwrap();
163        assert_eq!(result["x"], JsonValue::Number(1.into()));
164    }
165
166    #[test]
167    fn expression_text_body_with_valid_json() {
168        let lang = JsonPathLanguage;
169        let expr = lang.create_expression("$.name").unwrap();
170        let ex = exchange_with_text_body(r#"{"name":"test"}"#);
171        let result = expr.evaluate(&ex).unwrap();
172        assert_eq!(result, JsonValue::String("test".to_string()));
173    }
174
175    #[test]
176    fn expression_empty_body_is_error() {
177        let lang = JsonPathLanguage;
178        let expr = lang.create_expression("$.x").unwrap();
179        let ex = empty_exchange();
180        let result = expr.evaluate(&ex);
181        assert!(result.is_err());
182    }
183
184    #[test]
185    fn expression_invalid_jsonpath_syntax() {
186        let lang = JsonPathLanguage;
187        let result = lang.create_expression("$[invalid");
188        let err = match result {
189            Err(e) => e,
190            Ok(_) => panic!("expected ParseError"),
191        };
192        match err {
193            LanguageError::ParseError { expr, reason } => {
194                assert!(!expr.is_empty());
195                assert!(!reason.is_empty());
196            }
197            other => panic!("expected ParseError, got {other:?}"),
198        }
199    }
200
201    #[test]
202    fn predicate_non_empty_array_is_true() {
203        let lang = JsonPathLanguage;
204        let pred = lang.create_predicate("$.items[*]").unwrap();
205        let ex = exchange_with_json(r#"{"items":[1,2,3]}"#);
206        assert!(pred.matches(&ex).unwrap());
207    }
208
209    #[test]
210    fn predicate_empty_result_is_false() {
211        let lang = JsonPathLanguage;
212        let pred = lang.create_predicate("$.missing").unwrap();
213        let ex = exchange_with_json(r#"{"other":1}"#);
214        assert!(!pred.matches(&ex).unwrap());
215    }
216
217    #[test]
218    fn predicate_boolean_true() {
219        let lang = JsonPathLanguage;
220        let pred = lang.create_predicate("$.active").unwrap();
221        let ex = exchange_with_json(r#"{"active":true}"#);
222        assert!(pred.matches(&ex).unwrap());
223    }
224
225    #[test]
226    fn predicate_boolean_false() {
227        let lang = JsonPathLanguage;
228        let pred = lang.create_predicate("$.active").unwrap();
229        let ex = exchange_with_json(r#"{"active":false}"#);
230        assert!(!pred.matches(&ex).unwrap());
231    }
232
233    #[test]
234    fn predicate_found_value_is_true() {
235        let lang = JsonPathLanguage;
236        let pred = lang.create_predicate("$.name").unwrap();
237        let ex = exchange_with_json(r#"{"name":"test"}"#);
238        assert!(pred.matches(&ex).unwrap());
239    }
240}