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