feat(schema): RELAX NG pattern model, parser, and derivative validation
Add the RELAX NG pattern algebra (Pattern, NameClass), a parser for the XML syntax into a grammar of start plus named defines, and James Clark's derivative validation algorithm. The pattern is derived through each start-tag, attribute, text, and end-tag event with simplifying constructors; the document is valid when the residual pattern is nullable. Supports element, attribute, text, empty, notAllowed, group, choice, interleave, optional, zeroOrMore, oneOrMore, mixed, list, ref/define, data, value, and the name classes; handles interleave and recursive grammars, reusing the XSD datatype library for data and value. Closes #11 Closes #12
This commit is contained in:
parent
e845b63ff0
commit
b19e48a6eb
@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- RELAX NG validation by derivatives (toward #2). `PureXML.Schema.RelaxNG`
|
||||
parses a RELAX NG schema in the XML syntax into a pattern grammar (`element`,
|
||||
`attribute`, `text`, `empty`, `notAllowed`, `group`, `choice`, `interleave`,
|
||||
`optional`, `zeroOrMore`, `oneOrMore`, `mixed`, `list`, `ref`/`define`, `data`,
|
||||
`value`, and the name classes), and `validate(_:)` checks an instance document
|
||||
by James Clark's derivative algorithm: the pattern is derived through each
|
||||
start-tag, attribute, text, and end-tag event, and the document is valid when
|
||||
the residual pattern is nullable. Handles interleave, recursive grammars, and
|
||||
reuses the XSD datatype library for `data`/`value`.
|
||||
- XSD schema-document parser, completing the XSD validation pipeline (toward #2).
|
||||
`PureXML.Schema.Document(xsd)` compiles a schema document into its global
|
||||
element declarations and named-type table, and `validate(_:)` checks an
|
||||
|
||||
258
Sources/Schema/RelaxNGDerivative.swift
Normal file
258
Sources/Schema/RelaxNGDerivative.swift
Normal file
@ -0,0 +1,258 @@
|
||||
extension PureXML.Schema {
|
||||
/// Validates a document against a RELAX NG grammar by Brzozowski-style
|
||||
/// derivatives (James Clark's "An algorithm for RELAX NG validation"). The
|
||||
/// pattern is repeatedly derived with respect to each event of the instance;
|
||||
/// the document is valid when the residual pattern is nullable.
|
||||
struct RelaxNGEngine {
|
||||
let defines: [String: PureXML.Schema.Pattern]
|
||||
|
||||
/// Whether `root` matches `start`.
|
||||
func matches(start: PureXML.Schema.Pattern, root: PureXML.Model.Node) -> Bool {
|
||||
nullable(childDeriv(start, root))
|
||||
}
|
||||
|
||||
// MARK: Simplifying constructors
|
||||
|
||||
private func choice(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||
if case .notAllowed = lhs { return rhs }
|
||||
if case .notAllowed = rhs { return lhs }
|
||||
return .choice(lhs, rhs)
|
||||
}
|
||||
|
||||
private func group(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||
if case .notAllowed = lhs { return .notAllowed }
|
||||
if case .notAllowed = rhs { return .notAllowed }
|
||||
if case .empty = lhs { return rhs }
|
||||
if case .empty = rhs { return lhs }
|
||||
return .group(lhs, rhs)
|
||||
}
|
||||
|
||||
private func interleave(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||
if case .notAllowed = lhs { return .notAllowed }
|
||||
if case .notAllowed = rhs { return .notAllowed }
|
||||
if case .empty = lhs { return rhs }
|
||||
if case .empty = rhs { return lhs }
|
||||
return .interleave(lhs, rhs)
|
||||
}
|
||||
|
||||
private func after(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||
if case .notAllowed = lhs { return .notAllowed }
|
||||
if case .notAllowed = rhs { return .notAllowed }
|
||||
return .after(lhs, rhs)
|
||||
}
|
||||
|
||||
private func resolve(_ name: String) -> PureXML.Schema.Pattern {
|
||||
defines[name] ?? .notAllowed
|
||||
}
|
||||
|
||||
// MARK: Nullability
|
||||
|
||||
func nullable(_ pattern: PureXML.Schema.Pattern, visiting: Set<String> = []) -> Bool {
|
||||
switch pattern {
|
||||
case .empty, .text: return true
|
||||
case let .choice(lhs, rhs): return nullable(lhs, visiting: visiting) || nullable(rhs, visiting: visiting)
|
||||
case let .group(lhs, rhs), let .interleave(lhs, rhs):
|
||||
return nullable(lhs, visiting: visiting) && nullable(rhs, visiting: visiting)
|
||||
case let .oneOrMore(inner), let .list(inner): return nullable(inner, visiting: visiting)
|
||||
case let .ref(name):
|
||||
guard !visiting.contains(name) else { return false }
|
||||
return nullable(resolve(name), visiting: visiting.union([name]))
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Text and attribute derivatives
|
||||
|
||||
private func textDeriv(_ pattern: PureXML.Schema.Pattern, _ string: String) -> PureXML.Schema.Pattern {
|
||||
switch pattern {
|
||||
case let .choice(lhs, rhs): return choice(textDeriv(lhs, string), textDeriv(rhs, string))
|
||||
case let .interleave(lhs, rhs):
|
||||
return choice(interleave(textDeriv(lhs, string), rhs), interleave(lhs, textDeriv(rhs, string)))
|
||||
case let .group(lhs, rhs):
|
||||
let derived = group(textDeriv(lhs, string), rhs)
|
||||
return nullable(lhs) ? choice(derived, textDeriv(rhs, string)) : derived
|
||||
case let .after(lhs, rhs): return after(textDeriv(lhs, string), rhs)
|
||||
case let .oneOrMore(inner):
|
||||
return group(textDeriv(inner, string), choice(.oneOrMore(inner), .empty))
|
||||
case .text: return .text
|
||||
case let .ref(name): return textDeriv(resolve(name), string)
|
||||
default: return valueDeriv(pattern, string)
|
||||
}
|
||||
}
|
||||
|
||||
private func valueDeriv(_ pattern: PureXML.Schema.Pattern, _ string: String) -> PureXML.Schema.Pattern {
|
||||
switch pattern {
|
||||
case let .value(literal):
|
||||
SimpleType(base: .token).validate(string) == nil
|
||||
&& SimpleType.process(string, whiteSpace: .collapse) == literal ? .empty : .notAllowed
|
||||
case let .data(type): SimpleType(base: type).validate(string) == nil ? .empty : .notAllowed
|
||||
case let .list(inner): listMatches(inner, string) ? .empty : .notAllowed
|
||||
default: .notAllowed
|
||||
}
|
||||
}
|
||||
|
||||
private func listMatches(_ pattern: PureXML.Schema.Pattern, _ string: String) -> Bool {
|
||||
var residual = pattern
|
||||
for token in string.split(whereSeparator: \.isWhitespace) {
|
||||
residual = textDeriv(residual, String(token))
|
||||
}
|
||||
return nullable(residual)
|
||||
}
|
||||
|
||||
private func attributeDeriv(_ pattern: PureXML.Schema.Pattern, _ attribute: PureXML.Model.Attribute) -> PureXML.Schema.Pattern {
|
||||
switch pattern {
|
||||
case let .choice(lhs, rhs): return choice(attributeDeriv(lhs, attribute), attributeDeriv(rhs, attribute))
|
||||
case let .group(lhs, rhs):
|
||||
return choice(group(attributeDeriv(lhs, attribute), rhs), group(lhs, attributeDeriv(rhs, attribute)))
|
||||
case let .interleave(lhs, rhs):
|
||||
return choice(interleave(attributeDeriv(lhs, attribute), rhs), interleave(lhs, attributeDeriv(rhs, attribute)))
|
||||
case let .oneOrMore(inner):
|
||||
return group(attributeDeriv(inner, attribute), choice(.oneOrMore(inner), .empty))
|
||||
case let .after(lhs, rhs): return after(attributeDeriv(lhs, attribute), rhs)
|
||||
case let .attribute(nameClass, content):
|
||||
guard nameClass.contains(attribute.name), valueMatches(content, attribute.value) else { return .notAllowed }
|
||||
return .empty
|
||||
case let .ref(name): return attributeDeriv(resolve(name), attribute)
|
||||
default: return .notAllowed
|
||||
}
|
||||
}
|
||||
|
||||
private func valueMatches(_ content: PureXML.Schema.Pattern, _ value: String) -> Bool {
|
||||
value.isEmpty ? nullable(content) : nullable(textDeriv(content, value))
|
||||
}
|
||||
|
||||
private func oneOrMoreP(_ inner: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||
if case .notAllowed = inner { return .notAllowed }
|
||||
return .oneOrMore(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PureXML.Schema.RelaxNGEngine {
|
||||
// MARK: Element derivatives
|
||||
|
||||
func childDeriv(_ pattern: PureXML.Schema.Pattern, _ node: PureXML.Model.Node) -> PureXML.Schema.Pattern {
|
||||
switch node {
|
||||
case let .text(string), let .cdata(string):
|
||||
textDeriv(pattern, string)
|
||||
case let .element(element):
|
||||
processElement(pattern, element)
|
||||
default:
|
||||
pattern
|
||||
}
|
||||
}
|
||||
|
||||
private func processElement(_ pattern: PureXML.Schema.Pattern, _ element: PureXML.Model.Element) -> PureXML.Schema.Pattern {
|
||||
let opened = startTagOpenDeriv(pattern, element.name)
|
||||
let attributed = element.attributes
|
||||
.filter { !Self.isNamespaceDeclaration($0) }
|
||||
.reduce(opened) { attributeDeriv($0, $1) }
|
||||
let closed = startTagCloseDeriv(attributed)
|
||||
let withChildren = childrenDeriv(closed, element.children)
|
||||
return endTagDeriv(withChildren)
|
||||
}
|
||||
|
||||
private func startTagOpenDeriv(_ pattern: PureXML.Schema.Pattern, _ name: PureXML.Model.QualifiedName) -> PureXML.Schema.Pattern {
|
||||
switch pattern {
|
||||
case let .choice(lhs, rhs):
|
||||
return choice(startTagOpenDeriv(lhs, name), startTagOpenDeriv(rhs, name))
|
||||
case let .element(nameClass, content):
|
||||
return nameClass.contains(name) ? after(content, .empty) : .notAllowed
|
||||
case let .interleave(lhs, rhs):
|
||||
return choice(
|
||||
applyAfter({ interleave($0, rhs) }, startTagOpenDeriv(lhs, name)),
|
||||
applyAfter({ interleave(lhs, $0) }, startTagOpenDeriv(rhs, name)),
|
||||
)
|
||||
case let .group(lhs, rhs):
|
||||
let derived = applyAfter({ group($0, rhs) }, startTagOpenDeriv(lhs, name))
|
||||
return nullable(lhs) ? choice(derived, startTagOpenDeriv(rhs, name)) : derived
|
||||
case let .oneOrMore(inner):
|
||||
return applyAfter({ group($0, choice(oneOrMoreP(inner), .empty)) }, startTagOpenDeriv(inner, name))
|
||||
case let .after(lhs, rhs):
|
||||
return applyAfter({ after($0, rhs) }, startTagOpenDeriv(lhs, name))
|
||||
case let .ref(reference):
|
||||
return startTagOpenDeriv(resolve(reference), name)
|
||||
default:
|
||||
return .notAllowed
|
||||
}
|
||||
}
|
||||
|
||||
private func applyAfter(_ transform: (PureXML.Schema.Pattern) -> PureXML.Schema.Pattern, _ pattern: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||
switch pattern {
|
||||
case let .after(lhs, rhs): after(lhs, transform(rhs))
|
||||
case let .choice(lhs, rhs): choice(applyAfter(transform, lhs), applyAfter(transform, rhs))
|
||||
default: .notAllowed
|
||||
}
|
||||
}
|
||||
|
||||
private func startTagCloseDeriv(_ pattern: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||
switch pattern {
|
||||
case let .choice(lhs, rhs): choice(startTagCloseDeriv(lhs), startTagCloseDeriv(rhs))
|
||||
case let .group(lhs, rhs): group(startTagCloseDeriv(lhs), startTagCloseDeriv(rhs))
|
||||
case let .interleave(lhs, rhs): interleave(startTagCloseDeriv(lhs), startTagCloseDeriv(rhs))
|
||||
case let .oneOrMore(inner): oneOrMoreP(startTagCloseDeriv(inner))
|
||||
case let .after(lhs, rhs): after(startTagCloseDeriv(lhs), rhs)
|
||||
case .attribute: .notAllowed
|
||||
case let .ref(reference): startTagCloseDeriv(resolve(reference))
|
||||
default: pattern
|
||||
}
|
||||
}
|
||||
|
||||
private func childrenDeriv(_ pattern: PureXML.Schema.Pattern, _ children: [PureXML.Model.Node]) -> PureXML.Schema.Pattern {
|
||||
let coalesced = Self.coalesceText(children)
|
||||
if coalesced.isEmpty { return pattern }
|
||||
if coalesced.count == 1, case let .text(string) = coalesced[0] {
|
||||
let derived = textDeriv(pattern, string)
|
||||
return Self.isWhitespace(string) ? choice(pattern, derived) : derived
|
||||
}
|
||||
return coalesced.reduce(pattern) { accumulated, node in
|
||||
Self.isWhitespaceText(node) ? accumulated : childDeriv(accumulated, node)
|
||||
}
|
||||
}
|
||||
|
||||
private func endTagDeriv(_ pattern: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
|
||||
switch pattern {
|
||||
case let .choice(lhs, rhs): choice(endTagDeriv(lhs), endTagDeriv(rhs))
|
||||
case let .after(lhs, rhs): nullable(lhs) ? rhs : .notAllowed
|
||||
case let .ref(reference): endTagDeriv(resolve(reference))
|
||||
default: .notAllowed
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
/// Merges adjacent text and CDATA nodes into single text nodes.
|
||||
private static func coalesceText(_ children: [PureXML.Model.Node]) -> [PureXML.Model.Node] {
|
||||
var result: [PureXML.Model.Node] = []
|
||||
var run = ""
|
||||
for child in children {
|
||||
switch child {
|
||||
case let .text(value), let .cdata(value):
|
||||
run += value
|
||||
case .comment, .processingInstruction:
|
||||
continue
|
||||
default:
|
||||
if !run.isEmpty { result.append(.text(run))
|
||||
run = ""
|
||||
}
|
||||
result.append(child)
|
||||
}
|
||||
}
|
||||
if !run.isEmpty { result.append(.text(run)) }
|
||||
return result
|
||||
}
|
||||
|
||||
private static func isWhitespace(_ string: String) -> Bool {
|
||||
string.allSatisfy(\.isWhitespace)
|
||||
}
|
||||
|
||||
private static func isWhitespaceText(_ node: PureXML.Model.Node) -> Bool {
|
||||
if case let .text(value) = node { return isWhitespace(value) }
|
||||
return false
|
||||
}
|
||||
|
||||
private static func isNamespaceDeclaration(_ attribute: PureXML.Model.Attribute) -> Bool {
|
||||
attribute.name.prefix == "xmlns" || (attribute.name.prefix == nil && attribute.name.localName == "xmlns")
|
||||
}
|
||||
}
|
||||
170
Sources/Schema/RelaxNGParser.swift
Normal file
170
Sources/Schema/RelaxNGParser.swift
Normal file
@ -0,0 +1,170 @@
|
||||
private typealias Tree = PureXML.Model.TreeNode
|
||||
|
||||
/// Tree helpers for the RELAX NG parser. File-scope and private.
|
||||
private enum RNGNode {
|
||||
static func localName(_ node: Tree) -> String? {
|
||||
node.name?.localName
|
||||
}
|
||||
|
||||
static func attribute(_ node: Tree, _ name: String) -> String? {
|
||||
node.attributes.first { $0.name.localName == name }?.value
|
||||
}
|
||||
|
||||
static func elementChildren(_ node: Tree) -> [Tree] {
|
||||
node.children.filter { $0.kind == .element }
|
||||
}
|
||||
|
||||
static func children(_ node: Tree, named name: String) -> [Tree] {
|
||||
elementChildren(node).filter { localName($0) == name }
|
||||
}
|
||||
|
||||
static func text(_ node: Tree) -> String {
|
||||
node.stringValue.trimmingXMLWhitespace()
|
||||
}
|
||||
|
||||
static func strip(_ qualified: String) -> String {
|
||||
qualified.split(separator: ":").last.map(String.init) ?? qualified
|
||||
}
|
||||
}
|
||||
|
||||
extension PureXML.Schema {
|
||||
/// Parses a RELAX NG schema in the XML syntax into a grammar: a start pattern
|
||||
/// and the named `define` patterns it refers to. The vocabulary is matched by
|
||||
/// local name. Supports `element`, `attribute`, `text`, `empty`, `notAllowed`,
|
||||
/// `group`, `choice`, `interleave`, `optional`, `zeroOrMore`, `oneOrMore`,
|
||||
/// `mixed`, `list`, `ref`, `data`, `value`, and the name classes.
|
||||
enum RelaxNGParser {
|
||||
static func parse(_ rng: String) throws -> (start: Pattern, defines: [String: Pattern]) {
|
||||
let root = try PureXML.parseTree(rng)
|
||||
guard let top = RNGNode.elementChildren(root).first else {
|
||||
throw PureXML.Schema.SchemaError.notASchema
|
||||
}
|
||||
if RNGNode.localName(top) == "grammar" {
|
||||
return grammar(top)
|
||||
}
|
||||
return (pattern(top), [:])
|
||||
}
|
||||
|
||||
private static func grammar(_ node: Tree) -> (start: Pattern, defines: [String: Pattern]) {
|
||||
var defines: [String: Pattern] = [:]
|
||||
for define in RNGNode.children(node, named: "define") {
|
||||
if let name = RNGNode.attribute(define, "name") {
|
||||
defines[name] = combined(RNGNode.elementChildren(define), .sequence)
|
||||
}
|
||||
}
|
||||
let start = RNGNode.children(node, named: "start").first
|
||||
return (start.map { combined(RNGNode.elementChildren($0), .sequence) } ?? .notAllowed, defines)
|
||||
}
|
||||
|
||||
// MARK: Patterns
|
||||
|
||||
private static func pattern(_ node: Tree) -> Pattern {
|
||||
leafPattern(node) ?? compositePattern(node)
|
||||
}
|
||||
|
||||
/// The terminal and element/attribute patterns; nil for a compositor.
|
||||
private static func leafPattern(_ node: Tree) -> Pattern? {
|
||||
switch RNGNode.localName(node) {
|
||||
case "empty": .empty
|
||||
case "notAllowed": .notAllowed
|
||||
case "text": .text
|
||||
case "data": .data(BuiltinType(rawValue: RNGNode.strip(RNGNode.attribute(node, "type") ?? "string")) ?? .string)
|
||||
case "value": .value(RNGNode.text(node))
|
||||
case "ref": .ref(RNGNode.attribute(node, "name") ?? "")
|
||||
case "element": element(node)
|
||||
case "attribute": attribute(node)
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The compositor and quantifier patterns.
|
||||
private static func compositePattern(_ node: Tree) -> Pattern {
|
||||
switch RNGNode.localName(node) {
|
||||
case "group": combined(RNGNode.elementChildren(node), .sequence)
|
||||
case "choice": combined(RNGNode.elementChildren(node), .choice)
|
||||
case "interleave": combined(RNGNode.elementChildren(node), .all)
|
||||
case "optional": .choice(group(node), .empty)
|
||||
case "zeroOrMore": .choice(.oneOrMore(group(node)), .empty)
|
||||
case "oneOrMore": .oneOrMore(group(node))
|
||||
case "mixed": .interleave(group(node), .text)
|
||||
case "list": .list(group(node))
|
||||
default: .notAllowed
|
||||
}
|
||||
}
|
||||
|
||||
private static func group(_ node: Tree) -> Pattern {
|
||||
combined(RNGNode.elementChildren(node), .sequence)
|
||||
}
|
||||
|
||||
private static func element(_ node: Tree) -> Pattern {
|
||||
let (nameClass, contentNodes) = nameClassAndContent(node)
|
||||
return .element(nameClass, combined(contentNodes, .sequence))
|
||||
}
|
||||
|
||||
private static func attribute(_ node: Tree) -> Pattern {
|
||||
let (nameClass, contentNodes) = nameClassAndContent(node)
|
||||
let content = contentNodes.isEmpty ? Pattern.text : combined(contentNodes, .sequence)
|
||||
return .attribute(nameClass, content)
|
||||
}
|
||||
|
||||
/// Splits an `element`/`attribute` node into its name class and its pattern
|
||||
/// children: a `name` attribute gives the class and all children are
|
||||
/// content; otherwise the first child is the name class.
|
||||
private static func nameClassAndContent(_ node: Tree) -> (NameClass, [Tree]) {
|
||||
let children = RNGNode.elementChildren(node)
|
||||
if let name = RNGNode.attribute(node, "name") {
|
||||
let namespace = RNGNode.attribute(node, "ns") ?? ""
|
||||
return (.name(namespace: namespace, localName: name), children)
|
||||
}
|
||||
guard let first = children.first else { return (.anyName, []) }
|
||||
return (nameClass(first), Array(children.dropFirst()))
|
||||
}
|
||||
|
||||
// MARK: Combinators and name classes
|
||||
|
||||
private static func combined(_ nodes: [Tree], _ compositor: Compositor) -> Pattern {
|
||||
let patterns = nodes.map(pattern)
|
||||
let identity: Pattern = compositor == .choice ? .notAllowed : .empty
|
||||
guard let first = patterns.first else { return identity }
|
||||
return patterns.dropFirst().reduce(first) { combine($0, $1, compositor) }
|
||||
}
|
||||
|
||||
private static func combine(_ lhs: Pattern, _ rhs: Pattern, _ compositor: Compositor) -> Pattern {
|
||||
switch compositor {
|
||||
case .sequence: .group(lhs, rhs)
|
||||
case .choice: .choice(lhs, rhs)
|
||||
case .all: .interleave(lhs, rhs)
|
||||
}
|
||||
}
|
||||
|
||||
private static func nameClass(_ node: Tree) -> NameClass {
|
||||
switch RNGNode.localName(node) {
|
||||
case "name":
|
||||
.name(namespace: RNGNode.attribute(node, "ns") ?? "", localName: RNGNode.text(node))
|
||||
case "anyName":
|
||||
anyName(node)
|
||||
case "nsName":
|
||||
.nsName(RNGNode.attribute(node, "ns") ?? "")
|
||||
case "choice":
|
||||
nameClassChoice(RNGNode.elementChildren(node))
|
||||
default:
|
||||
.anyName
|
||||
}
|
||||
}
|
||||
|
||||
private static func anyName(_ node: Tree) -> NameClass {
|
||||
guard let except = RNGNode.children(node, named: "except").first,
|
||||
let inner = RNGNode.elementChildren(except).first
|
||||
else {
|
||||
return .anyName
|
||||
}
|
||||
return .anyNameExcept(nameClass(inner))
|
||||
}
|
||||
|
||||
private static func nameClassChoice(_ nodes: [Tree]) -> NameClass {
|
||||
let classes = nodes.map(nameClass)
|
||||
guard let first = classes.first else { return .anyName }
|
||||
return classes.dropFirst().reduce(first) { .choice($0, $1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Sources/Schema/RelaxNGPattern.swift
Normal file
42
Sources/Schema/RelaxNGPattern.swift
Normal file
@ -0,0 +1,42 @@
|
||||
public extension PureXML.Schema {
|
||||
/// A RELAX NG name class: which element or attribute names a pattern matches.
|
||||
indirect enum NameClass: Sendable {
|
||||
case anyName
|
||||
case anyNameExcept(NameClass)
|
||||
case name(namespace: String, localName: String)
|
||||
case nsName(String)
|
||||
case choice(NameClass, NameClass)
|
||||
|
||||
/// Whether `name` is in this class.
|
||||
func contains(_ name: PureXML.Model.QualifiedName) -> Bool {
|
||||
let namespace = name.namespaceURI ?? ""
|
||||
switch self {
|
||||
case .anyName: return true
|
||||
case let .anyNameExcept(except): return !except.contains(name)
|
||||
case let .name(wantedNamespace, wantedLocal): return namespace == wantedNamespace && name.localName == wantedLocal
|
||||
case let .nsName(wantedNamespace): return namespace == wantedNamespace
|
||||
case let .choice(left, right): return left.contains(name) || right.contains(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A RELAX NG pattern (the simplified algebra used by the derivative
|
||||
/// algorithm). `optional`, `zeroOrMore`, and `mixed` are expressed through
|
||||
/// these constructors; `after` is internal to derivation.
|
||||
indirect enum Pattern: Sendable {
|
||||
case empty
|
||||
case notAllowed
|
||||
case text
|
||||
case choice(Pattern, Pattern)
|
||||
case interleave(Pattern, Pattern)
|
||||
case group(Pattern, Pattern)
|
||||
case oneOrMore(Pattern)
|
||||
case element(NameClass, Pattern)
|
||||
case attribute(NameClass, Pattern)
|
||||
case data(BuiltinType)
|
||||
case value(String)
|
||||
case list(Pattern)
|
||||
case ref(String)
|
||||
case after(Pattern, Pattern)
|
||||
}
|
||||
}
|
||||
23
Sources/Schema/RelaxNGSchema.swift
Normal file
23
Sources/Schema/RelaxNGSchema.swift
Normal file
@ -0,0 +1,23 @@
|
||||
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.
|
||||
public init(_ rng: String) throws {
|
||||
(start, defines) = try RelaxNGParser.parse(rng)
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
115
Tests/RelaxNGTests.swift
Normal file
115
Tests/RelaxNGTests.swift
Normal file
@ -0,0 +1,115 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
@Suite("RELAX NG")
|
||||
struct RelaxNGTests {
|
||||
private func valid(_ rng: String, _ xml: String) throws -> Bool {
|
||||
try PureXML.Schema.RelaxNG(rng).validate(xml)
|
||||
}
|
||||
|
||||
private let rngNamespace = "xmlns=\"http://relaxng.org/ns/structure/1.0\""
|
||||
|
||||
@Test("An element with text content")
|
||||
func test_elementText() throws {
|
||||
let rng = "<element name=\"greeting\" \(rngNamespace)><text/></element>"
|
||||
#expect(try valid(rng, "<greeting>hello</greeting>"))
|
||||
#expect(try !valid(rng, "<other>hello</other>"))
|
||||
#expect(try !valid(rng, "<greeting><child/></greeting>"))
|
||||
}
|
||||
|
||||
@Test("A sequence of child elements in order")
|
||||
func test_sequence() throws {
|
||||
let rng = """
|
||||
<element name="book" \(rngNamespace)>
|
||||
<element name="title"><text/></element>
|
||||
<element name="author"><text/></element>
|
||||
</element>
|
||||
"""
|
||||
#expect(try valid(rng, "<book><title>T</title><author>A</author></book>"))
|
||||
#expect(try !valid(rng, "<book><author>A</author><title>T</title></book>"))
|
||||
#expect(try !valid(rng, "<book><title>T</title></book>"))
|
||||
}
|
||||
|
||||
@Test("choice, optional, and zeroOrMore")
|
||||
func test_combinators() throws {
|
||||
let rng = """
|
||||
<element name="r" \(rngNamespace)>
|
||||
<optional><element name="a"><text/></element></optional>
|
||||
<zeroOrMore><element name="b"><empty/></element></zeroOrMore>
|
||||
<choice><element name="x"><empty/></element><element name="y"><empty/></element></choice>
|
||||
</element>
|
||||
"""
|
||||
#expect(try valid(rng, "<r><a>1</a><b/><b/><x/></r>"))
|
||||
#expect(try valid(rng, "<r><y/></r>"))
|
||||
#expect(try !valid(rng, "<r><x/><y/></r>"))
|
||||
}
|
||||
|
||||
@Test("oneOrMore requires at least one")
|
||||
func test_oneOrMore() throws {
|
||||
let rng = """
|
||||
<element name="list" \(rngNamespace)>
|
||||
<oneOrMore><element name="item"><text/></element></oneOrMore>
|
||||
</element>
|
||||
"""
|
||||
#expect(try valid(rng, "<list><item>a</item><item>b</item></list>"))
|
||||
#expect(try !valid(rng, "<list></list>"))
|
||||
}
|
||||
|
||||
@Test("Attributes are validated and required")
|
||||
func test_attributes() throws {
|
||||
let rng = """
|
||||
<element name="e" \(rngNamespace)>
|
||||
<attribute name="id"><text/></attribute>
|
||||
<empty/>
|
||||
</element>
|
||||
"""
|
||||
#expect(try valid(rng, "<e id=\"1\"/>"))
|
||||
#expect(try !valid(rng, "<e/>"))
|
||||
}
|
||||
|
||||
@Test("interleave allows children in any order")
|
||||
func test_interleave() throws {
|
||||
let rng = """
|
||||
<element name="r" \(rngNamespace)>
|
||||
<interleave>
|
||||
<element name="a"><empty/></element>
|
||||
<element name="b"><empty/></element>
|
||||
</interleave>
|
||||
</element>
|
||||
"""
|
||||
#expect(try valid(rng, "<r><a/><b/></r>"))
|
||||
#expect(try valid(rng, "<r><b/><a/></r>"))
|
||||
#expect(try !valid(rng, "<r><a/></r>"))
|
||||
}
|
||||
|
||||
@Test("data types validate text content")
|
||||
func test_data() throws {
|
||||
let rng = "<element name=\"age\" \(rngNamespace)><data type=\"int\"/></element>"
|
||||
#expect(try valid(rng, "<age>42</age>"))
|
||||
#expect(try !valid(rng, "<age>x</age>"))
|
||||
}
|
||||
|
||||
@Test("value matches a literal")
|
||||
func test_value() throws {
|
||||
let rng = "<element name=\"flag\" \(rngNamespace)><value>yes</value></element>"
|
||||
#expect(try valid(rng, "<flag>yes</flag>"))
|
||||
#expect(try !valid(rng, "<flag>no</flag>"))
|
||||
}
|
||||
|
||||
@Test("A grammar with define and ref validates recursive structures")
|
||||
func test_recursiveGrammar() throws {
|
||||
let rng = """
|
||||
<grammar \(rngNamespace)>
|
||||
<start><ref name="node"/></start>
|
||||
<define name="node">
|
||||
<element name="node">
|
||||
<optional><ref name="node"/></optional>
|
||||
</element>
|
||||
</define>
|
||||
</grammar>
|
||||
"""
|
||||
#expect(try valid(rng, "<node/>"))
|
||||
#expect(try valid(rng, "<node><node><node/></node></node>"))
|
||||
#expect(try !valid(rng, "<node><other/></node>"))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user