Add RelaxNG.validate(streaming:), driving the Brzozowski derivative directly from the parse-event stream (the libxml2 xmlTextReader model). The derivative is already incremental: the after-threaded pattern chain is the validation stack, so only the residual pattern and a light per-element frame (for the childrenDeriv ignorable-whitespace rule) are retained, never the tree. A differential test asserts streaming agrees with the trusted tree engine across valid, invalid, whitespace, and mixed-content documents. XSD streaming remains. Part of #107, #105
79 lines
4.0 KiB
Swift
79 lines
4.0 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))
|
|
}
|
|
|
|
/// Whether `xml` is valid against the schema, validated while it is pulled
|
|
/// event by event (the libxml2 `xmlTextReader` model) rather than over a
|
|
/// built tree. The Brzozowski derivative is itself incremental, so only the
|
|
/// residual pattern and a light per-element whitespace frame are retained.
|
|
public func validate(streaming xml: String, limits: PureXML.Parsing.Limits = .default) throws -> Bool {
|
|
let engine = RelaxNGEngine(defines: defines)
|
|
var state = engine.streamingStart(start)
|
|
var reader = PureXML.Parsing.EventReader(xml, limits: limits)
|
|
while let event = try reader.next() {
|
|
engine.streamingConsume(event, into: &state)
|
|
}
|
|
return engine.streamingValid(state)
|
|
}
|
|
|
|
/// 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 },
|
|
)
|
|
}
|
|
}
|
|
}
|