Skip to main content

rusty_xml_valid/
schematron.rs

1//! ISO Schematron: `rule/@context` + `assert/@test` / `report/@test` via XPath 1.0.
2
3use rusty_xml_parser::{default_parse_options, xml_read_memory};
4use rusty_xml_tree::{NodeId, NodeKind, XmlDoc};
5use rusty_xml_xpath::{xml_xpath_cast_to_boolean, xml_xpath_eval, XmlXPathContext};
6
7const SCH: &[&str] = &[
8    "http://purl.oclc.org/dsdl/schematron",
9    "http://www.ascc.net/xml/schematron",
10];
11
12/// `xmlSchematronValidateDoc`.
13#[doc(alias = "xmlSchematronValidateDoc")]
14pub fn xml_schematron_validate_doc(sch: &[u8], doc: &XmlDoc) -> Result<(), String> {
15    let sdoc = xml_read_memory(sch, None, None, default_parse_options()).map_err(|e| e.to_string())?;
16    let mut errors = Vec::new();
17    walk_schema(&sdoc, sdoc.xml_doc_get_root_element(), doc, &mut errors);
18    if errors.is_empty() {
19        Ok(())
20    } else {
21        Err(errors.join("; "))
22    }
23}
24
25fn is_sch(doc: &XmlDoc, id: NodeId, name: &str) -> bool {
26    doc.kind(id) == NodeKind::Element
27        && doc.name(id) == name
28        && (doc.ns_uri(id).map(|u| SCH.contains(&u)).unwrap_or(true))
29}
30
31fn walk_schema(sdoc: &XmlDoc, node: Option<NodeId>, doc: &XmlDoc, errors: &mut Vec<String>) {
32    let Some(id) = node else { return };
33    if is_sch(sdoc, id, "rule") {
34        if let Some(ctx) = sdoc.xml_get_prop(id, "context") {
35            let nodes = eval_nodeset(doc, &ctx);
36            let mut c = sdoc.first_child(id);
37            while let Some(x) = c {
38                if is_sch(sdoc, x, "assert") {
39                    if let Some(test) = sdoc.xml_get_prop(x, "test") {
40                        for n in &nodes {
41                            if !eval_bool(doc, *n, &test) {
42                                errors.push(sdoc.xml_node_get_content(x));
43                            }
44                        }
45                    }
46                } else if is_sch(sdoc, x, "report") {
47                    if let Some(test) = sdoc.xml_get_prop(x, "test") {
48                        for n in &nodes {
49                            if eval_bool(doc, *n, &test) {
50                                errors.push(sdoc.xml_node_get_content(x));
51                            }
52                        }
53                    }
54                }
55                c = sdoc.next_sibling(x);
56            }
57        }
58    }
59    let mut c = sdoc.first_child(id);
60    while let Some(x) = c {
61        walk_schema(sdoc, Some(x), doc, errors);
62        c = sdoc.next_sibling(x);
63    }
64}
65
66fn eval_nodeset(doc: &XmlDoc, expr: &str) -> Vec<NodeId> {
67    let ctx = XmlXPathContext::xml_xpath_new_context(doc);
68    match xml_xpath_eval(expr, &ctx) {
69        Ok(rusty_xml_xpath::XPathObject::NodeSet(v)) => v,
70        _ => vec![],
71    }
72}
73
74fn eval_bool(doc: &XmlDoc, node: NodeId, expr: &str) -> bool {
75    let mut ctx = XmlXPathContext::xml_xpath_new_context(doc);
76    ctx.xml_xpath_set_context_node(node);
77    match xml_xpath_eval(expr, &ctx) {
78        Ok(o) => xml_xpath_cast_to_boolean(&o),
79        Err(_) => false,
80    }
81}