From 8438d94b44c12e43c75b1fa85c79b01abbd60d0c Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Mon, 8 Jun 2026 16:55:57 +0200 Subject: [PATCH] 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 support. The facet mapping is shared by both parsers through a new RelaxNGFacets helper. Part of #79 --- Sources/Schema/RelaxNGCompactParser.swift | 34 +++++++++++--------- Sources/Schema/RelaxNGFacets.swift | 33 ++++++++++++++++++++ Sources/Schema/RelaxNGParser.swift | 29 +---------------- Tests/RelaxNGCompactParamTests.swift | 38 +++++++++++++++++++++++ 4 files changed, 92 insertions(+), 42 deletions(-) create mode 100644 Sources/Schema/RelaxNGFacets.swift create mode 100644 Tests/RelaxNGCompactParamTests.swift diff --git a/Sources/Schema/RelaxNGCompactParser.swift b/Sources/Schema/RelaxNGCompactParser.swift index 8ce5528..cbaaf9a 100644 --- a/Sources/Schema/RelaxNGCompactParser.swift +++ b/Sources/Schema/RelaxNGCompactParser.swift @@ -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 ``). + 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 } diff --git a/Sources/Schema/RelaxNGFacets.swift b/Sources/Schema/RelaxNGFacets.swift new file mode 100644 index 0000000..26f4a5c --- /dev/null +++ b/Sources/Schema/RelaxNGFacets.swift @@ -0,0 +1,33 @@ +extension PureXML.Schema { + /// Builds the XSD-style ``Facets`` a RELAX NG datatype constrains its base type + /// with, from `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 + } + } + } +} diff --git a/Sources/Schema/RelaxNGParser.swift b/Sources/Schema/RelaxNGParser.swift index eefdbca..f7ee55e 100644 --- a/Sources/Schema/RelaxNGParser.swift +++ b/Sources/Schema/RelaxNGParser.swift @@ -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 { diff --git a/Tests/RelaxNGCompactParamTests.swift b/Tests/RelaxNGCompactParamTests.swift new file mode 100644 index 0000000..59eb45b --- /dev/null +++ b/Tests/RelaxNGCompactParamTests.swift @@ -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, "abc")) + #expect(try !valid(rnc, "ab")) + } + + @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, "5")) + #expect(try !valid(rnc, "20")) + } + + @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, "abc")) + #expect(try !valid(rnc, "a")) + #expect(try !valid(rnc, "abcde")) + } + + @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, "42")) + #expect(try !valid(rnc, "x")) + } +}