pub fn parse(source: &str) -> Result<oxml::Document, String> {
oxml::parse(source).map_err(|e| {
let (line, column) = e.line_column(source);
format!("{line}:{column}: {e}")
})
}
pub fn root_name(doc: &oxml::Document) -> Option<String> {
doc.root_element()
.and_then(|r| doc.element_name(r))
.map(|n| n.local.clone())
}
fn compile(expression: &str) -> Result<oxml::XPath, String> {
oxml::XPath::compile(expression).map_err(|e| e.to_string())
}
pub fn query_text(
doc: &oxml::Document,
expression: &str,
) -> Result<Vec<String>, String> {
let value = compile(expression)?.evaluate(doc);
Ok(value.nodes().map_or_else(
|| vec![value.to_str(doc)],
|nodes| nodes.iter().map(|n| doc.text(*n)).collect(),
))
}
pub fn query_value(
doc: &oxml::Document,
expression: &str,
) -> Result<String, String> {
Ok(compile(expression)?.evaluate(doc).to_str(doc))
}
pub fn query_count(
doc: &oxml::Document,
expression: &str,
) -> Result<usize, String> {
Ok(compile(expression)?
.evaluate(doc)
.nodes()
.map_or(0, <[oxml::NodeId]>::len))
}
pub fn is_well_formed(source: &str) -> bool {
oxml::parse(source).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
const DOC: &str = "<library count=\"2\">\
<book lang=\"en\"><title>Dune</title></book>\
<book lang=\"fr\"><title>Germinal</title></book>\
</library>";
fn doc() -> oxml::Document {
parse(DOC).expect("well-formed")
}
#[test]
fn parse_accepts_a_well_formed_document() {
assert!(parse(DOC).is_ok());
assert!(parse("<a/>").is_ok());
assert!(parse("<?xml version=\"1.0\"?><a>é 😀</a>").is_ok());
}
#[test]
fn a_parse_error_carries_a_position() {
let e = parse("<a><b></a>").expect_err("not well-formed");
assert!(e.contains(':'), "{e}");
let (line, rest) = e.split_once(':').expect("line:column");
assert!(line.parse::<u32>().is_ok(), "{e}");
assert!(
rest.split(':')
.next()
.and_then(|c| c.trim().parse::<u32>().ok())
.is_some(),
"{e}"
);
}
#[test]
fn root_name_reads_the_document_element() {
assert_eq!(root_name(&doc()).as_deref(), Some("library"));
}
#[test]
fn query_text_returns_one_entry_per_node() {
let titles = query_text(&doc(), "//title").expect("valid");
assert_eq!(titles, ["Dune", "Germinal"]);
}
#[test]
fn query_text_on_a_non_node_set_returns_the_value() {
assert_eq!(query_text(&doc(), "count(//book)").expect("valid"), ["2"]);
}
#[test]
fn query_text_reads_attributes() {
assert_eq!(
query_text(&doc(), "//book/@lang").expect("valid"),
["en", "fr"]
);
}
#[test]
fn query_value_formats_numbers_as_xpath_does() {
let d = doc();
assert_eq!(query_value(&d, "count(//book)").expect("valid"), "2");
assert_eq!(query_value(&d, "1 div 4").expect("valid"), "0.25");
assert_eq!(query_value(&d, "//book[1]/@lang").expect("valid"), "en");
assert_eq!(
query_value(&d, "count(//book) = 2").expect("valid"),
"true"
);
}
#[test]
fn query_count_counts_nodes_and_nothing_else() {
let d = doc();
assert_eq!(query_count(&d, "//book").expect("valid"), 2);
assert_eq!(query_count(&d, "//missing").expect("valid"), 0);
assert_eq!(query_count(&d, "count(//book)").expect("valid"), 0);
}
#[test]
fn a_malformed_expression_is_an_error_on_every_entry_point() {
let d = doc();
for bad in ["//[", "count(", "1 +", "//book[", "@@x"] {
assert!(query_text(&d, bad).is_err(), "query_text `{bad}`");
assert!(query_value(&d, bad).is_err(), "query_value `{bad}`");
assert!(query_count(&d, bad).is_err(), "query_count `{bad}`");
}
}
#[test]
fn is_well_formed_agrees_with_parse() {
for src in [
DOC,
"<a/>",
"<a><b/></a>",
"<a><b></a>",
"<a>",
"",
"not xml",
"<a></a><b></b>",
] {
assert_eq!(
is_well_formed(src),
parse(src).is_ok(),
"disagreement on `{src}`"
);
}
}
}