Adds RelaxNG.errors(in:) reporting every way a document fails the schema as individual located errors with coding paths, recovering past each failure so all of a faulty document's problems surface at once instead of only the first. Errors carry recovery hints 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 as a Validation<Node, Void>, composing with the same framework as the XSD, DTD, and Schematron rules. The boolean derivative engine remains the authority on validity; the diagnostic walk only places and explains the failures, recovering each step so it never cascades. Splits the engine and the diagnostics into separate files.
65 lines
3.2 KiB
Swift
65 lines
3.2 KiB
Swift
public extension PureXML.Schema {
|
|
/// A compiled RELAX NG schema (XML syntax). `validate(_:)` checks an instance
|
|
/// document by the derivative algorithm.
|
|
struct RelaxNG: Sendable {
|
|
private let start: Pattern
|
|
private let defines: [String: Pattern]
|
|
|
|
/// Compiles a RELAX NG schema document (XML syntax). `schemaLoader`
|
|
/// resolves the `href` of `include` and `externalRef` to schema source;
|
|
/// it returns nil (the default) when external schemas are not available.
|
|
public init(_ rng: String, schemaLoader: @escaping (String) -> String? = { _ in nil }) throws {
|
|
(start, defines) = try RelaxNGParser.parse(rng, loader: schemaLoader)
|
|
}
|
|
|
|
/// Compiles a RELAX NG schema in the compact syntax (RNC). `schemaLoader`
|
|
/// resolves `include` and `external` references to compact-syntax source.
|
|
public init(compact rnc: String, schemaLoader: @escaping (String) -> String? = { _ in nil }) throws {
|
|
(start, defines) = try RelaxNGCompactParser.parse(rnc, loader: schemaLoader)
|
|
}
|
|
|
|
/// Whether `xml` is valid against the schema.
|
|
public func validate(_ xml: String) throws -> Bool {
|
|
guard case let .document(children) = try PureXML.parse(xml),
|
|
let root = children.compactMap(\.element).first
|
|
else {
|
|
return false
|
|
}
|
|
return RelaxNGEngine(defines: defines).matches(start: start, root: .element(root))
|
|
}
|
|
|
|
/// Every way `xml` fails the schema, as located errors with recovery hints,
|
|
/// so an editor can show all of a faulty document's problems at once rather
|
|
/// than only the first. An empty array means the document is valid.
|
|
public func errors(in xml: String) throws -> [PureXML.Validation.ValidationError] {
|
|
guard case let .document(children) = try PureXML.parse(xml),
|
|
let root = children.compactMap(\.element).first
|
|
else {
|
|
return [PureXML.Validation.ValidationError(reason: "the document has no root element", at: [])]
|
|
}
|
|
return RelaxNGEngine(defines: defines).locatedErrors(start: start, root: root)
|
|
}
|
|
|
|
/// The schema as a single ``PureXML/Validation/Validation`` over the
|
|
/// document root, so RELAX NG composes with the rest of the validation
|
|
/// framework exactly like the XSD, DTD, and Schematron rules.
|
|
public func validation() -> PureXML.Validation.Validation<PureXML.Model.Node, Void> {
|
|
let start = start
|
|
let defines = defines
|
|
return .init(
|
|
description: "Document satisfies the RELAX NG schema",
|
|
check: { context in
|
|
let root: PureXML.Model.Element? = switch context.subject {
|
|
case let .document(children): children.compactMap(\.element).first
|
|
case let .element(element): element
|
|
default: nil
|
|
}
|
|
guard let root else { return [] }
|
|
return RelaxNGEngine(defines: defines).locatedErrors(start: start, root: root)
|
|
},
|
|
when: { $0.codingPath.isEmpty },
|
|
)
|
|
}
|
|
}
|
|
}
|