camel_language_xpath/
lib.rs1#![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}