use crate::tree::NodeId;
use std::fmt;
#[derive(Debug, Clone)]
pub enum XPathValue {
Boolean(bool),
Number(f64),
String(String),
NodeSet(Vec<NodeId>),
}
impl XPathValue {
#[must_use]
pub fn to_boolean(&self) -> bool {
match self {
Self::Boolean(b) => *b,
Self::Number(n) => *n != 0.0 && !n.is_nan(),
Self::String(s) => !s.is_empty(),
Self::NodeSet(nodes) => !nodes.is_empty(),
}
}
#[must_use]
pub fn to_number(&self) -> f64 {
match self {
Self::Number(n) => *n,
Self::Boolean(b) => {
if *b {
1.0
} else {
0.0
}
}
Self::String(s) => parse_xpath_number(s),
Self::NodeSet(_) => {
f64::NAN
}
}
}
#[must_use]
pub fn to_number_with_string_value(&self, first_node_string_value: Option<&str>) -> f64 {
match self {
Self::NodeSet(_) => first_node_string_value.map_or(f64::NAN, parse_xpath_number),
_ => self.to_number(),
}
}
#[must_use]
pub fn to_xpath_string(&self) -> String {
match self {
Self::String(s) => s.clone(),
Self::Boolean(b) => {
if *b {
"true".to_owned()
} else {
"false".to_owned()
}
}
Self::Number(n) => format_xpath_number(*n),
Self::NodeSet(_) => {
String::new()
}
}
}
#[must_use]
pub fn to_string_with_string_value(&self, first_node_string_value: Option<&str>) -> String {
match self {
Self::NodeSet(_) => first_node_string_value.map_or_else(String::new, str::to_owned),
_ => self.to_xpath_string(),
}
}
#[must_use]
pub fn as_node_set(&self) -> Option<&Vec<NodeId>> {
match self {
Self::NodeSet(nodes) => Some(nodes),
_ => None,
}
}
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
Self::Boolean(_) => "boolean",
Self::Number(_) => "number",
Self::String(_) => "string",
Self::NodeSet(_) => "node-set",
}
}
}
impl fmt::Display for XPathValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Boolean(b) => {
if *b {
write!(f, "true")
} else {
write!(f, "false")
}
}
Self::Number(n) => write!(f, "{}", format_xpath_number(*n)),
Self::String(s) => write!(f, "{s}"),
Self::NodeSet(nodes) => write!(f, "<node-set of {} nodes>", nodes.len()),
}
}
}
impl PartialEq for XPathValue {
#[allow(clippy::float_cmp)]
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Boolean(a), Self::Boolean(b)) => a == b,
(Self::Number(a), Self::Number(b)) => {
a == b
}
(Self::String(a), Self::String(b)) => a == b,
(Self::NodeSet(a), Self::NodeSet(b)) => a == b,
_ => false,
}
}
}
#[must_use]
pub fn format_xpath_number(n: f64) -> String {
if n.is_nan() {
return "NaN".to_owned();
}
if n.is_infinite() {
return if n.is_sign_positive() {
"Infinity".to_owned()
} else {
"-Infinity".to_owned()
};
}
if n == 0.0 {
return "0".to_owned();
}
#[allow(clippy::float_cmp, clippy::cast_possible_truncation)]
if n.fract() == 0.0 && n.abs() < 1e18 {
return format!("{}", n as i64);
}
format!("{n}")
}
fn parse_xpath_number(s: &str) -> f64 {
let trimmed = s.trim();
if trimmed.is_empty() {
return f64::NAN;
}
trimmed.parse::<f64>().unwrap_or(f64::NAN)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum XPathError {
TypeError {
expected: String,
found: String,
},
UndefinedVariable {
name: String,
},
UndefinedFunction {
name: String,
},
InvalidArgCount {
function: String,
expected: usize,
found: usize,
},
InvalidExpression {
message: String,
},
InternalError {
message: String,
},
}
impl fmt::Display for XPathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TypeError { expected, found } => {
write!(f, "type error: expected {expected}, found {found}")
}
Self::UndefinedVariable { name } => {
write!(f, "undefined variable: ${name}")
}
Self::UndefinedFunction { name } => {
write!(f, "undefined function: {name}()")
}
Self::InvalidArgCount {
function,
expected,
found,
} => {
write!(
f,
"invalid argument count for {function}(): \
expected {expected}, found {found}"
)
}
Self::InvalidExpression { message } => {
write!(f, "invalid XPath expression: {message}")
}
Self::InternalError { message } => {
write!(f, "internal XPath error: {message}")
}
}
}
}
impl std::error::Error for XPathError {}
impl From<super::lexer::XPathError> for XPathError {
fn from(err: super::lexer::XPathError) -> Self {
Self::InvalidExpression {
message: err.to_string(),
}
}
}
#[must_use]
pub fn compare_values_eq(lhs: &XPathValue, rhs: &XPathValue) -> Option<bool> {
if matches!(lhs, XPathValue::NodeSet(_)) || matches!(rhs, XPathValue::NodeSet(_)) {
return None;
}
Some(compare_non_nodeset_eq(lhs, rhs))
}
#[allow(clippy::float_cmp)]
fn compare_non_nodeset_eq(lhs: &XPathValue, rhs: &XPathValue) -> bool {
if matches!(lhs, XPathValue::Boolean(_)) || matches!(rhs, XPathValue::Boolean(_)) {
return lhs.to_boolean() == rhs.to_boolean();
}
if matches!(lhs, XPathValue::Number(_)) || matches!(rhs, XPathValue::Number(_)) {
let ln = lhs.to_number();
let rn = rhs.to_number();
return ln == rn;
}
lhs.to_xpath_string() == rhs.to_xpath_string()
}
#[must_use]
#[allow(clippy::float_cmp)]
pub fn compare_values_with_string_values(
lhs: &XPathValue,
rhs: &XPathValue,
lhs_strings: &[String],
rhs_strings: &[String],
) -> bool {
match (lhs, rhs) {
(XPathValue::NodeSet(_), XPathValue::NodeSet(_)) => {
for ls in lhs_strings {
for rs in rhs_strings {
if ls == rs {
return true;
}
}
}
false
}
(XPathValue::NodeSet(nodes), XPathValue::Boolean(b))
| (XPathValue::Boolean(b), XPathValue::NodeSet(nodes)) => nodes.is_empty() != *b,
(XPathValue::NodeSet(_), XPathValue::Number(n)) => {
lhs_strings.iter().any(|sv| parse_xpath_number(sv) == *n)
}
(XPathValue::Number(n), XPathValue::NodeSet(_)) => {
rhs_strings.iter().any(|sv| parse_xpath_number(sv) == *n)
}
(XPathValue::NodeSet(_), XPathValue::String(s)) => lhs_strings.iter().any(|sv| sv == s),
(XPathValue::String(s), XPathValue::NodeSet(_)) => rhs_strings.iter().any(|sv| sv == s),
_ => compare_non_nodeset_eq(lhs, rhs),
}
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::unwrap_used)]
mod tests {
use super::*;
use crate::tree::{Document, NodeKind};
fn make_node_id_in_doc(doc: &mut Document) -> NodeId {
doc.create_node(NodeKind::Text {
content: String::new(),
})
}
#[test]
fn test_to_boolean_from_boolean() {
assert!(XPathValue::Boolean(true).to_boolean());
assert!(!XPathValue::Boolean(false).to_boolean());
}
#[test]
fn test_to_boolean_from_number() {
assert!(XPathValue::Number(1.0).to_boolean());
assert!(XPathValue::Number(-1.0).to_boolean());
assert!(XPathValue::Number(0.5).to_boolean());
assert!(!XPathValue::Number(0.0).to_boolean());
assert!(!XPathValue::Number(-0.0).to_boolean());
assert!(!XPathValue::Number(f64::NAN).to_boolean());
}
#[test]
fn test_to_boolean_from_string() {
assert!(XPathValue::String("hello".to_owned()).to_boolean());
assert!(XPathValue::String(" ".to_owned()).to_boolean());
assert!(!XPathValue::String(String::new()).to_boolean());
}
#[test]
fn test_to_boolean_from_nodeset() {
let mut doc = Document::new();
let id = make_node_id_in_doc(&mut doc);
assert!(XPathValue::NodeSet(vec![id]).to_boolean());
assert!(!XPathValue::NodeSet(vec![]).to_boolean());
}
#[test]
fn test_to_number_from_number() {
assert_eq!(XPathValue::Number(42.0).to_number(), 42.0);
assert!(XPathValue::Number(f64::NAN).to_number().is_nan());
}
#[test]
fn test_to_number_from_boolean() {
assert_eq!(XPathValue::Boolean(true).to_number(), 1.0);
assert_eq!(XPathValue::Boolean(false).to_number(), 0.0);
}
#[test]
fn test_to_number_from_string() {
assert_eq!(XPathValue::String("42".to_owned()).to_number(), 42.0);
assert_eq!(XPathValue::String(" 3.5 ".to_owned()).to_number(), 3.5);
assert_eq!(XPathValue::String("-7".to_owned()).to_number(), -7.0);
assert!(XPathValue::String("not a number".to_owned())
.to_number()
.is_nan());
assert!(XPathValue::String(String::new()).to_number().is_nan());
}
#[test]
fn test_to_number_from_nodeset_without_doc() {
let mut doc = Document::new();
let id = make_node_id_in_doc(&mut doc);
assert!(XPathValue::NodeSet(vec![id]).to_number().is_nan());
}
#[test]
fn test_to_number_with_string_value() {
let mut doc = Document::new();
let id = make_node_id_in_doc(&mut doc);
let val = XPathValue::NodeSet(vec![id]);
assert_eq!(val.to_number_with_string_value(Some("42")), 42.0);
assert!(val.to_number_with_string_value(Some("abc")).is_nan());
assert!(val.to_number_with_string_value(None).is_nan());
}
#[test]
fn test_to_xpath_string_from_string() {
assert_eq!(
XPathValue::String("hello".to_owned()).to_xpath_string(),
"hello"
);
}
#[test]
fn test_to_xpath_string_from_boolean() {
assert_eq!(XPathValue::Boolean(true).to_xpath_string(), "true");
assert_eq!(XPathValue::Boolean(false).to_xpath_string(), "false");
}
#[test]
fn test_to_xpath_string_from_number() {
assert_eq!(XPathValue::Number(1.0).to_xpath_string(), "1");
assert_eq!(XPathValue::Number(-1.0).to_xpath_string(), "-1");
assert_eq!(XPathValue::Number(0.0).to_xpath_string(), "0");
assert_eq!(XPathValue::Number(1.5).to_xpath_string(), "1.5");
assert_eq!(XPathValue::Number(f64::NAN).to_xpath_string(), "NaN");
assert_eq!(
XPathValue::Number(f64::INFINITY).to_xpath_string(),
"Infinity"
);
assert_eq!(
XPathValue::Number(f64::NEG_INFINITY).to_xpath_string(),
"-Infinity"
);
}
#[test]
fn test_format_xpath_number_negative_zero() {
assert_eq!(format_xpath_number(-0.0), "0");
}
#[test]
fn test_format_xpath_number_integers() {
assert_eq!(format_xpath_number(0.0), "0");
assert_eq!(format_xpath_number(1.0), "1");
assert_eq!(format_xpath_number(-1.0), "-1");
assert_eq!(format_xpath_number(100.0), "100");
assert_eq!(format_xpath_number(999_999.0), "999999");
}
#[test]
fn test_format_xpath_number_fractional() {
assert_eq!(format_xpath_number(1.5), "1.5");
assert_eq!(format_xpath_number(0.1), "0.1");
assert_eq!(format_xpath_number(-2.75), "-2.75");
}
#[test]
fn test_format_xpath_number_special_values() {
assert_eq!(format_xpath_number(f64::NAN), "NaN");
assert_eq!(format_xpath_number(f64::INFINITY), "Infinity");
assert_eq!(format_xpath_number(f64::NEG_INFINITY), "-Infinity");
}
#[test]
fn test_as_node_set() {
let mut doc = Document::new();
let id = make_node_id_in_doc(&mut doc);
let ns = XPathValue::NodeSet(vec![id]);
assert!(ns.as_node_set().is_some());
assert_eq!(ns.as_node_set().unwrap().len(), 1);
assert!(XPathValue::Boolean(true).as_node_set().is_none());
assert!(XPathValue::Number(1.0).as_node_set().is_none());
assert!(XPathValue::String("x".to_owned()).as_node_set().is_none());
}
#[test]
fn test_type_name() {
assert_eq!(XPathValue::Boolean(true).type_name(), "boolean");
assert_eq!(XPathValue::Number(0.0).type_name(), "number");
assert_eq!(XPathValue::String(String::new()).type_name(), "string");
assert_eq!(XPathValue::NodeSet(vec![]).type_name(), "node-set");
}
#[test]
fn test_display() {
let mut doc = Document::new();
let id1 = make_node_id_in_doc(&mut doc);
let id2 = make_node_id_in_doc(&mut doc);
assert_eq!(XPathValue::Boolean(true).to_string(), "true");
assert_eq!(XPathValue::Boolean(false).to_string(), "false");
assert_eq!(XPathValue::Number(42.0).to_string(), "42");
assert_eq!(XPathValue::String("hi".to_owned()).to_string(), "hi");
assert_eq!(
XPathValue::NodeSet(vec![id1, id2]).to_string(),
"<node-set of 2 nodes>"
);
}
#[test]
fn test_partial_eq() {
assert_eq!(XPathValue::Boolean(true), XPathValue::Boolean(true));
assert_ne!(XPathValue::Boolean(true), XPathValue::Boolean(false));
assert_eq!(XPathValue::Number(1.0), XPathValue::Number(1.0));
assert_ne!(XPathValue::Number(f64::NAN), XPathValue::Number(f64::NAN));
assert_eq!(
XPathValue::String("a".to_owned()),
XPathValue::String("a".to_owned())
);
assert_ne!(XPathValue::Boolean(true), XPathValue::Number(1.0));
}
#[test]
fn test_compare_values_eq_booleans() {
let t = XPathValue::Boolean(true);
let f = XPathValue::Boolean(false);
assert_eq!(compare_values_eq(&t, &t), Some(true));
assert_eq!(compare_values_eq(&t, &f), Some(false));
}
#[test]
fn test_compare_values_eq_boolean_coercion() {
let t = XPathValue::Boolean(true);
let num = XPathValue::Number(42.0); assert_eq!(compare_values_eq(&t, &num), Some(true));
let f = XPathValue::Boolean(false);
let zero = XPathValue::Number(0.0); assert_eq!(compare_values_eq(&f, &zero), Some(true));
}
#[test]
fn test_compare_values_eq_number_and_string() {
let num = XPathValue::Number(42.0);
let s = XPathValue::String("42".to_owned());
assert_eq!(compare_values_eq(&num, &s), Some(true));
let bad = XPathValue::String("abc".to_owned());
assert_eq!(compare_values_eq(&num, &bad), Some(false));
}
#[test]
fn test_compare_values_eq_strings() {
let a = XPathValue::String("hello".to_owned());
let b = XPathValue::String("hello".to_owned());
let c = XPathValue::String("world".to_owned());
assert_eq!(compare_values_eq(&a, &b), Some(true));
assert_eq!(compare_values_eq(&a, &c), Some(false));
}
#[test]
fn test_compare_values_eq_with_nodeset_returns_none() {
let mut doc = Document::new();
let id = make_node_id_in_doc(&mut doc);
let ns = XPathValue::NodeSet(vec![id]);
let s = XPathValue::String("x".to_owned());
assert_eq!(compare_values_eq(&ns, &s), None);
assert_eq!(compare_values_eq(&s, &ns), None);
}
#[test]
fn test_compare_with_string_values_nodeset_to_string() {
let mut doc = Document::new();
let id1 = make_node_id_in_doc(&mut doc);
let id2 = make_node_id_in_doc(&mut doc);
let ns = XPathValue::NodeSet(vec![id1, id2]);
let s = XPathValue::String("hello".to_owned());
let node_strings = vec!["world".to_owned(), "hello".to_owned()];
assert!(compare_values_with_string_values(
&ns,
&s,
&node_strings,
&[]
));
}
#[test]
fn test_compare_with_string_values_nodeset_to_number() {
let mut doc = Document::new();
let id1 = make_node_id_in_doc(&mut doc);
let id2 = make_node_id_in_doc(&mut doc);
let ns = XPathValue::NodeSet(vec![id1, id2]);
let n = XPathValue::Number(42.0);
let node_strings = vec!["10".to_owned(), "42".to_owned()];
assert!(compare_values_with_string_values(
&ns,
&n,
&node_strings,
&[]
));
}
#[test]
fn test_compare_with_string_values_nodeset_to_boolean() {
let mut doc = Document::new();
let id = make_node_id_in_doc(&mut doc);
let ns = XPathValue::NodeSet(vec![id]);
let b = XPathValue::Boolean(true);
assert!(compare_values_with_string_values(&ns, &b, &[], &[]));
let empty_ns = XPathValue::NodeSet(vec![]);
assert!(!compare_values_with_string_values(&empty_ns, &b, &[], &[]));
}
#[test]
fn test_compare_with_string_values_nodeset_to_nodeset() {
let mut doc = Document::new();
let id1 = make_node_id_in_doc(&mut doc);
let id2 = make_node_id_in_doc(&mut doc);
let ns1 = XPathValue::NodeSet(vec![id1]);
let ns2 = XPathValue::NodeSet(vec![id2]);
let strings1 = vec!["hello".to_owned()];
let strings2 = vec!["hello".to_owned()];
assert!(compare_values_with_string_values(
&ns1, &ns2, &strings1, &strings2,
));
let strings2_diff = vec!["world".to_owned()];
assert!(!compare_values_with_string_values(
&ns1,
&ns2,
&strings1,
&strings2_diff,
));
}
#[test]
fn test_xpath_error_display_type_error() {
let err = XPathError::TypeError {
expected: "node-set".to_owned(),
found: "string".to_owned(),
};
assert_eq!(
err.to_string(),
"type error: expected node-set, found string"
);
}
#[test]
fn test_xpath_error_display_undefined_variable() {
let err = XPathError::UndefinedVariable {
name: "foo".to_owned(),
};
assert_eq!(err.to_string(), "undefined variable: $foo");
}
#[test]
fn test_xpath_error_display_undefined_function() {
let err = XPathError::UndefinedFunction {
name: "my-func".to_owned(),
};
assert_eq!(err.to_string(), "undefined function: my-func()");
}
#[test]
fn test_xpath_error_display_invalid_arg_count() {
let err = XPathError::InvalidArgCount {
function: "substring".to_owned(),
expected: 2,
found: 5,
};
assert_eq!(
err.to_string(),
"invalid argument count for substring(): expected 2, found 5"
);
}
#[test]
fn test_xpath_error_display_invalid_expression() {
let err = XPathError::InvalidExpression {
message: "unexpected token ')'".to_owned(),
};
assert_eq!(
err.to_string(),
"invalid XPath expression: unexpected token ')'"
);
}
#[test]
fn test_xpath_error_display_internal_error() {
let err = XPathError::InternalError {
message: "stack overflow".to_owned(),
};
assert_eq!(err.to_string(), "internal XPath error: stack overflow");
}
#[test]
fn test_xpath_error_is_error_trait() {
let err = XPathError::TypeError {
expected: "boolean".to_owned(),
found: "number".to_owned(),
};
let _: &dyn std::error::Error = &err;
}
#[test]
fn test_to_string_with_string_value_nodeset() {
let mut doc = Document::new();
let id = make_node_id_in_doc(&mut doc);
let val = XPathValue::NodeSet(vec![id]);
assert_eq!(val.to_string_with_string_value(Some("hello")), "hello");
assert_eq!(val.to_string_with_string_value(None), "");
}
#[test]
fn test_to_string_with_string_value_non_nodeset() {
let val = XPathValue::Number(42.0);
assert_eq!(val.to_string_with_string_value(Some("ignored")), "42");
}
}