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:
Mihaela Mihaljevic 2026-06-08 16:55:57 +02:00
parent 768d7b1134
commit 8438d94b44
4 changed files with 92 additions and 42 deletions

View File

@ -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
}

View 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
}
}
}
}

View File

@ -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 {

View 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>"))
}
}