schematron-engine 0.1.2

A pure-Rust ISO Schematron rule engine, generic over the document model
Documentation

schematron-engine

A pure-Rust implementation of ISO Schematron — parses a Schematron .sch schema (pattern/rule/assert/report structure) and evaluates it against a document, producing structured assertion results (which rules fired, passed, or failed, with their messages).

Generic and standalone — not tied to HTML, XML parsing, or any specific document type. Callers provide their own parsed document via a trait; XPath test="..." expressions are evaluated using xpath-eval, a separate crate, so that XPath evaluation stays independently reusable outside of Schematron.

Scope and the name "engine"

Two-stage goal, deliberately in that order:

  1. First: implement the subset of ISO Schematron that html-conform actually needs for its assertion layer — enough pattern/rule/ assert/report handling to run its rule set, nothing speculative.
  2. Then: full ISO Schematron conformance — phase selection, <let> variable bindings, <value-of> message interpolation, abstract patterns/extends, diagnostics. (Rule conflict resolution — "first matching rule per pattern wins" — is not a stage-1 placeholder awaiting a priority attribute; checked against the ISO grammar, rule has no such attribute, so that behavior already is stage 2's final semantics.)

Status

Both stages are complete: schema parsing, evaluation against a document, <let> variable bindings, phase selection, <value-of> message interpolation, abstract patterns/extends, and diagnostics, plus referential-integrity checking (duplicate/unknown ids) and hardened structural validation on top — see "Support matrix" below for exactly what's covered and what's explicitly out of scope.

Usage

use schematron_engine::{evaluate, parse};
use xpath_eval::Document;

# fn example<D: Document>(document: &D) -> Result<(), Box<dyn std::error::Error>> {
let schema = parse(
    r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">
        <pattern>
            <rule context="item">
                <assert test="@id">item must have an id attribute</assert>
            </rule>
        </pattern>
    </schema>"#,
)?;

let reports = evaluate(&schema, document)?;
for report in &reports {
    println!("{:?}: {}", report.kind, report.message);
}
# Ok(())
# }

This crate never parses XML itself — document above is anything implementing xpath_eval::Document/Node over your own tree (an XML DOM, an HTML parser's tree, ...). Note that this means callers depend on xpath-eval directly too, not just on schematron-engine: Document/ Node are part of evaluate's own signature (evaluate<D: Document>), so implementing them requires importing that crate's traits. See tests/integration.rs for a complete, runnable adapter over a real XML tree (via roxmltree), evaluating a realistic schema end to end.

xpath-eval's version is a compatibility boundary, not an implementation detail — Cargo.toml pins a version requirement, and CI always builds and tests against whatever version that requirement (and Cargo.lock) resolves to.

Support matrix

Both ISO Schematron stages this crate set out to implement (see "Scope and the name..." above) are complete. What's explicitly not supported, and what happens when a schema uses it anyway — surfaced as an error rather than silently doing the wrong thing:

Construct Behavior
<extends rule="..."> (same-schema rule reuse) supported
<extends href="..."> (external-file rule reuse) MissingAttribute (extends requires @rule; resolving @href would need a file/network I/O policy this crate deliberately doesn't have)
<let value="..."> supported; <let> with literal-XML (foreign-element+) content instead of value is a MissingAttribute error
<name>/<emph>/<dir>/<span> in messages accepted as plain, inert text containers — their own text still contributes to the message, but their specific semantics (e.g. <name/> resolving to the context node's name) are not evaluated. Only <value-of> is
rule/@context match-pattern semantics reduced to descendant-or-self::node()/context, a location-path selection — covers the common case, not every exotic XSLT-match-pattern-style context
<include> (schema composition) ParseError::UnexpectedElement — silently ignoring it would produce an incomplete, wrongly-passing schema, so this is a hard error rather than a silent gap
Unknown or misplaced Schematron-namespace elements (typos, elements in the wrong container) ParseError::UnexpectedElement, not silently dropped — <title>/<p> (prose/documentation) are accepted wherever ISO allows them, even though their content isn't read
Duplicate pattern/@id or diagnostic/@id ParseError::DuplicateId
active/@pattern referencing an unknown or abstract pattern id ParseError::UnknownActivePattern
assert|report/@diagnostics referencing an unknown diagnostic id ParseError::UnknownDiagnosticReference
Foreign-namespace elements (any namespace other than Schematron's) always allowed, anywhere — ISO's own "foreign" content model

Installation

[dependencies]
schematron-engine = "0.1"
xpath-eval = "0.2" # implement Document/Node over your own document tree

License

MIT — see LICENSE.