diff --git a/CHANGELOG.md b/CHANGELOG.md index dfd9564..ce88f5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 , `) 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`, 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. diff --git a/Sources/Schema/RelaxNGDiagnostics.swift b/Sources/Schema/RelaxNGDiagnostics.swift index 42b044d..c93ed63 100644 --- a/Sources/Schema/RelaxNGDiagnostics.swift +++ b/Sources/Schema/RelaxNGDiagnostics.swift @@ -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 { diff --git a/Tests/RelaxNGLocatedErrorTests.swift b/Tests/RelaxNGLocatedErrorTests.swift index 35576e8..4c652e8 100644 --- a/Tests/RelaxNGLocatedErrorTests.swift +++ b/Tests/RelaxNGLocatedErrorTests.swift @@ -90,6 +90,19 @@ struct RelaxNGLocatedErrorTests { """ let found = try reasons(rng, "abc") #expect(found.contains { $0.contains("") }) + // 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 = """ + + on + + """ + let found = try reasons(rng, "off") + #expect(found.contains { $0.contains("'off'") && $0.contains("'on'") }) } @Test("The validation() value composes with the framework like the other validators")