#[cfg(feature = "compare-libxml")]
#[allow(dead_code)]
pub mod libxml_compare {
use std::collections::HashMap;
#[derive(Debug)]
pub struct CompareResult {
pub matches: bool,
pub differences: Vec<String>,
}
impl CompareResult {
pub fn ok() -> Self {
Self {
matches: true,
differences: vec![],
}
}
pub fn diff(msg: impl Into<String>) -> Self {
Self {
matches: false,
differences: vec![msg.into()],
}
}
pub fn assert_match(&self) {
if !self.matches {
panic!(
"fastxml and libxml results differ:\n{}",
self.differences.join("\n")
);
}
}
}
pub fn libxml_parse(xml: &str) -> Result<LibxmlDoc, String> {
let parser = libxml::parser::Parser::default();
let doc = parser
.parse_string(xml)
.map_err(|e| format!("libxml parse error: {:?}", e))?;
Ok(LibxmlDoc { doc })
}
pub struct LibxmlDoc {
doc: libxml::tree::Document,
}
#[derive(Debug)]
pub enum XPathResult {
NodeSet(Vec<libxml::tree::Node>),
String(String),
Number(f64),
Boolean(bool),
}
impl LibxmlDoc {
pub fn root_name(&self) -> Option<String> {
self.doc.get_root_element().map(|n| n.get_name())
}
pub fn xpath_eval(&self, xpath: &str) -> Result<XPathResult, String> {
let ctx = libxml::xpath::Context::new(&self.doc)
.map_err(|_| "Failed to create xpath context")?;
if let Some(root) = self.doc.get_root_element() {
for ns in root.get_namespace_declarations() {
let prefix = ns.get_prefix();
if !prefix.is_empty() {
let href = ns.get_href();
let _ = ctx.register_namespace(&prefix, &href);
}
}
}
let result = ctx
.evaluate(xpath)
.map_err(|_| format!("XPath evaluation failed: {}", xpath))?;
let nodes = result.get_nodes_as_vec();
Ok(XPathResult::NodeSet(nodes))
}
pub fn root_attributes(&self) -> HashMap<String, String> {
self.doc
.get_root_element()
.map(|n| n.get_attributes().into_iter().collect())
.unwrap_or_default()
}
pub fn root_namespace_prefix(&self) -> Option<String> {
self.doc
.get_root_element()
.and_then(|n| n.get_namespace().map(|ns| ns.get_prefix()))
}
pub fn child_element_names(&self) -> Vec<String> {
self.doc
.get_root_element()
.map(|n| {
n.get_child_elements()
.into_iter()
.map(|c| c.get_name())
.collect()
})
.unwrap_or_default()
}
pub fn xpath_string_results(&self, xpath: &str) -> Result<Vec<String>, String> {
self.xpath_string_results_with_variables(xpath, &HashMap::new())
}
pub fn xpath_string_results_with_variables(
&self,
xpath: &str,
variables: &HashMap<String, XPathVarValue>,
) -> Result<Vec<String>, String> {
let root = self.doc.get_root_element().ok_or("No root element")?;
let ctx = libxml::xpath::Context::new(&self.doc)
.map_err(|_| "Failed to create xpath context")?;
for ns in root.get_namespace_declarations() {
let prefix = ns.get_prefix();
if !prefix.is_empty() {
let href = ns.get_href();
let _ = ctx.register_namespace(&prefix, &href);
}
}
let mut substituted_xpath = xpath.to_string();
for (name, value) in variables {
let var_pattern = format!("${}", name);
let replacement = match value {
XPathVarValue::String(s) => format!("'{}'", s.replace('\'', "''")),
XPathVarValue::Number(n) => n.to_string(),
XPathVarValue::Boolean(b) => {
if *b {
"true()".to_string()
} else {
"false()".to_string()
}
}
};
substituted_xpath = substituted_xpath.replace(&var_pattern, &replacement);
}
let result = ctx
.evaluate(&substituted_xpath)
.map_err(|_| format!("XPath evaluation failed: {}", substituted_xpath))?;
Ok(result
.get_nodes_as_vec()
.into_iter()
.map(|n| n.get_content())
.collect())
}
}
#[derive(Debug, Clone)]
pub enum XPathVarValue {
String(String),
Number(f64),
Boolean(bool),
}
pub fn compare_parse(xml: &str, fastxml_doc: &fastxml::XmlDocument) -> CompareResult {
let libxml_doc = match libxml_parse(xml) {
Ok(d) => d,
Err(e) => return CompareResult::diff(format!("libxml failed to parse: {}", e)),
};
let mut differences = vec![];
let fastxml_root = fastxml::get_root_node(fastxml_doc).ok();
let fastxml_root_name = fastxml_root.as_ref().map(|n| n.get_name());
let libxml_root_name = libxml_doc.root_name();
if fastxml_root_name != libxml_root_name {
differences.push(format!(
"Root name: fastxml={:?}, libxml={:?}",
fastxml_root_name, libxml_root_name
));
}
let fastxml_prefix = fastxml_root.as_ref().and_then(|n| n.get_prefix());
let libxml_prefix = libxml_doc.root_namespace_prefix();
let normalize_prefix = |p: Option<String>| match p {
Some(s) if s.is_empty() => None,
other => other,
};
if normalize_prefix(fastxml_prefix.clone()) != normalize_prefix(libxml_prefix.clone()) {
differences.push(format!(
"Root prefix: fastxml={:?}, libxml={:?}",
fastxml_prefix, libxml_prefix
));
}
let fastxml_children: Vec<String> = fastxml_root
.as_ref()
.map(|n| {
n.get_child_elements()
.into_iter()
.map(|c| c.get_name())
.collect()
})
.unwrap_or_default();
let libxml_children = libxml_doc.child_element_names();
if fastxml_children != libxml_children {
differences.push(format!(
"Child elements: fastxml={:?}, libxml={:?}",
fastxml_children, libxml_children
));
}
if differences.is_empty() {
CompareResult::ok()
} else {
CompareResult {
matches: false,
differences,
}
}
}
pub fn compare_xpath(
xml: &str,
xpath: &str,
fastxml_doc: &fastxml::XmlDocument,
) -> CompareResult {
if xpath.contains("namespace::") {
return CompareResult::ok();
}
let libxml_doc = match libxml_parse(xml) {
Ok(d) => d,
Err(e) => return CompareResult::diff(format!("libxml failed to parse: {}", e)),
};
let fastxml_result = match fastxml::xpath::evaluate(fastxml_doc, xpath) {
Ok(r) => r,
Err(e) => {
if libxml_doc.xpath_eval(xpath).is_err() {
return CompareResult::ok(); }
return CompareResult::diff(format!(
"fastxml XPath failed but libxml succeeded: {}",
e
));
}
};
let libxml_result = match libxml_doc.xpath_eval(xpath) {
Ok(r) => r,
Err(e) => {
return CompareResult::diff(format!(
"libxml XPath failed but fastxml succeeded: {}",
e
));
}
};
use fastxml::xpath::XPathResult as FastXmlResult;
match (&fastxml_result, &libxml_result) {
(FastXmlResult::Nodes(fastxml_nodes), XPathResult::NodeSet(libxml_nodes)) => {
if fastxml_nodes.len() != libxml_nodes.len() {
return CompareResult::diff(format!(
"XPath '{}' node count differs: fastxml={}, libxml={}",
xpath,
fastxml_nodes.len(),
libxml_nodes.len()
));
}
let fastxml_texts: Vec<String> = fastxml_nodes
.iter()
.filter_map(|n| n.get_content())
.filter(|s| !s.is_empty())
.collect();
let libxml_texts: Vec<String> = libxml_nodes
.iter()
.map(|n| n.get_content())
.filter(|s| !s.is_empty())
.collect();
if fastxml_texts != libxml_texts {
CompareResult::diff(format!(
"XPath '{}' text values differ:\n fastxml: {:?}\n libxml: {:?}",
xpath, fastxml_texts, libxml_texts
))
} else {
CompareResult::ok()
}
}
(FastXmlResult::String(_), XPathResult::NodeSet(nodes)) if nodes.is_empty() => {
CompareResult::ok()
}
(FastXmlResult::Number(_), XPathResult::NodeSet(nodes)) if nodes.is_empty() => {
CompareResult::ok()
}
(FastXmlResult::Boolean(_), XPathResult::NodeSet(nodes)) if nodes.is_empty() => {
CompareResult::ok()
}
_ => {
CompareResult::diff(format!(
"XPath '{}' result type mismatch:\n fastxml: {:?}\n libxml: {:?}",
xpath, fastxml_result, libxml_result
))
}
}
}
pub fn compare_xpath_with_variables(
xml: &str,
xpath: &str,
fastxml_doc: &fastxml::XmlDocument,
fastxml_vars: std::collections::HashMap<String, fastxml::xpath::XPathValue>,
libxml_vars: HashMap<String, XPathVarValue>,
) -> CompareResult {
let libxml_doc = match libxml_parse(xml) {
Ok(d) => d,
Err(e) => return CompareResult::diff(format!("libxml failed to parse: {}", e)),
};
let evaluator = fastxml::xpath::XPathEvaluator::new(fastxml_doc);
let (fastxml_count, fastxml_texts) =
match evaluator.evaluate_with_variables(xpath, fastxml_vars) {
Ok(r) => {
let nodes = r.clone().into_nodes();
let count = nodes.len();
let texts = fastxml::xpath::collect_text_values(&r);
(count, texts)
}
Err(e) => {
if libxml_doc
.xpath_string_results_with_variables(xpath, &libxml_vars)
.is_err()
{
return CompareResult::ok(); }
return CompareResult::diff(format!(
"fastxml XPath with variables failed but libxml succeeded: {}",
e
));
}
};
let libxml_result =
match libxml_doc.xpath_string_results_with_variables(xpath, &libxml_vars) {
Ok(r) => r,
Err(e) => {
return CompareResult::diff(format!(
"libxml XPath with variables failed but fastxml succeeded: {}",
e
));
}
};
let libxml_texts: Vec<String> = libxml_result
.iter()
.filter(|s| !s.is_empty())
.cloned()
.collect();
let libxml_count = libxml_result.len();
if fastxml_count != libxml_count {
return CompareResult::diff(format!(
"XPath '{}' with variables node count differs: fastxml={}, libxml={}",
xpath, fastxml_count, libxml_count
));
}
if fastxml_texts != libxml_texts {
CompareResult::diff(format!(
"XPath '{}' with variables text values differ:\n fastxml: {:?}\n libxml: {:?}",
xpath, fastxml_texts, libxml_texts
))
} else {
CompareResult::ok()
}
}
pub fn assert_parse_consistency(xml: &str) {
let fastxml_result = fastxml::parse(xml);
let libxml_result = libxml_parse(xml);
match (&fastxml_result, &libxml_result) {
(Ok(_), Ok(_)) => {} (Err(_), Err(_)) => {} (Ok(_), Err(e)) => {
panic!("fastxml succeeded but libxml failed: {}", e);
}
(Err(e), Ok(_)) => {
panic!("fastxml failed but libxml succeeded: {}", e);
}
}
}
pub fn validate_with_libxml(xml: &str, xsd: &str) -> (bool, Vec<String>) {
use libxml::parser::Parser;
use libxml::schemas::{SchemaParserContext, SchemaValidationContext};
let parser = Parser::default();
let doc = parser
.parse_string(xml)
.expect("libxml: Failed to parse XML");
let mut schema_parser = SchemaParserContext::from_buffer(xsd.as_bytes());
let mut ctx = SchemaValidationContext::from_parser(&mut schema_parser)
.expect("libxml: Failed to create validation context");
let result = ctx.validate_document(&doc);
let is_valid = result.is_ok();
let messages: Vec<String> = if let Err(errors) = result {
errors.iter().filter_map(|e| e.message.clone()).collect()
} else {
vec![]
};
(is_valid, messages)
}
pub fn compare_xsd_validation(xml: &str, xsd: &str) -> CompareResult {
use fastxml::schema::validator::XmlSchemaValidationContext;
use fastxml::schema::xsd::parse_xsd;
let fastxml_valid = match fastxml::parse(xml.as_bytes()) {
Ok(doc) => match parse_xsd(xsd.as_bytes()) {
Ok(schema) => {
let ctx = XmlSchemaValidationContext::new(schema);
ctx.validate(&doc)
.map(|errors| errors.iter().all(|e| !e.is_error()))
.unwrap_or(false)
}
Err(_) => return CompareResult::diff("fastxml: Failed to parse XSD"),
},
Err(_) => return CompareResult::diff("fastxml: Failed to parse XML"),
};
let (libxml_valid, _) = validate_with_libxml(xml, xsd);
if fastxml_valid == libxml_valid {
CompareResult::ok()
} else {
CompareResult::diff(format!(
"Validation result differs: fastxml={}, libxml={}",
fastxml_valid, libxml_valid
))
}
}
}
#[macro_export]
#[cfg(feature = "compare-libxml")]
macro_rules! compare_with_libxml {
(parse: $xml:expr, $doc:expr) => {
$crate::common::libxml_compare::compare_parse($xml, $doc).assert_match();
};
(xpath: $xml:expr, $xpath:expr, $doc:expr) => {
$crate::common::libxml_compare::compare_xpath($xml, $xpath, $doc).assert_match();
};
(xpath_vars: $xml:expr, $xpath:expr, $doc:expr, $fastxml_vars:expr, $libxml_vars:expr) => {
$crate::common::libxml_compare::compare_xpath_with_variables(
$xml,
$xpath,
$doc,
$fastxml_vars,
$libxml_vars,
)
.assert_match();
};
(consistency: $xml:expr) => {
$crate::common::libxml_compare::assert_parse_consistency($xml);
};
(validate: $xml:expr, $xsd:expr) => {
$crate::common::libxml_compare::compare_xsd_validation($xml, $xsd).assert_match();
};
}
#[macro_export]
#[cfg(not(feature = "compare-libxml"))]
macro_rules! compare_with_libxml {
(parse: $xml:expr, $doc:expr) => {};
(xpath: $xml:expr, $xpath:expr, $doc:expr) => {};
(xpath_vars: $xml:expr, $xpath:expr, $doc:expr, $fastxml_vars:expr, $libxml_vars:expr) => {};
(consistency: $xml:expr) => {};
(validate: $xml:expr, $xsd:expr) => {};
}
use std::sync::Arc;
use fastxml::StructuredError;
#[allow(deprecated)]
use fastxml::schema::validator::{
DomSchemaValidator, OnePassSchemaValidator, TwoPassSchemaValidator,
};
use fastxml::schema::xsd::parse_xsd;
#[allow(dead_code)]
#[derive(Debug)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<StructuredError>,
}
#[allow(dead_code)]
impl ValidationResult {
pub fn is_valid(&self) -> bool {
self.valid
}
pub fn errors(&self) -> &[StructuredError] {
&self.errors
}
pub fn assert_errors_have_line(&self) {
for (i, error) in self.errors.iter().filter(|e| e.is_error()).enumerate() {
assert!(
error.line().is_some(),
"Error {} should have line number: {:?}",
i,
error
);
}
}
pub fn assert_errors_have_line_column(&self) {
for (i, error) in self.errors.iter().filter(|e| e.is_error()).enumerate() {
assert!(
error.line().is_some(),
"Error {} should have line number: {:?}",
i,
error
);
assert!(
error.column().is_some(),
"Error {} should have column number: {:?}",
i,
error
);
}
}
pub fn assert_first_error_line(&self, expected_line: usize) {
let error = self
.errors
.iter()
.find(|e| e.is_error())
.expect("Expected at least one error");
assert_eq!(
error.line(),
Some(expected_line),
"First error line mismatch. Error: {:?}",
error
);
}
pub fn assert_first_error_position(&self, expected_line: usize, expected_column: usize) {
let error = self
.errors
.iter()
.find(|e| e.is_error())
.expect("Expected at least one error");
assert_eq!(
error.line(),
Some(expected_line),
"First error line mismatch. Error: {:?}",
error
);
assert_eq!(
error.column(),
Some(expected_column),
"First error column mismatch. Error: {:?}",
error
);
}
}
#[allow(dead_code)]
pub fn validate_dom(xml: &str, xsd: &str) -> ValidationResult {
let doc = fastxml::parse(xml.as_bytes()).expect("Failed to parse XML");
let schema = parse_xsd(xsd.as_bytes()).expect("Failed to parse XSD");
let validator = DomSchemaValidator::new(Arc::new(schema));
let errors = validator.validate(&doc).expect("Validation failed");
let valid = errors.iter().all(|e| !e.is_error());
ValidationResult { valid, errors }
}
#[allow(dead_code, deprecated)]
pub fn validate_twopass(xml: &str, xsd: &str) -> ValidationResult {
use std::io::Cursor;
let schema = parse_xsd(xsd.as_bytes()).expect("Failed to parse XSD");
let reader = Cursor::new(xml.as_bytes().to_vec());
let errors = TwoPassSchemaValidator::new(Arc::new(schema))
.validate(reader)
.expect("Validation failed");
let valid = errors.iter().all(|e| !e.is_error());
ValidationResult { valid, errors }
}
#[allow(dead_code)]
pub fn validate_onepass(xml: &str, xsd: &str) -> ValidationResult {
use std::io::BufReader;
let schema = parse_xsd(xsd.as_bytes()).expect("Failed to parse XSD");
let reader = BufReader::new(xml.as_bytes());
let errors = OnePassSchemaValidator::new(Arc::new(schema))
.validate(reader)
.expect("Validation failed");
let valid = errors.iter().all(|e| !e.is_error());
ValidationResult { valid, errors }
}
#[allow(dead_code)]
pub fn validate_all(
xml: &str,
xsd: &str,
) -> (ValidationResult, ValidationResult, ValidationResult) {
let dom = validate_dom(xml, xsd);
let twopass = validate_twopass(xml, xsd);
let onepass = validate_onepass(xml, xsd);
(dom, twopass, onepass)
}
#[allow(dead_code)]
pub fn assert_validators_consistent(xml: &str, xsd: &str) {
let (dom, twopass, onepass) = validate_all(xml, xsd);
assert_eq!(
dom.is_valid(),
twopass.is_valid(),
"DOM vs TwoPass mismatch for validity.\nDOM errors: {:?}\nTwoPass errors: {:?}",
dom.errors(),
twopass.errors()
);
assert_eq!(
twopass.is_valid(),
onepass.is_valid(),
"TwoPass vs OnePass mismatch for validity.\nTwoPass errors: {:?}\nOnePass errors: {:?}",
twopass.errors(),
onepass.errors()
);
}
#[macro_export]
macro_rules! test_validation {
($name:ident, $xml:expr, $xsd:expr, false, line: $line:expr, column: $col:expr) => {
mod $name {
use super::*;
#[test]
fn dom() {
let result = $crate::common::validate_dom($xml, $xsd);
assert!(
!result.is_valid(),
"DOM validation: expected invalid, got valid"
);
result.assert_first_error_position($line, $col);
}
#[test]
fn twopass() {
let result = $crate::common::validate_twopass($xml, $xsd);
assert!(
!result.is_valid(),
"TwoPass validation: expected invalid, got valid"
);
result.assert_first_error_position($line, $col);
}
#[test]
fn onepass() {
let result = $crate::common::validate_onepass($xml, $xsd);
assert!(
!result.is_valid(),
"OnePass validation: expected invalid, got valid"
);
result.assert_first_error_position($line, $col);
}
#[test]
fn consistency() {
$crate::common::assert_validators_consistent($xml, $xsd);
}
#[test]
fn libxml_comparison() {
$crate::compare_with_libxml!(validate: $xml, $xsd);
}
}
};
($name:ident, $xml:expr, $xsd:expr, $expected_valid:expr) => {
mod $name {
use super::*;
#[test]
fn dom() {
let result = $crate::common::validate_dom($xml, $xsd);
assert_eq!(
result.is_valid(),
$expected_valid,
"DOM validation: expected valid={}, got valid={}\nErrors: {:?}",
$expected_valid,
result.is_valid(),
result.errors()
);
if !$expected_valid {
result.assert_errors_have_line();
}
}
#[test]
fn twopass() {
let result = $crate::common::validate_twopass($xml, $xsd);
assert_eq!(
result.is_valid(),
$expected_valid,
"TwoPass validation: expected valid={}, got valid={}\nErrors: {:?}",
$expected_valid,
result.is_valid(),
result.errors()
);
if !$expected_valid {
result.assert_errors_have_line();
}
}
#[test]
fn onepass() {
let result = $crate::common::validate_onepass($xml, $xsd);
assert_eq!(
result.is_valid(),
$expected_valid,
"OnePass validation: expected valid={}, got valid={}\nErrors: {:?}",
$expected_valid,
result.is_valid(),
result.errors()
);
if !$expected_valid {
result.assert_errors_have_line();
}
}
#[test]
fn consistency() {
$crate::common::assert_validators_consistent($xml, $xsd);
}
#[test]
fn libxml_comparison() {
$crate::compare_with_libxml!(validate: $xml, $xsd);
}
}
};
}