use assertables::*;
use schematron::{Document, Schema};
fn schema_with(body: &str) -> String {
format!(r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron">{body}</schema>"#)
}
fn failures(body: &str, document: &str) -> Vec<String> {
let schema = Schema::from_str(&schema_with(body)).expect("schema should compile");
let document = Document::from_str(document).expect("document should parse");
schema
.validate(&document)
.expect("validation should run")
.failures()
.map(|f| f.text.trim().to_string())
.collect()
}
const CATALOGUE: &str = r#"
<order>
<parts><part id="A"/><part id="B"/></parts>
<line ref="A"/>
<line ref="Z"/>
</order>
"#;
#[test]
fn a_key_resolves_a_cross_reference() {
let found = failures(
r#"<key name="parts" match="part" use="@id"/>
<pattern><rule context="line">
<assert test="key('parts', @ref)">no part <value-of select="@ref"/></assert>
</rule></pattern>"#,
CATALOGUE,
);
assert_eq!(found, vec!["no part Z"]);
}
#[test]
fn a_lookup_returns_the_indexed_nodes_themselves() {
let found = failures(
r#"<key name="parts" match="part" use="@id"/>
<pattern><rule context="line">
<assert test="key('parts', @ref)/@id = @ref">mismatch</assert>
</rule></pattern>"#,
r#"<order><parts><part id="A"/></parts><line ref="A"/></order>"#,
);
assert_is_empty!(found);
}
#[test]
fn a_use_selecting_several_nodes_indexes_under_each() {
let found = failures(
r#"<key name="parts" match="part" use="alias"/>
<pattern><rule context="line">
<assert test="key('parts', @ref)">no part <value-of select="@ref"/></assert>
</rule></pattern>"#,
r#"<order>
<part><alias>A</alias><alias>AA</alias></part>
<line ref="A"/><line ref="AA"/><line ref="Z"/>
</order>"#,
);
assert_eq!(found, vec!["no part Z"]);
}
#[test]
fn a_node_set_lookup_value_is_existential() {
let found = failures(
r#"<key name="parts" match="part" use="@id"/>
<pattern><rule context="order">
<assert test="count(key('parts', line/@ref)) = 2">
found <value-of select="count(key('parts', line/@ref))"/>
</assert>
</rule></pattern>"#,
r#"<order>
<part id="A"/><part id="B"/><part id="C"/>
<line ref="A"/><line ref="B"/>
</order>"#,
);
assert_is_empty!(found);
}
#[test]
fn a_key_matching_nothing_is_still_declared() {
let found = failures(
r#"<key name="parts" match="nothing" use="@id"/>
<pattern><rule context="line">
<assert test="key('parts', @ref)">no part</assert>
</rule></pattern>"#,
r#"<order><line ref="A"/></order>"#,
);
assert_eq!(found, vec!["no part"]);
}
#[test]
fn looking_up_an_undeclared_key_is_an_error() {
let schema = Schema::from_str(&schema_with(
r#"<key name="parts" match="part" use="@id"/>
<pattern><rule context="line">
<assert test="key('prats', @ref)">no part</assert>
</rule></pattern>"#,
))
.expect("schema should compile");
let document = Document::from_str(CATALOGUE).unwrap();
let error = schema.validate(&document).unwrap_err();
assert_contains!(error.to_string(), "prats");
assert_contains!(error.to_string(), "no key named");
}
#[test]
fn two_keys_may_not_share_a_name() {
let error = Schema::from_str(&schema_with(
r#"<key name="parts" match="part" use="@id"/>
<key name="parts" match="item" use="@id"/>
<pattern><rule context="line"><assert test="key('parts', @ref)">m</assert></rule></pattern>"#,
))
.unwrap_err();
assert_contains!(error.to_string(), "ambiguous");
}
#[test]
fn a_key_needs_all_three_attributes() {
for body in [
r#"<key match="part" use="@id"/>"#,
r#"<key name="parts" use="@id"/>"#,
r#"<key name="parts" match="part"/>"#,
] {
let source = schema_with(&format!(
r#"{body}<pattern><rule context="a"><assert test="b">m</assert></rule></pattern>"#
));
assert!(Schema::from_str(&source).is_err(), "{body} should be rejected");
}
}
#[test]
fn a_key_match_is_a_match_pattern() {
let error = Schema::from_str(&schema_with(
r#"<key name="parts" match="ancestor::part" use="@id"/>
<pattern><rule context="line"><assert test="key('parts', @ref)">m</assert></rule></pattern>"#,
))
.unwrap_err();
assert_contains!(error.to_string(), "not allowed in a rule context");
}
#[test]
fn a_broken_key_expression_fails_when_the_schema_loads() {
let error = Schema::from_str(&schema_with(
r#"<key name="parts" match="part" use="count(@id"/>
<pattern><rule context="line"><assert test="key('parts', @ref)">m</assert></rule></pattern>"#,
))
.unwrap_err();
assert_contains!(error.to_string(), "@use");
}
#[test]
fn keys_work_under_an_xpath_two_binding_too() {
let source = r#"<schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2">
<key name="parts" match="part" use="@id"/>
<pattern><rule context="line">
<assert test="exists(key('parts', @ref))">no part</assert>
</rule></pattern>
</schema>"#;
let schema = Schema::from_str(source).expect("schema should compile");
let document = Document::from_str(CATALOGUE).unwrap();
assert_eq!(schema.validate(&document).unwrap().count_failures(), 1);
}
#[test]
fn a_key_nothing_looks_up_is_linted() {
let schema = Schema::from_str(&schema_with(
r#"<key name="unused" match="part" use="@id"/>
<pattern><rule context="line"><assert test="@ref">m</assert></rule></pattern>"#,
))
.unwrap();
let lints = schema.lint();
assert_eq!(lints.len(), 1, "{lints:?}");
assert_eq!(lints[0].kind.as_str(), "unreferenced-key");
assert_contains!(lints[0].location, "unused");
}
#[test]
fn a_key_that_is_looked_up_is_not_linted() {
let schema = Schema::from_str(&schema_with(
r#"<key name="parts" match="part" use="@id"/>
<pattern><rule context="line">
<assert test="key('parts', @ref)">m</assert>
</rule></pattern>"#,
))
.unwrap();
assert_is_empty!(schema.lint());
}
#[test]
fn calling_key_without_indexes_is_an_error() {
use schematron::xpath::{evaluate, parse, EvalContext, Namespaces, Variables};
let document = Document::from_str("<a/>").unwrap();
let expr = parse("key('parts', 'A')").unwrap();
let variables = Variables::new();
let namespaces = Namespaces::new();
let context = EvalContext::new(&document, document.root(), &variables, &namespaces);
let error = evaluate(&expr, &context).unwrap_err();
assert_contains!(error.message, "key indexes");
}
#[test]
fn keys_are_built_per_document_for_a_documents_pattern() {
use std::sync::Arc;
use schematron::schema::{MemoryResolver, SchemaOptions};
let resolver = MemoryResolver::new().with(
"external.xml",
r#"<catalogue><part id="X"/><line ref="X"/><line ref="Y"/></catalogue>"#,
);
let options = SchemaOptions::new().with_resolver(Arc::new(resolver));
let schema = Schema::from_str_with(
&schema_with(
r#"<key name="parts" match="part" use="@id"/>
<pattern documents="root/ref/@href">
<rule context="line">
<assert test="key('parts', @ref)">no part <value-of select="@ref"/></assert>
</rule>
</pattern>"#,
),
&options,
)
.expect("schema should compile");
let document =
Document::from_str(r#"<root><ref href="external.xml"/></root>"#).unwrap();
let report = schema.validate(&document).unwrap();
assert_eq!(report.count_failures(), 1);
assert_contains!(report.failures().next().unwrap().text, "Y");
}