sup_xml_core/xpath/
bindings_builder.rs1use std::collections::HashMap;
34use std::sync::Arc;
35
36use crate::error::XmlError;
37use super::eval::{Value, XPathBindings};
38use super::index::NodeId;
39
40type BoxedFn = Arc<dyn Fn(Vec<Value>) -> Result<Value, XmlError> + Send + Sync>;
41
42#[derive(Clone, Default)]
47pub struct XPathBindingsBuilder {
48 namespaces: HashMap<String, String>,
49 variables: HashMap<String, Value>,
50 functions: HashMap<String, HashMap<String, BoxedFn>>,
53}
54
55impl XPathBindingsBuilder {
56 pub fn new() -> Self {
58 Self::default()
59 }
60
61 pub fn namespace(
65 &mut self,
66 prefix: impl Into<String>,
67 uri: impl Into<String>,
68 ) -> &mut Self {
69 self.namespaces.insert(prefix.into(), uri.into());
70 self
71 }
72
73 pub fn bind_variable(
81 &mut self,
82 name: impl Into<String>,
83 value: Value,
84 ) -> &mut Self {
85 self.variables.insert(name.into(), value);
86 self
87 }
88
89 pub fn function<F>(
95 &mut self,
96 ns_uri: impl Into<String>,
97 name: impl Into<String>,
98 f: F,
99 ) -> &mut Self
100 where
101 F: Fn(Vec<Value>) -> Result<Value, XmlError> + Send + Sync + 'static,
102 {
103 self.functions
104 .entry(ns_uri.into())
105 .or_default()
106 .insert(name.into(), Arc::new(f));
107 self
108 }
109}
110
111impl XPathBindings for XPathBindingsBuilder {
112 fn resolve_prefix(&self, prefix: &str) -> Option<String> {
113 self.namespaces.get(prefix).cloned()
114 }
115 fn variable(&self, name: &str) -> Option<Value> {
116 if let Some(v) = self.variables.get(name) { return Some(v.clone()); }
120 if let Some((prefix, local)) = name.split_once(':') {
121 if let Some(uri) = self.namespaces.get(prefix) {
122 let clark = format!("{{{uri}}}{local}");
123 if let Some(v) = self.variables.get(&clark) { return Some(v.clone()); }
124 }
125 }
126 None
127 }
128 fn call_function(
129 &self, ns_uri: &str, name: &str, args: Vec<Value>,
130 ) -> Option<Result<Value, XmlError>> {
131 let f = self.functions.get(ns_uri)?.get(name)?;
132 Some(f(args))
133 }
134 fn call_function_in(
135 &self, ns_uri: &str, name: &str, args: Vec<Value>,
136 _xpath_context_node: NodeId,
137 ) -> Option<Result<Value, XmlError>> {
138 self.call_function(ns_uri, name, args)
139 }
140}
141
142impl std::fmt::Debug for XPathBindingsBuilder {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 let mut fn_names: Vec<String> = Vec::new();
145 for (ns, m) in &self.functions {
146 for k in m.keys() {
147 fn_names.push(format!("{{{ns}}}{k}"));
148 }
149 }
150 fn_names.sort();
151 f.debug_struct("XPathBindingsBuilder")
152 .field("namespaces", &self.namespaces)
153 .field("variables", &self.variables.keys().collect::<Vec<_>>())
154 .field("functions", &fn_names)
155 .finish()
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use super::super::eval::Numeric;
163 use crate::{parse_str, ParseOptions, XPathContext};
164
165 fn doc() -> sup_xml_tree::dom::Document {
166 parse_str("<r><n>1</n><n>2</n><n>3</n></r>", &ParseOptions::default()).unwrap()
167 }
168
169 #[test]
170 fn function_registered_and_called() {
171 let d = doc();
172 let ctx = XPathContext::new(&d);
173 let mut b = XPathBindingsBuilder::new();
174 b.namespace("my", "urn:my:ns")
175 .function("urn:my:ns", "square", |args| {
176 let n = match args.into_iter().next() {
177 Some(Value::Number(n)) => n.as_f64(),
178 _ => return Ok(Value::Number(Numeric::Double(f64::NAN))),
179 };
180 Ok(Value::Number(Numeric::Double(n * n)))
181 });
182 let v = ctx.eval_with("my:square(7)", 0, &b).unwrap();
183 match v { Value::Number(n) => assert_eq!(n.as_f64(), 49.0), _ => panic!() }
184 }
185
186 #[test]
187 fn variable_resolved() {
188 let d = doc();
189 let ctx = XPathContext::new(&d);
190 let mut b = XPathBindingsBuilder::new();
191 b.bind_variable("answer", Value::Number(Numeric::Double(42.0)));
192 let v = ctx.eval_with("$answer + 1", 0, &b).unwrap();
193 match v { Value::Number(n) => assert_eq!(n.as_f64(), 43.0), _ => panic!() }
194 }
195
196 #[test]
197 fn namespace_prefix_resolves() {
198 let d = parse_str(
199 r#"<r xmlns:x="urn:x"><x:item>found</x:item></r>"#,
200 &ParseOptions { namespace_aware: true, ..Default::default() },
201 ).unwrap();
202 let ctx = XPathContext::new(&d);
203 let mut b = XPathBindingsBuilder::new();
204 b.namespace("x", "urn:x");
205 let v = ctx.eval_with("string(/r/x:item)", 0, &b).unwrap();
206 match v { Value::String(s) => assert_eq!(s, "found"), _ => panic!() }
207 }
208
209 #[test]
210 fn function_propagates_error() {
211 use crate::error::{ErrorDomain, ErrorLevel};
212 let d = doc();
213 let ctx = XPathContext::new(&d);
214 let mut b = XPathBindingsBuilder::new();
215 b.namespace("my", "urn:my:ns")
216 .function("urn:my:ns", "fail", |_args| {
217 Err(XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, "boom"))
218 });
219 let r = ctx.eval_with("my:fail()", 0, &b);
220 assert!(r.is_err());
221 assert_eq!(r.unwrap_err().message, "boom");
222 }
223
224 #[test]
225 fn unregistered_function_falls_through_to_unknown_error() {
226 let d = doc();
227 let ctx = XPathContext::new(&d);
228 let mut b = XPathBindingsBuilder::new();
229 b.namespace("my", "urn:my:ns");
230 let r = ctx.eval_with("my:missing()", 0, &b);
231 assert!(r.is_err());
232 }
233}