PureXML/Tests/SchematronDynamicTests.swift
Mihaela Mihaljevic 68122ad887 feat(schematron): dynamic message content and phase support
Two ISO Schematron gaps, the most validator-relevant first:

- Dynamic messages: an assert/report message is now a template, so
  <value-of select=> renders the string value of its XPath at the context
  node and <name> renders the node's name. A finding can report actual
  values ("Age 120 must be under 100") instead of static text.
- Phases: <phase id><active pattern=> scopes which patterns run.
  validate(_:phase:) honors a requested phase or the schema's
  defaultPhase; nil or #ALL runs every pattern.

let variables, abstract rules/extends, diagnostics, and key()/document()
remain (the last depends on XSLT-style function injection).

Part of #80
2026-06-08 15:45:27 +02:00

82 lines
3.1 KiB
Swift

@testable import PureXML
import Testing
@Suite("Schematron dynamic messages and phases")
struct SchematronDynamicTests {
private func validate(_ schema: String, _ xml: String, phase: String? = nil) throws -> [PureXML.Validation.ValidationError] {
try PureXML.Validation.Schematron(schema: schema).validate(xml, phase: phase)
}
@Test("value-of renders the actual value in an assertion message")
func test_valueOf() throws {
let schema = """
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern>
<rule context="age">
<assert test="number(.) &lt; 100">Age <value-of select="."/> must be under 100</assert>
</rule>
</pattern>
</schema>
"""
let errors = try validate(schema, "<age>120</age>")
#expect(errors.count == 1)
#expect(errors.first?.reason == "Age 120 must be under 100")
}
@Test("name renders the context element's name")
func test_name() throws {
let schema = """
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern>
<rule context="item">
<assert test="@id">Element <name/> needs an id</assert>
</rule>
</pattern>
</schema>
"""
let errors = try validate(schema, "<item/>")
#expect(errors.count == 1)
#expect(errors.first?.reason == "Element item needs an id")
}
@Test("A phase activates only its listed patterns")
func test_phase() throws {
let schema = """
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<phase id="basic">
<active pattern="p1"/>
</phase>
<pattern id="p1">
<rule context="a"><assert test="false()">a is bad</assert></rule>
</pattern>
<pattern id="p2">
<rule context="b"><assert test="false()">b is bad</assert></rule>
</pattern>
</schema>
"""
let xml = "<root><a/><b/></root>"
// The basic phase runs only p1, so only the a-rule fires.
let basic = try validate(schema, xml, phase: "basic")
#expect(basic.count == 1)
#expect(basic.first?.reason == "a is bad")
// With no phase, every pattern runs.
#expect(try validate(schema, xml).count == 2)
// #ALL runs every pattern.
#expect(try validate(schema, xml, phase: "#ALL").count == 2)
}
@Test("defaultPhase scopes patterns when no phase is requested")
func test_defaultPhase() throws {
let schema = """
<schema xmlns="http://purl.oclc.org/dsdl/schematron" defaultPhase="only-a">
<phase id="only-a"><active pattern="p1"/></phase>
<pattern id="p1"><rule context="a"><assert test="false()">a is bad</assert></rule></pattern>
<pattern id="p2"><rule context="b"><assert test="false()">b is bad</assert></rule></pattern>
</schema>
"""
let errors = try validate(schema, "<root><a/><b/></root>")
#expect(errors.count == 1)
#expect(errors.first?.reason == "a is bad")
}
}