feat(relaxng): quote the offending value and name the datatype in text errors
A RELAX NG text/datatype mismatch now reports the rich datatype reason (quoting the value and naming the type, e.g. 'abc' is not a valid integer) and a value mismatch quotes both the actual text and the required literal, instead of a generic "text content is not valid". The detail is peeled from the content pattern at the failure point through choice, group, list, and ref.
This commit is contained in:
parent
3021615b66
commit
3be6832c23
@ -31,7 +31,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
once rather than only the first. Errors carry recovery hints (`expected <a>,
|
||||
<b>`) naming the element types accepted at the failure point, and distinguish
|
||||
unexpected elements, invalid or missing attributes, invalid text/datatype
|
||||
content, and missing required content. `RelaxNG.validation()` exposes the schema
|
||||
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
|
||||
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.
|
||||
|
||||
@ -73,7 +73,7 @@ extension PureXML.Schema.RelaxNGEngine {
|
||||
case let .text(value), let .cdata(value):
|
||||
let next = textDeriv(residual, value)
|
||||
if isNotAllowed(next) {
|
||||
errors.append(.init(reason: "text content is not valid in <\(name)>", at: path))
|
||||
errors.append(.init(reason: "\(textDetail(residual, value)) in <\(name)>", at: path))
|
||||
sequenceBroken = true
|
||||
} else {
|
||||
residual = next
|
||||
@ -115,6 +115,31 @@ extension PureXML.Schema.RelaxNGEngine {
|
||||
return (forceClose(opened), false)
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// generic message when the content admits no text at all.
|
||||
private func textDetail(_ pattern: Pattern, _ value: String) -> String {
|
||||
switch pattern {
|
||||
case let .data(type):
|
||||
type.validate(value) ?? "text content '\(value)' is not valid"
|
||||
case let .value(type, literal):
|
||||
type.valueMatches(value, literal: literal)
|
||||
? "text content '\(value)' is not valid"
|
||||
: "text content '\(value)' must be '\(literal)'"
|
||||
case let .list(inner):
|
||||
textDetail(inner, value)
|
||||
case let .choice(lhs, rhs):
|
||||
textDetail(isNotAllowed(textDeriv(lhs, value)) ? rhs : lhs, value)
|
||||
case let .group(lhs, _), let .after(lhs, _):
|
||||
textDetail(lhs, value)
|
||||
case let .ref(name):
|
||||
textDetail(resolve(name), value)
|
||||
default:
|
||||
"text content is not allowed"
|
||||
}
|
||||
}
|
||||
|
||||
/// The content (the `after` left side) an opened element's children must match.
|
||||
private func contentOf(_ pattern: Pattern) -> Pattern {
|
||||
switch pattern {
|
||||
|
||||
@ -90,6 +90,19 @@ struct RelaxNGLocatedErrorTests {
|
||||
"""
|
||||
let found = try reasons(rng, "<n>abc</n>")
|
||||
#expect(found.contains { $0.contains("<n>") })
|
||||
// The message quotes the offending value and names the expected datatype.
|
||||
#expect(found.contains { $0.contains("'abc'") && $0.contains("integer") })
|
||||
}
|
||||
|
||||
@Test("A value mismatch quotes both the actual text and the required literal")
|
||||
func test_valueMismatchDetail() throws {
|
||||
let rng = """
|
||||
<element name="flag" xmlns="http://relaxng.org/ns/structure/1.0">
|
||||
<value>on</value>
|
||||
</element>
|
||||
"""
|
||||
let found = try reasons(rng, "<flag>off</flag>")
|
||||
#expect(found.contains { $0.contains("'off'") && $0.contains("'on'") })
|
||||
}
|
||||
|
||||
@Test("The validation() value composes with the framework like the other validators")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user