Skip to main content

camel_language_xpath/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use camel_language_api::{Body, Exchange, Value};
4use camel_language_api::{Expression, Language, LanguageError, Predicate};
5use serde_json::Value as JsonValue;
6use sxd_document::parser;
7use sxd_xpath::{Context, Factory, Value as SxdValue};
8
9pub struct XPathLanguage;
10
11struct XPathExpression {
12    query: String,
13}
14
15struct XPathPredicate {
16    query: String,
17}
18
19fn extract_xml(exchange: &Exchange) -> Result<String, LanguageError> {
20    match &exchange.input.body {
21        Body::Xml(s) => Ok(s.clone()),
22        other => other
23            .clone()
24            .try_into_xml()
25            .map_err(|e| {
26                LanguageError::EvalError(format!("body is not XML and cannot be coerced: {e}"))
27            })
28            .and_then(|b| match b {
29                Body::Xml(s) => Ok(s),
30                _ => Err(LanguageError::EvalError(
31                    "body coercion did not produce XML".into(),
32                )),
33            }),
34    }
35}
36
37fn compile_xpath(query: &str) -> Result<sxd_xpath::XPath, LanguageError> {
38    let factory = Factory::new();
39    factory
40        .build(query)
41        .map_err(|e| LanguageError::ParseError {
42            expr: query.to_string(),
43            reason: e.to_string(),
44        })
45        .and_then(|opt| {
46            opt.ok_or_else(|| LanguageError::ParseError {
47                expr: query.to_string(),
48                reason: "empty XPath expression".into(),
49            })
50        })
51}
52
53fn run_query(query: &str, xml: &str) -> Result<JsonValue, LanguageError> {
54    let package = parser::parse(xml).map_err(|e| {
55        LanguageError::EvalError(format!("xml parse error for xpath '{query}': {e}"))
56    })?;
57    let doc = package.as_document();
58    let xpath = compile_xpath(query)?;
59    let context = Context::new();
60    let result = xpath
61        .evaluate(&context, doc.root())
62        .map_err(|e| LanguageError::EvalError(format!("xpath query '{query}' failed: {e}")))?;
63
64    Ok(match result {
65        SxdValue::Nodeset(ns) => {
66            let nodes: Vec<_> = ns.document_order();
67            match nodes.len() {
68                0 => JsonValue::Null,
69                1 => JsonValue::String(nodes[0].string_value()),
70                _ => JsonValue::Array(
71                    nodes
72                        .into_iter()
73                        .map(|n| JsonValue::String(n.string_value()))
74                        .collect(),
75                ),
76            }
77        }
78        SxdValue::Boolean(b) => JsonValue::Bool(b),
79        SxdValue::Number(n) => serde_json::Number::from_f64(n)
80            .map(JsonValue::Number)
81            .unwrap_or(JsonValue::Null),
82        SxdValue::String(s) => JsonValue::String(s),
83    })
84}
85
86impl Expression for XPathExpression {
87    fn evaluate(&self, exchange: &Exchange) -> Result<Value, LanguageError> {
88        let xml = extract_xml(exchange)?;
89        run_query(&self.query, &xml)
90    }
91}
92
93impl Predicate for XPathPredicate {
94    fn matches(&self, exchange: &Exchange) -> Result<bool, LanguageError> {
95        let xml = extract_xml(exchange)?;
96        let result = run_query(&self.query, &xml)?;
97        Ok(match &result {
98            JsonValue::Null => false,
99            JsonValue::Bool(b) => *b,
100            JsonValue::Number(n) => n.as_f64().is_some_and(|f| f != 0.0),
101            JsonValue::String(s) => !s.is_empty(),
102            JsonValue::Array(arr) => !arr.is_empty(),
103            _ => true,
104        })
105    }
106}
107
108impl Language for XPathLanguage {
109    fn name(&self) -> &'static str {
110        "xpath"
111    }
112
113    fn create_expression(&self, script: &str) -> Result<Box<dyn Expression>, LanguageError> {
114        compile_xpath(script)?;
115        Ok(Box::new(XPathExpression {
116            query: script.to_string(),
117        }))
118    }
119
120    fn create_predicate(&self, script: &str) -> Result<Box<dyn Predicate>, LanguageError> {
121        compile_xpath(script)?;
122        Ok(Box::new(XPathPredicate {
123            query: script.to_string(),
124        }))
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use camel_language_api::Message;
132
133    fn exchange_with_xml(xml: &str) -> Exchange {
134        Exchange::new(Message::new(Body::Xml(xml.to_string())))
135    }
136
137    fn exchange_with_text_body(text: &str) -> Exchange {
138        Exchange::new(Message::new(Body::Text(text.to_string())))
139    }
140
141    fn empty_exchange() -> Exchange {
142        Exchange::new(Message::default())
143    }
144
145    #[test]
146    fn expression_simple_path() {
147        let lang = XPathLanguage;
148        let expr = lang.create_expression("/root/name").unwrap();
149        let ex = exchange_with_xml("<root><name>books</name></root>");
150        let result = expr.evaluate(&ex).unwrap();
151        assert_eq!(result, JsonValue::String("books".to_string()));
152    }
153
154    #[test]
155    fn expression_nested_path() {
156        let lang = XPathLanguage;
157        let expr = lang.create_expression("/root/inner/value").unwrap();
158        let ex = exchange_with_xml("<root><inner><value>42</value></inner></root>");
159        let result = expr.evaluate(&ex).unwrap();
160        assert_eq!(result, JsonValue::String("42".to_string()));
161    }
162
163    #[test]
164    fn expression_attribute_access() {
165        let lang = XPathLanguage;
166        let expr = lang.create_expression("/root/item/@id").unwrap();
167        let ex = exchange_with_xml("<root><item id=\"123\"/></root>");
168        let result = expr.evaluate(&ex).unwrap();
169        assert_eq!(result, JsonValue::String("123".to_string()));
170    }
171
172    #[test]
173    fn expression_text_function() {
174        let lang = XPathLanguage;
175        let expr = lang.create_expression("/root/name/text()").unwrap();
176        let ex = exchange_with_xml("<root><name>hello</name></root>");
177        let result = expr.evaluate(&ex).unwrap();
178        assert_eq!(result, JsonValue::String("hello".to_string()));
179    }
180
181    #[test]
182    fn expression_wildcard() {
183        let lang = XPathLanguage;
184        let expr = lang.create_expression("/root/item").unwrap();
185        let ex = exchange_with_xml("<root><item>a</item><item>b</item></root>");
186        let result = expr.evaluate(&ex).unwrap();
187        assert_eq!(
188            result,
189            JsonValue::Array(vec![
190                JsonValue::String("a".to_string()),
191                JsonValue::String("b".to_string()),
192            ])
193        );
194    }
195
196    #[test]
197    fn expression_predicate_position() {
198        let lang = XPathLanguage;
199        let expr = lang.create_expression("/root/item[2]").unwrap();
200        let ex = exchange_with_xml("<root><item>a</item><item>b</item><item>c</item></root>");
201        let result = expr.evaluate(&ex).unwrap();
202        assert_eq!(result, JsonValue::String("b".to_string()));
203    }
204
205    #[test]
206    fn expression_count_function() {
207        let lang = XPathLanguage;
208        let expr = lang.create_expression("count(/root/item)").unwrap();
209        let ex = exchange_with_xml("<root><item>a</item><item>b</item></root>");
210        let result = expr.evaluate(&ex).unwrap();
211        assert_eq!(
212            result,
213            JsonValue::Number(serde_json::Number::from_f64(2.0).unwrap())
214        );
215    }
216
217    #[test]
218    fn expression_text_body_with_valid_xml() {
219        let lang = XPathLanguage;
220        let expr = lang.create_expression("/root/value").unwrap();
221        let ex = exchange_with_text_body("<root><value>test</value></root>");
222        let result = expr.evaluate(&ex).unwrap();
223        assert_eq!(result, JsonValue::String("test".to_string()));
224    }
225
226    #[test]
227    fn expression_text_body_with_invalid_xml() {
228        let lang = XPathLanguage;
229        let expr = lang.create_expression("/root").unwrap();
230        let ex = exchange_with_text_body("not xml at all");
231        let result = expr.evaluate(&ex);
232        assert!(result.is_err());
233    }
234
235    #[test]
236    fn expression_empty_body_is_error() {
237        let lang = XPathLanguage;
238        let expr = lang.create_expression("/root").unwrap();
239        let ex = empty_exchange();
240        let result = expr.evaluate(&ex);
241        assert!(result.is_err());
242    }
243
244    #[test]
245    fn expression_empty_result_is_null() {
246        let lang = XPathLanguage;
247        let expr = lang.create_expression("/root/missing").unwrap();
248        let ex = exchange_with_xml("<root><name>test</name></root>");
249        let result = expr.evaluate(&ex).unwrap();
250        assert_eq!(result, JsonValue::Null);
251    }
252
253    #[test]
254    fn expression_invalid_xpath_syntax() {
255        let lang = XPathLanguage;
256        let result = lang.create_expression("//[invalid");
257        let err = match result {
258            Err(e) => e,
259            Ok(_) => panic!("expected ParseError"),
260        };
261        match err {
262            LanguageError::ParseError { expr, reason } => {
263                assert!(!expr.is_empty());
264                assert!(!reason.is_empty());
265            }
266            other => panic!("expected ParseError, got {other:?}"),
267        }
268    }
269
270    #[test]
271    fn predicate_non_empty_nodeset_is_true() {
272        let lang = XPathLanguage;
273        let pred = lang.create_predicate("/root/item").unwrap();
274        let ex = exchange_with_xml("<root><item>a</item><item>b</item></root>");
275        assert!(pred.matches(&ex).unwrap());
276    }
277
278    #[test]
279    fn predicate_empty_result_is_false() {
280        let lang = XPathLanguage;
281        let pred = lang.create_predicate("/root/missing").unwrap();
282        let ex = exchange_with_xml("<root><name>test</name></root>");
283        assert!(!pred.matches(&ex).unwrap());
284    }
285
286    #[test]
287    fn predicate_boolean_expression() {
288        let lang = XPathLanguage;
289        let pred = lang.create_predicate("count(/root/item) > 2").unwrap();
290        let ex = exchange_with_xml("<root><item>a</item><item>b</item><item>c</item></root>");
291        assert!(pred.matches(&ex).unwrap());
292    }
293
294    #[test]
295    fn predicate_numeric_comparison_false() {
296        let lang = XPathLanguage;
297        let pred = lang.create_predicate("count(/root/item) > 5").unwrap();
298        let ex = exchange_with_xml("<root><item>a</item></root>");
299        assert!(!pred.matches(&ex).unwrap());
300    }
301}