use std::sync::Arc;
use crate::document::XmlDocument;
use crate::namespace::NamespaceResolver;
use crate::node::XmlNode;
#[derive(Debug, Clone)]
pub enum XPathValue {
NodeSet(Vec<XmlNode>),
String(String),
Number(f64),
Boolean(bool),
}
impl XPathValue {
pub fn empty_nodeset() -> Self {
XPathValue::NodeSet(Vec::new())
}
pub fn from_node(node: XmlNode) -> Self {
XPathValue::NodeSet(vec![node])
}
pub fn from_nodes(nodes: Vec<XmlNode>) -> Self {
XPathValue::NodeSet(nodes)
}
pub fn is_nodeset(&self) -> bool {
matches!(self, XPathValue::NodeSet(_))
}
pub fn is_string(&self) -> bool {
matches!(self, XPathValue::String(_))
}
pub fn is_number(&self) -> bool {
matches!(self, XPathValue::Number(_))
}
pub fn is_boolean(&self) -> bool {
matches!(self, XPathValue::Boolean(_))
}
pub fn into_nodes(self) -> Vec<XmlNode> {
match self {
XPathValue::NodeSet(nodes) => nodes,
_ => Vec::new(),
}
}
pub fn as_nodes(&self) -> Option<&[XmlNode]> {
match self {
XPathValue::NodeSet(nodes) => Some(nodes),
_ => None,
}
}
pub fn to_string_value(&self) -> String {
match self {
XPathValue::NodeSet(nodes) => nodes
.first()
.and_then(|n| n.get_content())
.unwrap_or_default(),
XPathValue::String(s) => s.clone(),
XPathValue::Boolean(b) => b.to_string(),
XPathValue::Number(n) => {
if n.is_nan() {
"NaN".to_string()
} else if n.is_infinite() {
if *n > 0.0 { "Infinity" } else { "-Infinity" }.to_string()
} else if *n == 0.0 {
"0".to_string()
} else {
let s = n.to_string();
if s.contains('.') && !s.contains('e') && !s.contains('E') {
s.trim_end_matches('0').trim_end_matches('.').to_string()
} else {
s
}
}
}
}
}
pub fn to_boolean(&self) -> bool {
match self {
XPathValue::NodeSet(nodes) => !nodes.is_empty(),
XPathValue::String(s) => !s.is_empty(),
XPathValue::Boolean(b) => *b,
XPathValue::Number(n) => *n != 0.0 && !n.is_nan(),
}
}
pub fn to_number(&self) -> f64 {
match self {
XPathValue::NodeSet(nodes) => nodes
.first()
.and_then(|n| n.get_content())
.and_then(|s| parse_xpath_number(&s))
.unwrap_or(f64::NAN),
XPathValue::String(s) => parse_xpath_number(s).unwrap_or(f64::NAN),
XPathValue::Boolean(b) => {
if *b {
1.0
} else {
0.0
}
}
XPathValue::Number(n) => *n,
}
}
pub fn collect_text_values(&self) -> Vec<String> {
match self {
XPathValue::NodeSet(nodes) => nodes.iter().filter_map(|n| n.get_content()).collect(),
XPathValue::String(s) => vec![s.clone()],
_ => Vec::new(),
}
}
}
fn parse_xpath_number(s: &str) -> Option<f64> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Some(f64::NAN);
}
trimmed.parse::<f64>().ok()
}
#[derive(Clone)]
pub struct EvaluationContext<'a> {
pub node: XmlNode,
pub position: usize,
pub size: usize,
pub doc: &'a XmlDocument,
pub resolver: NamespaceResolver,
variables: Option<Arc<std::collections::HashMap<String, XPathValue>>>,
}
impl<'a> EvaluationContext<'a> {
pub fn new(node: XmlNode, doc: &'a XmlDocument, resolver: NamespaceResolver) -> Self {
Self {
node,
position: 1,
size: 1,
doc,
resolver,
variables: None,
}
}
pub fn with_position(mut self, position: usize, size: usize) -> Self {
self.position = position;
self.size = size;
self
}
pub fn with_node(&self, node: XmlNode) -> Self {
Self {
node,
position: self.position,
size: self.size,
doc: self.doc,
resolver: self.resolver.clone(),
variables: self.variables.clone(),
}
}
pub fn for_predicate(&self, node: XmlNode, position: usize, size: usize) -> Self {
Self {
node,
position,
size,
doc: self.doc,
resolver: self.resolver.clone(),
variables: self.variables.clone(),
}
}
pub fn with_variables(
mut self,
variables: std::collections::HashMap<String, XPathValue>,
) -> Self {
self.variables = Some(Arc::new(variables));
self
}
pub fn get_variable(&self, name: &str) -> Option<&XPathValue> {
self.variables.as_ref()?.get(name)
}
pub fn position(&self) -> usize {
self.position
}
pub fn size(&self) -> usize {
self.size
}
pub fn resolve_prefix(&self, prefix: &str) -> Option<&str> {
self.resolver.resolve_prefix(prefix)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_xpath_value_to_string() {
assert_eq!(
XPathValue::String("hello".into()).to_string_value(),
"hello"
);
assert_eq!(XPathValue::Number(42.0).to_string_value(), "42");
assert_eq!(XPathValue::Number(2.75).to_string_value(), "2.75");
assert_eq!(XPathValue::Number(f64::NAN).to_string_value(), "NaN");
assert_eq!(
XPathValue::Number(f64::INFINITY).to_string_value(),
"Infinity"
);
assert_eq!(XPathValue::Boolean(true).to_string_value(), "true");
assert_eq!(XPathValue::Boolean(false).to_string_value(), "false");
}
#[test]
fn test_xpath_value_to_boolean() {
assert!(XPathValue::Boolean(true).to_boolean());
assert!(!XPathValue::Boolean(false).to_boolean());
assert!(XPathValue::String("hello".into()).to_boolean());
assert!(!XPathValue::String("".into()).to_boolean());
assert!(XPathValue::Number(1.0).to_boolean());
assert!(!XPathValue::Number(0.0).to_boolean());
assert!(!XPathValue::Number(f64::NAN).to_boolean());
}
#[test]
fn test_xpath_value_to_number() {
assert_eq!(XPathValue::Number(42.0).to_number(), 42.0);
assert_eq!(XPathValue::String("2.75".into()).to_number(), 2.75);
assert!(
XPathValue::String("not a number".into())
.to_number()
.is_nan()
);
assert_eq!(XPathValue::Boolean(true).to_number(), 1.0);
assert_eq!(XPathValue::Boolean(false).to_number(), 0.0);
}
#[test]
fn test_parse_xpath_number() {
assert_eq!(parse_xpath_number("42"), Some(42.0));
assert_eq!(parse_xpath_number(" 2.75 "), Some(2.75));
assert_eq!(parse_xpath_number("-1.5"), Some(-1.5));
assert!(parse_xpath_number("").unwrap().is_nan());
assert!(parse_xpath_number("abc").is_none());
}
}