feat(relaxng): compact-syntax datatype parameter blocks
The RNC datatype parameter block xsd:int { minInclusive = "5" } is now
parsed into facets (previously skipped), matching the XML-syntax <param>
support. The facet mapping is shared by both parsers through a new
RelaxNGFacets helper.
Part of #79
This commit is contained in:
parent
768d7b1134
commit
8438d94b44
@ -229,11 +229,29 @@ private final class RNCParser {
|
||||
advance()
|
||||
guard word.contains(":") else { return .ref(word) }
|
||||
let type = PureXML.Schema.BuiltinType(rawValue: strip(word)) ?? .string
|
||||
if peek() == .symbol("{") { skipBraces() }
|
||||
var facets = PureXML.Schema.Facets()
|
||||
if peek() == .symbol("{") { parseParams(into: &facets) }
|
||||
if case let .string(value) = peek() { advance()
|
||||
return .value(value)
|
||||
}
|
||||
return .data(PureXML.Schema.SimpleType(base: type))
|
||||
return .data(PureXML.Schema.SimpleType(base: type, facets: facets))
|
||||
}
|
||||
|
||||
/// Parses a datatype parameter block `{ name = "value" ... }` into facets (the
|
||||
/// compact-syntax form of `<param>`).
|
||||
private func parseParams(into facets: inout PureXML.Schema.Facets) {
|
||||
advance() // {
|
||||
while position < tokens.count, peek() != .symbol("}") {
|
||||
guard case let .word(name) = peek() else { advance()
|
||||
continue
|
||||
}
|
||||
advance()
|
||||
if peek() == .symbol("=") { advance() }
|
||||
guard case let .string(value) = peek() else { continue }
|
||||
advance()
|
||||
PureXML.Schema.RelaxNGFacets.apply(name, value, into: &facets)
|
||||
}
|
||||
if peek() == .symbol("}") { advance() }
|
||||
}
|
||||
|
||||
private func parseElement() -> Pattern {
|
||||
@ -353,18 +371,6 @@ private extension RNCParser {
|
||||
return ""
|
||||
}
|
||||
|
||||
private func skipBraces() {
|
||||
guard peek() == .symbol("{") else { return }
|
||||
var depth = 0
|
||||
repeat {
|
||||
switch advance() {
|
||||
case .symbol("{"): depth += 1
|
||||
case .symbol("}"): depth -= 1
|
||||
default: break
|
||||
}
|
||||
} while depth > 0 && position < tokens.count
|
||||
}
|
||||
|
||||
private func strip(_ qualified: String) -> String {
|
||||
qualified.split(separator: ":").last.map(String.init) ?? qualified
|
||||
}
|
||||
|
||||
33
Sources/Schema/RelaxNGFacets.swift
Normal file
33
Sources/Schema/RelaxNGFacets.swift
Normal file
@ -0,0 +1,33 @@
|
||||
extension PureXML.Schema {
|
||||
/// Builds the XSD-style ``Facets`` a RELAX NG datatype constrains its base type
|
||||
/// with, from `<param name= >value` pairs (XML syntax) or `name "value"` pairs
|
||||
/// (compact syntax). Shared by both RELAX NG parsers.
|
||||
enum RelaxNGFacets {
|
||||
static func apply(_ name: String, _ value: String, into facets: inout Facets) {
|
||||
applyValue(name, value, into: &facets)
|
||||
applyNumeric(name, Int(value), into: &facets)
|
||||
}
|
||||
|
||||
private static func applyValue(_ name: String, _ value: String, into facets: inout Facets) {
|
||||
switch name {
|
||||
case "pattern": facets.patterns.append(value)
|
||||
case "minInclusive": facets.minInclusive = value
|
||||
case "maxInclusive": facets.maxInclusive = value
|
||||
case "minExclusive": facets.minExclusive = value
|
||||
case "maxExclusive": facets.maxExclusive = value
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
private static func applyNumeric(_ name: String, _ number: Int?, into facets: inout Facets) {
|
||||
switch name {
|
||||
case "length": facets.length = number
|
||||
case "minLength": facets.minLength = number
|
||||
case "maxLength": facets.maxLength = number
|
||||
case "totalDigits": facets.totalDigits = number
|
||||
case "fractionDigits": facets.fractionDigits = number
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,39 +122,12 @@ private final class RNGCompiler {
|
||||
var facets = PureXML.Schema.Facets()
|
||||
for param in RNGNode.children(node, named: "param") {
|
||||
if let name = RNGNode.attribute(param, "name") {
|
||||
Self.applyParam(name, RNGNode.text(param), into: &facets)
|
||||
PureXML.Schema.RelaxNGFacets.apply(name, RNGNode.text(param), into: &facets)
|
||||
}
|
||||
}
|
||||
return PureXML.Schema.SimpleType(base: base, facets: facets)
|
||||
}
|
||||
|
||||
private static func applyParam(_ name: String, _ value: String, into facets: inout PureXML.Schema.Facets) {
|
||||
applyValueParam(name, value, into: &facets)
|
||||
applyNumericParam(name, Int(value), into: &facets)
|
||||
}
|
||||
|
||||
private static func applyValueParam(_ name: String, _ value: String, into facets: inout PureXML.Schema.Facets) {
|
||||
switch name {
|
||||
case "pattern": facets.patterns.append(value)
|
||||
case "minInclusive": facets.minInclusive = value
|
||||
case "maxInclusive": facets.maxInclusive = value
|
||||
case "minExclusive": facets.minExclusive = value
|
||||
case "maxExclusive": facets.maxExclusive = value
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
private static func applyNumericParam(_ name: String, _ number: Int?, into facets: inout PureXML.Schema.Facets) {
|
||||
switch name {
|
||||
case "length": facets.length = number
|
||||
case "minLength": facets.minLength = number
|
||||
case "maxLength": facets.maxLength = number
|
||||
case "totalDigits": facets.totalDigits = number
|
||||
case "fractionDigits": facets.fractionDigits = number
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves an `externalRef` to the referenced schema's pattern, merging any
|
||||
/// grammar `define`s it carries into this compiler's table.
|
||||
private func externalRef(_ node: Tree) -> Pattern {
|
||||
|
||||
38
Tests/RelaxNGCompactParamTests.swift
Normal file
38
Tests/RelaxNGCompactParamTests.swift
Normal file
@ -0,0 +1,38 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
@Suite("RELAX NG compact datatype parameter blocks")
|
||||
struct RelaxNGCompactParamTests {
|
||||
private func valid(_ rnc: String, _ xml: String) throws -> Bool {
|
||||
try PureXML.Schema.RelaxNG(compact: rnc).validate(xml)
|
||||
}
|
||||
|
||||
@Test("A minLength param block constrains a data value")
|
||||
func test_minLength() throws {
|
||||
let rnc = "element code { xsd:string { minLength = \"3\" } }"
|
||||
#expect(try valid(rnc, "<code>abc</code>"))
|
||||
#expect(try !valid(rnc, "<code>ab</code>"))
|
||||
}
|
||||
|
||||
@Test("A maxInclusive param block constrains a numeric value")
|
||||
func test_maxInclusive() throws {
|
||||
let rnc = "element n { xsd:integer { maxInclusive = \"10\" } }"
|
||||
#expect(try valid(rnc, "<n>5</n>"))
|
||||
#expect(try !valid(rnc, "<n>20</n>"))
|
||||
}
|
||||
|
||||
@Test("Multiple params in one block all apply")
|
||||
func test_multipleParams() throws {
|
||||
let rnc = "element code { xsd:string { minLength = \"2\" maxLength = \"4\" } }"
|
||||
#expect(try valid(rnc, "<code>abc</code>"))
|
||||
#expect(try !valid(rnc, "<code>a</code>"))
|
||||
#expect(try !valid(rnc, "<code>abcde</code>"))
|
||||
}
|
||||
|
||||
@Test("A datatype with no param block still validates against its base")
|
||||
func test_noParams() throws {
|
||||
let rnc = "element n { xsd:integer }"
|
||||
#expect(try valid(rnc, "<n>42</n>"))
|
||||
#expect(try !valid(rnc, "<n>x</n>"))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user