use num_bigint::BigInt;
use crate::namespace::qname::QualifiedName;
use super::arena::{AstArena, AstNodeId, SourceSpan};
use super::bind::bind_node;
use super::context::{DynamicContext, NameBinder, VarSlotId, XPathContext};
use super::error::XPathError;
use super::eval::eval_node;
use super::functions::{effective_boolean_value, XPathValue};
use super::iterator::XmlItem;
use super::parser::{parse, parse_with_mode};
use super::DomNavigator;
#[derive(Debug, Clone)]
pub struct ExternalVar {
pub name: QualifiedName,
pub slot: VarSlotId,
}
#[derive(Debug, Clone)]
pub struct XPathExpr {
source: String,
arena: AstArena,
root: AstNodeId,
span: SourceSpan,
var_slots: usize,
external_vars: Vec<ExternalVar>,
}
impl XPathExpr {
pub fn compile(expr: &str, ctx: &XPathContext<'_>) -> Result<Self, XPathError> {
Self::compile_with_vars(expr, ctx, &[])
}
pub fn compile_with_vars(
expr: &str,
ctx: &XPathContext<'_>,
vars: &[&str],
) -> Result<Self, XPathError> {
let parsed = if ctx.mode() == super::XPathMode::XPath20 {
parse(expr)?
} else {
parse_with_mode(expr, ctx.mode())?
};
let mut arena = parsed.arena;
let root = parsed.root;
let span = parsed.span;
let mut binder = NameBinder::new();
let mut external_vars = Vec::with_capacity(vars.len());
for var_name in vars {
let qname = parse_variable_name(var_name, ctx)?;
if external_vars.iter().any(|v: &ExternalVar| v.name == qname) {
return Err(XPathError::XPST0003 {
message: format!("Duplicate external variable declaration: ${}", var_name),
});
}
let var_ref = binder.push_var(qname.clone());
external_vars.push(ExternalVar {
name: qname,
slot: var_ref.slot,
});
}
binder.mark_external_boundary();
bind_node(&mut arena, root, ctx, &mut binder)?;
let var_slots = binder.len();
Ok(Self {
source: expr.to_string(),
arena,
root,
span,
var_slots,
external_vars,
})
}
pub fn source(&self) -> &str {
&self.source
}
pub fn span(&self) -> SourceSpan {
self.span
}
pub fn external_vars(&self) -> &[ExternalVar] {
&self.external_vars
}
pub fn arena(&self) -> &AstArena {
&self.arena
}
pub fn evaluator<'a, 'ctx>(
&'a self,
ctx: &'ctx XPathContext<'ctx>,
) -> XPathEvaluator<'a, 'ctx> {
XPathEvaluator::new(self, ctx)
}
}
#[derive(Debug, Clone)]
pub enum EvalValue {
Bool(bool),
Integer(i64),
BigInteger(BigInt),
Double(f64),
String(String),
}
impl From<bool> for EvalValue {
fn from(b: bool) -> Self {
EvalValue::Bool(b)
}
}
impl From<i32> for EvalValue {
fn from(i: i32) -> Self {
EvalValue::Integer(i as i64)
}
}
impl From<i64> for EvalValue {
fn from(i: i64) -> Self {
EvalValue::Integer(i)
}
}
impl From<f32> for EvalValue {
fn from(f: f32) -> Self {
EvalValue::Double(f as f64)
}
}
impl From<f64> for EvalValue {
fn from(f: f64) -> Self {
EvalValue::Double(f)
}
}
impl From<String> for EvalValue {
fn from(s: String) -> Self {
EvalValue::String(s)
}
}
impl From<&str> for EvalValue {
fn from(s: &str) -> Self {
EvalValue::String(s.to_string())
}
}
impl From<BigInt> for EvalValue {
fn from(i: BigInt) -> Self {
EvalValue::BigInteger(i)
}
}
#[derive(Debug, Clone)]
enum PendingValue {
Bool(bool),
Integer(i64),
BigInteger(BigInt),
Double(f64),
String(String),
}
impl PendingValue {
fn into_xpath_value<N: DomNavigator>(self) -> XPathValue<N> {
match self {
PendingValue::Bool(b) => XPathValue::boolean(b),
PendingValue::Integer(i) => XPathValue::integer(BigInt::from(i)),
PendingValue::BigInteger(i) => XPathValue::integer(i),
PendingValue::Double(d) => XPathValue::double(d),
PendingValue::String(s) => XPathValue::string(s),
}
}
}
impl From<EvalValue> for PendingValue {
fn from(v: EvalValue) -> Self {
match v {
EvalValue::Bool(b) => PendingValue::Bool(b),
EvalValue::Integer(i) => PendingValue::Integer(i),
EvalValue::BigInteger(i) => PendingValue::BigInteger(i),
EvalValue::Double(d) => PendingValue::Double(d),
EvalValue::String(s) => PendingValue::String(s),
}
}
}
pub struct XPathEvaluator<'expr, 'ctx> {
expr: &'expr XPathExpr,
static_ctx: &'ctx XPathContext<'ctx>,
pending_vars: Vec<(VarSlotId, PendingValue)>,
}
impl<'expr, 'ctx> XPathEvaluator<'expr, 'ctx> {
fn new(expr: &'expr XPathExpr, static_ctx: &'ctx XPathContext<'ctx>) -> Self {
Self {
expr,
static_ctx,
pending_vars: Vec::new(),
}
}
pub fn with_variable(
mut self,
name: &str,
value: impl Into<EvalValue>,
) -> Result<Self, XPathError> {
let slot = find_external_var(name, &self.expr.external_vars, self.static_ctx)?;
self.pending_vars.push((slot, value.into().into()));
Ok(self)
}
pub fn run<N: DomNavigator>(self) -> Result<XPathValue<N>, XPathError> {
self.run_internal(None)
}
pub fn run_with_node<N: DomNavigator>(self, node: N) -> Result<XPathValue<N>, XPathError> {
self.run_internal(Some(node))
}
fn run_internal<N: DomNavigator>(
self,
context_node: Option<N>,
) -> Result<XPathValue<N>, XPathError> {
let mut dyn_ctx = DynamicContext::new(self.static_ctx, self.expr.var_slots);
if let Some(node) = context_node {
dyn_ctx = dyn_ctx.with_context_node(node);
}
for (slot, pending) in self.pending_vars {
let value: XPathValue<N> = pending.into_xpath_value();
dyn_ctx.set_variable(slot, value);
}
eval_node(&self.expr.arena, self.expr.root, &mut dyn_ctx)
}
pub fn run_bool<N: DomNavigator>(self) -> Result<bool, XPathError> {
let value = self.run::<N>()?;
effective_boolean_value(&value)
}
pub fn run_string<N: DomNavigator>(self) -> Result<String, XPathError> {
let value = self.run::<N>()?;
Ok(xpath_value_to_string(&value))
}
pub fn run_number<N: DomNavigator>(self) -> Result<f64, XPathError> {
let value = self.run::<N>()?;
Ok(xpath_value_to_number(&value))
}
pub fn run_nodes<N: DomNavigator>(self) -> Result<Vec<N>, XPathError> {
let value = self.run::<N>()?;
let items = value.into_vec();
let nodes = items
.into_iter()
.filter_map(|item| match item {
XmlItem::Node(n) => Some(n),
XmlItem::Atomic(_) => None,
})
.collect();
Ok(nodes)
}
pub fn run_with<N, F>(self, setup: F) -> Result<XPathValue<N>, XPathError>
where
N: DomNavigator,
F: for<'a> FnOnce(&mut TypedEvaluator<'_, '_, 'a, N>),
{
self.run_with_node_and_setup(None, setup)
}
pub fn run_with_node_and_setup<N, F>(
self,
context_node: Option<N>,
setup: F,
) -> Result<XPathValue<N>, XPathError>
where
N: DomNavigator,
F: for<'a> FnOnce(&mut TypedEvaluator<'_, '_, 'a, N>),
{
let mut dyn_ctx = DynamicContext::new(self.static_ctx, self.expr.var_slots);
if let Some(node) = context_node {
dyn_ctx = dyn_ctx.with_context_node(node);
}
for (slot, pending) in self.pending_vars {
let value: XPathValue<N> = pending.into_xpath_value();
dyn_ctx.set_variable(slot, value);
}
{
let mut typed_eval = TypedEvaluator {
expr: self.expr,
static_ctx: self.static_ctx,
dyn_ctx: &mut dyn_ctx,
};
setup(&mut typed_eval);
}
eval_node(&self.expr.arena, self.expr.root, &mut dyn_ctx)
}
}
pub struct TypedEvaluator<'expr, 'ctx, 'dyn_ctx, N: DomNavigator> {
expr: &'expr XPathExpr,
static_ctx: &'ctx XPathContext<'ctx>,
dyn_ctx: &'dyn_ctx mut DynamicContext<'ctx, N>,
}
impl<'expr, 'ctx, 'dyn_ctx, N: DomNavigator> TypedEvaluator<'expr, 'ctx, 'dyn_ctx, N> {
pub fn set_variable_by_name(
&mut self,
name: &str,
value: XPathValue<N>,
) -> Result<(), XPathError> {
let slot = find_external_var(name, &self.expr.external_vars, self.static_ctx)?;
self.dyn_ctx.set_variable(slot, value);
Ok(())
}
pub fn set_variable(&mut self, slot: VarSlotId, value: XPathValue<N>) {
self.dyn_ctx.set_variable(slot, value);
}
pub fn context(&mut self) -> &mut DynamicContext<'ctx, N> {
&mut *self.dyn_ctx
}
}
fn parse_variable_name(name: &str, ctx: &XPathContext<'_>) -> Result<QualifiedName, XPathError> {
if let Some(colon_pos) = name.find(':') {
let prefix = &name[..colon_pos];
let local = &name[colon_pos + 1..];
if prefix.is_empty() || local.is_empty() {
return Err(XPathError::XPST0003 {
message: format!("Invalid variable name: '{}'", name),
});
}
let prefix_id = ctx.names.add(prefix);
let local_id = ctx.names.add(local);
let ns_id = ctx
.resolve_prefix_id(prefix_id)
.ok_or_else(|| XPathError::undefined_prefix(prefix))?;
Ok(QualifiedName::new(Some(ns_id), local_id, Some(prefix_id)))
} else {
let local_id = ctx.names.add(name);
Ok(QualifiedName::local(local_id))
}
}
fn find_external_var(
name: &str,
external_vars: &[ExternalVar],
ctx: &XPathContext<'_>,
) -> Result<VarSlotId, XPathError> {
let qname = parse_variable_name(name, ctx)?;
external_vars
.iter()
.rev()
.find(|v| v.name == qname)
.map(|v| v.slot)
.ok_or_else(|| XPathError::XPST0008 {
qname: format!("${}", name),
})
}
fn xpath_value_to_string<N: DomNavigator>(value: &XPathValue<N>) -> String {
match value {
XPathValue::Empty => String::new(),
XPathValue::Item(item) => item_to_string(item),
XPathValue::Sequence(items) => {
if let Some(first) = items.first() {
item_to_string(first)
} else {
String::new()
}
}
}
}
fn item_to_string<N: DomNavigator>(item: &XmlItem<N>) -> String {
match item {
XmlItem::Node(nav) => nav.value(),
XmlItem::Atomic(val) => val.to_string_value(),
}
}
fn xpath_value_to_number<N: DomNavigator>(value: &XPathValue<N>) -> f64 {
match value {
XPathValue::Empty => f64::NAN,
XPathValue::Item(item) => item_to_number(item),
XPathValue::Sequence(items) => {
if let Some(first) = items.first() {
item_to_number(first)
} else {
f64::NAN
}
}
}
}
fn item_to_number<N: DomNavigator>(item: &XmlItem<N>) -> f64 {
match item {
XmlItem::Node(nav) => nav.value().trim().parse().unwrap_or(f64::NAN),
XmlItem::Atomic(val) => {
if let Some(d) = val.as_double() {
d
} else if let Some(i) = val.as_integer() {
i.to_string().parse().unwrap_or(f64::NAN)
} else {
val.to_string_value().trim().parse().unwrap_or(f64::NAN)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::namespace::table::NameTable;
use crate::xpath::RoXmlNavigator;
#[test]
fn test_compile_simple_expression() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let expr = XPathExpr::compile("1 + 2", &ctx);
assert!(expr.is_ok());
let expr = expr.unwrap();
assert_eq!(expr.source(), "1 + 2");
assert!(expr.external_vars().is_empty());
}
#[test]
fn test_compile_with_variables() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let expr = XPathExpr::compile_with_vars("$x + $y", &ctx, &["x", "y"]);
assert!(expr.is_ok());
let expr = expr.unwrap();
assert_eq!(expr.external_vars().len(), 2);
}
#[test]
fn test_eval_simple_arithmetic() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let expr = XPathExpr::compile("1 + 2", &ctx).unwrap();
let result = expr
.evaluator(&ctx)
.run_number::<RoXmlNavigator<'static>>()
.unwrap();
assert_eq!(result, 3.0);
}
#[test]
fn test_eval_with_variable() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let expr = XPathExpr::compile_with_vars("$x + 1", &ctx, &["x"]).unwrap();
let result = expr
.evaluator(&ctx)
.with_variable("x", 41)
.unwrap()
.run_number::<RoXmlNavigator<'static>>()
.unwrap();
assert_eq!(result, 42.0);
}
#[test]
fn test_eval_with_string_variable() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let expr = XPathExpr::compile_with_vars(
"concat($greeting, ' ', $name)",
&ctx,
&["greeting", "name"],
)
.unwrap();
let result = expr
.evaluator(&ctx)
.with_variable("greeting", "Hello")
.unwrap()
.with_variable("name", "World")
.unwrap()
.run_string::<RoXmlNavigator<'static>>()
.unwrap();
assert_eq!(result, "Hello World");
}
#[test]
fn test_eval_run_bool() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let expr = XPathExpr::compile("1 < 2", &ctx).unwrap();
let result = expr
.evaluator(&ctx)
.run_bool::<RoXmlNavigator<'static>>()
.unwrap();
assert!(result);
let expr = XPathExpr::compile("2 < 1", &ctx).unwrap();
let result = expr
.evaluator(&ctx)
.run_bool::<RoXmlNavigator<'static>>()
.unwrap();
assert!(!result);
}
#[test]
fn test_eval_run_number() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let expr = XPathExpr::compile("2.5", &ctx).unwrap();
let result = expr
.evaluator(&ctx)
.run_number::<RoXmlNavigator<'static>>()
.unwrap();
assert!((result - 2.5).abs() < 0.001);
}
#[test]
fn test_undefined_variable_error() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let result = XPathExpr::compile("$x", &ctx);
assert!(result.is_err());
if let Err(XPathError::XPST0008 { qname }) = result {
assert!(qname.contains("x"));
} else {
panic!("Expected XPST0008 error");
}
}
#[test]
fn test_setting_undeclared_variable_error() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let expr = XPathExpr::compile_with_vars("$x", &ctx, &["x"]).unwrap();
let result = expr
.evaluator(&ctx)
.with_variable("x", 1)
.unwrap()
.with_variable("y", 2);
assert!(result.is_err());
if let Err(XPathError::XPST0008 { qname }) = result {
assert!(qname.contains("y"));
} else {
panic!("Expected XPST0008 error");
}
}
#[test]
fn test_eval_with_context_node() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let expr = XPathExpr::compile("child::item", &ctx).unwrap();
let doc = roxmltree::Document::parse("<root><item>value</item></root>").unwrap();
let mut nav = RoXmlNavigator::new(&doc);
nav.move_to_first_child();
let result = expr.evaluator(&ctx).run_with_node(nav).unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_expr_is_clone() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let expr1 = XPathExpr::compile("1 + 2", &ctx).unwrap();
let expr2 = expr1.clone();
let result1 = expr1
.evaluator(&ctx)
.run_number::<RoXmlNavigator<'static>>()
.unwrap();
let result2 = expr2
.evaluator(&ctx)
.run_number::<RoXmlNavigator<'static>>()
.unwrap();
assert_eq!(result1, result2);
}
#[test]
fn test_eval_value_conversions() {
let _: EvalValue = true.into();
let _: EvalValue = 42i32.into();
let _: EvalValue = 42i64.into();
let _: EvalValue = 2.5f32.into();
let _: EvalValue = 2.5f64.into();
let _: EvalValue = "hello".into();
let _: EvalValue = String::from("hello").into();
let _: EvalValue = BigInt::from(1000000000000i64).into();
}
#[test]
fn test_multiple_evaluations_same_expr() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let expr = XPathExpr::compile_with_vars("$x * 2", &ctx, &["x"]).unwrap();
let result1 = expr
.evaluator(&ctx)
.with_variable("x", 5)
.unwrap()
.run_number::<RoXmlNavigator<'static>>()
.unwrap();
let result2 = expr
.evaluator(&ctx)
.with_variable("x", 10)
.unwrap()
.run_number::<RoXmlNavigator<'static>>()
.unwrap();
let result3 = expr
.evaluator(&ctx)
.with_variable("x", 21)
.unwrap()
.run_number::<RoXmlNavigator<'static>>()
.unwrap();
assert_eq!(result1, 10.0);
assert_eq!(result2, 20.0);
assert_eq!(result3, 42.0);
}
#[test]
fn test_duplicate_variable_error() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let result = XPathExpr::compile_with_vars("$x + $x", &ctx, &["x", "x"]);
assert!(result.is_err());
if let Err(XPathError::XPST0003 { message }) = result {
assert!(message.contains("Duplicate"));
} else {
panic!("Expected XPST0003 error for duplicate variable");
}
}
#[test]
fn test_prefixed_variable() {
let names = NameTable::new();
let my_ns = names.add("http://example.com/my");
let my_prefix = names.add("my");
let mut namespaces = crate::namespace::context::NamespaceContextSnapshot::default();
namespaces.bindings.push((my_prefix, my_ns));
let ctx = XPathContext::new(&names).with_namespaces(namespaces);
let expr = XPathExpr::compile_with_vars("$my:value + 1", &ctx, &["my:value"]).unwrap();
let result = expr
.evaluator(&ctx)
.with_variable("my:value", 41)
.unwrap()
.run_number::<RoXmlNavigator<'static>>()
.unwrap();
assert_eq!(result, 42.0);
}
#[test]
fn test_run_with_sequence() {
use crate::types::XmlValue;
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let expr = XPathExpr::compile_with_vars("count($items)", &ctx, &["items"]).unwrap();
let result = expr
.evaluator(&ctx)
.run_with::<RoXmlNavigator<'static>, _>(|eval| {
let seq = XPathValue::from_sequence(vec![
XmlItem::Atomic(XmlValue::integer(1.into())),
XmlItem::Atomic(XmlValue::integer(2.into())),
XmlItem::Atomic(XmlValue::integer(3.into())),
]);
eval.set_variable_by_name("items", seq).unwrap();
})
.unwrap();
assert_eq!(
result.as_integer().map(|i| i.to_string()),
Some("3".to_string())
);
}
#[test]
fn test_run_with_empty_sequence() {
let names = NameTable::new();
let ctx = XPathContext::new(&names);
let expr = XPathExpr::compile_with_vars("empty($items)", &ctx, &["items"]).unwrap();
let result = expr
.evaluator(&ctx)
.run_with::<RoXmlNavigator<'static>, _>(|eval| {
eval.set_variable_by_name("items", XPathValue::empty())
.unwrap();
})
.unwrap();
assert_eq!(result.as_bool(), Some(true));
}
}