use crate::error::{ErrorDomain, ErrorLevel, XmlError};
use crate::xpath::eval::{Value, value_to_string};
use crate::xpath::index::DocIndexLike;
use super::Result;
fn err(msg: impl Into<String>) -> XmlError {
XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, msg)
}
pub fn dispatch<I: DocIndexLike>(
name: &str, args: Vec<Value>, idx: &I,
) -> Option<Result<Value>> {
match name {
"node-set" => Some(node_set_fn(&args, idx)),
"object-type" => Some(object_type_fn(&args, idx)),
_ => None,
}
}
fn node_set_fn<I: DocIndexLike>(args: &[Value], idx: &I) -> Result<Value> {
if args.len() != 1 {
return Err(err("exsl:node-set takes 1 argument"));
}
match &args[0] {
Value::NodeSet(_) | Value::ForeignNodeSet(_) => Ok(args[0].clone()),
scalar => {
let s = value_to_string(scalar, idx);
let ids = idx.allocate_rtf_text_nodes(vec![s]).ok_or_else(|| err(
"exsl:node-set: this XPath context does not support RTF allocation",
))?;
Ok(Value::NodeSet(ids))
}
}
}
fn object_type_fn<I: DocIndexLike>(args: &[Value], _idx: &I) -> Result<Value> {
if args.len() != 1 {
return Err(err("exsl:object-type takes 1 argument"));
}
let label = match &args[0] {
Value::NodeSet(_) | Value::ForeignNodeSet(_) => "node-set",
Value::String(_) => "string",
Value::Number(_) => "number",
Value::Boolean(_) => "boolean",
Value::Typed(t) => {
if t.numeric.is_some() { "number" }
else if t.boolean.is_some() { "boolean" }
else { "string" }
}
Value::Sequence(_) | Value::IntRange { .. } => "node-set",
Value::Map(_) => "map",
Value::Array(_) => "array",
Value::Function(_) => "function",
};
Ok(Value::String(label.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xpath::eval::Numeric;
use crate::xpath::XPathContext;
use crate::{parse_str, ParseOptions};
fn doc() -> sup_xml_tree::dom::Document {
parse_str("<r><a/><b/></r>", &ParseOptions::default()).unwrap()
}
#[test]
fn node_set_passes_nodeset_through() {
let d = doc();
let ctx = XPathContext::new(&d);
let ns = ctx.eval("/r/*").unwrap();
let original_len = match &ns { Value::NodeSet(n) => n.len(), _ => panic!() };
let r = dispatch("node-set", vec![ns], &ctx.index).unwrap().unwrap();
match r {
Value::NodeSet(n) => assert_eq!(n.len(), original_len),
_ => panic!("expected node-set"),
}
}
#[test]
fn node_set_wraps_string_in_single_text_node() {
let d = doc();
let ctx = XPathContext::new(&d);
let r = dispatch("node-set",
vec![Value::String("hello".into())], &ctx.index).unwrap().unwrap();
let ns = match r { Value::NodeSet(ns) => ns, _ => panic!() };
assert_eq!(ns.len(), 1);
assert_eq!(ctx.index.string_value(ns[0]), "hello");
}
#[test]
fn node_set_wraps_number_in_text_node_with_xpath_string_form() {
let d = doc();
let ctx = XPathContext::new(&d);
let r = dispatch("node-set",
vec![Value::Number(Numeric::Double(42.0))], &ctx.index).unwrap().unwrap();
let ns = match r { Value::NodeSet(ns) => ns, _ => panic!() };
assert_eq!(ctx.index.string_value(ns[0]), "42");
}
#[test]
fn object_type_returns_correct_label_per_variant() {
let d = doc();
let ctx = XPathContext::new(&d);
let cases: &[(Value, &str)] = &[
(Value::String("x".into()), "string"),
(Value::Number(Numeric::Double(1.0)), "number"),
(Value::Boolean(true), "boolean"),
(Value::NodeSet(vec![]), "node-set"),
];
for (arg, expected) in cases {
let r = dispatch("object-type", vec![arg.clone()], &ctx.index)
.unwrap().unwrap();
assert!(matches!(r, Value::String(ref s) if s == expected),
"got {r:?} for arg {arg:?}");
}
}
#[test]
fn unknown_function_returns_none() {
let d = doc();
let ctx = XPathContext::new(&d);
assert!(dispatch("nonsense", vec![], &ctx.index).is_none());
}
#[test]
fn dyn_evaluate_runs_string_as_xpath_against_context() {
use crate::xpath::XPathBindingsBuilder;
let d = parse_str(
"<r><a>alpha</a><a>beta</a><a>gamma</a></r>",
&ParseOptions::default(),
).unwrap();
let ctx = crate::xpath::XPathContext::new(&d);
let mut bind = XPathBindingsBuilder::new();
bind.namespace("dyn", "http://exslt.org/dynamic");
let v = ctx.eval_with("dyn:evaluate('count(/r/a)')", 0, &bind).unwrap();
assert_eq!(crate::xpath::eval::value_to_number(&v, &ctx.index), 3.0);
}
#[test]
fn dyn_evaluate_invalid_expression_yields_empty_nodeset() {
use crate::xpath::XPathBindingsBuilder;
let d = doc();
let ctx = crate::xpath::XPathContext::new(&d);
let mut bind = XPathBindingsBuilder::new();
bind.namespace("dyn", "http://exslt.org/dynamic");
let v = ctx.eval_with("dyn:evaluate('not a valid xpath /// ')", 0, &bind).unwrap();
assert!(matches!(v, Value::NodeSet(ref ns) if ns.is_empty()));
}
}