sup_xml_core/xpath/exslt/
common.rs1use crate::error::{ErrorDomain, ErrorLevel, XmlError};
18use crate::xpath::eval::{Value, value_to_string};
19use crate::xpath::index::DocIndexLike;
20
21use super::Result;
22
23fn err(msg: impl Into<String>) -> XmlError {
24 XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, msg)
25}
26
27pub fn dispatch<I: DocIndexLike>(
28 name: &str, args: Vec<Value>, idx: &I,
29) -> Option<Result<Value>> {
30 match name {
31 "node-set" => Some(node_set_fn(&args, idx)),
32 "object-type" => Some(object_type_fn(&args, idx)),
33 _ => None,
34 }
35}
36
37fn node_set_fn<I: DocIndexLike>(args: &[Value], idx: &I) -> Result<Value> {
52 if args.len() != 1 {
53 return Err(err("exsl:node-set takes 1 argument"));
54 }
55 match &args[0] {
56 Value::NodeSet(_) | Value::ForeignNodeSet(_) => Ok(args[0].clone()),
57 scalar => {
58 let s = value_to_string(scalar, idx);
59 let ids = idx.allocate_rtf_text_nodes(vec![s]).ok_or_else(|| err(
60 "exsl:node-set: this XPath context does not support RTF allocation",
61 ))?;
62 Ok(Value::NodeSet(ids))
63 }
64 }
65}
66
67fn object_type_fn<I: DocIndexLike>(args: &[Value], _idx: &I) -> Result<Value> {
80 if args.len() != 1 {
81 return Err(err("exsl:object-type takes 1 argument"));
82 }
83 let label = match &args[0] {
84 Value::NodeSet(_) | Value::ForeignNodeSet(_) => "node-set",
85 Value::String(_) => "string",
86 Value::Number(_) => "number",
87 Value::Boolean(_) => "boolean",
88 Value::Typed(t) => {
89 if t.numeric.is_some() { "number" }
94 else if t.boolean.is_some() { "boolean" }
95 else { "string" }
96 }
97 Value::Sequence(_) | Value::IntRange { .. } => "node-set",
101 Value::Map(_) => "map",
102 Value::Array(_) => "array",
103 Value::Function(_) => "function",
104 };
105 Ok(Value::String(label.to_string()))
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::xpath::eval::Numeric;
112 use crate::xpath::XPathContext;
113 use crate::{parse_str, ParseOptions};
114
115 fn doc() -> sup_xml_tree::dom::Document {
116 parse_str("<r><a/><b/></r>", &ParseOptions::default()).unwrap()
117 }
118
119 #[test]
120 fn node_set_passes_nodeset_through() {
121 let d = doc();
122 let ctx = XPathContext::new(&d);
123 let ns = ctx.eval("/r/*").unwrap();
124 let original_len = match &ns { Value::NodeSet(n) => n.len(), _ => panic!() };
125 let r = dispatch("node-set", vec![ns], &ctx.index).unwrap().unwrap();
126 match r {
127 Value::NodeSet(n) => assert_eq!(n.len(), original_len),
128 _ => panic!("expected node-set"),
129 }
130 }
131
132 #[test]
133 fn node_set_wraps_string_in_single_text_node() {
134 let d = doc();
135 let ctx = XPathContext::new(&d);
136 let r = dispatch("node-set",
137 vec![Value::String("hello".into())], &ctx.index).unwrap().unwrap();
138 let ns = match r { Value::NodeSet(ns) => ns, _ => panic!() };
139 assert_eq!(ns.len(), 1);
140 assert_eq!(ctx.index.string_value(ns[0]), "hello");
141 }
142
143 #[test]
144 fn node_set_wraps_number_in_text_node_with_xpath_string_form() {
145 let d = doc();
146 let ctx = XPathContext::new(&d);
147 let r = dispatch("node-set",
148 vec![Value::Number(Numeric::Double(42.0))], &ctx.index).unwrap().unwrap();
149 let ns = match r { Value::NodeSet(ns) => ns, _ => panic!() };
150 assert_eq!(ctx.index.string_value(ns[0]), "42");
151 }
152
153 #[test]
154 fn object_type_returns_correct_label_per_variant() {
155 let d = doc();
156 let ctx = XPathContext::new(&d);
157 let cases: &[(Value, &str)] = &[
158 (Value::String("x".into()), "string"),
159 (Value::Number(Numeric::Double(1.0)), "number"),
160 (Value::Boolean(true), "boolean"),
161 (Value::NodeSet(vec![]), "node-set"),
162 ];
163 for (arg, expected) in cases {
164 let r = dispatch("object-type", vec![arg.clone()], &ctx.index)
165 .unwrap().unwrap();
166 assert!(matches!(r, Value::String(ref s) if s == expected),
167 "got {r:?} for arg {arg:?}");
168 }
169 }
170
171 #[test]
172 fn unknown_function_returns_none() {
173 let d = doc();
174 let ctx = XPathContext::new(&d);
175 assert!(dispatch("nonsense", vec![], &ctx.index).is_none());
176 }
177
178 #[test]
181 fn dyn_evaluate_runs_string_as_xpath_against_context() {
182 use crate::xpath::XPathBindingsBuilder;
183 let d = parse_str(
184 "<r><a>alpha</a><a>beta</a><a>gamma</a></r>",
185 &ParseOptions::default(),
186 ).unwrap();
187 let ctx = crate::xpath::XPathContext::new(&d);
188 let mut bind = XPathBindingsBuilder::new();
189 bind.namespace("dyn", "http://exslt.org/dynamic");
190 let v = ctx.eval_with("dyn:evaluate('count(/r/a)')", 0, &bind).unwrap();
191 assert_eq!(crate::xpath::eval::value_to_number(&v, &ctx.index), 3.0);
192 }
193
194 #[test]
195 fn dyn_evaluate_invalid_expression_yields_empty_nodeset() {
196 use crate::xpath::XPathBindingsBuilder;
197 let d = doc();
198 let ctx = crate::xpath::XPathContext::new(&d);
199 let mut bind = XPathBindingsBuilder::new();
200 bind.namespace("dyn", "http://exslt.org/dynamic");
201 let v = ctx.eval_with("dyn:evaluate('not a valid xpath /// ')", 0, &bind).unwrap();
202 assert!(matches!(v, Value::NodeSet(ref ns) if ns.is_empty()));
203 }
204}