Skip to content

Commit f8caa16

Browse files
committed
schematron: add --schematron to xmllint CLI, fix message interpolation
Add --schematron flag to xmllint following existing --relaxng/--schema patterns. Fix value-of message interpolation to use document context for NodeSet string values (was returning empty for element NodeSets). Add sum(item/@attr) test exercising the XPath attribute NodeSet fix.
1 parent 8d964c5 commit f8caa16

2 files changed

Lines changed: 83 additions & 1 deletion

File tree

src/bin/xmllint.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ struct Cli {
8181
#[arg(long, value_name = "FILE")]
8282
schema: Option<String>,
8383

84+
/// Validate against an ISO Schematron schema file.
85+
#[arg(long, value_name = "FILE")]
86+
schematron: Option<String>,
87+
8488
// -- XPath -------------------------------------------------------------
8589
/// Evaluate an XPath expression and print the result.
8690
#[allow(clippy::doc_markdown)]
@@ -219,6 +223,13 @@ fn process_file(cli: &Cli, filename: &str) -> u8 {
219223
}
220224
}
221225

226+
if let Some(ref sch_file) = cli.schematron {
227+
let code = validate_schematron_file(filename, &doc, sch_file);
228+
if code > exit_code {
229+
exit_code = code;
230+
}
231+
}
232+
222233
// -- XPath evaluation --------------------------------------------------
223234
if let Some(ref expr) = cli.xpath {
224235
evaluate_xpath(filename, &doc, expr);
@@ -377,6 +388,28 @@ fn validate_xsd_file(filename: &str, doc: &Document, xsd_file: &str) -> u8 {
377388
}
378389
}
379390

391+
/// Validates a document against an ISO Schematron schema file (--schematron).
392+
fn validate_schematron_file(filename: &str, doc: &Document, sch_file: &str) -> u8 {
393+
let schema_content = match fs::read_to_string(sch_file) {
394+
Ok(content) => content,
395+
Err(e) => {
396+
eprintln!("{sch_file}: failed to read Schematron schema: {e}");
397+
return EXIT_VALIDATION_ERROR;
398+
}
399+
};
400+
401+
match xmloxide::validation::schematron::parse_schematron(&schema_content) {
402+
Ok(schema) => {
403+
let result = xmloxide::validation::schematron::validate_schematron(doc, &schema);
404+
print_validation_result(filename, &result)
405+
}
406+
Err(e) => {
407+
eprintln!("{sch_file}: failed to parse Schematron schema: {e}");
408+
EXIT_VALIDATION_ERROR
409+
}
410+
}
411+
}
412+
380413
/// Prints validation errors/warnings and returns the exit code.
381414
fn print_validation_result(filename: &str, result: &xmloxide::validation::ValidationResult) -> u8 {
382415
for warning in &result.warnings {

src/validation/schematron.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -797,7 +797,7 @@ fn interpolate_message(
797797
ctx.set_variable(name, val.clone());
798798
}
799799
if let Ok(val) = ctx.evaluate(&expr) {
800-
result.push_str(&val.to_xpath_string());
800+
result.push_str(&xpath_value_to_string(doc, &val));
801801
}
802802
}
803803
}
@@ -806,6 +806,22 @@ fn interpolate_message(
806806
result
807807
}
808808

809+
/// Converts an `XPath` value to a string, computing string-value for
810+
/// node-sets using the document (unlike `to_xpath_string()` which returns
811+
/// empty for node-sets without document access).
812+
fn xpath_value_to_string(doc: &Document, val: &XPathValue) -> String {
813+
match val {
814+
XPathValue::NodeSet(nodes) => {
815+
if let Some(&first) = nodes.first() {
816+
doc.text_content(first)
817+
} else {
818+
String::new()
819+
}
820+
}
821+
_ => val.to_xpath_string(),
822+
}
823+
}
824+
809825
// ---------------------------------------------------------------------------
810826
// Tests
811827
// ---------------------------------------------------------------------------
@@ -1524,6 +1540,39 @@ mod tests {
15241540
);
15251541
}
15261542

1543+
#[test]
1544+
fn test_validate_sum_attribute_path() {
1545+
// Tests that sum(child/@attr) works correctly now that
1546+
// attribute paths return proper NodeSets.
1547+
let schema = parse_schematron(
1548+
r#"
1549+
<schema xmlns="http://purl.oclc.org/dml/schematron">
1550+
<pattern>
1551+
<rule context="/order">
1552+
<let name="total" value="sum(item/@price)"/>
1553+
<assert test="$total = @expected">Total <value-of select="$total"/> does not match expected <value-of select="@expected"/></assert>
1554+
</rule>
1555+
</pattern>
1556+
</schema>
1557+
"#,
1558+
)
1559+
.unwrap();
1560+
1561+
let doc_pass = Document::parse_str(
1562+
r#"<order expected="30"><item price="10"/><item price="20"/></order>"#,
1563+
)
1564+
.unwrap();
1565+
let result = validate_schematron(&doc_pass, &schema);
1566+
assert!(result.is_valid, "sum should equal 30: {:?}", result.errors);
1567+
1568+
let doc_fail = Document::parse_str(
1569+
r#"<order expected="99"><item price="10"/><item price="20"/></order>"#,
1570+
)
1571+
.unwrap();
1572+
let result = validate_schematron(&doc_fail, &schema);
1573+
assert!(!result.is_valid);
1574+
}
1575+
15271576
// ===================================================================
15281577
// Additional edge case tests
15291578
// ===================================================================

0 commit comments

Comments
 (0)