Merge: datatypeLibrary resolution and nsName subtraction (#79)
This commit is contained in:
commit
c1e181db50
@ -13,20 +13,29 @@ extension RNCParser {
|
||||
}
|
||||
|
||||
private func parseSimpleNameClass(attribute: Bool) -> NameClass {
|
||||
let base = parseNameClassPrimary(attribute: attribute)
|
||||
// `*` and `ns:*` may be narrowed by `- nameClass` subtraction.
|
||||
guard peek() == .symbol("-") else { return base }
|
||||
advance()
|
||||
return subtract(base, parseSimpleNameClass(attribute: attribute))
|
||||
}
|
||||
|
||||
private func parseNameClassPrimary(attribute: Bool) -> NameClass {
|
||||
switch peek() {
|
||||
case .symbol("*"):
|
||||
advance()
|
||||
// `* - nameClass` is any name except the subtracted class.
|
||||
if peek() == .symbol("-") {
|
||||
advance()
|
||||
return .anyNameExcept(parseSimpleNameClass(attribute: attribute))
|
||||
}
|
||||
return .anyName
|
||||
case .symbol("("):
|
||||
advance()
|
||||
let nameClass = parseNameClass(attribute: attribute)
|
||||
expectSymbol(")")
|
||||
return nameClass
|
||||
// `prefix:*` lexes as the word `prefix:` (a `:` is a name character)
|
||||
// followed by a `*` symbol: a namespace wildcard.
|
||||
case let .word(word) where word.hasSuffix(":") && peek(1) == .symbol("*"):
|
||||
advance()
|
||||
advance()
|
||||
return .nsName(namespaces[String(word.dropLast())] ?? "")
|
||||
case let .word(word):
|
||||
advance()
|
||||
return qualifiedName(word, attribute: attribute)
|
||||
@ -36,6 +45,16 @@ extension RNCParser {
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies name-class subtraction, which RELAX NG defines only for the open
|
||||
/// classes `anyName` (`*`) and `nsName` (`prefix:*`).
|
||||
private func subtract(_ base: NameClass, _ except: NameClass) -> NameClass {
|
||||
switch base {
|
||||
case .anyName: .anyNameExcept(except)
|
||||
case let .nsName(namespace): .nsNameExcept(namespace: namespace, except: except)
|
||||
default: base
|
||||
}
|
||||
}
|
||||
|
||||
private func qualifiedName(_ word: String, attribute: Bool) -> NameClass {
|
||||
let parts = word.split(separator: ":", maxSplits: 1).map(String.init)
|
||||
if parts.count == 2 {
|
||||
|
||||
@ -104,8 +104,8 @@ private final class RNGCompiler {
|
||||
case "empty": .empty
|
||||
case "notAllowed": .notAllowed
|
||||
case "text": .text
|
||||
case "data": .data(dataType(node))
|
||||
case "value": .value(RNGNode.text(node))
|
||||
case "data": dataPattern(node)
|
||||
case "value": valuePattern(node)
|
||||
case "ref": .ref(RNGNode.attribute(node, "name") ?? "")
|
||||
case "externalRef": externalRef(node)
|
||||
case "element": element(node)
|
||||
@ -114,18 +114,61 @@ private final class RNGCompiler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the datatype of a `<data type=>` pattern: its base built-in plus the
|
||||
/// facets its `<param name= >value</param>` children constrain it with (the
|
||||
/// same facet set the XSD datatypes use).
|
||||
private func dataType(_ node: Tree) -> PureXML.Schema.SimpleType {
|
||||
let base = PureXML.Schema.BuiltinType(rawValue: RNGNode.strip(RNGNode.attribute(node, "type") ?? "string")) ?? .string
|
||||
/// The URI of the W3C XML Schema datatype library; the only non-default
|
||||
/// library RELAX NG processors are required to support.
|
||||
private static let xsdDatatypeLibrary = "http://www.w3.org/2001/XMLSchema-datatypes"
|
||||
|
||||
/// Builds a `<data type=>` pattern: its base built-in plus the facets its
|
||||
/// `<param name= >value</param>` children constrain it with (the same facet
|
||||
/// set the XSD datatypes use). A `type` that the in-scope `datatypeLibrary`
|
||||
/// does not define is an unknown datatype, so the pattern matches nothing.
|
||||
private func dataPattern(_ node: Tree) -> Pattern {
|
||||
let typeName = RNGNode.strip(RNGNode.attribute(node, "type") ?? "string")
|
||||
guard let base = resolvedBuiltin(typeName, library: datatypeLibrary(of: node)) else { return .notAllowed }
|
||||
var facets = PureXML.Schema.Facets()
|
||||
for param in RNGNode.children(node, named: "param") {
|
||||
if let name = RNGNode.attribute(param, "name") {
|
||||
PureXML.Schema.RelaxNGFacets.apply(name, RNGNode.text(param), into: &facets)
|
||||
}
|
||||
}
|
||||
return PureXML.Schema.SimpleType(base: base, facets: facets)
|
||||
return .data(PureXML.Schema.SimpleType(base: base, facets: facets))
|
||||
}
|
||||
|
||||
/// Builds a `<value>` pattern. An explicit `type` that the in-scope
|
||||
/// `datatypeLibrary` does not define makes the value unmatchable; otherwise
|
||||
/// the literal is compared lexically.
|
||||
private func valuePattern(_ node: Tree) -> Pattern {
|
||||
if hasUnknownType(node) { return .notAllowed }
|
||||
return .value(RNGNode.text(node))
|
||||
}
|
||||
|
||||
/// Whether `node` declares a `type` the in-scope `datatypeLibrary` does not
|
||||
/// define (an untyped `<value>` is always known: it compares lexically).
|
||||
private func hasUnknownType(_ node: Tree) -> Bool {
|
||||
guard let declared = RNGNode.attribute(node, "type") else { return false }
|
||||
return resolvedBuiltin(RNGNode.strip(declared), library: datatypeLibrary(of: node)) == nil
|
||||
}
|
||||
|
||||
/// Resolves a datatype name within a library. The default (empty) library
|
||||
/// defines only `string` and `token`; the XSD library defines every built-in;
|
||||
/// any other library is unsupported, so its names are unknown.
|
||||
private func resolvedBuiltin(_ typeName: String, library: String) -> PureXML.Schema.BuiltinType? {
|
||||
switch library {
|
||||
case "": typeName == "string" || typeName == "token" ? PureXML.Schema.BuiltinType(rawValue: typeName) : nil
|
||||
case Self.xsdDatatypeLibrary: PureXML.Schema.BuiltinType(rawValue: typeName)
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The `datatypeLibrary` in scope at `node`: the nearest value on the node or
|
||||
/// an ancestor, defaulting to the empty (RELAX NG built-in) library.
|
||||
private func datatypeLibrary(of node: Tree) -> String {
|
||||
var current: Tree? = node
|
||||
while let candidate = current {
|
||||
if let library = RNGNode.attribute(candidate, "datatypeLibrary") { return library }
|
||||
current = candidate.parent
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/// Resolves an `externalRef` to the referenced schema's pattern, merging any
|
||||
@ -204,7 +247,7 @@ private final class RNGCompiler {
|
||||
case "anyName":
|
||||
anyName(node)
|
||||
case "nsName":
|
||||
.nsName(RNGNode.attribute(node, "ns") ?? "")
|
||||
nsName(node)
|
||||
case "choice":
|
||||
nameClassChoice(RNGNode.elementChildren(node))
|
||||
default:
|
||||
@ -213,12 +256,22 @@ private final class RNGCompiler {
|
||||
}
|
||||
|
||||
private 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))
|
||||
guard let except = exceptClass(node) else { return .anyName }
|
||||
return .anyNameExcept(except)
|
||||
}
|
||||
|
||||
private func nsName(_ node: Tree) -> NameClass {
|
||||
let namespace = RNGNode.attribute(node, "ns") ?? ""
|
||||
guard let except = exceptClass(node) else { return .nsName(namespace) }
|
||||
return .nsNameExcept(namespace: namespace, except: except)
|
||||
}
|
||||
|
||||
/// The name class of a `<name-class>`'s `<except>` child, treating multiple
|
||||
/// children as an implicit choice (`anyName`/`nsName` minus the union).
|
||||
private func exceptClass(_ node: Tree) -> NameClass? {
|
||||
guard let except = RNGNode.children(node, named: "except").first else { return nil }
|
||||
let children = RNGNode.elementChildren(except)
|
||||
return children.isEmpty ? nil : nameClassChoice(children)
|
||||
}
|
||||
|
||||
private func nameClassChoice(_ nodes: [Tree]) -> NameClass {
|
||||
|
||||
@ -5,6 +5,7 @@ public extension PureXML.Schema {
|
||||
case anyNameExcept(NameClass)
|
||||
case name(namespace: String, localName: String)
|
||||
case nsName(String)
|
||||
case nsNameExcept(namespace: String, except: NameClass)
|
||||
case choice(NameClass, NameClass)
|
||||
|
||||
/// Whether `name` is in this class.
|
||||
@ -15,6 +16,7 @@ public extension PureXML.Schema {
|
||||
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 .nsNameExcept(wantedNamespace, except): return namespace == wantedNamespace && !except.contains(name)
|
||||
case let .choice(left, right): return left.contains(name) || right.contains(name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import Testing
|
||||
|
||||
@Suite("RELAX NG data param facets")
|
||||
struct RelaxNGDataParamTests {
|
||||
private let rngNamespace = "xmlns=\"http://relaxng.org/ns/structure/1.0\""
|
||||
private let rngNamespace = "xmlns=\"http://relaxng.org/ns/structure/1.0\" datatypeLibrary=\"http://www.w3.org/2001/XMLSchema-datatypes\""
|
||||
|
||||
private func valid(_ rng: String, _ xml: String) throws -> Bool {
|
||||
try PureXML.Schema.RelaxNG(rng).validate(xml)
|
||||
|
||||
110
Tests/RelaxNGDatatypeLibraryTests.swift
Normal file
110
Tests/RelaxNGDatatypeLibraryTests.swift
Normal file
@ -0,0 +1,110 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
@Suite("RELAX NG datatypeLibrary resolution and nsName subtraction")
|
||||
struct RelaxNGDatatypeLibraryTests {
|
||||
private let xsd = "http://www.w3.org/2001/XMLSchema-datatypes"
|
||||
|
||||
private func validXML(_ rng: String, _ xml: String) throws -> Bool {
|
||||
try PureXML.Schema.RelaxNG(rng).validate(xml)
|
||||
}
|
||||
|
||||
private func validRNC(_ rnc: String, _ xml: String) throws -> Bool {
|
||||
try PureXML.Schema.RelaxNG(compact: rnc).validate(xml)
|
||||
}
|
||||
|
||||
@Test("The default library defines only string and token")
|
||||
func test_defaultLibraryStringToken() throws {
|
||||
let rng = """
|
||||
<element name="v" xmlns="http://relaxng.org/ns/structure/1.0">
|
||||
<data type="token"/>
|
||||
</element>
|
||||
"""
|
||||
#expect(try validXML(rng, "<v>anything here</v>"))
|
||||
}
|
||||
|
||||
@Test("An XSD type without the XSD library is an unknown datatype and matches nothing")
|
||||
func test_unknownTypeInDefaultLibrary() throws {
|
||||
let rng = """
|
||||
<element name="n" xmlns="http://relaxng.org/ns/structure/1.0">
|
||||
<data type="integer"/>
|
||||
</element>
|
||||
"""
|
||||
// `integer` is not in the default library, so no value validates.
|
||||
#expect(try !validXML(rng, "<n>5</n>"))
|
||||
#expect(try !validXML(rng, "<n>x</n>"))
|
||||
}
|
||||
|
||||
@Test("Declaring the XSD datatypeLibrary makes xsd types resolve")
|
||||
func test_xsdLibraryResolves() throws {
|
||||
let rng = """
|
||||
<element name="n" datatypeLibrary="\(xsd)" xmlns="http://relaxng.org/ns/structure/1.0">
|
||||
<data type="integer"/>
|
||||
</element>
|
||||
"""
|
||||
#expect(try validXML(rng, "<n>5</n>"))
|
||||
#expect(try !validXML(rng, "<n>x</n>"))
|
||||
}
|
||||
|
||||
@Test("datatypeLibrary is inherited from an ancestor element")
|
||||
func test_libraryInheritedFromAncestor() throws {
|
||||
let rng = """
|
||||
<grammar datatypeLibrary="\(xsd)" xmlns="http://relaxng.org/ns/structure/1.0">
|
||||
<start>
|
||||
<element name="n"><data type="integer"/></element>
|
||||
</start>
|
||||
</grammar>
|
||||
"""
|
||||
#expect(try validXML(rng, "<n>42</n>"))
|
||||
#expect(try !validXML(rng, "<n>4.2</n>"))
|
||||
}
|
||||
|
||||
@Test("A value with an unknown explicit type matches nothing")
|
||||
func test_valueUnknownType() throws {
|
||||
let rng = """
|
||||
<element name="n" xmlns="http://relaxng.org/ns/structure/1.0">
|
||||
<value type="integer">5</value>
|
||||
</element>
|
||||
"""
|
||||
#expect(try !validXML(rng, "<n>5</n>"))
|
||||
}
|
||||
|
||||
@Test("nsName with except excludes a name in the namespace")
|
||||
func test_nsNameExceptXML() throws {
|
||||
let rng = """
|
||||
<element name="root" xmlns="http://relaxng.org/ns/structure/1.0">
|
||||
<element>
|
||||
<nsName ns="urn:x"><except><name ns="urn:x">skip</name></except></nsName>
|
||||
<empty/>
|
||||
</element>
|
||||
</element>
|
||||
"""
|
||||
// Any element in urn:x except local name "skip" is allowed.
|
||||
let allowed = "<root xmlns:p=\"urn:x\"><p:keep/></root>"
|
||||
let excluded = "<root xmlns:p=\"urn:x\"><p:skip/></root>"
|
||||
let wrongNS = "<root xmlns:p=\"urn:y\"><p:keep/></root>"
|
||||
#expect(try validXML(rng, allowed))
|
||||
#expect(try !validXML(rng, excluded))
|
||||
#expect(try !validXML(rng, wrongNS))
|
||||
}
|
||||
|
||||
@Test("RNC ns:* wildcard matches any name in the namespace")
|
||||
func test_rncNsWildcard() throws {
|
||||
let rnc = """
|
||||
namespace p = "urn:x"
|
||||
element root { element p:* { empty } }
|
||||
"""
|
||||
#expect(try validRNC(rnc, "<root xmlns:p=\"urn:x\"><p:anything/></root>"))
|
||||
#expect(try !validRNC(rnc, "<root xmlns:q=\"urn:y\"><q:anything/></root>"))
|
||||
}
|
||||
|
||||
@Test("RNC ns:* - name subtracts a name from the namespace wildcard")
|
||||
func test_rncNsWildcardExcept() throws {
|
||||
let rnc = """
|
||||
namespace p = "urn:x"
|
||||
element root { element p:* - p:skip { empty } }
|
||||
"""
|
||||
#expect(try validRNC(rnc, "<root xmlns:p=\"urn:x\"><p:keep/></root>"))
|
||||
#expect(try !validRNC(rnc, "<root xmlns:p=\"urn:x\"><p:skip/></root>"))
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ struct RelaxNGTests {
|
||||
try PureXML.Schema.RelaxNG(rng).validate(xml)
|
||||
}
|
||||
|
||||
private let rngNamespace = "xmlns=\"http://relaxng.org/ns/structure/1.0\""
|
||||
private let rngNamespace = "xmlns=\"http://relaxng.org/ns/structure/1.0\" datatypeLibrary=\"http://www.w3.org/2001/XMLSchema-datatypes\""
|
||||
|
||||
@Test("An element with text content")
|
||||
func test_elementText() throws {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user