Merge: RELAX NG attribute-error detail
This commit is contained in:
commit
2551fd16fd
@ -33,7 +33,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
unexpected elements, invalid or missing attributes, invalid text/datatype
|
||||
content, and missing required content. A datatype mismatch quotes the offending
|
||||
value and names the expected type (`'abc' is not a valid integer`), and a value
|
||||
mismatch quotes both the actual text and the required literal. `RelaxNG.validation()` exposes the schema
|
||||
mismatch quotes both the actual text and the required literal. An attribute
|
||||
failure distinguishes an undeclared attribute from a declared one with a bad
|
||||
value, quoting the value and naming its type. `RelaxNG.validation()` exposes the schema
|
||||
as a `Validation<Node, Void>`, so RELAX NG composes with the same validation
|
||||
framework as the XSD, DTD, and Schematron rules. The boolean engine remains the
|
||||
authority on validity; the walk only places and explains the failures.
|
||||
|
||||
@ -37,10 +37,7 @@ extension PureXML.Schema.RelaxNGEngine {
|
||||
for attribute in element.attributes where !Self.isNamespaceDeclaration(attribute) {
|
||||
let next = attributeDeriv(residual, attribute)
|
||||
if isNotAllowed(next) {
|
||||
errors.append(.init(
|
||||
reason: "attribute @\(attribute.name.description) is not allowed on <\(name)> or has an invalid value",
|
||||
at: path + [.attribute(attribute.name.description)],
|
||||
))
|
||||
errors.append(.init(reason: attributeDetail(residual, attribute, on: name), at: path + [.attribute(attribute.name.description)]))
|
||||
} else {
|
||||
residual = next
|
||||
}
|
||||
@ -115,6 +112,32 @@ extension PureXML.Schema.RelaxNGEngine {
|
||||
return (forceClose(opened), false)
|
||||
}
|
||||
|
||||
/// Explains an attribute failure: an undeclared attribute, or, when the name
|
||||
/// is declared, why its value is invalid (reusing the datatype detail).
|
||||
private func attributeDetail(_ residual: Pattern, _ attribute: PureXML.Model.Attribute, on name: String) -> String {
|
||||
guard let content = attributeContent(residual, attribute.name, visiting: []) else {
|
||||
return "attribute @\(attribute.name.description) is not allowed on <\(name)>"
|
||||
}
|
||||
return "attribute @\(attribute.name.description) on <\(name)>: \(textDetail(content, attribute.value))"
|
||||
}
|
||||
|
||||
/// The value pattern of a declared attribute whose name matches, or nil when
|
||||
/// no attribute of that name is permitted at this point.
|
||||
private func attributeContent(_ pattern: Pattern, _ wanted: PureXML.Model.QualifiedName, visiting: Set<String>) -> Pattern? {
|
||||
switch pattern {
|
||||
case let .attribute(nameClass, content):
|
||||
nameClass.contains(wanted) ? content : nil
|
||||
case let .choice(lhs, rhs), let .group(lhs, rhs), let .interleave(lhs, rhs):
|
||||
attributeContent(lhs, wanted, visiting: visiting) ?? attributeContent(rhs, wanted, visiting: visiting)
|
||||
case let .after(lhs, _), let .oneOrMore(lhs):
|
||||
attributeContent(lhs, wanted, visiting: visiting)
|
||||
case let .ref(reference):
|
||||
visiting.contains(reference) ? nil : attributeContent(resolve(reference), wanted, visiting: visiting.union([reference]))
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Explains why `value` does not satisfy the text/datatype `pattern`, quoting
|
||||
/// the offending value and naming the datatype it failed (`'abc' is not a
|
||||
/// valid integer`) or the literal it should have matched, falling back to a
|
||||
|
||||
@ -78,9 +78,25 @@ struct RelaxNGLocatedErrorTests {
|
||||
let found = try errors(rng, "<n count='abc'/>")
|
||||
#expect(found.count == 1)
|
||||
#expect(found.first?.reason.contains("@count") == true)
|
||||
// The declared attribute's bad value is quoted and the type named.
|
||||
#expect(found.first?.reason.contains("'abc'") == true)
|
||||
#expect(found.first?.reason.contains("integer") == true)
|
||||
#expect(found.first?.codingPath.map(\.stringValue) == ["n", "@count"])
|
||||
}
|
||||
|
||||
@Test("An undeclared attribute is distinguished from a bad value")
|
||||
func test_undeclaredAttribute() throws {
|
||||
let rng = """
|
||||
<element name="n" xmlns="http://relaxng.org/ns/structure/1.0">
|
||||
<attribute name="a"><text/></attribute>
|
||||
</element>
|
||||
"""
|
||||
let found = try errors(rng, "<n a='x' b='y'/>")
|
||||
#expect(found.count == 1)
|
||||
#expect(found.first?.reason.contains("@b") == true)
|
||||
#expect(found.first?.reason.contains("is not allowed") == true)
|
||||
}
|
||||
|
||||
@Test("Invalid datatype content is located on the element")
|
||||
func test_dataValue() throws {
|
||||
let rng = """
|
||||
|
||||
Loading…
Reference in New Issue
Block a user