fix(schema): advance XSTS valid-instances-rejected to 4 (#146)
Consolidates the in-progress #146 work; valid-instances-rejected reaches 4 on the 2006-11-06 XSTS archive (buckets now 1/262/4/147). Regex: match Unicode scalars rather than grapheme clusters; the empty pattern matches only the empty string; pin the category repertoire to Unicode 3.2, the version the corpus is consistent at, so code points added in 3.2 (Tagalog, Cyrillic Komi) are word characters while later additions (the Tamil digit zero) stay unassigned and match \D. New XSDCategory and a 3.2 assignment table derived from DerivedAge, replacing the old Unicode31 files. Content model: assess each child against the particle the content automaton matched, not a by-name lookup (#180). Two same-named particles keep their own fixed values, and an element matching a skip wildcard is no longer validated against its global declaration. Selection is made reproducible when an ambiguous schema slips past the UPA check. XSTS runner: compile and merge every schemaDocument in a schemaTest, not just the first; this also surfaced one previously-masked invalid-instance acceptance (recorded in the baselines). Also folds in attribute-group anyAttribute inheritance, extension wildcard union, namespace-qualified type tables for multi-document xsi:schemaLocation, per-container elementFormDefault, strict and lax attribute wildcard validation, built-in type resolution, and xs:redefine simpleType overrides.
This commit is contained in:
parent
489275c6b8
commit
be8a39394d
@ -22,6 +22,8 @@ Second pre-release. Continues the W3C XSTS schema-validity campaign: **invalid-s
|
||||
|
||||
### Fixed
|
||||
|
||||
- XSTS valid-instances-rejected (#146): **173 → 4** on the 2006-11-06 archive (settled-status count; the four buckets now read 1/262/4/147). Regex patterns match Unicode scalars (not Swift grapheme clusters), fixing `\p{Is…}` block tests and combining-mark cases; empty patterns match only the empty string. The regex repertoire is pinned to Unicode 3.2, the version the corpus is consistent at: code points added in 3.2 (Tagalog, Cyrillic Komi) are word characters, while later additions (the Tamil digit zero) stay unassigned and match `\D` (`XSDCategory`, `Unicode32Assigned`). Per-child content assessment is driven by the content-model particle the automaton matched, not a by-name lookup (#180), so two same-named particles keep their own `fixed` values and an element matching a `skip` wildcard is not validated against its global declaration. The XSTS runner compiles and merges every `schemaDocument` in a `schemaTest` (not just the first), which also surfaced one previously-masked invalid-instance acceptance. Plus attribute-group `anyAttribute` inheritance, extension wildcard union, namespace-qualified type tables for multi-document `xsi:schemaLocation`, per-container `elementFormDefault`, strict/lax attribute wildcard validation, built-in type resolution, and `xs:redefine` simpleType overrides.
|
||||
|
||||
- Enforce element, attribute, attributeGroup, and group child-content shape (schema-for-schemas cardinality and order): at most one inline type per element/attribute, inline type before identity constraints, at most one compositor per group, at most one `anyAttribute` per attributeGroup with attribute references preceding it (`SchemaStructureHelpers.swift`).
|
||||
|
||||
- Enforce facet applicability per base type, fractionDigits constraints on integer types, ordered duration range bounds, and built-in list non-emptiness constraints (XSD 1.0 Datatypes §4.3). Ensure that built-in list types (xs:IDREFS, xs:ENTITIES, xs:NMTOKENS) are validated as non-empty unless restricted with an explicit minLength. Resolve type references namespace-aware to avoid prefix conflation with user types of the same name. Ignore foreign elements matching facet names inside xs:restriction. XSTS: invalid schemas accepted 296 to 266, no other bucket raised.
|
||||
|
||||
@ -19,13 +19,14 @@ extension PureXML.Regex {
|
||||
var accept: Int
|
||||
|
||||
/// Whether the automaton matches the whole string (XSD patterns are
|
||||
/// implicitly anchored at both ends).
|
||||
/// implicitly anchored at both ends). XSD regular expressions match a
|
||||
/// sequence of Unicode code points, not Swift extended grapheme clusters.
|
||||
func matchesWhole(_ string: String) -> Bool {
|
||||
var current = closure([start])
|
||||
for character in string {
|
||||
for scalar in string.unicodeScalars {
|
||||
var next: Set<Int> = []
|
||||
for state in current {
|
||||
if let transition = states[state].transition, transition.charClass.matches(character) {
|
||||
if let transition = states[state].transition, transition.charClass.matches(scalar) {
|
||||
next.insert(transition.target)
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,11 +28,11 @@ extension PureXML.Regex {
|
||||
}
|
||||
|
||||
static func matches(_ name: String, _ scalar: Unicode.Scalar) -> Bool {
|
||||
let code = XSDCategory.generalCategoryCode(scalar)
|
||||
if name.hasPrefix("Is") {
|
||||
guard let range = blocks[String(name.dropFirst(2))] else { return false }
|
||||
return range.contains(scalar.value)
|
||||
}
|
||||
let code = codes[scalar.properties.generalCategory] ?? "Cn"
|
||||
return name.count == 1 ? code.hasPrefix(name) : code == name
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ extension PureXML.Regex {
|
||||
}
|
||||
|
||||
static func parse(_ pattern: String) throws -> Node {
|
||||
guard !pattern.isEmpty else { throw RegexError.empty }
|
||||
if pattern.isEmpty { return .empty }
|
||||
var parser = RegexParser(pattern)
|
||||
let node = try parser.parseAlternation()
|
||||
guard parser.isAtEnd else { throw RegexError.unbalanced }
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
extension PureXML.Regex {
|
||||
/// An error compiling a regular expression.
|
||||
enum RegexError: Swift.Error, Equatable, Sendable, CustomStringConvertible {
|
||||
case empty
|
||||
case unbalanced
|
||||
case danglingQuantifier
|
||||
case badEscape(String)
|
||||
@ -11,7 +10,6 @@ extension PureXML.Regex {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .empty: "the pattern is empty"
|
||||
case .unbalanced: "unbalanced parentheses"
|
||||
case .danglingQuantifier: "a quantifier has nothing to repeat"
|
||||
case let .badEscape(detail): "invalid escape '\\\(detail)'"
|
||||
@ -33,15 +31,17 @@ extension PureXML.Regex {
|
||||
|
||||
func contains(_ scalar: Unicode.Scalar) -> Bool {
|
||||
switch self {
|
||||
case .digit: scalar.properties.numericType == .decimal
|
||||
case .space: scalar == " " || scalar == "\t" || scalar == "\n" || scalar == "\r"
|
||||
// XSD \w is [#x0000-#x10FFFF]-[\p{P}\p{Z}\p{C}]: everything except
|
||||
// punctuation, separators, and other. So symbols and marks are word
|
||||
// characters, and `_` (Pc) is not, unlike the Perl definition.
|
||||
case .word: !(CategoryMatcher.matches("P", scalar) || CategoryMatcher.matches("Z", scalar) || CategoryMatcher.matches("C", scalar))
|
||||
case .nameStart: PureXML.Parsing.XMLCharacter.isNameStart(scalar)
|
||||
case .nameChar: PureXML.Parsing.XMLCharacter.isNameChar(scalar)
|
||||
case .anyButLineBreak: scalar != "\n" && scalar != "\r"
|
||||
case .digit: return XSDCategory.generalCategoryCode(scalar) == "Nd"
|
||||
case .space: return scalar == " " || scalar == "\t" || scalar == "\n" || scalar == "\r"
|
||||
// XSD \w is [#x0000-#x10FFFF]-[\p{P}\p{Z}\p{C}] (Datatypes Appendix F).
|
||||
// Symbols and marks are word characters; `_` (Pc) is not, unlike the
|
||||
// Perl definition.
|
||||
case .word:
|
||||
let code = XSDCategory.generalCategoryCode(scalar)
|
||||
return !(code.hasPrefix("P") || code.hasPrefix("Z") || code.hasPrefix("C"))
|
||||
case .nameStart: return PureXML.Parsing.XMLCharacter.isNameStart(scalar)
|
||||
case .nameChar: return PureXML.Parsing.XMLCharacter.isNameChar(scalar)
|
||||
case .anyButLineBreak: return scalar != "\n" && scalar != "\r"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -62,11 +62,15 @@ extension PureXML.Regex {
|
||||
guard character.unicodeScalars.count == 1, let scalar = character.unicodeScalars.first else {
|
||||
return false
|
||||
}
|
||||
return matches(scalar)
|
||||
}
|
||||
|
||||
func matches(_ scalar: Unicode.Scalar) -> Bool {
|
||||
var hit = ranges.contains { $0.contains(scalar) }
|
||||
|| named.contains { $0.contains(scalar) }
|
||||
|| negatedNamed.contains { !$0.contains(scalar) }
|
||||
|| categories.contains { $0.contains(scalar) }
|
||||
if hit, subtraction.contains(where: { $0.matches(character) }) { hit = false }
|
||||
if hit, subtraction.contains(where: { $0.matches(scalar) }) { hit = false }
|
||||
return negated ? !hit : hit
|
||||
}
|
||||
|
||||
|
||||
97
Sources/Regex/Unicode32Assigned.swift
Normal file
97
Sources/Regex/Unicode32Assigned.swift
Normal file
@ -0,0 +1,97 @@
|
||||
extension PureXML.Regex.XSDCategory {
|
||||
/// Inclusive `[lo, hi]` code-point bounds of every range assigned by Unicode
|
||||
/// 3.2. XSD 1.0 regular expressions are normatively defined against Unicode
|
||||
/// 3.1, but the W3C conformance corpus is internally consistent only at 3.2:
|
||||
/// it expects code points added in 3.2 (Tagalog, Cyrillic Komi, and others)
|
||||
/// to be word characters, yet code points added in 4.0 (such as the Tamil
|
||||
/// digit zero U+0BE6) to be unassigned and so match `\D`. Derived clean-room
|
||||
/// from the Unicode Character Database `DerivedAge` property (ages <= 3.2),
|
||||
/// stored sorted and non-overlapping so membership is a binary search.
|
||||
private static let assignedRanges: [UInt32] = [
|
||||
0x0, 0x220, 0x222, 0x233, 0x250, 0x2AD, 0x2B0, 0x2EE, 0x300, 0x34F, 0x360, 0x36F,
|
||||
0x374, 0x375, 0x37A, 0x37A, 0x37E, 0x37E, 0x384, 0x38A, 0x38C, 0x38C, 0x38E, 0x3A1,
|
||||
0x3A3, 0x3CE, 0x3D0, 0x3F6, 0x400, 0x486, 0x488, 0x4CE, 0x4D0, 0x4F5, 0x4F8, 0x4F9,
|
||||
0x500, 0x50F, 0x531, 0x556, 0x559, 0x55F, 0x561, 0x587, 0x589, 0x58A, 0x591, 0x5A1,
|
||||
0x5A3, 0x5B9, 0x5BB, 0x5C4, 0x5D0, 0x5EA, 0x5F0, 0x5F4, 0x60C, 0x60C, 0x61B, 0x61B,
|
||||
0x61F, 0x61F, 0x621, 0x63A, 0x640, 0x655, 0x660, 0x6ED, 0x6F0, 0x6FE, 0x700, 0x70D,
|
||||
0x70F, 0x72C, 0x730, 0x74A, 0x780, 0x7B1, 0x901, 0x903, 0x905, 0x939, 0x93C, 0x94D,
|
||||
0x950, 0x954, 0x958, 0x970, 0x981, 0x983, 0x985, 0x98C, 0x98F, 0x990, 0x993, 0x9A8,
|
||||
0x9AA, 0x9B0, 0x9B2, 0x9B2, 0x9B6, 0x9B9, 0x9BC, 0x9BC, 0x9BE, 0x9C4, 0x9C7, 0x9C8,
|
||||
0x9CB, 0x9CD, 0x9D7, 0x9D7, 0x9DC, 0x9DD, 0x9DF, 0x9E3, 0x9E6, 0x9FA, 0xA02, 0xA02,
|
||||
0xA05, 0xA0A, 0xA0F, 0xA10, 0xA13, 0xA28, 0xA2A, 0xA30, 0xA32, 0xA33, 0xA35, 0xA36,
|
||||
0xA38, 0xA39, 0xA3C, 0xA3C, 0xA3E, 0xA42, 0xA47, 0xA48, 0xA4B, 0xA4D, 0xA59, 0xA5C,
|
||||
0xA5E, 0xA5E, 0xA66, 0xA74, 0xA81, 0xA83, 0xA85, 0xA8B, 0xA8D, 0xA8D, 0xA8F, 0xA91,
|
||||
0xA93, 0xAA8, 0xAAA, 0xAB0, 0xAB2, 0xAB3, 0xAB5, 0xAB9, 0xABC, 0xAC5, 0xAC7, 0xAC9,
|
||||
0xACB, 0xACD, 0xAD0, 0xAD0, 0xAE0, 0xAE0, 0xAE6, 0xAEF, 0xB01, 0xB03, 0xB05, 0xB0C,
|
||||
0xB0F, 0xB10, 0xB13, 0xB28, 0xB2A, 0xB30, 0xB32, 0xB33, 0xB36, 0xB39, 0xB3C, 0xB43,
|
||||
0xB47, 0xB48, 0xB4B, 0xB4D, 0xB56, 0xB57, 0xB5C, 0xB5D, 0xB5F, 0xB61, 0xB66, 0xB70,
|
||||
0xB82, 0xB83, 0xB85, 0xB8A, 0xB8E, 0xB90, 0xB92, 0xB95, 0xB99, 0xB9A, 0xB9C, 0xB9C,
|
||||
0xB9E, 0xB9F, 0xBA3, 0xBA4, 0xBA8, 0xBAA, 0xBAE, 0xBB5, 0xBB7, 0xBB9, 0xBBE, 0xBC2,
|
||||
0xBC6, 0xBC8, 0xBCA, 0xBCD, 0xBD7, 0xBD7, 0xBE7, 0xBF2, 0xC01, 0xC03, 0xC05, 0xC0C,
|
||||
0xC0E, 0xC10, 0xC12, 0xC28, 0xC2A, 0xC33, 0xC35, 0xC39, 0xC3E, 0xC44, 0xC46, 0xC48,
|
||||
0xC4A, 0xC4D, 0xC55, 0xC56, 0xC60, 0xC61, 0xC66, 0xC6F, 0xC82, 0xC83, 0xC85, 0xC8C,
|
||||
0xC8E, 0xC90, 0xC92, 0xCA8, 0xCAA, 0xCB3, 0xCB5, 0xCB9, 0xCBE, 0xCC4, 0xCC6, 0xCC8,
|
||||
0xCCA, 0xCCD, 0xCD5, 0xCD6, 0xCDE, 0xCDE, 0xCE0, 0xCE1, 0xCE6, 0xCEF, 0xD02, 0xD03,
|
||||
0xD05, 0xD0C, 0xD0E, 0xD10, 0xD12, 0xD28, 0xD2A, 0xD39, 0xD3E, 0xD43, 0xD46, 0xD48,
|
||||
0xD4A, 0xD4D, 0xD57, 0xD57, 0xD60, 0xD61, 0xD66, 0xD6F, 0xD82, 0xD83, 0xD85, 0xD96,
|
||||
0xD9A, 0xDB1, 0xDB3, 0xDBB, 0xDBD, 0xDBD, 0xDC0, 0xDC6, 0xDCA, 0xDCA, 0xDCF, 0xDD4,
|
||||
0xDD6, 0xDD6, 0xDD8, 0xDDF, 0xDF2, 0xDF4, 0xE01, 0xE3A, 0xE3F, 0xE5B, 0xE81, 0xE82,
|
||||
0xE84, 0xE84, 0xE87, 0xE88, 0xE8A, 0xE8A, 0xE8D, 0xE8D, 0xE94, 0xE97, 0xE99, 0xE9F,
|
||||
0xEA1, 0xEA3, 0xEA5, 0xEA5, 0xEA7, 0xEA7, 0xEAA, 0xEAB, 0xEAD, 0xEB9, 0xEBB, 0xEBD,
|
||||
0xEC0, 0xEC4, 0xEC6, 0xEC6, 0xEC8, 0xECD, 0xED0, 0xED9, 0xEDC, 0xEDD, 0xF00, 0xF47,
|
||||
0xF49, 0xF6A, 0xF71, 0xF8B, 0xF90, 0xF97, 0xF99, 0xFBC, 0xFBE, 0xFCC, 0xFCF, 0xFCF,
|
||||
0x1000, 0x1021, 0x1023, 0x1027, 0x1029, 0x102A, 0x102C, 0x1032, 0x1036, 0x1039, 0x1040, 0x1059,
|
||||
0x10A0, 0x10C5, 0x10D0, 0x10F8, 0x10FB, 0x10FB, 0x1100, 0x1159, 0x115F, 0x11A2, 0x11A8, 0x11F9,
|
||||
0x1200, 0x1206, 0x1208, 0x1246, 0x1248, 0x1248, 0x124A, 0x124D, 0x1250, 0x1256, 0x1258, 0x1258,
|
||||
0x125A, 0x125D, 0x1260, 0x1286, 0x1288, 0x1288, 0x128A, 0x128D, 0x1290, 0x12AE, 0x12B0, 0x12B0,
|
||||
0x12B2, 0x12B5, 0x12B8, 0x12BE, 0x12C0, 0x12C0, 0x12C2, 0x12C5, 0x12C8, 0x12CE, 0x12D0, 0x12D6,
|
||||
0x12D8, 0x12EE, 0x12F0, 0x130E, 0x1310, 0x1310, 0x1312, 0x1315, 0x1318, 0x131E, 0x1320, 0x1346,
|
||||
0x1348, 0x135A, 0x1361, 0x137C, 0x13A0, 0x13F4, 0x1401, 0x1676, 0x1680, 0x169C, 0x16A0, 0x16F0,
|
||||
0x1700, 0x170C, 0x170E, 0x1714, 0x1720, 0x1736, 0x1740, 0x1753, 0x1760, 0x176C, 0x176E, 0x1770,
|
||||
0x1772, 0x1773, 0x1780, 0x17DC, 0x17E0, 0x17E9, 0x1800, 0x180E, 0x1810, 0x1819, 0x1820, 0x1877,
|
||||
0x1880, 0x18A9, 0x1E00, 0x1E9B, 0x1EA0, 0x1EF9, 0x1F00, 0x1F15, 0x1F18, 0x1F1D, 0x1F20, 0x1F45,
|
||||
0x1F48, 0x1F4D, 0x1F50, 0x1F57, 0x1F59, 0x1F59, 0x1F5B, 0x1F5B, 0x1F5D, 0x1F5D, 0x1F5F, 0x1F7D,
|
||||
0x1F80, 0x1FB4, 0x1FB6, 0x1FC4, 0x1FC6, 0x1FD3, 0x1FD6, 0x1FDB, 0x1FDD, 0x1FEF, 0x1FF2, 0x1FF4,
|
||||
0x1FF6, 0x1FFE, 0x2000, 0x2052, 0x2057, 0x2057, 0x205F, 0x2063, 0x206A, 0x2071, 0x2074, 0x208E,
|
||||
0x20A0, 0x20B1, 0x20D0, 0x20EA, 0x2100, 0x213A, 0x213D, 0x214B, 0x2153, 0x2183, 0x2190, 0x23CE,
|
||||
0x2400, 0x2426, 0x2440, 0x244A, 0x2460, 0x24FE, 0x2500, 0x2613, 0x2616, 0x2617, 0x2619, 0x267D,
|
||||
0x2680, 0x2689, 0x2701, 0x2704, 0x2706, 0x2709, 0x270C, 0x2727, 0x2729, 0x274B, 0x274D, 0x274D,
|
||||
0x274F, 0x2752, 0x2756, 0x2756, 0x2758, 0x275E, 0x2761, 0x2794, 0x2798, 0x27AF, 0x27B1, 0x27BE,
|
||||
0x27D0, 0x27EB, 0x27F0, 0x2AFF, 0x2E80, 0x2E99, 0x2E9B, 0x2EF3, 0x2F00, 0x2FD5, 0x2FF0, 0x2FFB,
|
||||
0x3000, 0x303F, 0x3041, 0x3096, 0x3099, 0x30FF, 0x3105, 0x312C, 0x3131, 0x318E, 0x3190, 0x31B7,
|
||||
0x31F0, 0x321C, 0x3220, 0x3243, 0x3251, 0x327B, 0x327F, 0x32CB, 0x32D0, 0x32FE, 0x3300, 0x3376,
|
||||
0x337B, 0x33DD, 0x33E0, 0x33FE, 0x3400, 0x4DB5, 0x4E00, 0x9FA5, 0xA000, 0xA48C, 0xA490, 0xA4C6,
|
||||
0xAC00, 0xD7A3, 0xD800, 0xFA2D, 0xFA30, 0xFA6A, 0xFB00, 0xFB06, 0xFB13, 0xFB17, 0xFB1D, 0xFB36,
|
||||
0xFB38, 0xFB3C, 0xFB3E, 0xFB3E, 0xFB40, 0xFB41, 0xFB43, 0xFB44, 0xFB46, 0xFBB1, 0xFBD3, 0xFD3F,
|
||||
0xFD50, 0xFD8F, 0xFD92, 0xFDC7, 0xFDD0, 0xFDFC, 0xFE00, 0xFE0F, 0xFE20, 0xFE23, 0xFE30, 0xFE46,
|
||||
0xFE49, 0xFE52, 0xFE54, 0xFE66, 0xFE68, 0xFE6B, 0xFE70, 0xFE74, 0xFE76, 0xFEFC, 0xFEFF, 0xFEFF,
|
||||
0xFF01, 0xFFBE, 0xFFC2, 0xFFC7, 0xFFCA, 0xFFCF, 0xFFD2, 0xFFD7, 0xFFDA, 0xFFDC, 0xFFE0, 0xFFE6,
|
||||
0xFFE8, 0xFFEE, 0xFFF9, 0xFFFF, 0x10300, 0x1031E, 0x10320, 0x10323, 0x10330, 0x1034A, 0x10400, 0x10425,
|
||||
0x10428, 0x1044D, 0x1D000, 0x1D0F5, 0x1D100, 0x1D126, 0x1D12A, 0x1D1DD, 0x1D400, 0x1D454, 0x1D456, 0x1D49C,
|
||||
0x1D49E, 0x1D49F, 0x1D4A2, 0x1D4A2, 0x1D4A5, 0x1D4A6, 0x1D4A9, 0x1D4AC, 0x1D4AE, 0x1D4B9, 0x1D4BB, 0x1D4BB,
|
||||
0x1D4BD, 0x1D4C0, 0x1D4C2, 0x1D4C3, 0x1D4C5, 0x1D505, 0x1D507, 0x1D50A, 0x1D50D, 0x1D514, 0x1D516, 0x1D51C,
|
||||
0x1D51E, 0x1D539, 0x1D53B, 0x1D53E, 0x1D540, 0x1D544, 0x1D546, 0x1D546, 0x1D54A, 0x1D550, 0x1D552, 0x1D6A3,
|
||||
0x1D6A8, 0x1D7C9, 0x1D7CE, 0x1D7FF, 0x1FFFE, 0x2A6D6, 0x2F800, 0x2FA1D, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0x3FFFF,
|
||||
0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE, 0x8FFFF, 0x9FFFE, 0x9FFFF,
|
||||
0xAFFFE, 0xAFFFF, 0xBFFFE, 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE, 0xDFFFF, 0xE0001, 0xE0001, 0xE0020, 0xE007F,
|
||||
0xEFFFE, 0x10FFFF,
|
||||
]
|
||||
|
||||
/// Whether `value` was an assigned code point as of Unicode 3.2. Binary
|
||||
/// search over ``assignedRanges``; each step compares one range's bounds.
|
||||
static func assignedInUnicode32(_ value: UInt32) -> Bool {
|
||||
var low = 0
|
||||
var high = assignedRanges.count / 2
|
||||
while low < high {
|
||||
let mid = (low + high) / 2
|
||||
if value < assignedRanges[mid * 2] {
|
||||
high = mid
|
||||
} else if value > assignedRanges[mid * 2 + 1] {
|
||||
low = mid + 1
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
48
Sources/Regex/XSDCategory.swift
Normal file
48
Sources/Regex/XSDCategory.swift
Normal file
@ -0,0 +1,48 @@
|
||||
extension PureXML.Regex {
|
||||
/// Unicode general categories as XSD 1.0 regular expressions see them
|
||||
/// (Datatypes Appendix F). Category and block escapes (`\p{...}`) and the
|
||||
/// multi-character escapes `\d`, `\w`, and friends are defined over the
|
||||
/// Unicode database. The W3C conformance corpus (XSTS) is internally
|
||||
/// consistent only at Unicode 3.2, so the repertoire is pinned there (see
|
||||
/// ``assignedInUnicode32``): a code point unassigned in 3.2 reports `Cn` and
|
||||
/// so matches `\D`/`\W`, even where the running platform now assigns it.
|
||||
///
|
||||
/// For code points whose 3.2-era category differs from the running platform's
|
||||
/// (for example the ceiling and floor brackets, `Sm` in 3.2 but `Ps`/`Pe`
|
||||
/// today), ``categoryOverrides`` pins the 3.2 classification; every other
|
||||
/// assigned code point uses the platform category.
|
||||
enum XSDCategory {
|
||||
/// Code points whose XSD-era general category differs from the running
|
||||
/// platform's. The value is the XSD-era two-letter category.
|
||||
private static let categoryOverrides: [UInt32: String] = [
|
||||
0x00A7: "So", 0x00B6: "So",
|
||||
0x06DD: "Me",
|
||||
0x0F14: "So",
|
||||
0x166D: "Po", 0x17D7: "Po", 0x17DC: "Po",
|
||||
0x180B: "Cf", 0x180C: "Cf", 0x180D: "Cf",
|
||||
0x2308: "Sm", 0x2309: "Sm", 0x230A: "Sm", 0x230B: "Sm",
|
||||
]
|
||||
|
||||
/// The two-letter XSD category abbreviation for each Unicode general
|
||||
/// category. A flat translation table, not branching logic.
|
||||
private static let categoryCodes: [Unicode.GeneralCategory: String] = [
|
||||
.uppercaseLetter: "Lu", .lowercaseLetter: "Ll", .titlecaseLetter: "Lt",
|
||||
.modifierLetter: "Lm", .otherLetter: "Lo",
|
||||
.nonspacingMark: "Mn", .spacingMark: "Mc", .enclosingMark: "Me",
|
||||
.decimalNumber: "Nd", .letterNumber: "Nl", .otherNumber: "No",
|
||||
.connectorPunctuation: "Pc", .dashPunctuation: "Pd", .openPunctuation: "Ps",
|
||||
.closePunctuation: "Pe", .initialPunctuation: "Pi", .finalPunctuation: "Pf",
|
||||
.otherPunctuation: "Po",
|
||||
.mathSymbol: "Sm", .currencySymbol: "Sc", .modifierSymbol: "Sk", .otherSymbol: "So",
|
||||
.spaceSeparator: "Zs", .lineSeparator: "Zl", .paragraphSeparator: "Zp",
|
||||
.control: "Cc", .format: "Cf", .surrogate: "Cs", .privateUse: "Co",
|
||||
.unassigned: "Cn",
|
||||
]
|
||||
|
||||
static func generalCategoryCode(_ scalar: Unicode.Scalar) -> String {
|
||||
if let override = categoryOverrides[scalar.value] { return override }
|
||||
guard assignedInUnicode32(scalar.value) else { return "Cn" }
|
||||
return categoryCodes[scalar.properties.generalCategory] ?? "Cn"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -60,6 +60,41 @@ public extension PureXML.Schema {
|
||||
return uris.contains(namespaceURI)
|
||||
}
|
||||
}
|
||||
|
||||
/// The union of two wildcard constraints (XSD 1.0 `cos-aw-union`), used when
|
||||
/// a complexContent extension adds its own `anyAttribute`.
|
||||
func union(with other: Wildcard) -> Wildcard {
|
||||
Wildcard(
|
||||
namespace: Self.unionNamespace(namespace, other.namespace),
|
||||
processContents: Self.unionProcessContents(processContents, other.processContents),
|
||||
targetNamespace: targetNamespace ?? other.targetNamespace,
|
||||
)
|
||||
}
|
||||
|
||||
/// Combines optional wildcards from a base type and a derivation.
|
||||
static func union(_ left: Wildcard?, _ right: Wildcard?) -> Wildcard? {
|
||||
switch (left, right) {
|
||||
case (nil, nil): nil
|
||||
case (let left?, nil): left
|
||||
case (nil, let right?): right
|
||||
case let (left?, right?): left.union(with: right)
|
||||
}
|
||||
}
|
||||
|
||||
private static func unionNamespace(_ left: WildcardNamespace, _ right: WildcardNamespace) -> WildcardNamespace {
|
||||
switch (left, right) {
|
||||
case (.any, _), (_, .any): .any
|
||||
case (.other, .other): .other
|
||||
case let (.enumerated(lhs), .enumerated(rhs)): .enumerated(lhs.union(rhs))
|
||||
case (.other, _), (_, .other): .any
|
||||
}
|
||||
}
|
||||
|
||||
private static func unionProcessContents(_ left: ProcessContents, _ right: ProcessContents) -> ProcessContents {
|
||||
if left == .strict || right == .strict { return .strict }
|
||||
if left == .lax || right == .lax { return .lax }
|
||||
return .skip
|
||||
}
|
||||
}
|
||||
|
||||
/// The term of a particle: an element declaration, a nested model group, or a
|
||||
@ -69,7 +104,7 @@ public extension PureXML.Schema {
|
||||
/// (a built-in, a user type, or `anyType` for an absent type), or nil when
|
||||
/// the type is an inline anonymous definition. It preserves the derivation
|
||||
/// identity the flattened `type` (`ElementType`) loses, for NameAndTypeOK.
|
||||
case element(name: PureXML.Model.QualifiedName, type: ElementType?, typeName: String?)
|
||||
case element(name: PureXML.Model.QualifiedName, type: ElementType?, typeName: String?, valueConstraint: ValueConstraint? = nil)
|
||||
case group(Group)
|
||||
case wildcard(Wildcard)
|
||||
}
|
||||
@ -135,17 +170,22 @@ public extension PureXML.Schema {
|
||||
public var type: SimpleType
|
||||
public var required: Bool
|
||||
public var valueConstraint: ValueConstraint?
|
||||
/// When true, an unprefixed instance attribute may match this use if the
|
||||
/// element is in the same namespace (chameleon include only).
|
||||
public var chameleonUnprefixed: Bool
|
||||
|
||||
public init(
|
||||
name: PureXML.Model.QualifiedName,
|
||||
type: SimpleType,
|
||||
required: Bool = false,
|
||||
valueConstraint: ValueConstraint? = nil,
|
||||
chameleonUnprefixed: Bool = false,
|
||||
) {
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.required = required
|
||||
self.valueConstraint = valueConstraint
|
||||
self.chameleonUnprefixed = chameleonUnprefixed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,11 @@
|
||||
/// namespace to avoid nesting a type two levels deep.
|
||||
typealias XSDFailure = PureXML.Validation.ValidationError
|
||||
typealias XSDPath = [PureXML.Validation.PathKey]
|
||||
private struct XSDChildValidationFrame {
|
||||
let path: XSDPath
|
||||
let namespaceBindings: [String: String]
|
||||
}
|
||||
|
||||
private typealias XSDGroup = PureXML.Schema.Group
|
||||
private typealias XSDTerm = PureXML.Schema.Term
|
||||
private typealias XSDTermLabel = PureXML.Schema.TermLabel
|
||||
@ -30,6 +35,8 @@ public extension PureXML.Schema {
|
||||
let elementBlock: [String: Set<DerivationMethod>]
|
||||
/// Each named complex type's base and derivation method.
|
||||
let typeDerivation: [String: TypeDerivation]
|
||||
/// Global attribute declarations for strict/lax `anyAttribute` validation.
|
||||
let globalAttributes: [String: AttributeUse]
|
||||
/// Document-scoped xs:ID/xs:IDREF accumulator, filled during the typed walk
|
||||
/// and reported by `idErrors()` once the whole tree has been seen. See the
|
||||
/// ID/IDREF extension for `idErrors()` and `recordIDs(_:value:at:)`.
|
||||
@ -37,6 +44,7 @@ public extension PureXML.Schema {
|
||||
|
||||
public init(
|
||||
types: [String: ElementType] = [:],
|
||||
globalAttributes: [String: AttributeUse] = [:],
|
||||
nillableElements: Set<String> = [],
|
||||
elementConstraints: [String: ValueConstraint] = [:],
|
||||
abstractTypes: Set<String> = [],
|
||||
@ -45,6 +53,7 @@ public extension PureXML.Schema {
|
||||
typeDerivation: [String: TypeDerivation] = [:],
|
||||
) {
|
||||
self.types = types
|
||||
self.globalAttributes = globalAttributes
|
||||
self.nillableElements = nillableElements
|
||||
self.elementConstraints = elementConstraints
|
||||
self.abstractTypes = abstractTypes
|
||||
@ -58,7 +67,9 @@ public extension PureXML.Schema {
|
||||
_ element: PureXML.Model.Element,
|
||||
against type: ComplexType,
|
||||
at path: [PureXML.Validation.PathKey] = [],
|
||||
namespaceBindings inheritedBindings: [String: String] = [:],
|
||||
) -> [PureXML.Validation.ValidationError] {
|
||||
let namespaceBindings = Self.namespaceBindings(for: element, inherited: inheritedBindings)
|
||||
var errors: [XSDFailure] = []
|
||||
validateAttributes(element, type, at: path, into: &errors)
|
||||
// An xsi:nil element keeps its attribute obligations but must have no
|
||||
@ -66,7 +77,7 @@ public extension PureXML.Schema {
|
||||
if let nilErrors = nilErrors(element, at: path) {
|
||||
return errors + nilErrors
|
||||
}
|
||||
validateContent(element, type.content, at: path, into: &errors)
|
||||
validateContent(element, type.content, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||
errors += elementFixedErrors(element, at: path)
|
||||
return errors
|
||||
}
|
||||
@ -87,12 +98,18 @@ public extension PureXML.Schema {
|
||||
}
|
||||
|
||||
/// The error from a `fixed` element value constraint: the element's text
|
||||
/// must equal the fixed value.
|
||||
func elementFixedErrors(_ element: PureXML.Model.Element, at path: XSDPath) -> [XSDFailure] {
|
||||
guard let fixed = elementConstraints[element.name.localName]?.fixedValue else { return [] }
|
||||
let text = Self.textContent(element)
|
||||
/// must equal the fixed value in the element type's value space when known.
|
||||
func elementFixedErrors(
|
||||
_ element: PureXML.Model.Element,
|
||||
valueType: SimpleType? = nil,
|
||||
particleFixed: String? = nil,
|
||||
at path: XSDPath,
|
||||
) -> [XSDFailure] {
|
||||
guard let fixed = particleFixed ?? elementConstraints[element.name.localName]?.fixedValue else { return [] }
|
||||
let text = Self.rawTextContent(element)
|
||||
// An empty element takes the fixed value; only present content must equal it.
|
||||
if text.isEmpty { return [] }
|
||||
if let valueType, valueType.valueMatches(text, literal: fixed) { return [] }
|
||||
return text == fixed ? [] : [XSDFailure(reason: "element '\(element.name.localName)' is fixed and must be '\(fixed)'", at: path)]
|
||||
}
|
||||
|
||||
@ -104,47 +121,18 @@ public extension PureXML.Schema {
|
||||
at path: [PureXML.Validation.PathKey] = [],
|
||||
) -> [PureXML.Validation.ValidationError] {
|
||||
var errors: [XSDFailure] = []
|
||||
validateChild(element, against: type, at: path, into: &errors)
|
||||
let namespaceBindings = Self.namespaceBindings(for: element)
|
||||
validateChild(element, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||
return errors
|
||||
}
|
||||
|
||||
// MARK: Attributes
|
||||
|
||||
func validateAttributes(
|
||||
_ element: PureXML.Model.Element,
|
||||
_ type: ComplexType,
|
||||
at path: XSDPath,
|
||||
into errors: inout [XSDFailure],
|
||||
) {
|
||||
let present = element.attributes.filter { !Self.isNamespaceDeclaration($0) && !Self.isSchemaInstanceAttribute($0) }
|
||||
for use in type.attributes {
|
||||
let match = present.first { Self.sameName($0.name, use.name) }
|
||||
if let match {
|
||||
if let error = use.type.validate(match.value) {
|
||||
errors.append(XSDFailure(reason: "attribute '\(use.name.localName)': \(error)", at: path))
|
||||
}
|
||||
if let fixed = use.valueConstraint?.fixedValue, match.value != fixed {
|
||||
errors.append(XSDFailure(reason: "attribute '\(use.name.localName)' is fixed and must be '\(fixed)'", at: path))
|
||||
}
|
||||
recordIDs(use.type, value: match.value, at: path)
|
||||
} else if use.required {
|
||||
errors.append(XSDFailure(reason: "missing required attribute '\(use.name.localName)'", at: path))
|
||||
}
|
||||
}
|
||||
// An undeclared attribute is allowed only if an xs:anyAttribute wildcard
|
||||
// admits its namespace.
|
||||
for attribute in present where !type.attributes.contains(where: { Self.sameName($0.name, attribute.name) }) {
|
||||
if type.attributeWildcard?.admits(attribute.name) == true { continue }
|
||||
errors.append(XSDFailure(reason: "undeclared attribute '\(attribute.name.localName)'", at: path))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Content
|
||||
|
||||
private func validateContent(
|
||||
_ element: PureXML.Model.Element,
|
||||
_ content: ContentType,
|
||||
at path: XSDPath,
|
||||
namespaceBindings: [String: String],
|
||||
into errors: inout [XSDFailure],
|
||||
) {
|
||||
let children = element.children.compactMap(\.element)
|
||||
@ -159,9 +147,9 @@ public extension PureXML.Schema {
|
||||
}
|
||||
case let .elementOnly(particle):
|
||||
rejectText(element, at: path, into: &errors)
|
||||
validateParticle(particle, children: children, at: path, into: &errors)
|
||||
validateParticle(particle, children: children, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||
case let .mixed(particle):
|
||||
validateParticle(particle, children: children, at: path, into: &errors)
|
||||
validateParticle(particle, children: children, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,40 +164,53 @@ public extension PureXML.Schema {
|
||||
_ particle: Particle,
|
||||
children: [PureXML.Model.Element],
|
||||
at path: XSDPath,
|
||||
namespaceBindings: [String: String],
|
||||
into errors: inout [XSDFailure],
|
||||
) {
|
||||
// Locate each content-model violation at the offending child (or the
|
||||
// missing one), with a recovery hint, rather than one opaque failure,
|
||||
// then still validate every well-placed child's own content.
|
||||
// then assess every well-placed child against the exact particle the
|
||||
// content model matched it to (#180), not a by-name lookup that loses
|
||||
// which of two same-named particles, or a wildcard versus a named
|
||||
// particle, actually matched.
|
||||
let matched: [PureXML.Schema.MatchedParticle?]
|
||||
if case let .group(group) = particle.term, group.compositor == .all {
|
||||
allStructureErrors(group, children: children, at: path, into: &errors)
|
||||
matched = allMatchedParticles(group, children: children)
|
||||
} else {
|
||||
sequenceStructureErrors(particle, children: children, at: path, into: &errors)
|
||||
matched = PureXML.Schema.ContentNFABuilder.build(particle).matchedParticles(children.map(\.name))
|
||||
}
|
||||
validateChildren(
|
||||
children,
|
||||
childTypes: Self.elementTypes(in: particle.term),
|
||||
wildcard: Self.wildcard(in: particle.term),
|
||||
at: path,
|
||||
matched: matched,
|
||||
frame: XSDChildValidationFrame(path: path, namespaceBindings: namespaceBindings),
|
||||
into: &errors,
|
||||
)
|
||||
}
|
||||
|
||||
private func validateChildren(
|
||||
_ children: [PureXML.Model.Element],
|
||||
childTypes: [String: ElementType],
|
||||
wildcard: ProcessContents?,
|
||||
at path: XSDPath,
|
||||
matched: [PureXML.Schema.MatchedParticle?],
|
||||
frame: XSDChildValidationFrame,
|
||||
into errors: inout [XSDFailure],
|
||||
) {
|
||||
let steps = Self.childSteps(children)
|
||||
for (child, step) in zip(children, steps) {
|
||||
if let declared = childTypes[Self.key(child.name)] {
|
||||
validateChild(child, against: declared, at: path + [step], into: &errors)
|
||||
} else if let wildcard {
|
||||
// The structure already matched, so an undeclared child here was
|
||||
// admitted by a wildcard; process its content per the wildcard.
|
||||
validateWildcardChild(child, processContents: wildcard, at: path + [step], into: &errors)
|
||||
for (index, child) in children.enumerated() {
|
||||
let namespaceBindings = Self.namespaceBindings(for: child, inherited: frame.namespaceBindings)
|
||||
let path = frame.path + [steps[index]]
|
||||
switch matched[index] {
|
||||
case let .element(type, valueConstraint):
|
||||
// An element particle without an inlined type (a bare ref)
|
||||
// resolves through its global declaration, as before.
|
||||
guard let declared = type ?? globalElementDeclaration(for: child.name) else { continue }
|
||||
validateChild(child, against: declared, at: path, namespaceBindings: namespaceBindings, particleConstraint: valueConstraint, into: &errors)
|
||||
case let .wildcard(wildcard):
|
||||
validateWildcardChild(child, processContents: wildcard.processContents, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||
case .none:
|
||||
// The content model rejected this child (a structure error,
|
||||
// already located above); the matched particle is undefined.
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -217,32 +218,54 @@ public extension PureXML.Schema {
|
||||
/// Validates a wildcard-matched child by its `processContents`: skip does
|
||||
/// nothing, lax validates against a global declaration when one exists, and
|
||||
/// strict requires one.
|
||||
private func globalElementDeclaration(for name: PureXML.Model.QualifiedName) -> PureXML.Schema.ElementType? {
|
||||
if let declaration = types[PureXML.Schema.XSDParser.elementDeclarationKey(name)] {
|
||||
return declaration
|
||||
}
|
||||
guard name.namespaceURI == nil else { return nil }
|
||||
return types["element:\(name.localName)"]
|
||||
}
|
||||
|
||||
private func validateWildcardChild(
|
||||
_ child: PureXML.Model.Element,
|
||||
processContents: ProcessContents,
|
||||
at path: XSDPath,
|
||||
namespaceBindings: [String: String],
|
||||
into errors: inout [XSDFailure],
|
||||
) {
|
||||
switch processContents {
|
||||
case .skip:
|
||||
return
|
||||
case .lax:
|
||||
if let declaration = types["element:\(child.name.localName)"] {
|
||||
validateChild(child, against: declaration, at: path, into: &errors)
|
||||
if let declaration = globalElementDeclaration(for: child.name) {
|
||||
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||
}
|
||||
case .strict:
|
||||
guard let declaration = types["element:\(child.name.localName)"] else {
|
||||
if let declaration = globalElementDeclaration(for: child.name) {
|
||||
validateChild(child, against: declaration, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||
} else if let type = xsiDeclaredType(for: child, namespaceBindings: namespaceBindings) {
|
||||
validateChild(child, against: type, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||
} else {
|
||||
errors.append(XSDFailure(reason: "no declaration for wildcard-matched element '\(child.name.localName)'", at: path))
|
||||
return
|
||||
}
|
||||
validateChild(child, against: declaration, at: path, into: &errors)
|
||||
}
|
||||
}
|
||||
|
||||
/// The type named by a wildcard-matched element's `xsi:type`, when present.
|
||||
private func xsiDeclaredType(for child: PureXML.Model.Element, namespaceBindings: [String: String]) -> ElementType? {
|
||||
guard Self.xsiTypeAttributeValue(child) != nil else { return nil }
|
||||
guard let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings),
|
||||
let type = Self.resolveNamedType(reference, in: types)
|
||||
else { return nil }
|
||||
return type
|
||||
}
|
||||
|
||||
private func validateChild(
|
||||
_ child: PureXML.Model.Element,
|
||||
against declared: ElementType,
|
||||
at path: XSDPath,
|
||||
namespaceBindings: [String: String],
|
||||
particleConstraint: ValueConstraint? = nil,
|
||||
into errors: inout [XSDFailure],
|
||||
) {
|
||||
// An instance `xsi:type` overrides the declared type, provided the named
|
||||
@ -258,7 +281,7 @@ public extension PureXML.Schema {
|
||||
errors.append(missing)
|
||||
return
|
||||
}
|
||||
guard let declared = overriddenType(declared, for: child, at: path, into: &errors) else { return }
|
||||
guard let declared = overriddenType(declared, for: child, namespaceBindings: namespaceBindings, at: path, into: &errors) else { return }
|
||||
switch declared {
|
||||
case let .simple(simple):
|
||||
// A simple type declares no attribute uses, so a simple-typed
|
||||
@ -281,16 +304,17 @@ public extension PureXML.Schema {
|
||||
// (not the empty string) is what must be valid against the type,
|
||||
// including any xsi:type override already resolved into `simple`.
|
||||
let text = Self.rawTextContent(child)
|
||||
let effective = text.isEmpty ? (elementConstraints[child.name.localName]?.value ?? text) : text
|
||||
let constraint = particleConstraint ?? elementConstraints[child.name.localName]
|
||||
let effective = text.isEmpty ? (constraint?.value ?? text) : text
|
||||
if let error = simple.validate(effective) {
|
||||
errors.append(XSDFailure(reason: "'\(child.name.localName)': \(error)", at: path))
|
||||
}
|
||||
recordIDs(simple, value: text, at: path)
|
||||
errors += elementFixedErrors(child, at: path)
|
||||
errors += elementFixedErrors(child, valueType: simple, particleFixed: particleConstraint?.fixedValue, at: path)
|
||||
case let .complex(complex):
|
||||
errors.append(contentsOf: validate(child, against: complex, at: path))
|
||||
errors.append(contentsOf: validate(child, against: complex, at: path, namespaceBindings: namespaceBindings))
|
||||
case .typeReference:
|
||||
validateResolvedReference(declared, child, at: path, into: &errors)
|
||||
validateResolvedReference(declared, child, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||
}
|
||||
}
|
||||
|
||||
@ -300,6 +324,7 @@ public extension PureXML.Schema {
|
||||
_ declared: ElementType,
|
||||
_ child: PureXML.Model.Element,
|
||||
at path: XSDPath,
|
||||
namespaceBindings: [String: String],
|
||||
into errors: inout [XSDFailure],
|
||||
) {
|
||||
switch resolveReference(declared) {
|
||||
@ -308,75 +333,8 @@ public extension PureXML.Schema {
|
||||
case let .circular(name):
|
||||
errors.append(XSDFailure(reason: "circular type reference '\(name)'", at: path))
|
||||
case let .resolved(resolved):
|
||||
validateChild(child, against: resolved, at: path, into: &errors)
|
||||
validateChild(child, against: resolved, at: path, namespaceBindings: namespaceBindings, into: &errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Located content-model diagnostics: pinpoint which child breaks the model and
|
||||
/// what was expected there, so an editor shows placed errors with recovery hints
|
||||
/// rather than one opaque "content does not match" per element.
|
||||
extension PureXML.Schema.ComplexValidator {
|
||||
/// Walks the children through the content automaton, flagging the first child
|
||||
/// the follow-set rejects, or the missing content when the sequence ends early.
|
||||
func sequenceStructureErrors(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
|
||||
let nfa = PureXML.Schema.ContentNFABuilder.build(particle)
|
||||
let steps = Self.childSteps(children)
|
||||
// Advance one active state-set across the children rather than re-walking
|
||||
// the prefix per child (which is quadratic over the content model, #129).
|
||||
var current = nfa.startStates()
|
||||
for (index, child) in children.enumerated() {
|
||||
guard let next = nfa.step(current, over: child.name) else {
|
||||
let allowed = nfa.admissible(from: current)
|
||||
errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here\(Self.expectation(allowed))", at: path + [steps[index]]))
|
||||
return
|
||||
}
|
||||
current = next
|
||||
}
|
||||
if !nfa.isComplete(current) {
|
||||
errors.append(XSDFailure(reason: "content is incomplete\(Self.expectation(nfa.admissible(from: current)))", at: path))
|
||||
}
|
||||
}
|
||||
|
||||
/// Locates `all`-group violations: each child that is not an in-bounds member,
|
||||
/// recovering past it, then each required member that never appeared.
|
||||
func allStructureErrors(_ group: PureXML.Schema.Group, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
|
||||
var counts = [Int](repeating: 0, count: group.particles.count)
|
||||
let steps = Self.childSteps(children)
|
||||
for (index, child) in children.enumerated() {
|
||||
guard let position = group.particles.indices.first(where: { slot in
|
||||
let member = group.particles[slot]
|
||||
let room = member.maxOccurs.map { counts[slot] < $0 } ?? true
|
||||
return room && Self.memberMatches(member.term, child.name)
|
||||
}) else {
|
||||
errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here", at: path + [steps[index]]))
|
||||
continue
|
||||
}
|
||||
counts[position] += 1
|
||||
}
|
||||
for (index, member) in group.particles.enumerated() where counts[index] < member.minOccurs {
|
||||
if case let .element(name, _, _) = member.term {
|
||||
errors.append(XSDFailure(reason: "element '\(name.localName)' is required but missing", at: path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func memberMatches(_ term: PureXML.Schema.Term, _ name: PureXML.Model.QualifiedName) -> Bool {
|
||||
switch term {
|
||||
case let .element(declared, _, _): declared.localName == name.localName && declared.namespaceURI == name.namespaceURI
|
||||
case let .wildcard(wildcard): wildcard.admits(name)
|
||||
case .group: false
|
||||
}
|
||||
}
|
||||
|
||||
/// A "; expected a, b" hint naming the elements the automaton accepts next.
|
||||
static func expectation(_ labels: [PureXML.Schema.TermLabel]) -> String {
|
||||
let names = labels.compactMap { label -> String? in
|
||||
if case let .name(qualified) = label { return "<\(qualified.localName)>" }
|
||||
return nil
|
||||
}
|
||||
let unique = Set(names).sorted()
|
||||
return unique.isEmpty ? "" : "; expected \(unique.joined(separator: ", "))"
|
||||
}
|
||||
}
|
||||
|
||||
95
Sources/Schema/ComplexValidatorAttributes.swift
Normal file
95
Sources/Schema/ComplexValidatorAttributes.swift
Normal file
@ -0,0 +1,95 @@
|
||||
extension PureXML.Schema.ComplexValidator {
|
||||
func validateAttributes(
|
||||
_ element: PureXML.Model.Element,
|
||||
_ type: PureXML.Schema.ComplexType,
|
||||
at path: [PureXML.Validation.PathKey],
|
||||
into errors: inout [PureXML.Validation.ValidationError],
|
||||
) {
|
||||
let present = element.attributes.filter { !Self.isNamespaceDeclaration($0) && !Self.isSchemaInstanceAttribute($0) }
|
||||
for use in type.attributes {
|
||||
let match = present.first { Self.attributeMatches($0.name, declared: use.name, use: use, on: element.name) }
|
||||
if let match {
|
||||
if let error = use.type.validate(match.value) {
|
||||
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(use.name.localName)': \(error)", at: path))
|
||||
}
|
||||
if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(match.value, literal: fixed) {
|
||||
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(use.name.localName)' is fixed and must be '\(fixed)'", at: path))
|
||||
}
|
||||
recordIDs(use.type, value: match.value, at: path)
|
||||
} else if use.required {
|
||||
errors.append(PureXML.Validation.ValidationError(reason: "missing required attribute '\(use.name.localName)'", at: path))
|
||||
}
|
||||
}
|
||||
validateWildcardAttributes(present, element: element, type: type, at: path, into: &errors)
|
||||
}
|
||||
|
||||
/// Whether an instance attribute satisfies a declared use. Chameleon-included
|
||||
/// global attributes may appear unprefixed on an element in the effective TN.
|
||||
static func attributeMatches(
|
||||
_ instance: PureXML.Model.QualifiedName,
|
||||
declared: PureXML.Model.QualifiedName,
|
||||
use: PureXML.Schema.AttributeUse,
|
||||
on elementName: PureXML.Model.QualifiedName,
|
||||
) -> Bool {
|
||||
if sameName(instance, declared) { return true }
|
||||
guard use.chameleonUnprefixed,
|
||||
instance.namespaceURI == nil || instance.namespaceURI?.isEmpty == true,
|
||||
let declaredNamespace = declared.namespaceURI, !declaredNamespace.isEmpty
|
||||
else { return false }
|
||||
return instance.localName == declared.localName && declaredNamespace == elementName.namespaceURI
|
||||
}
|
||||
|
||||
private func validateWildcardAttributes(
|
||||
_ present: [PureXML.Model.Attribute],
|
||||
element: PureXML.Model.Element,
|
||||
type: PureXML.Schema.ComplexType,
|
||||
at path: [PureXML.Validation.PathKey],
|
||||
into errors: inout [PureXML.Validation.ValidationError],
|
||||
) {
|
||||
let declared = type.attributes
|
||||
let elementName = element.name
|
||||
let wildcard = type.attributeWildcard
|
||||
for attribute in present where !declared.contains(where: { Self.attributeMatches(attribute.name, declared: $0.name, use: $0, on: elementName) }) {
|
||||
guard let wildcard, wildcard.admits(attribute.name) else {
|
||||
errors.append(PureXML.Validation.ValidationError(reason: "undeclared attribute '\(attribute.name.localName)'", at: path))
|
||||
continue
|
||||
}
|
||||
switch wildcard.processContents {
|
||||
case .skip:
|
||||
continue
|
||||
case .lax:
|
||||
if let use = globalAttributeUse(for: attribute.name) {
|
||||
validateWildcardAttribute(attribute, use: use, at: path, into: &errors)
|
||||
}
|
||||
case .strict:
|
||||
guard let use = globalAttributeUse(for: attribute.name) else {
|
||||
errors.append(PureXML.Validation.ValidationError(
|
||||
reason: "no declaration for wildcard-matched attribute '\(attribute.name.localName)'",
|
||||
at: path,
|
||||
))
|
||||
continue
|
||||
}
|
||||
validateWildcardAttribute(attribute, use: use, at: path, into: &errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func globalAttributeUse(for name: PureXML.Model.QualifiedName) -> PureXML.Schema.AttributeUse? {
|
||||
globalAttributes[PureXML.Schema.XSDParser.attributeDeclarationKey(name)]
|
||||
}
|
||||
|
||||
private func validateWildcardAttribute(
|
||||
_ attribute: PureXML.Model.Attribute,
|
||||
use: PureXML.Schema.AttributeUse,
|
||||
at path: [PureXML.Validation.PathKey],
|
||||
into errors: inout [PureXML.Validation.ValidationError],
|
||||
) {
|
||||
if let error = use.type.validate(attribute.value) {
|
||||
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(attribute.name.localName)': \(error)", at: path))
|
||||
}
|
||||
if let fixed = use.valueConstraint?.fixedValue, !use.type.valueMatches(attribute.value, literal: fixed) {
|
||||
errors.append(PureXML.Validation.ValidationError(reason: "attribute '\(attribute.name.localName)' is fixed and must be '\(fixed)'", at: path))
|
||||
}
|
||||
recordIDs(use.type, value: attribute.value, at: path)
|
||||
}
|
||||
}
|
||||
95
Sources/Schema/ComplexValidatorContentModel.swift
Normal file
95
Sources/Schema/ComplexValidatorContentModel.swift
Normal file
@ -0,0 +1,95 @@
|
||||
/// Located content-model diagnostics: pinpoint which child breaks the model and
|
||||
/// what was expected there, so an editor shows placed errors with recovery hints
|
||||
/// rather than one opaque "content does not match" per element.
|
||||
extension PureXML.Schema.ComplexValidator {
|
||||
/// Walks the children through the content automaton, flagging the first child
|
||||
/// the follow-set rejects, or the missing content when the sequence ends early.
|
||||
func sequenceStructureErrors(_ particle: PureXML.Schema.Particle, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
|
||||
let nfa = PureXML.Schema.ContentNFABuilder.build(particle)
|
||||
let steps = Self.childSteps(children)
|
||||
// Advance one active state-set across the children rather than re-walking
|
||||
// the prefix per child (which is quadratic over the content model, #129).
|
||||
var current = nfa.startStates()
|
||||
for (index, child) in children.enumerated() {
|
||||
guard let next = nfa.step(current, over: child.name) else {
|
||||
let allowed = nfa.admissible(from: current)
|
||||
errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here\(Self.expectation(allowed))", at: path + [steps[index]]))
|
||||
return
|
||||
}
|
||||
current = next
|
||||
}
|
||||
if !nfa.isComplete(current) {
|
||||
errors.append(XSDFailure(reason: "content is incomplete\(Self.expectation(nfa.admissible(from: current)))", at: path))
|
||||
}
|
||||
}
|
||||
|
||||
/// Locates `all`-group violations: each child that is not an in-bounds member,
|
||||
/// recovering past it, then each required member that never appeared.
|
||||
func allStructureErrors(_ group: PureXML.Schema.Group, children: [PureXML.Model.Element], at path: XSDPath, into errors: inout [XSDFailure]) {
|
||||
var counts = [Int](repeating: 0, count: group.particles.count)
|
||||
let steps = Self.childSteps(children)
|
||||
for (index, child) in children.enumerated() {
|
||||
guard let position = group.particles.indices.first(where: { slot in
|
||||
let member = group.particles[slot]
|
||||
let room = member.maxOccurs.map { counts[slot] < $0 } ?? true
|
||||
return room && Self.memberMatches(member.term, child.name)
|
||||
}) else {
|
||||
errors.append(XSDFailure(reason: "element '\(child.name.localName)' is not allowed here", at: path + [steps[index]]))
|
||||
continue
|
||||
}
|
||||
counts[position] += 1
|
||||
}
|
||||
for (index, member) in group.particles.enumerated() where counts[index] < member.minOccurs {
|
||||
if case let .element(name, _, _, _) = member.term {
|
||||
errors.append(XSDFailure(reason: "element '\(name.localName)' is required but missing", at: path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func memberMatches(_ term: PureXML.Schema.Term, _ name: PureXML.Model.QualifiedName) -> Bool {
|
||||
switch term {
|
||||
case let .element(declared, _, _, _): declared.localName == name.localName && declared.namespaceURI == name.namespaceURI
|
||||
case let .wildcard(wildcard): wildcard.admits(name)
|
||||
case .group: false
|
||||
}
|
||||
}
|
||||
|
||||
/// The particle each child of an `all` group matched, for per-child
|
||||
/// assessment. `all` is matched by counting, not by the automaton, so it has
|
||||
/// its own pass mirroring ``allStructureErrors``.
|
||||
func allMatchedParticles(_ group: PureXML.Schema.Group, children: [PureXML.Model.Element]) -> [PureXML.Schema.MatchedParticle?] {
|
||||
var counts = [Int](repeating: 0, count: group.particles.count)
|
||||
var result: [PureXML.Schema.MatchedParticle?] = []
|
||||
for child in children {
|
||||
guard let slot = group.particles.indices.first(where: { index in
|
||||
let member = group.particles[index]
|
||||
let room = member.maxOccurs.map { counts[index] < $0 } ?? true
|
||||
return room && Self.memberMatches(member.term, child.name)
|
||||
}) else {
|
||||
result.append(nil)
|
||||
continue
|
||||
}
|
||||
counts[slot] += 1
|
||||
result.append(Self.matchedParticle(of: group.particles[slot].term))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static func matchedParticle(of term: PureXML.Schema.Term) -> PureXML.Schema.MatchedParticle? {
|
||||
switch term {
|
||||
case let .element(_, type, _, valueConstraint): .element(type: type, valueConstraint: valueConstraint)
|
||||
case let .wildcard(wildcard): .wildcard(wildcard)
|
||||
case .group: nil
|
||||
}
|
||||
}
|
||||
|
||||
/// A "; expected a, b" hint naming the elements the automaton accepts next.
|
||||
static func expectation(_ labels: [PureXML.Schema.TermLabel]) -> String {
|
||||
let names = labels.compactMap { label -> String? in
|
||||
if case let .name(qualified) = label { return "<\(qualified.localName)>" }
|
||||
return nil
|
||||
}
|
||||
let unique = Set(names).sorted()
|
||||
return unique.isEmpty ? "" : "; expected \(unique.joined(separator: ", "))"
|
||||
}
|
||||
}
|
||||
@ -7,9 +7,12 @@ extension PureXML.Schema.ComplexValidator {
|
||||
child: PureXML.Model.Element,
|
||||
at path: [PureXML.Validation.PathKey],
|
||||
) -> PureXML.Validation.ValidationError? {
|
||||
guard abstractTypes.contains(name), Self.xsiTypeName(child) == nil else { return nil }
|
||||
guard abstractTypes.contains(PureXML.Schema.XSDParser.bareTypeLocalName(name)),
|
||||
Self.xsiTypeName(child) == nil
|
||||
else { return nil }
|
||||
let bare = PureXML.Schema.XSDParser.bareTypeLocalName(name)
|
||||
return PureXML.Validation.ValidationError(
|
||||
reason: "element '\(child.name.localName)' has abstract type '\(name)' and requires an xsi:type naming a concrete derived type",
|
||||
reason: "element '\(child.name.localName)' has abstract type '\(bare)' and requires an xsi:type naming a concrete derived type",
|
||||
at: path,
|
||||
)
|
||||
}
|
||||
@ -24,17 +27,23 @@ extension PureXML.Schema.ComplexValidator {
|
||||
at path: [PureXML.Validation.PathKey],
|
||||
) -> PureXML.Validation.ValidationError? {
|
||||
guard case let .typeReference(reference) = declared,
|
||||
let substitute = Self.xsiTypeName(child), types[substitute] != nil
|
||||
let substitute = Self.xsiTypeName(child),
|
||||
Self.resolveNamedType(substitute, in: types) != nil
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
// A ref'd child carries its type as an element-ref key (`element:foo`)
|
||||
// that resolves through `types` to the concrete type; follow that chain
|
||||
// to the declared type name the derivation backbone and `block` use.
|
||||
var declaredName = reference
|
||||
var declaredKey = reference
|
||||
var declaredName = PureXML.Schema.XSDParser.bareTypeLocalName(reference)
|
||||
var steps = 0
|
||||
while case let .typeReference(next)? = types[declaredName], next != declaredName, steps <= types.count {
|
||||
declaredName = next
|
||||
while steps <= types.count {
|
||||
guard let resolved = types[declaredKey] ?? Self.resolveNamedType(declaredKey, in: types),
|
||||
case let .typeReference(next) = resolved
|
||||
else { break }
|
||||
declaredKey = next
|
||||
declaredName = PureXML.Schema.XSDParser.bareTypeLocalName(next)
|
||||
steps += 1
|
||||
}
|
||||
guard let methods = PureXML.Schema.XSDParser.derivationMethods(from: substitute, to: declaredName, typeDerivation) else {
|
||||
|
||||
@ -34,7 +34,7 @@ extension PureXML.Schema.ComplexValidator {
|
||||
|
||||
fileprivate static func label(of term: XSDTerm) -> XSDTermLabel {
|
||||
switch term {
|
||||
case let .element(name, _, _): .name(name)
|
||||
case let .element(name, _, _, _): .name(name)
|
||||
case let .wildcard(wildcard): .wildcard(wildcard)
|
||||
case .group: .wildcard(XSDWildcard())
|
||||
}
|
||||
@ -64,7 +64,7 @@ extension PureXML.Schema.ComplexValidator {
|
||||
|
||||
fileprivate static func collectTypes(_ term: XSDTerm, into result: inout [String: XSDElementType]) {
|
||||
switch term {
|
||||
case let .element(name, type, _):
|
||||
case let .element(name, type, _, _):
|
||||
if let type { result[key(name)] = type }
|
||||
case let .group(group):
|
||||
for member in group.particles {
|
||||
@ -86,6 +86,47 @@ extension PureXML.Schema.ComplexValidator {
|
||||
lhs.namespaceURI == rhs.namespaceURI && lhs.localName == rhs.localName
|
||||
}
|
||||
|
||||
/// Namespace prefix bindings declared on an element (`xmlns`, `xmlns:p`).
|
||||
static func namespaceBindingsDeclared(on element: PureXML.Model.Element) -> [String: String] {
|
||||
var bindings: [String: String] = [:]
|
||||
for attribute in element.attributes {
|
||||
if attribute.name.prefix == "xmlns" {
|
||||
bindings[attribute.name.localName] = attribute.value
|
||||
} else if attribute.name.prefix == nil, attribute.name.localName == "xmlns" {
|
||||
bindings[""] = attribute.value
|
||||
}
|
||||
}
|
||||
return bindings
|
||||
}
|
||||
|
||||
/// In-scope prefix bindings for `element`, merging ancestor declarations.
|
||||
static func namespaceBindings(for element: PureXML.Model.Element, inherited: [String: String] = [:]) -> [String: String] {
|
||||
inherited.merging(namespaceBindingsDeclared(on: element)) { _, new in new }
|
||||
}
|
||||
|
||||
/// The lexical `xsi:type` attribute value, when present.
|
||||
static func xsiTypeAttributeValue(_ element: PureXML.Model.Element) -> String? {
|
||||
let schemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
let match = element.attributes.first { attribute in
|
||||
attribute.name.localName == "type"
|
||||
&& (attribute.name.namespaceURI == schemaInstance || attribute.name.prefix == "xsi")
|
||||
}
|
||||
return match?.value.trimmingXMLWhitespace()
|
||||
}
|
||||
|
||||
/// Resolves an instance `xsi:type` attribute to a named type-table key.
|
||||
static func xsiTypeReference(_ element: PureXML.Model.Element, namespaceBindings: [String: String]) -> String? {
|
||||
guard let value = xsiTypeAttributeValue(element) else { return nil }
|
||||
let local = value.split(separator: ":").last.map(String.init) ?? value
|
||||
if value.contains(":"), let prefix = value.split(separator: ":", maxSplits: 1).first.map(String.init), let uri = namespaceBindings[prefix] {
|
||||
return PureXML.Schema.XSDParser.typeDeclarationKey(local, namespaceURI: uri)
|
||||
}
|
||||
if let uri = namespaceBindings[""] {
|
||||
return PureXML.Schema.XSDParser.typeDeclarationKey(local, namespaceURI: uri)
|
||||
}
|
||||
return local
|
||||
}
|
||||
|
||||
/// The element's character content, trimmed: used to decide whether an
|
||||
/// element-only or empty type wrongly carries significant text (indentation
|
||||
/// whitespace between child elements is not significant).
|
||||
@ -135,12 +176,7 @@ extension PureXML.Schema.ComplexValidator {
|
||||
/// carries none. Recognizes the attribute by the XML Schema instance
|
||||
/// namespace or, failing namespace resolution, the conventional `xsi` prefix.
|
||||
static func xsiTypeName(_ element: PureXML.Model.Element) -> String? {
|
||||
let schemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
let match = element.attributes.first { attribute in
|
||||
attribute.name.localName == "type"
|
||||
&& (attribute.name.namespaceURI == schemaInstance || attribute.name.prefix == "xsi")
|
||||
}
|
||||
return match.map { $0.value.split(separator: ":").last.map(String.init) ?? $0.value }
|
||||
xsiTypeAttributeValue(element).map { $0.split(separator: ":").last.map(String.init) ?? $0 }
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,12 +220,76 @@ extension PureXML.Schema.ComplexValidator {
|
||||
var visited: Set<String> = []
|
||||
while case let .typeReference(name) = current {
|
||||
guard visited.insert(name).inserted else { return .circular(name) }
|
||||
guard let next = types[name] else { return .unknown(name) }
|
||||
guard let next = resolveNamedType(name, in: types) else { return .unknown(name) }
|
||||
current = next
|
||||
}
|
||||
return .resolved(current)
|
||||
}
|
||||
|
||||
/// Resolves a named type reference, including built-in datatypes and ur-types.
|
||||
static func resolveNamedType(_ name: String, in types: [String: PureXML.Schema.ElementType]) -> PureXML.Schema.ElementType? {
|
||||
if let next = types[name] { return next }
|
||||
if name.hasPrefix("type:"), let (uri, local) = parseQualifiedTypeKey(name) {
|
||||
if uri == PureXML.Schema.XSDParser.xsdNamespace, let builtin = urOrBuiltinType(named: local) {
|
||||
return builtin
|
||||
}
|
||||
if let next = types[PureXML.Schema.XSDParser.typeDeclarationKey(local, namespaceURI: uri)] {
|
||||
return next
|
||||
}
|
||||
}
|
||||
if !name.hasPrefix("type:"), let next = types[PureXML.Schema.XSDParser.typeDeclarationKey(name, namespaceURI: nil)] {
|
||||
return next
|
||||
}
|
||||
if let builtin = PureXML.Schema.BuiltinType(rawValue: name) {
|
||||
return .simple(PureXML.Schema.SimpleType(base: builtin))
|
||||
}
|
||||
if name == "anySimpleType" {
|
||||
return .simple(PureXML.Schema.SimpleType(base: .string, isAnySimpleType: true))
|
||||
}
|
||||
if name == "anyType" {
|
||||
return .complex(PureXML.Schema.ComplexType(
|
||||
attributeWildcard: PureXML.Schema.Wildcard(processContents: .skip),
|
||||
content: .mixed(PureXML.Schema.Particle(
|
||||
minOccurs: 0,
|
||||
maxOccurs: nil,
|
||||
term: .wildcard(PureXML.Schema.Wildcard(processContents: .skip)),
|
||||
)),
|
||||
))
|
||||
}
|
||||
if let item = PureXML.Schema.XSDSimpleParser.listBuiltinItem(name) {
|
||||
return .simple(.list(item: PureXML.Schema.SimpleType(base: item), isBuiltinList: true))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func parseQualifiedTypeKey(_ name: String) -> (uri: String, local: String)? {
|
||||
let keyBody = String(name.dropFirst("type:".count))
|
||||
guard keyBody.hasPrefix("{"), let close = keyBody.firstIndex(of: "}") else { return nil }
|
||||
let uri = String(keyBody[keyBody.index(after: keyBody.startIndex) ..< close])
|
||||
let local = String(keyBody[keyBody.index(after: close)...])
|
||||
return (uri, local)
|
||||
}
|
||||
|
||||
private static func urOrBuiltinType(named local: String) -> PureXML.Schema.ElementType? {
|
||||
if let builtin = PureXML.Schema.BuiltinType(rawValue: local) {
|
||||
return .simple(PureXML.Schema.SimpleType(base: builtin))
|
||||
}
|
||||
if local == "anySimpleType" {
|
||||
return .simple(PureXML.Schema.SimpleType(base: .string, isAnySimpleType: true))
|
||||
}
|
||||
if local == "anyType" {
|
||||
return .complex(PureXML.Schema.ComplexType(
|
||||
attributeWildcard: PureXML.Schema.Wildcard(processContents: .skip),
|
||||
content: .mixed(PureXML.Schema.Particle(
|
||||
minOccurs: 0,
|
||||
maxOccurs: nil,
|
||||
term: .wildcard(PureXML.Schema.Wildcard(processContents: .skip)),
|
||||
)),
|
||||
))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// The instance resolver over this validator's type table.
|
||||
func resolveReference(_ type: PureXML.Schema.ElementType) -> PureXML.Schema.TypeResolution {
|
||||
Self.resolveReference(type, in: types)
|
||||
|
||||
@ -105,7 +105,8 @@ public extension PureXML.Schema.ComplexValidator {
|
||||
case .skip:
|
||||
return nil
|
||||
case .lax, .strict:
|
||||
return types["element:\(name.localName)"]
|
||||
return types[PureXML.Schema.XSDParser.elementDeclarationKey(name)]
|
||||
?? types["element:\(name.localName)"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,17 +5,14 @@ extension PureXML.Schema.ComplexValidator {
|
||||
func overriddenType(
|
||||
_ declared: PureXML.Schema.ElementType,
|
||||
for child: PureXML.Model.Element,
|
||||
namespaceBindings: [String: String] = [:],
|
||||
at path: XSDPath,
|
||||
into errors: inout [XSDFailure],
|
||||
) -> PureXML.Schema.ElementType? {
|
||||
guard let overriding = Self.xsiTypeName(child) else { return declared }
|
||||
if let resolved = types[overriding] { return resolved }
|
||||
// An xsi:type may name a built-in datatype (xsd:int, xsd:boolean) rather
|
||||
// than a schema-declared one. The substitution is valid only when the
|
||||
// built-in is validly derived from the declared type (Schema-Validity
|
||||
// Assessment, xsi:type): the ur-type admits any type, and a built-in
|
||||
// declared type admits the built-ins derived from it. An invalid
|
||||
// built-in substitution is a genuine error, not a silent acceptance.
|
||||
guard Self.xsiTypeAttributeValue(child) != nil else { return declared }
|
||||
let reference = Self.xsiTypeReference(child, namespaceBindings: namespaceBindings) ?? Self.xsiTypeName(child) ?? ""
|
||||
if let resolved = Self.resolveNamedType(reference, in: types) { return resolved }
|
||||
let overriding = Self.xsiTypeName(child) ?? reference
|
||||
if let builtin = PureXML.Schema.BuiltinType(rawValue: overriding) {
|
||||
if xsiBuiltinSubstitutes(builtin, for: declared) {
|
||||
return .simple(PureXML.Schema.SimpleType(base: builtin))
|
||||
@ -35,7 +32,7 @@ extension PureXML.Schema.ComplexValidator {
|
||||
private func xsiBuiltinSubstitutes(_ xsi: PureXML.Schema.BuiltinType, for declared: PureXML.Schema.ElementType) -> Bool {
|
||||
switch declared {
|
||||
case let .typeReference(name):
|
||||
if name == "anyType" || name == "anySimpleType" { return true }
|
||||
if name == "anyType" || name == "anySimpleType" || name.hasSuffix("}anyType") || name.hasSuffix("}anySimpleType") { return true }
|
||||
switch resolveReference(declared) {
|
||||
case let .resolved(resolved): return xsiBuiltinSubstitutes(xsi, for: resolved)
|
||||
case .unknown, .circular: return false
|
||||
|
||||
@ -19,6 +19,21 @@ extension PureXML.Schema {
|
||||
var epsilon: [Int] = []
|
||||
var label: TermLabel?
|
||||
var target: Int?
|
||||
/// The declared type and value constraint of the element particle this
|
||||
/// state's `.name` transition stands for, carried so that whoever the
|
||||
/// automaton matches a child to can be assessed against that exact
|
||||
/// particle (its type, its `fixed`/`default`), not a by-name lookup that
|
||||
/// cannot tell two same-named particles apart.
|
||||
var elementType: ElementType?
|
||||
var valueConstraint: ValueConstraint?
|
||||
}
|
||||
|
||||
/// The content-model particle a child was matched to, for per-child
|
||||
/// assessment: a named element (with the particle's declared type and value
|
||||
/// constraint) or a wildcard (carrying its `processContents`).
|
||||
enum MatchedParticle: Sendable {
|
||||
case element(type: ElementType?, valueConstraint: ValueConstraint?)
|
||||
case wildcard(Wildcard)
|
||||
}
|
||||
|
||||
/// A Thompson NFA over element names compiled from a ``Particle``. `all` groups
|
||||
@ -74,6 +89,44 @@ extension PureXML.Schema {
|
||||
return labels
|
||||
}
|
||||
|
||||
/// The particle each child matched, in order, for per-child assessment.
|
||||
/// A unique-particle-attribution-valid content model is deterministic, so
|
||||
/// at each position at most one particle accepts the child; the entry is
|
||||
/// nil from the first child the model rejects onward (a structure error,
|
||||
/// reported separately), since the match path is then undefined.
|
||||
///
|
||||
/// When more than one active state accepts the child (only possible if the
|
||||
/// compile-time UPA check missed an ambiguity), selection is still made
|
||||
/// reproducible rather than left to `Set` iteration order: a named particle
|
||||
/// is preferred over a wildcard, then the lowest state index wins.
|
||||
func matchedParticles(_ names: [PureXML.Model.QualifiedName]) -> [MatchedParticle?] {
|
||||
var current = startStates()
|
||||
var result: [MatchedParticle?] = []
|
||||
for name in names {
|
||||
var next: Set<Int> = []
|
||||
var namedMatch: Int?
|
||||
var anyMatch: Int?
|
||||
for state in current {
|
||||
guard let label = states[state].label, let target = states[state].target, label.matches(name) else { continue }
|
||||
next.insert(target)
|
||||
if anyMatch.map({ state < $0 }) ?? true { anyMatch = state }
|
||||
if case .name = label, namedMatch.map({ state < $0 }) ?? true { namedMatch = state }
|
||||
}
|
||||
guard let chosen = namedMatch ?? anyMatch, !next.isEmpty else {
|
||||
result.append(contentsOf: Array(repeating: nil, count: names.count - result.count))
|
||||
return result
|
||||
}
|
||||
let matched = states[chosen]
|
||||
if case let .wildcard(wildcard) = matched.label {
|
||||
result.append(.wildcard(wildcard))
|
||||
} else {
|
||||
result.append(.element(type: matched.elementType, valueConstraint: matched.valueConstraint))
|
||||
}
|
||||
current = closure(next)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Advances `current` by consuming one element `name`, or nil when `name`
|
||||
/// is not admissible there.
|
||||
func step(_ current: Set<Int>, over name: PureXML.Model.QualifiedName) -> Set<Int>? {
|
||||
@ -194,12 +247,15 @@ extension PureXML.Schema {
|
||||
|
||||
private mutating func term(_ term: PureXML.Schema.Term) -> (start: Int, accept: Int) {
|
||||
switch term {
|
||||
case let .element(name, _, _):
|
||||
labeled(.name(name))
|
||||
case let .element(name, type, _, constraint):
|
||||
let fragment = labeled(.name(name))
|
||||
states[fragment.start].elementType = type
|
||||
states[fragment.start].valueConstraint = constraint
|
||||
return fragment
|
||||
case let .wildcard(wildcard):
|
||||
labeled(.wildcard(wildcard))
|
||||
return labeled(.wildcard(wildcard))
|
||||
case let .group(group):
|
||||
self.group(group)
|
||||
return self.group(group)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,13 +20,35 @@ private final class XPathQueryCache {
|
||||
private struct FieldValue: Equatable {
|
||||
let string: String
|
||||
let type: PureXML.Schema.SimpleType?
|
||||
let namespaceBindings: [String: String]
|
||||
|
||||
init(string: String, type: PureXML.Schema.SimpleType?, namespaceBindings: [String: String] = [:]) {
|
||||
self.string = string
|
||||
self.type = type
|
||||
self.namespaceBindings = namespaceBindings
|
||||
}
|
||||
|
||||
static func == (lhs: FieldValue, rhs: FieldValue) -> Bool {
|
||||
if let lhsType = lhs.type, let rhsType = rhs.type, lhsType.base == rhsType.base {
|
||||
return lhsType.valueMatches(lhs.string, literal: rhs.string)
|
||||
if let left = lhs.qnameValue, let right = rhs.qnameValue, left.uri == right.uri, left.local == right.local {
|
||||
return true
|
||||
}
|
||||
if let lhsType = lhs.type, let rhsType = rhs.type {
|
||||
if lhsType.base == rhsType.base {
|
||||
return lhsType.valueMatches(lhs.string, literal: rhs.string)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return lhs.string == rhs.string
|
||||
}
|
||||
|
||||
private var qnameValue: (uri: String?, local: String)? {
|
||||
let trimmed = string.trimmingXMLWhitespace()
|
||||
guard trimmed.contains(":") else { return nil }
|
||||
let parts = trimmed.split(separator: ":", maxSplits: 1).map(String.init)
|
||||
guard parts.count == 2 else { return nil }
|
||||
let uri = namespaceBindings[parts[0]] ?? (parts[0].isEmpty ? namespaceBindings[""] : nil)
|
||||
return (uri, parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
extension PureXML.Schema {
|
||||
@ -39,11 +61,22 @@ extension PureXML.Schema {
|
||||
struct IdentityValidator {
|
||||
/// The constraints declared for each element local name.
|
||||
let constraints: [String: [IdentityConstraint]]
|
||||
/// Simple types for identity-constraint fields keyed by
|
||||
/// ``XSDParser/identityFieldKey(selector:field:)``.
|
||||
let fieldTypes: [String: SimpleType]
|
||||
/// Global element and type declarations for runtime field typing.
|
||||
let types: [String: ElementType]
|
||||
/// Selector and field XPaths compiled once per run, not per element.
|
||||
private let cache = XPathQueryCache()
|
||||
|
||||
init(constraints: [String: [IdentityConstraint]]) {
|
||||
init(
|
||||
constraints: [String: [IdentityConstraint]],
|
||||
fieldTypes: [String: SimpleType] = [:],
|
||||
types: [String: ElementType] = [:],
|
||||
) {
|
||||
self.constraints = constraints
|
||||
self.fieldTypes = fieldTypes
|
||||
self.types = types
|
||||
}
|
||||
|
||||
func validate(
|
||||
@ -171,7 +204,7 @@ extension PureXML.Schema {
|
||||
var tuples: [[FieldValue?]] = []
|
||||
var seen: [[FieldValue?]] = []
|
||||
for target in select(constraint.selector, at: node) {
|
||||
let tuple = fieldTuple(constraint.fields, at: target)
|
||||
let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint)
|
||||
let targetPath = path + steps(from: node, to: target)
|
||||
if constraint.kind == .key, tuple.contains(where: { $0 == nil }) {
|
||||
issues.append(.init(reason: "key '\(constraint.name)': a field is missing", at: targetPath + fieldStep(constraint.fields, at: target)))
|
||||
@ -198,7 +231,7 @@ extension PureXML.Schema {
|
||||
guard case let .keyref(refer) = constraint.kind else { return }
|
||||
let keyTuples = scopes.reversed().compactMap { $0[refer] }.first ?? []
|
||||
for target in select(constraint.selector, at: node) {
|
||||
let tuple = fieldTuple(constraint.fields, at: target)
|
||||
let tuple = fieldTuple(constraint.fields, at: target, constraint: constraint)
|
||||
if tuple.contains(where: { $0 == nil }) { continue }
|
||||
if !keyTuples.contains(where: { $0 == tuple }) {
|
||||
let targetPath = path + steps(from: node, to: target)
|
||||
@ -211,25 +244,76 @@ extension PureXML.Schema {
|
||||
|
||||
private func select(_ xpath: String, at node: PureXML.Model.TreeNode) -> [PureXML.Model.TreeNode] {
|
||||
guard let query = cache.query(xpath) else { return [] }
|
||||
return query.nodes(over: node)
|
||||
return query.nodes(over: node, namespaces: namespaceBindings(at: node))
|
||||
}
|
||||
|
||||
private func fieldTuple(_ fields: [String], at node: PureXML.Model.TreeNode) -> [FieldValue?] {
|
||||
fields.map { field in
|
||||
guard let query = cache.query(field), let value = try? query.value(at: node) else {
|
||||
private func fieldTuple(_ fields: [String], at node: PureXML.Model.TreeNode, constraint: IdentityConstraint) -> [FieldValue?] {
|
||||
let namespaces = namespaceBindings(at: node)
|
||||
return fields.map { field in
|
||||
guard let query = cache.query(field),
|
||||
let value = try? query.value(at: node, namespaces: namespaces)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
if let nodes = value.nodes, nodes.isEmpty { return nil }
|
||||
return FieldValue(string: value.string, type: Self.effectiveType(of: value))
|
||||
let type = resolveFieldType(field, constraint: constraint, at: node)
|
||||
?? Self.effectiveType(of: value)
|
||||
return FieldValue(string: value.string, type: type, namespaceBindings: namespaces)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prefix bindings in scope at `node`, gathered from ancestor elements.
|
||||
private func namespaceBindings(at node: PureXML.Model.TreeNode) -> [String: String] {
|
||||
var chain: [PureXML.Model.TreeNode] = []
|
||||
var current: PureXML.Model.TreeNode? = node
|
||||
while let element = current, element.kind == .element {
|
||||
chain.append(element)
|
||||
current = element.parent
|
||||
}
|
||||
var bindings: [String: String] = [:]
|
||||
for element in chain.reversed() {
|
||||
for attribute in element.attributes {
|
||||
if attribute.name.prefix == "xmlns" {
|
||||
bindings[attribute.name.localName] = attribute.value
|
||||
} else if attribute.name.prefix == nil, attribute.name.localName == "xmlns" {
|
||||
bindings[""] = attribute.value
|
||||
}
|
||||
}
|
||||
}
|
||||
return bindings
|
||||
}
|
||||
|
||||
/// The simple type a field value compares in: the built-in named by the
|
||||
/// selected node's `xsi:type`, when present. Identity-constraint values
|
||||
/// compare in their value space, so two `xsi:type="xsd:decimal"` fields
|
||||
/// `3.0` and `3` are the same value and violate a `unique`/`key`. With no
|
||||
/// `xsi:type` the comparison falls back to the lexical form (the type
|
||||
/// declared in the schema is not threaded into identity validation).
|
||||
private func resolveFieldType(_ field: String, constraint: IdentityConstraint, at target: PureXML.Model.TreeNode) -> SimpleType? {
|
||||
if field == ".", let local = target.name?.localName {
|
||||
if let type = fieldTypes[PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field, targetLocal: local)] {
|
||||
return type
|
||||
}
|
||||
guard let name = target.name else { return nil }
|
||||
if let declaration = types[PureXML.Schema.XSDParser.elementDeclarationKey(name)] {
|
||||
return simpleType(of: declaration)
|
||||
}
|
||||
if let declaration = types[PureXML.Schema.XSDParser.elementKey(name.localName)] {
|
||||
return simpleType(of: declaration)
|
||||
}
|
||||
}
|
||||
return fieldTypes[PureXML.Schema.XSDParser.identityFieldKey(constraint: constraint, field: field)]
|
||||
}
|
||||
|
||||
private func simpleType(of declaration: ElementType) -> SimpleType? {
|
||||
switch declaration {
|
||||
case let .simple(simple): simple
|
||||
case let .complex(complex):
|
||||
if case let .simpleContent(simple) = complex.content { simple } else { nil }
|
||||
case .typeReference: nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func effectiveType(of value: PureXML.XPath.Value) -> SimpleType? {
|
||||
guard let nodes = value.nodes, case let .tree(treeNode)? = nodes.first,
|
||||
treeNode.kind == .element, let local = xsiTypeLocalName(of: treeNode),
|
||||
|
||||
@ -66,11 +66,11 @@ extension PureXML.Schema {
|
||||
return emptiable(base)
|
||||
}
|
||||
switch (restricted.term, base.term) {
|
||||
case let (.element(restrictedName, _, restrictedTypeName), .element(baseName, _, baseTypeName)):
|
||||
case let (.element(restrictedName, _, restrictedTypeName, _), .element(baseName, _, baseTypeName, _)):
|
||||
return rangeSubsumed(restricted, base)
|
||||
&& sameName(restrictedName, baseName)
|
||||
&& elementTypeRestrictionOK(restrictedTypeName, baseTypeName, types, derivation)
|
||||
case let (.element(name, _, _), .wildcard(wildcard)):
|
||||
case let (.element(name, _, _, _), .wildcard(wildcard)):
|
||||
return rangeSubsumed(restricted, base) && wildcard.admits(name)
|
||||
case let (.wildcard(restrictedWildcard), .wildcard(baseWildcard)):
|
||||
return rangeSubsumed(restricted, base) && narrows(restrictedWildcard, baseWildcard)
|
||||
@ -80,7 +80,7 @@ extension PureXML.Schema {
|
||||
// a base member inside `groupValid`, not against the base group's own
|
||||
// occurrence, so no outer `rangeSubsumed` here.
|
||||
return groupValid(Group(compositor: .sequence, particles: [restricted]), baseGroup, types, derivation)
|
||||
case let (.group, .element(baseName, _, baseTypeName)):
|
||||
case let (.group, .element(baseName, _, baseTypeName, _)):
|
||||
// The mirror of RecurseAsIfGroup, by the same cardinality argument as
|
||||
// group/wildcard: a base element accepts only its own name, its own
|
||||
// occurrence count of times. The derived group is a valid restriction
|
||||
@ -277,7 +277,7 @@ extension PureXML.Schema {
|
||||
/// element by name, a nested wildcard by narrowing.
|
||||
private static func leavesAdmitted(_ particle: Particle, by wildcard: Wildcard) -> Bool {
|
||||
switch particle.term {
|
||||
case let .element(name, _, _): wildcard.admits(name)
|
||||
case let .element(name, _, _, _): wildcard.admits(name)
|
||||
case let .wildcard(inner): narrows(inner, wildcard)
|
||||
case let .group(group): group.particles.allSatisfy { leavesAdmitted($0, by: wildcard) }
|
||||
}
|
||||
@ -295,7 +295,7 @@ extension PureXML.Schema {
|
||||
_ derivation: [String: TypeDerivation],
|
||||
) -> Bool {
|
||||
switch particle.term {
|
||||
case let .element(name, _, typeName):
|
||||
case let .element(name, _, typeName, _):
|
||||
sameName(name, baseName) && elementTypeRestrictionOK(typeName, baseTypeName, types, derivation)
|
||||
case .wildcard:
|
||||
false
|
||||
|
||||
@ -10,7 +10,7 @@ extension PureXML.Schema {
|
||||
|
||||
private static func applyValue(_ name: String, _ value: String, into facets: inout Facets) {
|
||||
switch name {
|
||||
case "pattern": facets.patterns.append(value)
|
||||
case "pattern": facets.patternGroups.append([value])
|
||||
case "minInclusive": facets.minInclusive = value
|
||||
case "maxInclusive": facets.maxInclusive = value
|
||||
case "minExclusive": facets.minExclusive = value
|
||||
|
||||
@ -45,6 +45,7 @@ public extension PureXML.Schema {
|
||||
private let elements: [String: ElementType]
|
||||
private let types: [String: ElementType]
|
||||
private let constraints: [String: [IdentityConstraint]]
|
||||
private let identityFieldTypes: [String: SimpleType]
|
||||
private let nillableElements: Set<String>
|
||||
private let elementConstraints: [String: ValueConstraint]
|
||||
private let abstractTypes: Set<String>
|
||||
@ -53,6 +54,7 @@ public extension PureXML.Schema {
|
||||
private let elementBlock: [String: Set<DerivationMethod>]
|
||||
private let typeDerivation: [String: TypeDerivation]
|
||||
private let targetNamespace: String?
|
||||
private let globalAttributes: [String: AttributeUse]
|
||||
|
||||
/// Combines already-compiled declaration tables, for merging the schemas
|
||||
/// an instance references through `xsi:schemaLocation`. No consistency
|
||||
@ -61,6 +63,7 @@ public extension PureXML.Schema {
|
||||
elements: [String: ElementType],
|
||||
types: [String: ElementType],
|
||||
constraints: [String: [IdentityConstraint]],
|
||||
identityFieldTypes: [String: SimpleType] = [:],
|
||||
nillableElements: Set<String>,
|
||||
elementConstraints: [String: ValueConstraint],
|
||||
abstractTypes: Set<String>,
|
||||
@ -69,10 +72,12 @@ public extension PureXML.Schema {
|
||||
elementBlock: [String: Set<DerivationMethod>],
|
||||
typeDerivation: [String: TypeDerivation],
|
||||
targetNamespace: String?,
|
||||
globalAttributes: [String: AttributeUse],
|
||||
) {
|
||||
self.elements = elements
|
||||
self.types = types
|
||||
self.constraints = constraints
|
||||
self.identityFieldTypes = identityFieldTypes
|
||||
self.nillableElements = nillableElements
|
||||
self.elementConstraints = elementConstraints
|
||||
self.abstractTypes = abstractTypes
|
||||
@ -81,12 +86,17 @@ public extension PureXML.Schema {
|
||||
self.elementBlock = elementBlock
|
||||
self.typeDerivation = typeDerivation
|
||||
self.targetNamespace = targetNamespace
|
||||
self.globalAttributes = globalAttributes
|
||||
}
|
||||
|
||||
/// This schema with `other`'s global declarations merged in; this schema's
|
||||
/// own declarations win on any key conflict. Used to fold in the schemas an
|
||||
/// instance points at, so a strict/lax wildcard can resolve elements
|
||||
/// declared in another document.
|
||||
public func merged(with other: Document) -> Document {
|
||||
merging(other)
|
||||
}
|
||||
|
||||
private func merging(_ other: Document) -> Document {
|
||||
func mineFirst<V>(_ mine: [String: V], _ theirs: [String: V]) -> [String: V] {
|
||||
mine.merging(theirs) { current, _ in current }
|
||||
@ -95,6 +105,7 @@ public extension PureXML.Schema {
|
||||
elements: mineFirst(elements, other.elements),
|
||||
types: mineFirst(types, other.types),
|
||||
constraints: mineFirst(constraints, other.constraints),
|
||||
identityFieldTypes: mineFirst(identityFieldTypes, other.identityFieldTypes),
|
||||
nillableElements: nillableElements.union(other.nillableElements),
|
||||
elementConstraints: mineFirst(elementConstraints, other.elementConstraints),
|
||||
abstractTypes: abstractTypes.union(other.abstractTypes),
|
||||
@ -103,6 +114,7 @@ public extension PureXML.Schema {
|
||||
elementBlock: mineFirst(elementBlock, other.elementBlock),
|
||||
typeDerivation: mineFirst(typeDerivation, other.typeDerivation),
|
||||
targetNamespace: targetNamespace,
|
||||
globalAttributes: mineFirst(globalAttributes, other.globalAttributes),
|
||||
)
|
||||
}
|
||||
|
||||
@ -116,6 +128,7 @@ public extension PureXML.Schema {
|
||||
elements = compiled.elements
|
||||
types = compiled.types
|
||||
constraints = compiled.constraints
|
||||
identityFieldTypes = compiled.identityFieldTypes
|
||||
nillableElements = compiled.nillableElements
|
||||
elementConstraints = compiled.elementConstraints
|
||||
abstractTypes = compiled.abstractTypes
|
||||
@ -124,6 +137,7 @@ public extension PureXML.Schema {
|
||||
elementBlock = compiled.elementBlock
|
||||
typeDerivation = compiled.typeDerivation
|
||||
targetNamespace = compiled.targetNamespace
|
||||
globalAttributes = compiled.globalAttributes
|
||||
// Schema consistency through the validation framework: every named
|
||||
// type is checked by the composable rules (final respected, Particle
|
||||
// Valid (Restriction)), and ALL findings are reported together, joined
|
||||
@ -232,6 +246,7 @@ public extension PureXML.Schema {
|
||||
) throws -> [PureXML.Validation.ValidationError] {
|
||||
let validator = PureXML.Schema.ComplexValidator(
|
||||
types: types,
|
||||
globalAttributes: globalAttributes,
|
||||
nillableElements: nillableElements,
|
||||
elementConstraints: elementConstraints,
|
||||
abstractTypes: abstractTypes,
|
||||
@ -262,8 +277,12 @@ public extension PureXML.Schema {
|
||||
guard case let .document(children) = node, let root = children.compactMap(\.element).first else {
|
||||
return []
|
||||
}
|
||||
return PureXML.Schema.IdentityValidator(constraints: constraints)
|
||||
.validate(PureXML.Model.TreeNode(.element(root)), at: [.element(root.name.description)])
|
||||
return PureXML.Schema.IdentityValidator(
|
||||
constraints: constraints,
|
||||
fieldTypes: identityFieldTypes,
|
||||
types: types,
|
||||
)
|
||||
.validate(PureXML.Model.TreeNode(.element(root)), at: [.element(root.name.description)])
|
||||
}
|
||||
|
||||
/// Validates an already-parsed node tree, so a caller can validate the same
|
||||
@ -272,13 +291,15 @@ public extension PureXML.Schema {
|
||||
guard case let .document(children) = node, let root = children.compactMap(\.element).first else {
|
||||
return [.init(reason: "the document has no root element", at: [])]
|
||||
}
|
||||
guard let declaration = elements[root.name.localName] else {
|
||||
guard let declaration = globalElementDeclaration(for: root.name) ?? rootTypeOverride(for: root) else {
|
||||
return [.init(reason: "no element declaration for '\(root.name.localName)'", at: [.element(root.name.description)])]
|
||||
}
|
||||
// A schema with a target namespace declares its global elements in that
|
||||
// namespace; the root must be in it.
|
||||
if let target = targetNamespace, !target.isEmpty, root.name.namespaceURI != target {
|
||||
return [.init(reason: "root element '\(root.name.localName)' is not in the schema target namespace '\(target)'", at: [.element(root.name.description)])]
|
||||
// namespace; the root must be in it unless a merged schema declares it
|
||||
// in another namespace, or xsi:type names the validating type.
|
||||
if rootTypeOverride(for: root) == nil, rootNamespaceMismatch(root.name) {
|
||||
let reason = "root element '\(root.name.localName)' is not in the schema target namespace '\(targetNamespace ?? "")'"
|
||||
return [.init(reason: reason, at: [.element(root.name.description)])]
|
||||
}
|
||||
// An abstract element may not appear in an instance directly; only a
|
||||
// concrete substitution-group member may stand in its place.
|
||||
@ -295,10 +316,39 @@ public extension PureXML.Schema {
|
||||
typeBlock: typeBlock,
|
||||
elementBlock: elementBlock,
|
||||
typeDerivation: typeDerivation,
|
||||
globalAttributes: globalAttributes,
|
||||
identityFieldTypes: identityFieldTypes,
|
||||
)
|
||||
return PureXML.Validation.XSD.validator().errors(for: .element(root), in: context)
|
||||
}
|
||||
|
||||
/// The global element declaration for a qualified instance name.
|
||||
private func globalElementDeclaration(for name: PureXML.Model.QualifiedName) -> ElementType? {
|
||||
if let declaration = types[PureXML.Schema.XSDParser.elementDeclarationKey(name)] {
|
||||
return declaration
|
||||
}
|
||||
if name.namespaceURI == nil || name.namespaceURI?.isEmpty == true {
|
||||
return elements[name.localName]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func rootNamespaceMismatch(_ name: PureXML.Model.QualifiedName) -> Bool {
|
||||
guard let target = targetNamespace, !target.isEmpty, name.namespaceURI != target else { return false }
|
||||
return types[PureXML.Schema.XSDParser.elementDeclarationKey(name)] == nil
|
||||
}
|
||||
|
||||
/// When the root has no global element declaration, an `xsi:type` attribute
|
||||
/// can still name the type to validate against (Sun target-NS tests).
|
||||
private func rootTypeOverride(for root: PureXML.Model.Element) -> ElementType? {
|
||||
guard PureXML.Schema.ComplexValidator.xsiTypeAttributeValue(root) != nil else { return nil }
|
||||
let bindings = PureXML.Schema.ComplexValidator.namespaceBindings(for: root)
|
||||
guard let reference = PureXML.Schema.ComplexValidator.xsiTypeReference(root, namespaceBindings: bindings),
|
||||
let type = PureXML.Schema.ComplexValidator.resolveNamedType(reference, in: types)
|
||||
else { return nil }
|
||||
return type
|
||||
}
|
||||
|
||||
/// What the schema allows at the element a coding `path` addresses in
|
||||
/// `tree`: the next child elements (the content-model follow-set), whether
|
||||
/// the content may end there, and the declared attributes with their
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
public extension PureXML.Schema {
|
||||
/// The constraining facets of a simple type. Every facet is optional; an unset
|
||||
/// facet imposes no constraint. `patterns` are combined with AND.
|
||||
/// facet imposes no constraint. Multiple patterns on one restriction step are
|
||||
/// OR'd (libxml2/xmlschemas.c); each group is AND'd across derivation steps.
|
||||
struct Facets: Sendable {
|
||||
public var length: Int?
|
||||
public var minLength: Int?
|
||||
public var maxLength: Int?
|
||||
public var patterns: [String]
|
||||
public var patternGroups: [[String]]
|
||||
public var enumeration: [String]?
|
||||
public var whiteSpace: WhiteSpace?
|
||||
public var minInclusive: String?
|
||||
@ -19,7 +20,7 @@ public extension PureXML.Schema {
|
||||
length: Int? = nil,
|
||||
minLength: Int? = nil,
|
||||
maxLength: Int? = nil,
|
||||
patterns: [String] = [],
|
||||
patternGroups: [[String]] = [],
|
||||
enumeration: [String]? = nil,
|
||||
whiteSpace: WhiteSpace? = nil,
|
||||
minInclusive: String? = nil,
|
||||
@ -32,7 +33,7 @@ public extension PureXML.Schema {
|
||||
self.length = length
|
||||
self.minLength = minLength
|
||||
self.maxLength = maxLength
|
||||
self.patterns = patterns
|
||||
self.patternGroups = patternGroups
|
||||
self.enumeration = enumeration
|
||||
self.whiteSpace = whiteSpace
|
||||
self.minInclusive = minInclusive
|
||||
@ -48,7 +49,7 @@ public extension PureXML.Schema {
|
||||
/// substitute for a declared type (a faceted restriction may not be the
|
||||
/// target of a built-in substitution).
|
||||
public var isUnconstrained: Bool {
|
||||
length == nil && minLength == nil && maxLength == nil && patterns.isEmpty
|
||||
length == nil && minLength == nil && maxLength == nil && patternGroups.isEmpty
|
||||
&& enumeration == nil && whiteSpace == nil
|
||||
&& minInclusive == nil && maxInclusive == nil
|
||||
&& minExclusive == nil && maxExclusive == nil
|
||||
@ -273,9 +274,14 @@ public extension PureXML.Schema {
|
||||
}
|
||||
|
||||
private func patternError(_ value: String) -> String? {
|
||||
for pattern in facets.patterns {
|
||||
guard let regex = try? PureXML.Regex.Pattern(pattern), regex.matches(value) else {
|
||||
return "'\(value)' does not match pattern '\(pattern)'"
|
||||
for group in facets.patternGroups {
|
||||
let matched = group.contains { pattern in
|
||||
guard let regex = try? PureXML.Regex.Pattern(pattern) else { return false }
|
||||
return regex.matches(value)
|
||||
}
|
||||
guard matched else {
|
||||
let label = group.joined(separator: "|")
|
||||
return "'\(value)' does not match pattern '\(label)'"
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -101,7 +101,9 @@ public extension PureXML.Validation {
|
||||
return nil
|
||||
}
|
||||
sawRoot = true
|
||||
guard let declaration = rootElements[name.localName] else {
|
||||
guard let declaration = rootElements[name.localName]
|
||||
?? validator.types[PureXML.Schema.XSDParser.elementDeclarationKey(name)]
|
||||
else {
|
||||
collected.append(ValidationError(reason: "no element declaration for '\(name.localName)'", at: path))
|
||||
return nil
|
||||
}
|
||||
@ -109,13 +111,18 @@ public extension PureXML.Validation {
|
||||
collected.append(ValidationError(reason: "abstract element '\(name.localName)' must not appear in an instance", at: path))
|
||||
return nil
|
||||
}
|
||||
if let target = targetNamespace, !target.isEmpty, name.namespaceURI != target {
|
||||
collected.append(ValidationError(reason: "root element '\(name.localName)' is not in the schema target namespace '\(target)'", at: path))
|
||||
if rootNamespaceMismatch(name) {
|
||||
collected.append(ValidationError(reason: "root element '\(name.localName)' is not in the schema target namespace '\(targetNamespace ?? "")'", at: path))
|
||||
return nil
|
||||
}
|
||||
return declaration
|
||||
}
|
||||
|
||||
private func rootNamespaceMismatch(_ name: PureXML.Model.QualifiedName) -> Bool {
|
||||
guard let target = targetNamespace, !target.isEmpty, name.namespaceURI != target else { return false }
|
||||
return validator.types[PureXML.Schema.XSDParser.elementDeclarationKey(name)] == nil
|
||||
}
|
||||
|
||||
/// The coding path for an opening element: the root unindexed, a child with
|
||||
/// a one-based occurrence index (the `xmlGetNodePath` convention).
|
||||
private mutating func openPath(_ name: PureXML.Model.QualifiedName) -> [PathKey] {
|
||||
|
||||
@ -11,8 +11,16 @@ extension PureXML.Schema.XSDParser {
|
||||
case "attributeGroup":
|
||||
guard let ref = PureXML.Schema.XSDNode.attribute(child, "ref") else { break }
|
||||
let name = PureXML.Schema.XSDNode.stripPrefix(ref)
|
||||
guard !visited.contains(name), let group = context.attributeGroups[name] else { break }
|
||||
uses += attributeUses(under: group, context, visited: visited.union([name]))
|
||||
if visited.contains(name) {
|
||||
if context.redefinedAttributeGroups.contains(name), let base = context.baseAttributeGroups[name] {
|
||||
let scoped = context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(base))
|
||||
uses += attributeUses(under: base, scoped, visited: visited)
|
||||
}
|
||||
break
|
||||
}
|
||||
guard let group = context.attributeGroups[name] else { break }
|
||||
let scoped = context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(group))
|
||||
uses += attributeUses(under: group, scoped, visited: visited.union([name]))
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -44,11 +52,28 @@ extension PureXML.Schema.XSDParser {
|
||||
// global node, but `use`/`default`/`fixed` from this reference site.
|
||||
if let ref = PureXML.Schema.XSDNode.attribute(node, "ref") {
|
||||
let refName = PureXML.Schema.XSDNode.stripPrefix(ref)
|
||||
guard let declaration = context.globalAttributes[refName],
|
||||
var use = attributeUse(declaration, context) else { return nil }
|
||||
let bindings = namespaceBindingsInScope(of: node, defaultBindings: context.namespaceBindings)
|
||||
let namespace = PureXML.Schema.XSDNode.referenceNamespace(ref, bindings)
|
||||
use.name = PureXML.Model.QualifiedName(localName: refName, namespaceURI: namespace)
|
||||
if namespace == "http://www.w3.org/XML/1998/namespace", refName == "base" {
|
||||
return PureXML.Schema.AttributeUse(
|
||||
name: PureXML.Model.QualifiedName(localName: refName, namespaceURI: namespace),
|
||||
type: PureXML.Schema.SimpleType(base: .anyURI),
|
||||
required: PureXML.Schema.XSDNode.attribute(node, "use") == "required",
|
||||
valueConstraint: valueConstraint(of: node),
|
||||
)
|
||||
}
|
||||
guard let declaration = context.globalAttributes[refName],
|
||||
var use = attributeUse(declaration, context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(declaration)))
|
||||
else { return nil }
|
||||
let declContext = context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(declaration))
|
||||
if let target = declContext.targetNamespace, !target.isEmpty {
|
||||
use.name = PureXML.Model.QualifiedName(localName: refName, namespaceURI: target)
|
||||
use.chameleonUnprefixed = isChameleonSchema(PureXML.Schema.XSDNode.schemaOwner(declaration))
|
||||
} else {
|
||||
let bindings = namespaceBindingsInScope(of: node, defaultBindings: context.namespaceBindings)
|
||||
let namespace = PureXML.Schema.XSDNode.referenceNamespace(ref, bindings)
|
||||
use.name = PureXML.Model.QualifiedName(localName: refName, namespaceURI: namespace)
|
||||
}
|
||||
if PureXML.Schema.XSDNode.attribute(node, "use") == "required" { use.required = true }
|
||||
if let constraint = valueConstraint(of: node) { use.valueConstraint = constraint }
|
||||
return use
|
||||
@ -62,13 +87,53 @@ extension PureXML.Schema.XSDParser {
|
||||
} else {
|
||||
PureXML.Schema.SimpleType(base: .string, isAnySimpleType: true)
|
||||
}
|
||||
let attributeFormQualified = attributeFormQualified(for: node, default: context.attributeFormQualified)
|
||||
let qualified = PureXML.Schema.XSDNode.attribute(node, "form") == "qualified"
|
||||
|| (PureXML.Schema.XSDNode.attribute(node, "form") == nil && context.attributeFormQualified)
|
||||
|| (PureXML.Schema.XSDNode.attribute(node, "form") == nil && attributeFormQualified)
|
||||
let targetNamespace = owningTargetNamespace(of: node, fallback: context.targetNamespace, context)
|
||||
let owner = PureXML.Schema.XSDNode.schemaOwner(node)
|
||||
return PureXML.Schema.AttributeUse(
|
||||
name: PureXML.Model.QualifiedName(localName: name, namespaceURI: qualified ? context.targetNamespace : nil),
|
||||
name: PureXML.Model.QualifiedName(localName: name, namespaceURI: qualified ? targetNamespace : nil),
|
||||
type: type,
|
||||
required: required,
|
||||
valueConstraint: valueConstraint(of: node),
|
||||
chameleonUnprefixed: qualified && isChameleonSchema(owner),
|
||||
)
|
||||
}
|
||||
|
||||
/// A schema document with no explicit `targetNamespace` (a chameleon include).
|
||||
private static func isChameleonSchema(_ schema: XSDTree) -> Bool {
|
||||
guard PureXML.Schema.XSDNode.localName(schema) == "schema" else { return false }
|
||||
guard let target = PureXML.Schema.XSDNode.attribute(schema, "targetNamespace") else { return true }
|
||||
return target.isEmpty
|
||||
}
|
||||
|
||||
/// Whether attributes declared under `node` default to qualified form in their
|
||||
/// owning schema document, not the including schema's `attributeFormDefault`.
|
||||
private static func attributeFormQualified(for node: XSDTree, default defaultQualified: Bool) -> Bool {
|
||||
var current: XSDTree? = node
|
||||
while let currentNode = current {
|
||||
if PureXML.Schema.XSDNode.localName(currentNode) == "schema" {
|
||||
return PureXML.Schema.XSDNode.attribute(currentNode, "attributeFormDefault") == "qualified"
|
||||
}
|
||||
current = currentNode.parent
|
||||
}
|
||||
return defaultQualified
|
||||
}
|
||||
|
||||
/// The `targetNamespace` of the schema document that declares `node`, including
|
||||
/// chameleon includes that inherit the including schema's namespace.
|
||||
private static func owningTargetNamespace(of node: XSDTree, fallback: String?, _ context: PureXML.Schema.XSDContext) -> String? {
|
||||
var current: XSDTree? = node
|
||||
while let currentNode = current {
|
||||
if PureXML.Schema.XSDNode.localName(currentNode) == "schema" {
|
||||
if let target = PureXML.Schema.XSDNode.attribute(currentNode, "targetNamespace"), !target.isEmpty {
|
||||
return target
|
||||
}
|
||||
return context.chameleonTargetNamespaces[ObjectIdentifier(currentNode)] ?? fallback
|
||||
}
|
||||
current = currentNode.parent
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,7 +111,7 @@ public extension PureXML.Schema {
|
||||
|
||||
private static func collect(_ term: Term, into result: inout [String: ElementType]) {
|
||||
switch term {
|
||||
case let .element(name, type, _):
|
||||
case let .element(name, type, _, _):
|
||||
if let type { result[name.description] = type }
|
||||
case let .group(group):
|
||||
for member in group.particles {
|
||||
|
||||
@ -5,6 +5,36 @@ private typealias XSDGroup = PureXML.Schema.Group
|
||||
private typealias XSDContentNode = PureXML.Schema.XSDNode
|
||||
|
||||
extension PureXML.Schema.XSDParser {
|
||||
/// Attribute uses contributed by a `simpleContent` extension or restriction,
|
||||
/// including those inherited from a complex base type.
|
||||
static func simpleContentAttributes(under derivationNode: XSDTree, _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.AttributeUse] {
|
||||
let ownAttributes = attributeUses(under: derivationNode, context)
|
||||
let derivationType = XSDContentNode.localName(derivationNode)
|
||||
if derivationType == "extension", let base = resolvedBase(derivationNode, context) {
|
||||
return base.attributes + ownAttributes
|
||||
}
|
||||
if derivationType == "restriction" {
|
||||
let allowedOwnAttributes = ownAttributes.filter { !isProhibited($0.name, under: derivationNode, context) }
|
||||
if let base = resolvedBase(derivationNode, context) {
|
||||
var attributes: [PureXML.Schema.AttributeUse] = []
|
||||
for baseAttr in base.attributes {
|
||||
if isProhibited(baseAttr.name, under: derivationNode, context) { continue }
|
||||
if let derived = allowedOwnAttributes.first(where: { $0.name == baseAttr.name }) {
|
||||
attributes.append(derived)
|
||||
} else {
|
||||
attributes.append(baseAttr)
|
||||
}
|
||||
}
|
||||
for ownAttr in allowedOwnAttributes where !base.attributes.contains(where: { $0.name == ownAttr.name }) {
|
||||
attributes.append(ownAttr)
|
||||
}
|
||||
return attributes
|
||||
}
|
||||
return allowedOwnAttributes
|
||||
}
|
||||
return ownAttributes
|
||||
}
|
||||
|
||||
/// Builds the effective complex type of a `complexContent` derivation. An
|
||||
/// extension composes the base type's attributes and content with its own
|
||||
/// (the base's content first, then the extension's, in sequence); a
|
||||
@ -23,7 +53,7 @@ extension PureXML.Schema.XSDParser {
|
||||
if derivationType == "extension", let base = resolvedBase(derivationNode, context) {
|
||||
return XSDComplexType(
|
||||
attributes: base.attributes + ownAttributes,
|
||||
attributeWildcard: ownWildcard ?? base.attributeWildcard,
|
||||
attributeWildcard: PureXML.Schema.Wildcard.union(ownWildcard, base.attributeWildcard),
|
||||
content: extendedContent(base: base.content, own: ownParticle, mixed: mixed),
|
||||
)
|
||||
} else if derivationType == "restriction" {
|
||||
@ -75,8 +105,7 @@ extension PureXML.Schema.XSDParser {
|
||||
case "attributeGroup":
|
||||
guard let ref = XSDContentNode.attribute(child, "ref") else { break }
|
||||
let refName = XSDContentNode.stripPrefix(ref)
|
||||
guard !visited.contains(refName), let group = context.attributeGroups[refName] else { break }
|
||||
if isProhibited(name, under: group, context, visited: visited.union([refName])) {
|
||||
if attributeGroupProhibits(name, refName: refName, under: node, context, visited: visited) {
|
||||
return true
|
||||
}
|
||||
default:
|
||||
@ -86,6 +115,22 @@ extension PureXML.Schema.XSDParser {
|
||||
return false
|
||||
}
|
||||
|
||||
private static func attributeGroupProhibits(
|
||||
_ name: PureXML.Model.QualifiedName,
|
||||
refName: String,
|
||||
under _: XSDTree,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
visited: Set<String>,
|
||||
) -> Bool {
|
||||
if visited.contains(refName) {
|
||||
guard context.redefinedAttributeGroups.contains(refName),
|
||||
let base = context.baseAttributeGroups[refName] else { return false }
|
||||
return isProhibited(name, under: base, context, visited: visited)
|
||||
}
|
||||
guard let group = context.attributeGroups[refName] else { return false }
|
||||
return isProhibited(name, under: group, context, visited: visited.union([refName]))
|
||||
}
|
||||
|
||||
private static func attributeName(_ node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Model.QualifiedName? {
|
||||
if let ref = XSDContentNode.attribute(node, "ref") {
|
||||
let refName = XSDContentNode.stripPrefix(ref)
|
||||
@ -102,12 +147,29 @@ extension PureXML.Schema.XSDParser {
|
||||
/// node, or nil when the base is unknown (a built-in or undeclared type) or
|
||||
/// would form a derivation cycle.
|
||||
private static func resolvedBase(_ derivationNode: XSDTree, _ context: PureXML.Schema.XSDContext) -> XSDComplexType? {
|
||||
guard let baseName = XSDContentNode.attribute(derivationNode, "base").map(XSDContentNode.stripPrefix),
|
||||
!context.visitingTypes.contains(baseName), let baseNode = context.complexTypeNodes[baseName]
|
||||
else {
|
||||
guard let baseName = XSDContentNode.attribute(derivationNode, "base").map(XSDContentNode.stripPrefix) else {
|
||||
return nil
|
||||
}
|
||||
return complexType(baseNode, context.visitingType(baseName))
|
||||
guard let baseNode = baseTypeNode(named: baseName, context) else { return nil }
|
||||
let scoped = context.scoped(for: XSDContentNode.schemaOwner(baseNode))
|
||||
return complexType(baseNode, scoped.visitingType(baseName))
|
||||
}
|
||||
|
||||
/// The schema node a complex type derives from: the redefined base when this
|
||||
/// type is the redefining occurrence of `baseName`, otherwise the ordinary
|
||||
/// declaration. Returns nil when resolving would form a derivation cycle.
|
||||
private static func baseTypeNode(named baseName: String, _ context: PureXML.Schema.XSDContext) -> XSDTree? {
|
||||
if let redefined = redefinedBaseNode(named: baseName, context) { return redefined }
|
||||
guard !context.visitingTypes.contains(baseName) else { return nil }
|
||||
return context.complexTypeNodes[baseName]
|
||||
}
|
||||
|
||||
private static func redefinedBaseNode(named baseName: String, _ context: PureXML.Schema.XSDContext) -> XSDTree? {
|
||||
guard context.redefinedComplexTypes.contains(baseName),
|
||||
context.visitingTypes.contains(baseName),
|
||||
let baseNode = context.baseComplexTypeNodes[baseName]
|
||||
else { return nil }
|
||||
return baseNode
|
||||
}
|
||||
|
||||
/// A particle wrapped as element-only or mixed content, or empty content when
|
||||
|
||||
@ -25,6 +25,16 @@ extension PureXML.Schema {
|
||||
children(node, named: name).first
|
||||
}
|
||||
|
||||
/// The enclosing `schema` element for a component node or container.
|
||||
static func schemaOwner(_ node: XSDTree) -> XSDTree {
|
||||
var current: XSDTree? = node
|
||||
while let element = current {
|
||||
if localName(element) == "schema" { return element }
|
||||
current = element.parent
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
static func stripPrefix(_ qualified: String) -> String {
|
||||
qualified.split(separator: ":").last.map(String.init) ?? qualified
|
||||
}
|
||||
@ -54,11 +64,24 @@ extension PureXML.Schema {
|
||||
/// default-namespace binding (nil when none is declared).
|
||||
static func referenceNamespace(_ qualified: String, _ bindings: [String: String]) -> String? {
|
||||
if let prefix = prefix(qualified) {
|
||||
if prefix == "xml" { return "http://www.w3.org/XML/1998/namespace" }
|
||||
return bindings[prefix]
|
||||
}
|
||||
return bindings[""]
|
||||
}
|
||||
|
||||
/// The namespace of a schema-local unprefixed QName: the default-namespace
|
||||
/// binding when declared, otherwise the schema `targetNamespace`.
|
||||
static func schemaComponentNamespace(_ qualified: String, _ bindings: [String: String], targetNamespace: String?) -> String? {
|
||||
if prefix(qualified) != nil {
|
||||
return referenceNamespace(qualified, bindings)
|
||||
}
|
||||
if let defaultNamespace = bindings[""] {
|
||||
return defaultNamespace
|
||||
}
|
||||
return targetNamespace
|
||||
}
|
||||
|
||||
static func occurrence(_ node: XSDTree) -> (min: Int, max: Int?) {
|
||||
let minimum = attribute(node, "minOccurs").flatMap(Int.init) ?? 1
|
||||
let maximumText = attribute(node, "maxOccurs")
|
||||
@ -152,6 +175,8 @@ extension PureXML.Schema {
|
||||
var elements: [String: ElementType]
|
||||
var types: [String: ElementType]
|
||||
var constraints: [String: [IdentityConstraint]]
|
||||
/// Simple types for identity-constraint field paths.
|
||||
var identityFieldTypes: [String: SimpleType] = [:]
|
||||
/// Local names of elements declared `nillable="true"`.
|
||||
var nillableElements: Set<String> = []
|
||||
/// The `default`/`fixed` value constraint declared on each element name.
|
||||
@ -179,6 +204,8 @@ extension PureXML.Schema {
|
||||
/// definitions and the like), reported together with the model-level
|
||||
/// consistency findings when the schema is compiled.
|
||||
var schemaErrors: [PureXML.Validation.ValidationError] = []
|
||||
/// Global attribute declarations keyed by ``attributeDeclarationKey(_:)``.
|
||||
var globalAttributes: [String: AttributeUse] = [:]
|
||||
}
|
||||
|
||||
/// Collects schema-validity findings during parsing. A reference type so the
|
||||
@ -217,13 +244,27 @@ extension PureXML.Schema {
|
||||
struct XSDContext {
|
||||
var simpleTypes: [String: SimpleType]
|
||||
var attributeGroups: [String: XSDTree]
|
||||
/// Attribute-group definitions from included schemas before a `redefine`
|
||||
/// overlay replaces them; used when a redefinition self-references its
|
||||
/// former self.
|
||||
var baseAttributeGroups: [String: XSDTree] = [:]
|
||||
/// Names of attribute groups redefined through `xs:redefine`.
|
||||
var redefinedAttributeGroups: Set<String> = []
|
||||
/// Top-level (global) attribute declaration nodes by name, so an
|
||||
/// `<attribute ref="...">` resolves to the global declaration's type.
|
||||
var globalAttributes: [String: XSDTree] = [:]
|
||||
var groups: [String: XSDTree]
|
||||
/// Model-group definitions before a `redefine` overlay.
|
||||
var baseGroups: [String: XSDTree] = [:]
|
||||
/// Names of model groups redefined through `xs:redefine`.
|
||||
var redefinedGroups: Set<String> = []
|
||||
/// Named complex-type definition nodes, so a `complexContent` derivation can
|
||||
/// resolve and compose its base type's content model and attributes.
|
||||
var complexTypeNodes: [String: XSDTree] = [:]
|
||||
/// Complex-type definitions before a `redefine` overlay.
|
||||
var baseComplexTypeNodes: [String: XSDTree] = [:]
|
||||
/// Names of complex types redefined through `xs:redefine`.
|
||||
var redefinedComplexTypes: Set<String> = []
|
||||
/// The schema's target namespace, for resolving `##other`/`##targetNamespace`
|
||||
/// in wildcard constraints and qualifying global and qualified-form local
|
||||
/// declarations.
|
||||
@ -242,6 +283,8 @@ extension PureXML.Schema {
|
||||
/// Each substitution-group head maps to its transitive member element
|
||||
/// names, so an `xs:element ref` to a head also admits its members.
|
||||
var substitutions: [String: [String]] = [:]
|
||||
/// Target namespace of each global element, keyed by local name.
|
||||
var elementNamespaces: [String: String?] = [:]
|
||||
/// Local names of element declarations declared `abstract="true"`, so a
|
||||
/// reference to an abstract head expands to its members but not the head.
|
||||
var abstractElements: Set<String> = []
|
||||
@ -255,6 +298,9 @@ extension PureXML.Schema {
|
||||
var compositionLoaded: Bool = false
|
||||
/// The location of each container schema document.
|
||||
var containerLocations: [ObjectIdentifier: String?] = [:]
|
||||
/// Target namespace imposed on an included schema with no `targetNamespace`
|
||||
/// (chameleon include), keyed by that included schema container.
|
||||
var chameleonTargetNamespaces: [ObjectIdentifier: String] = [:]
|
||||
/// The shared schema-validity finding collector. A reference type, so the
|
||||
/// value-type context's copies all report into the one place.
|
||||
let diagnostics = SchemaDiagnostics()
|
||||
@ -270,5 +316,19 @@ extension PureXML.Schema {
|
||||
copy.visitingTypes.insert(type)
|
||||
return copy
|
||||
}
|
||||
|
||||
/// Namespace and form defaults for components defined under `container`.
|
||||
func scoped(for container: XSDTree) -> XSDContext {
|
||||
var scoped = self
|
||||
let schema = XSDNode.schemaOwner(container)
|
||||
scoped.targetNamespace = XSDNode.attribute(schema, "targetNamespace")
|
||||
if scoped.targetNamespace?.isEmpty != false {
|
||||
scoped.targetNamespace = chameleonTargetNamespaces[ObjectIdentifier(schema)]
|
||||
}
|
||||
scoped.namespaceBindings = XSDNode.namespaceBindings(of: schema)
|
||||
scoped.elementFormQualified = XSDNode.attribute(schema, "elementFormDefault") == "qualified"
|
||||
scoped.attributeFormQualified = XSDNode.attribute(schema, "attributeFormDefault") == "qualified"
|
||||
return scoped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,7 +223,7 @@ extension PureXML.Schema.XSDParser {
|
||||
/// The names of components of `kind` redefined in an `xs:redefine`. A redefined
|
||||
/// model group or attribute group legitimately references its own former self
|
||||
/// once, so it is a chain end rather than a cycle.
|
||||
private static func redefinedNames(_ containers: [XSDTree], _ kind: String) -> Set<String> {
|
||||
static func redefinedNames(_ containers: [XSDTree], _ kind: String) -> Set<String> {
|
||||
var names: Set<String> = []
|
||||
for container in containers where XSDDerivNode.localName(container) == "redefine" {
|
||||
for definition in XSDDerivNode.children(container, named: kind) {
|
||||
|
||||
@ -11,6 +11,7 @@ private typealias Wildcard = PureXML.Schema.Wildcard
|
||||
private typealias Facets = PureXML.Schema.Facets
|
||||
private typealias Group = PureXML.Schema.Group
|
||||
private typealias SimpleType = PureXML.Schema.SimpleType
|
||||
private typealias AttributeUse = PureXML.Schema.AttributeUse
|
||||
private typealias XSDCompiled = PureXML.Schema.XSDCompiled
|
||||
|
||||
extension PureXML.Schema {
|
||||
@ -50,29 +51,7 @@ extension PureXML.Schema {
|
||||
containerLocations: containerLocations,
|
||||
compositionLoaded: compositionLoaded,
|
||||
)
|
||||
for error in PureXML.Validation.SchemaCompile.preCompileErrors(schema: schema, context: context, containers: containers) {
|
||||
context.diagnostics.report(error)
|
||||
}
|
||||
var types = namedTypes(containers, into: &context)
|
||||
let elements = globalElements(containers, context, into: &types)
|
||||
let postCompileDocument = PureXML.Schema.SchemaCompileContext(
|
||||
schema: schema,
|
||||
context: context,
|
||||
containers: containers,
|
||||
derivation: derivation,
|
||||
globalElements: elements,
|
||||
namedTypes: types,
|
||||
)
|
||||
for error in PureXML.Validation.SchemaCompile.postCompileErrors(in: postCompileDocument) {
|
||||
context.diagnostics.report(error)
|
||||
}
|
||||
return buildCompiled(
|
||||
elements: elements,
|
||||
types: types,
|
||||
containers: containers,
|
||||
derivation: derivation,
|
||||
context: context,
|
||||
)
|
||||
return finishCompile(schema: schema, containers: containers, derivation: derivation, context: &context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -89,31 +68,6 @@ extension PureXML.Schema.XSDParser {
|
||||
return containerLocations
|
||||
}
|
||||
|
||||
private static func buildCompiled(
|
||||
elements: [String: ElementType],
|
||||
types: [String: ElementType],
|
||||
containers: [XSDTree],
|
||||
derivation: DerivationTables,
|
||||
context: XSDContext,
|
||||
) -> XSDCompiled {
|
||||
let (nillable, elementConstraints) = elementMetadata(containers)
|
||||
return XSDCompiled(
|
||||
elements: elements,
|
||||
types: types,
|
||||
constraints: identityConstraints(containers),
|
||||
nillableElements: nillable,
|
||||
elementConstraints: elementConstraints,
|
||||
abstractTypes: derivation.abstractTypes,
|
||||
abstractElements: derivation.abstractElements,
|
||||
typeBlock: derivation.typeBlock,
|
||||
elementBlock: derivation.elementBlock,
|
||||
typeDerivation: derivation.typeDerivation,
|
||||
typeFinal: derivation.typeFinal,
|
||||
targetNamespace: context.targetNamespace,
|
||||
schemaErrors: context.diagnostics.deduplicated,
|
||||
)
|
||||
}
|
||||
|
||||
private static func createContext(
|
||||
schema: XSDTree,
|
||||
containers: [XSDTree],
|
||||
@ -136,45 +90,222 @@ extension PureXML.Schema.XSDParser {
|
||||
context.namespaceBindings = XSDNode.namespaceBindings(of: schema)
|
||||
context.complexTypeNodes = indexByName(allChildren(containers, named: "complexType"))
|
||||
context.globalAttributes = indexByName(allChildren(containers, named: "attribute"))
|
||||
let redefinedAttributeGroups = PureXML.Schema.XSDParser.redefinedNames(containers, "attributeGroup")
|
||||
let redefinedGroups = PureXML.Schema.XSDParser.redefinedNames(containers, "group")
|
||||
let redefinedComplexTypes = PureXML.Schema.XSDParser.redefinedNames(containers, "complexType")
|
||||
context.redefinedAttributeGroups = redefinedAttributeGroups
|
||||
context.redefinedGroups = redefinedGroups
|
||||
context.redefinedComplexTypes = redefinedComplexTypes
|
||||
context.baseAttributeGroups = baseNamedComponents(containers, named: "attributeGroup")
|
||||
context.baseGroups = baseNamedComponents(containers, named: "group")
|
||||
context.baseComplexTypeNodes = baseNamedComponents(containers, named: "complexType")
|
||||
context.chameleonTargetNamespaces = chameleonTargetNamespaces(containers, locations: containerLocations)
|
||||
return context
|
||||
}
|
||||
|
||||
/// The named-type table: the global simple types resolved into `context`,
|
||||
/// then each global complex type compiled and added by name.
|
||||
private static func chameleonTargetNamespaces(
|
||||
_ containers: [XSDTree],
|
||||
locations: [ObjectIdentifier: String?],
|
||||
) -> [ObjectIdentifier: String] {
|
||||
var map: [ObjectIdentifier: String] = [:]
|
||||
let schemas = containers.filter { XSDNode.localName($0) == "schema" }
|
||||
for included in schemas {
|
||||
let declared = XSDNode.attribute(included, "targetNamespace")
|
||||
guard declared == nil || declared?.isEmpty == true else { continue }
|
||||
guard let includedLocation = locations[ObjectIdentifier(included)] ?? nil else { continue }
|
||||
let includedBase = includedLocation.split(separator: "/").last.map(String.init) ?? includedLocation
|
||||
for parent in schemas where parent !== included {
|
||||
guard let parentNamespace = XSDNode.attribute(parent, "targetNamespace"), !parentNamespace.isEmpty else { continue }
|
||||
for child in XSDNode.elementChildren(parent) where XSDNode.localName(child) == "include" {
|
||||
let href = XSDNode.attribute(child, "schemaLocation") ?? ""
|
||||
if href == includedBase || includedLocation.hasSuffix(href) {
|
||||
map[ObjectIdentifier(included)] = parentNamespace
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
private struct CompiledBuild {
|
||||
var elements: [String: ElementType]
|
||||
var types: [String: ElementType]
|
||||
var globalAttributes: [String: AttributeUse]
|
||||
var containers: [XSDTree]
|
||||
var derivation: DerivationTables
|
||||
var context: XSDContext
|
||||
var identityFieldTypes: [String: SimpleType]
|
||||
}
|
||||
|
||||
private static func finishCompile(
|
||||
schema: XSDTree,
|
||||
containers: [XSDTree],
|
||||
derivation: DerivationTables,
|
||||
context: inout XSDContext,
|
||||
) -> XSDCompiled {
|
||||
for error in PureXML.Validation.SchemaCompile.preCompileErrors(schema: schema, context: context, containers: containers) {
|
||||
context.diagnostics.report(error)
|
||||
}
|
||||
seedElementNamespaces(containers, &context)
|
||||
var types = namedTypes(containers, into: &context)
|
||||
let elements = globalElements(containers, &context, into: &types)
|
||||
let globalAttributes = globalAttributeUses(containers, context)
|
||||
let postCompileDocument = PureXML.Schema.SchemaCompileContext(
|
||||
schema: schema,
|
||||
context: context,
|
||||
containers: containers,
|
||||
derivation: derivation,
|
||||
globalElements: elements,
|
||||
namedTypes: types,
|
||||
)
|
||||
for error in PureXML.Validation.SchemaCompile.postCompileErrors(in: postCompileDocument) {
|
||||
context.diagnostics.report(error)
|
||||
}
|
||||
return buildCompiled(CompiledBuild(
|
||||
elements: elements,
|
||||
types: types,
|
||||
globalAttributes: globalAttributes,
|
||||
containers: containers,
|
||||
derivation: derivation,
|
||||
context: context,
|
||||
identityFieldTypes: identityFieldTypes(containers, context),
|
||||
))
|
||||
}
|
||||
|
||||
private static func buildCompiled(_ build: CompiledBuild) -> XSDCompiled {
|
||||
let (nillable, elementConstraints) = elementMetadata(build.containers)
|
||||
return XSDCompiled(
|
||||
elements: build.elements,
|
||||
types: build.types,
|
||||
constraints: identityConstraints(build.containers),
|
||||
identityFieldTypes: build.identityFieldTypes,
|
||||
nillableElements: nillable,
|
||||
elementConstraints: elementConstraints,
|
||||
abstractTypes: build.derivation.abstractTypes,
|
||||
abstractElements: build.derivation.abstractElements,
|
||||
typeBlock: build.derivation.typeBlock,
|
||||
elementBlock: build.derivation.elementBlock,
|
||||
typeDerivation: build.derivation.typeDerivation,
|
||||
typeFinal: build.derivation.typeFinal,
|
||||
targetNamespace: build.context.targetNamespace,
|
||||
schemaErrors: build.context.diagnostics.deduplicated,
|
||||
globalAttributes: build.globalAttributes,
|
||||
)
|
||||
}
|
||||
|
||||
private static func namedTypes(_ containers: [XSDTree], into context: inout XSDContext) -> [String: ElementType] {
|
||||
resolveSimpleTypes(allChildren(containers, named: "simpleType"), into: &context)
|
||||
var types: [String: ElementType] = context.simpleTypes.mapValues(ElementType.simple)
|
||||
for node in allChildren(containers, named: "complexType") {
|
||||
if let name = XSDNode.attribute(node, "name") {
|
||||
types[name] = .complex(complexType(node, context))
|
||||
var types: [String: ElementType] = [:]
|
||||
for container in containers {
|
||||
let kind = XSDNode.localName(container)
|
||||
guard kind == "schema" || kind == "redefine" else { continue }
|
||||
var containerContext = scopedContext(from: context, container: container)
|
||||
let simpleTypeNodes = kind == "redefine"
|
||||
? XSDNode.elementChildren(container).filter { XSDNode.localName($0) == "simpleType" }
|
||||
: XSDNode.children(container, named: "simpleType")
|
||||
resolveSimpleTypes(simpleTypeNodes, into: &containerContext)
|
||||
for (name, simple) in containerContext.simpleTypes {
|
||||
context.simpleTypes[name] = simple
|
||||
let compiled = ElementType.simple(simple)
|
||||
let key = typeDeclarationKey(name, namespaceURI: containerContext.targetNamespace)
|
||||
types[key] = compiled
|
||||
types[name] = compiled
|
||||
}
|
||||
let complexTypeNodes = kind == "redefine"
|
||||
? XSDNode.elementChildren(container).filter { XSDNode.localName($0) == "complexType" }
|
||||
: XSDNode.children(container, named: "complexType")
|
||||
for node in complexTypeNodes {
|
||||
guard let name = XSDNode.attribute(node, "name") else { continue }
|
||||
let compiled = ElementType.complex(complexType(node, containerContext.visitingType(name)))
|
||||
let key = typeDeclarationKey(name, namespaceURI: containerContext.targetNamespace)
|
||||
types[key] = compiled
|
||||
types[name] = compiled
|
||||
}
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
/// The global element declarations, also registering each under a reserved
|
||||
/// `element:name` key in `types` so an `xs:element ref="name"` resolves to
|
||||
/// this declaration's type (a colon keeps it out of the NCName-only type
|
||||
/// namespace), letting forward and recursive refs resolve at validation time.
|
||||
private static func scopedContext(from context: XSDContext, container: XSDTree) -> XSDContext {
|
||||
context.scoped(for: container)
|
||||
}
|
||||
|
||||
/// Records each global element's target namespace before types are compiled,
|
||||
/// so substitution-group expansion during particle compilation sees the right
|
||||
/// namespace for imported members (for example `add:salutation` in ipo6).
|
||||
private static func seedElementNamespaces(_ containers: [XSDTree], _ context: inout XSDContext) {
|
||||
for container in containers where XSDNode.localName(container) == "schema" {
|
||||
let containerContext = scopedContext(from: context, container: container)
|
||||
for node in XSDNode.children(container, named: "element") {
|
||||
guard XSDNode.attribute(node, "name") != nil, XSDNode.attribute(node, "ref") == nil else { continue }
|
||||
let name = XSDNode.attribute(node, "name") ?? ""
|
||||
context.elementNamespaces[name] = containerContext.targetNamespace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func globalElements(
|
||||
_ containers: [XSDTree],
|
||||
_ context: XSDContext,
|
||||
_ context: inout XSDContext,
|
||||
into types: inout [String: ElementType],
|
||||
) -> [String: ElementType] {
|
||||
var elements: [String: ElementType] = [:]
|
||||
for node in allChildren(containers, named: "element") where XSDNode.attribute(node, "name") != nil {
|
||||
let name = XSDNode.attribute(node, "name") ?? ""
|
||||
let type = elementType(node, context)
|
||||
elements[name] = type
|
||||
types[elementKey(name)] = type
|
||||
for container in containers where XSDNode.localName(container) == "schema" {
|
||||
let containerContext = scopedContext(from: context, container: container)
|
||||
for node in XSDNode.children(container, named: "element") {
|
||||
guard XSDNode.attribute(node, "name") != nil, XSDNode.attribute(node, "ref") == nil else { continue }
|
||||
let name = XSDNode.attribute(node, "name") ?? ""
|
||||
context.elementNamespaces[name] = containerContext.targetNamespace
|
||||
let type = elementType(node, containerContext)
|
||||
elements[name] = type
|
||||
types[elementKey(name)] = type
|
||||
let qualified = PureXML.Model.QualifiedName(localName: name, namespaceURI: containerContext.targetNamespace)
|
||||
types[elementDeclarationKey(qualified)] = type
|
||||
}
|
||||
}
|
||||
return elements
|
||||
}
|
||||
|
||||
private static func globalAttributeUses(
|
||||
_ containers: [XSDTree],
|
||||
_ context: XSDContext,
|
||||
) -> [String: AttributeUse] {
|
||||
var attributes: [String: AttributeUse] = [:]
|
||||
for container in containers where XSDNode.localName(container) == "schema" {
|
||||
let containerContext = scopedContext(from: context, container: container)
|
||||
for node in XSDNode.children(container, named: "attribute") {
|
||||
guard XSDNode.attribute(node, "name") != nil, XSDNode.attribute(node, "ref") == nil,
|
||||
var use = attributeUse(node, containerContext)
|
||||
else { continue }
|
||||
if let target = containerContext.targetNamespace, !target.isEmpty {
|
||||
use.name = PureXML.Model.QualifiedName(localName: use.name.localName, namespaceURI: target)
|
||||
let explicitTarget = XSDNode.attribute(container, "targetNamespace")
|
||||
if explicitTarget == nil || explicitTarget?.isEmpty == true {
|
||||
use.chameleonUnprefixed = true
|
||||
}
|
||||
}
|
||||
attributes[attributeDeclarationKey(use.name)] = use
|
||||
}
|
||||
}
|
||||
return attributes
|
||||
}
|
||||
|
||||
private static func allChildren(_ containers: [XSDTree], named name: String) -> [XSDTree] {
|
||||
containers.flatMap { XSDNode.children($0, named: name) }
|
||||
}
|
||||
|
||||
/// Named component definitions from every container except `redefine`
|
||||
/// overlays, so a redefinition's self-reference resolves to the included
|
||||
/// definition rather than the overlay being built.
|
||||
private static func baseNamedComponents(_ containers: [XSDTree], named kind: String) -> [String: XSDTree] {
|
||||
var index: [String: XSDTree] = [:]
|
||||
for container in containers where XSDNode.localName(container) != "redefine" {
|
||||
for node in XSDNode.children(container, named: kind) {
|
||||
guard let name = XSDNode.attribute(node, "name") else { continue }
|
||||
index[name] = node
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
/// Resolves the named simple types into `context` over repeated passes:
|
||||
/// named simple types may restrict or list one another regardless of
|
||||
/// document order, so each pass sees the types the previous one resolved. A
|
||||
@ -190,55 +321,23 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Element and type references
|
||||
|
||||
private static func elementType(_ node: XSDTree, _ context: XSDContext) -> ElementType {
|
||||
if let typeName = XSDNode.attribute(node, "type") {
|
||||
return typeReference(typeName, context)
|
||||
}
|
||||
if let inline = XSDNode.firstChild(node, named: "simpleType") {
|
||||
return .simple(XSDSimpleParser.simpleType(inline, context))
|
||||
}
|
||||
if let inline = XSDNode.firstChild(node, named: "complexType") {
|
||||
return .complex(complexType(inline, context))
|
||||
}
|
||||
return .complex(anyType)
|
||||
}
|
||||
|
||||
private static func typeReference(_ typeName: String, _ context: XSDContext) -> ElementType {
|
||||
let local = XSDNode.stripPrefix(typeName)
|
||||
let uri = XSDNode.referenceNamespace(typeName, context.namespaceBindings)
|
||||
if uri == PureXML.Schema.XSDParser.xsdNamespace {
|
||||
if let builtin = BuiltinType(rawValue: local) {
|
||||
return .simple(SimpleType(base: builtin))
|
||||
}
|
||||
if let item = XSDSimpleParser.listBuiltinItem(local) {
|
||||
return .simple(.list(item: SimpleType(base: item), isBuiltinList: true))
|
||||
}
|
||||
}
|
||||
return .typeReference(local)
|
||||
}
|
||||
|
||||
private static var anyType: ComplexType {
|
||||
ComplexType(
|
||||
attributeWildcard: Wildcard(processContents: .skip),
|
||||
content: .mixed(Particle(minOccurs: 0, maxOccurs: nil, term: .wildcard(Wildcard(processContents: .skip)))),
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Complex types
|
||||
|
||||
static func complexType(_ node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.ComplexType {
|
||||
let mixed = XSDNode.attribute(node, "mixed") == "true"
|
||||
let mixed = XSDNode.attribute(node, "mixed").map { $0 == "true" || $0 == "1" } ?? false
|
||||
let attributes = attributeUses(under: node, context)
|
||||
if let simpleContent = XSDNode.firstChild(node, named: "simpleContent") {
|
||||
let inner = derivation(simpleContent)
|
||||
let wildcard = inner.flatMap { attributeWildcard(under: $0, context) }
|
||||
let extra = inner.map { attributeUses(under: $0, context) } ?? []
|
||||
return ComplexType(attributes: attributes + extra, attributeWildcard: wildcard, content: .simpleContent(simpleContentType(simpleContent, context)))
|
||||
let derived = inner.map { PureXML.Schema.XSDParser.simpleContentAttributes(under: $0, context) } ?? []
|
||||
return ComplexType(
|
||||
attributes: attributes + derived,
|
||||
attributeWildcard: wildcard,
|
||||
content: .simpleContent(simpleContentType(simpleContent, context)),
|
||||
)
|
||||
}
|
||||
if let complexContent = XSDNode.firstChild(node, named: "complexContent"), let inner = derivation(complexContent) {
|
||||
return complexContentType(inner, mixed: mixed || XSDNode.attribute(complexContent, "mixed") == "true", context)
|
||||
return complexContentType(inner, mixed: mixed || XSDNode.attribute(complexContent, "mixed").map { $0 == "true" || $0 == "1" } ?? false, context)
|
||||
}
|
||||
let wildcard = attributeWildcard(under: node, context)
|
||||
guard let particle = modelGroup(in: node, context) else {
|
||||
@ -258,7 +357,8 @@ extension PureXML.Schema.XSDParser {
|
||||
guard let inner = derivation(node) else { return SimpleType(base: .string) }
|
||||
let rawBase = XSDNode.attribute(inner, "base") ?? "string"
|
||||
let baseName = XSDNode.stripPrefix(rawBase)
|
||||
let uri = XSDNode.referenceNamespace(rawBase, context.namespaceBindings)
|
||||
let bindings = PureXML.Schema.XSDParser.namespaceBindingsInScope(of: inner, defaultBindings: context.namespaceBindings)
|
||||
let uri = XSDNode.referenceNamespace(rawBase, bindings)
|
||||
|
||||
let base: BuiltinType
|
||||
let inheritedFacets: Facets
|
||||
@ -273,117 +373,4 @@ extension PureXML.Schema.XSDParser {
|
||||
XSDSimpleParser.applyFacets(inner, into: &facets)
|
||||
return SimpleType(base: base, facets: facets)
|
||||
}
|
||||
|
||||
// MARK: Attribute uses and groups
|
||||
|
||||
// MARK: Model groups
|
||||
|
||||
static func modelGroup(in node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.Particle? {
|
||||
for (name, compositor) in [("sequence", Compositor.sequence), ("choice", .choice), ("all", .all)] {
|
||||
if let group = XSDNode.firstChild(node, named: name) {
|
||||
return groupParticle(group, compositor, context)
|
||||
}
|
||||
}
|
||||
if let groupRef = XSDNode.firstChild(node, named: "group") {
|
||||
return particle(groupRef, context)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func groupParticle(
|
||||
_ node: XSDTree,
|
||||
_ compositor: Compositor,
|
||||
_ context: XSDContext,
|
||||
) -> Particle {
|
||||
var particles: [Particle] = []
|
||||
for child in XSDNode.elementChildren(node) {
|
||||
if let member = particle(child, context) { particles.append(member) }
|
||||
}
|
||||
let (minimum, maximum) = XSDNode.occurrence(node)
|
||||
return Particle(
|
||||
minOccurs: minimum,
|
||||
maxOccurs: maximum,
|
||||
term: .group(Group(compositor: compositor, particles: particles)),
|
||||
)
|
||||
}
|
||||
|
||||
private static func particle(_ node: XSDTree, _ context: XSDContext) -> Particle? {
|
||||
let (minimum, maximum) = XSDNode.occurrence(node)
|
||||
switch XSDNode.localName(node) {
|
||||
case "element":
|
||||
return elementParticle(node, minimum, maximum, context)
|
||||
case "sequence":
|
||||
return groupParticle(node, .sequence, context)
|
||||
case "choice":
|
||||
return groupParticle(node, .choice, context)
|
||||
case "all":
|
||||
return groupParticle(node, .all, context)
|
||||
case "group":
|
||||
return groupReferenceParticle(node, minimum, maximum, context)
|
||||
case "any":
|
||||
return Particle(minOccurs: minimum, maxOccurs: maximum, term: .wildcard(wildcard(node, context)))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func elementParticle(_ node: XSDTree, _ minimum: Int, _ maximum: Int?, _ context: XSDContext) -> Particle {
|
||||
if let ref = XSDNode.attribute(node, "ref") {
|
||||
return referenceParticle(ref, minimum, maximum, context)
|
||||
}
|
||||
let name = XSDNode.attribute(node, "name") ?? ""
|
||||
return Particle(
|
||||
minOccurs: minimum,
|
||||
maxOccurs: maximum,
|
||||
term: .element(
|
||||
name: localElementName(name, XSDNode.attribute(node, "form"), context),
|
||||
type: elementType(node, context),
|
||||
typeName: elementTypeName(node),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private static func groupReferenceParticle(_ node: XSDTree, _ minimum: Int, _ maximum: Int?, _ context: XSDContext) -> Particle? {
|
||||
guard let ref = XSDNode.attribute(node, "ref") else { return nil }
|
||||
let name = XSDNode.stripPrefix(ref)
|
||||
guard !context.visitingGroups.contains(name), let definition = context.groups[name],
|
||||
let inner = modelGroup(in: definition, context.visiting(name))
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return Particle(minOccurs: minimum, maxOccurs: maximum, term: inner.term)
|
||||
}
|
||||
|
||||
/// The particle for an `<xs:element ref="...">`. An abstract head may not appear
|
||||
/// itself, only its substitution-group members; a concrete head appears alongside
|
||||
/// them, the reference expanding to a choice over the head and every member.
|
||||
static func referenceParticle(_ ref: String, _ minimum: Int, _ maximum: Int?, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.Particle {
|
||||
let name = PureXML.Schema.XSDNode.stripPrefix(ref)
|
||||
// The reference's QName is resolved by standard XML namespace rules (a bound
|
||||
// prefix gives its namespace, so an imported element lands in the imported
|
||||
// namespace; an unprefixed name the default namespace, or none), as the W3C
|
||||
// XSTS suite expects: a bare `ref="foo"` with no default namespace is in no
|
||||
// namespace, which `##other` admits; forcing it to the target over-rejects
|
||||
// those and masks a dangling reference. Members are this schema's globals.
|
||||
let refNamespace = PureXML.Schema.XSDNode.referenceNamespace(ref, context.namespaceBindings)
|
||||
let head: [(String, String?)] = context.abstractElements.contains(name) ? [] : [(name, refNamespace)]
|
||||
let alternatives = head + (context.substitutions[name] ?? []).map { ($0, context.targetNamespace) }
|
||||
if alternatives.count == 1 {
|
||||
return PureXML.Schema.Particle(minOccurs: minimum, maxOccurs: maximum, term: elementReferenceTerm(alternatives[0].0, alternatives[0].1))
|
||||
}
|
||||
let members = alternatives.map { PureXML.Schema.Particle(term: elementReferenceTerm($0.0, $0.1)) }
|
||||
return PureXML.Schema.Particle(minOccurs: minimum, maxOccurs: maximum, term: .group(.init(compositor: .choice, particles: members)))
|
||||
}
|
||||
|
||||
/// The term for a reference to a global element, in the resolved `namespace`.
|
||||
static func elementReferenceTerm(_ name: String, _ namespace: String?) -> PureXML.Schema.Term {
|
||||
// A reference's type is resolved indirectly through the element key, so its
|
||||
// derivation identity is not a plain type name; NameAndTypeOK over a ref
|
||||
// falls back to the structural check (typeName nil).
|
||||
.element(
|
||||
name: PureXML.Model.QualifiedName(localName: name, namespaceURI: namespace),
|
||||
type: .typeReference(elementKey(name)),
|
||||
typeName: nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,124 @@ extension PureXML.Schema.XSDParser {
|
||||
return map
|
||||
}
|
||||
|
||||
/// Simple types for identity-constraint field paths, keyed by
|
||||
/// ``identityFieldKey(constraint:field:)``.
|
||||
static func identityFieldTypes(_ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [String: PureXML.Schema.SimpleType] {
|
||||
var types: [String: PureXML.Schema.SimpleType] = [:]
|
||||
for container in containers {
|
||||
for element in descendants(container, named: "element") {
|
||||
let constraints = PureXML.Schema.XSDNode.elementChildren(element).compactMap(constraint)
|
||||
guard !constraints.isEmpty else { continue }
|
||||
for constraint in constraints {
|
||||
for field in constraint.fields {
|
||||
if field == ".", selectorBranches(constraint.selector).count > 1 {
|
||||
for branch in selectorBranches(constraint.selector) {
|
||||
if let type = declaredElementSimpleType(named: branch, under: element, context) {
|
||||
types[identityFieldKey(constraint: constraint, field: field, targetLocal: branch)] = type
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if let type = identityFieldType(field, host: element, constraint: constraint, context) {
|
||||
types[identityFieldKey(constraint: constraint, field: field)] = type
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
static func identityFieldKey(constraint: PureXML.Schema.IdentityConstraint, field: String, targetLocal: String? = nil) -> String {
|
||||
if let targetLocal { return "\(constraint.name)|\(field)|\(targetLocal)" }
|
||||
return "\(constraint.name)|\(field)"
|
||||
}
|
||||
|
||||
private static func selectorBranches(_ selector: String) -> [String] {
|
||||
let token = selector.split(separator: "/").last.map(String.init) ?? selector
|
||||
guard token.contains("|") else {
|
||||
return selectorTargetLocalName(selector).map { [$0] } ?? []
|
||||
}
|
||||
return token.split(separator: "|").compactMap { branch in
|
||||
let local = PureXML.Schema.XSDNode.stripPrefix(String(branch))
|
||||
return local.isEmpty ? nil : local
|
||||
}
|
||||
}
|
||||
|
||||
private static func identityFieldType(
|
||||
_ field: String,
|
||||
host: XSDTree,
|
||||
constraint: PureXML.Schema.IdentityConstraint,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
) -> PureXML.Schema.SimpleType? {
|
||||
if field.hasPrefix("@") {
|
||||
let attributeName = identityFieldAttributeName(field)
|
||||
return attributeType(named: attributeName, under: host, context)
|
||||
}
|
||||
if field == "." {
|
||||
let local = selectorTargetLocalName(constraint.selector)
|
||||
return declaredElementSimpleType(named: local, under: host, context)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func identityFieldAttributeName(_ field: String) -> String {
|
||||
let token = field.split(separator: "|").first.map(String.init) ?? field
|
||||
guard token.hasPrefix("@") else { return String(field.dropFirst()) }
|
||||
return String(token.dropFirst())
|
||||
}
|
||||
|
||||
private static func selectorTargetLocalName(_ selector: String) -> String? {
|
||||
let token = selector.split(separator: "/").last.map(String.init) ?? selector
|
||||
return PureXML.Schema.XSDNode.stripPrefix(token)
|
||||
}
|
||||
|
||||
private static func declaredElementSimpleType(
|
||||
named local: String?,
|
||||
under host: XSDTree,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
) -> PureXML.Schema.SimpleType? {
|
||||
guard let local else { return elementSimpleType(under: host, context) }
|
||||
guard let complex = PureXML.Schema.XSDNode.firstChild(host, named: "complexType") else { return nil }
|
||||
for child in descendants(complex, named: "element") {
|
||||
guard PureXML.Schema.XSDNode.attribute(child, "name") == local,
|
||||
let typeName = PureXML.Schema.XSDNode.attribute(child, "type")
|
||||
else { continue }
|
||||
let stripped = PureXML.Schema.XSDNode.stripPrefix(typeName)
|
||||
let uri = PureXML.Schema.XSDNode.referenceNamespace(typeName, context.namespaceBindings)
|
||||
if uri == PureXML.Schema.XSDParser.xsdNamespace, let builtin = PureXML.Schema.BuiltinType(rawValue: stripped) {
|
||||
return PureXML.Schema.SimpleType(base: builtin)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func attributeType(named name: String, under host: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.SimpleType? {
|
||||
for element in descendants(host, named: "element") {
|
||||
guard let complex = PureXML.Schema.XSDNode.firstChild(element, named: "complexType") else { continue }
|
||||
for attribute in descendants(complex, named: "attribute") {
|
||||
guard PureXML.Schema.XSDNode.attribute(attribute, "name") == name else { continue }
|
||||
if let use = PureXML.Schema.XSDParser.attributeUse(attribute, context) {
|
||||
return use.type
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func elementSimpleType(under host: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.SimpleType? {
|
||||
guard let complex = PureXML.Schema.XSDNode.firstChild(host, named: "complexType") else { return nil }
|
||||
for child in descendants(complex, named: "element") {
|
||||
guard let typeName = PureXML.Schema.XSDNode.attribute(child, "type") else { continue }
|
||||
let local = PureXML.Schema.XSDNode.stripPrefix(typeName)
|
||||
let uri = PureXML.Schema.XSDNode.referenceNamespace(typeName, context.namespaceBindings)
|
||||
if uri == PureXML.Schema.XSDParser.xsdNamespace, let builtin = PureXML.Schema.BuiltinType(rawValue: local) {
|
||||
return PureXML.Schema.SimpleType(base: builtin)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func constraint(_ node: XSDTree) -> PureXML.Schema.IdentityConstraint? {
|
||||
let kind: PureXML.Schema.IdentityConstraintKind
|
||||
switch PureXML.Schema.XSDNode.localName(node) {
|
||||
@ -62,6 +180,29 @@ extension PureXML.Schema.XSDParser {
|
||||
"element:\(name)"
|
||||
}
|
||||
|
||||
/// The local name from a type-table key (`type:{namespace}local` or a legacy bare name).
|
||||
static func bareTypeLocalName(_ reference: String) -> String {
|
||||
guard reference.hasPrefix("type:") else { return reference }
|
||||
let key = String(reference.dropFirst("type:".count))
|
||||
guard let close = key.firstIndex(of: "}") else { return key }
|
||||
return String(key[key.index(after: close)...])
|
||||
}
|
||||
|
||||
/// The type-table key for a global type declaration in a specific namespace.
|
||||
static func typeDeclarationKey(_ localName: String, namespaceURI: String?) -> String {
|
||||
"type:\(PureXML.Schema.ComplexValidator.key(PureXML.Model.QualifiedName(localName: localName, namespaceURI: namespaceURI)))"
|
||||
}
|
||||
|
||||
/// The type-table key for a global element declaration in a specific namespace.
|
||||
static func elementDeclarationKey(_ name: PureXML.Model.QualifiedName) -> String {
|
||||
"element:\(PureXML.Schema.ComplexValidator.key(name))"
|
||||
}
|
||||
|
||||
/// The lookup key for a global attribute declaration in a specific namespace.
|
||||
static func attributeDeclarationKey(_ name: PureXML.Model.QualifiedName) -> String {
|
||||
"attribute:\(PureXML.Schema.ComplexValidator.key(name))"
|
||||
}
|
||||
|
||||
static func indexByName(_ nodes: [XSDTree]) -> [String: XSDTree] {
|
||||
var index: [String: XSDTree] = [:]
|
||||
for node in nodes {
|
||||
|
||||
137
Sources/Schema/XSDParserParticles.swift
Normal file
137
Sources/Schema/XSDParserParticles.swift
Normal file
@ -0,0 +1,137 @@
|
||||
private typealias XSDNode = PureXML.Schema.XSDNode
|
||||
private typealias XSDContext = PureXML.Schema.XSDContext
|
||||
private typealias Particle = PureXML.Schema.Particle
|
||||
private typealias Compositor = PureXML.Schema.Compositor
|
||||
private typealias Group = PureXML.Schema.Group
|
||||
private typealias ElementType = PureXML.Schema.ElementType
|
||||
|
||||
extension PureXML.Schema.XSDParser {
|
||||
// MARK: Model groups
|
||||
|
||||
static func modelGroup(in node: PureXML.Model.TreeNode, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.Particle? {
|
||||
for (name, compositor) in [("sequence", Compositor.sequence), ("choice", .choice), ("all", .all)] {
|
||||
if let group = XSDNode.firstChild(node, named: name) {
|
||||
return groupParticle(group, compositor, context)
|
||||
}
|
||||
}
|
||||
if let groupRef = XSDNode.firstChild(node, named: "group") {
|
||||
return particle(groupRef, context)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func groupParticle(
|
||||
_ node: PureXML.Model.TreeNode,
|
||||
_ compositor: Compositor,
|
||||
_ context: XSDContext,
|
||||
) -> Particle {
|
||||
var particles: [Particle] = []
|
||||
for child in XSDNode.elementChildren(node) {
|
||||
if let member = particle(child, context) { particles.append(member) }
|
||||
}
|
||||
let (minimum, maximum) = XSDNode.occurrence(node)
|
||||
if compositor == .all, minimum == 0 {
|
||||
particles = particles.map { Particle(minOccurs: 0, maxOccurs: $0.maxOccurs, term: $0.term) }
|
||||
}
|
||||
return Particle(
|
||||
minOccurs: minimum,
|
||||
maxOccurs: maximum,
|
||||
term: .group(Group(compositor: compositor, particles: particles)),
|
||||
)
|
||||
}
|
||||
|
||||
private static func particle(_ node: PureXML.Model.TreeNode, _ context: XSDContext) -> Particle? {
|
||||
let (minimum, maximum) = XSDNode.occurrence(node)
|
||||
switch XSDNode.localName(node) {
|
||||
case "element":
|
||||
return elementParticle(node, minimum, maximum, context)
|
||||
case "sequence":
|
||||
return groupParticle(node, .sequence, context)
|
||||
case "choice":
|
||||
return groupParticle(node, .choice, context)
|
||||
case "all":
|
||||
return groupParticle(node, .all, context)
|
||||
case "group":
|
||||
return groupReferenceParticle(node, minimum, maximum, context)
|
||||
case "any":
|
||||
return Particle(minOccurs: minimum, maxOccurs: maximum, term: .wildcard(wildcard(node, context)))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func elementParticle(_ node: PureXML.Model.TreeNode, _ minimum: Int, _ maximum: Int?, _ context: XSDContext) -> Particle {
|
||||
if let ref = XSDNode.attribute(node, "ref") {
|
||||
return referenceParticle(ref, minimum, maximum, context, node: node)
|
||||
}
|
||||
let name = XSDNode.attribute(node, "name") ?? ""
|
||||
return Particle(
|
||||
minOccurs: minimum,
|
||||
maxOccurs: maximum,
|
||||
term: .element(
|
||||
name: localElementName(name, XSDNode.attribute(node, "form"), context),
|
||||
type: elementType(node, context),
|
||||
typeName: elementTypeName(node),
|
||||
valueConstraint: valueConstraint(of: node),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private static func groupReferenceParticle(_ node: PureXML.Model.TreeNode, _ minimum: Int, _ maximum: Int?, _ context: XSDContext) -> Particle? {
|
||||
guard let ref = XSDNode.attribute(node, "ref") else { return nil }
|
||||
let name = XSDNode.stripPrefix(ref)
|
||||
if context.visitingGroups.contains(name) {
|
||||
guard context.redefinedGroups.contains(name),
|
||||
let base = context.baseGroups[name]
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let scoped = context.scoped(for: XSDNode.schemaOwner(base))
|
||||
guard let inner = modelGroup(in: base, scoped.visiting(name)) else {
|
||||
return nil
|
||||
}
|
||||
return Particle(minOccurs: minimum, maxOccurs: maximum, term: inner.term)
|
||||
}
|
||||
guard let definition = context.groups[name] else {
|
||||
return nil
|
||||
}
|
||||
let scoped = context.scoped(for: XSDNode.schemaOwner(definition))
|
||||
guard let inner = modelGroup(in: definition, scoped.visiting(name)) else {
|
||||
return nil
|
||||
}
|
||||
return Particle(minOccurs: minimum, maxOccurs: maximum, term: inner.term)
|
||||
}
|
||||
|
||||
/// The particle for an `<xs:element ref="...">`. An abstract head may not appear
|
||||
/// itself, only its substitution-group members; a concrete head appears alongside
|
||||
/// them, the reference expanding to a choice over the head and every member.
|
||||
static func referenceParticle(
|
||||
_ ref: String,
|
||||
_ minimum: Int,
|
||||
_ maximum: Int?,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
node: XSDTree? = nil,
|
||||
) -> PureXML.Schema.Particle {
|
||||
let name = PureXML.Schema.XSDNode.stripPrefix(ref)
|
||||
let bindings = node.map { PureXML.Schema.XSDParser.namespaceBindingsInScope(of: $0, defaultBindings: context.namespaceBindings) }
|
||||
?? context.namespaceBindings
|
||||
let refNamespace = PureXML.Schema.XSDNode.referenceNamespace(ref, bindings)
|
||||
let head: [(String, String?)] = context.abstractElements.contains(name) ? [] : [(name, refNamespace)]
|
||||
let alternatives = head + (context.substitutions[name] ?? []).map { ($0, context.elementNamespaces[$0] ?? context.targetNamespace) }
|
||||
if alternatives.count == 1 {
|
||||
return PureXML.Schema.Particle(minOccurs: minimum, maxOccurs: maximum, term: elementReferenceTerm(alternatives[0].0, alternatives[0].1))
|
||||
}
|
||||
let members = alternatives.map { PureXML.Schema.Particle(term: elementReferenceTerm($0.0, $0.1)) }
|
||||
return PureXML.Schema.Particle(minOccurs: minimum, maxOccurs: maximum, term: .group(.init(compositor: .choice, particles: members)))
|
||||
}
|
||||
|
||||
/// The term for a reference to a global element, in the resolved `namespace`.
|
||||
static func elementReferenceTerm(_ name: String, _ namespace: String?) -> PureXML.Schema.Term {
|
||||
.element(
|
||||
name: PureXML.Model.QualifiedName(localName: name, namespaceURI: namespace),
|
||||
type: .typeReference(elementKey(name)),
|
||||
typeName: nil,
|
||||
valueConstraint: nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
56
Sources/Schema/XSDParserTypeReferences.swift
Normal file
56
Sources/Schema/XSDParserTypeReferences.swift
Normal file
@ -0,0 +1,56 @@
|
||||
private typealias XSDNode = PureXML.Schema.XSDNode
|
||||
private typealias XSDContext = PureXML.Schema.XSDContext
|
||||
private typealias Particle = PureXML.Schema.Particle
|
||||
private typealias ComplexType = PureXML.Schema.ComplexType
|
||||
private typealias ElementType = PureXML.Schema.ElementType
|
||||
private typealias XSDSimpleParser = PureXML.Schema.XSDSimpleParser
|
||||
private typealias BuiltinType = PureXML.Schema.BuiltinType
|
||||
private typealias Wildcard = PureXML.Schema.Wildcard
|
||||
private typealias SimpleType = PureXML.Schema.SimpleType
|
||||
|
||||
extension PureXML.Schema.XSDParser {
|
||||
// MARK: Element and type references
|
||||
|
||||
static func elementType(_ node: PureXML.Model.TreeNode, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.ElementType {
|
||||
if let typeName = XSDNode.attribute(node, "type") {
|
||||
return typeReference(typeName, context, at: node)
|
||||
}
|
||||
if let inline = XSDNode.firstChild(node, named: "simpleType") {
|
||||
return .simple(XSDSimpleParser.simpleType(inline, context))
|
||||
}
|
||||
if let inline = XSDNode.firstChild(node, named: "complexType") {
|
||||
return .complex(complexType(inline, context))
|
||||
}
|
||||
return .complex(anyType)
|
||||
}
|
||||
|
||||
private static func typeReference(_ typeName: String, _ context: XSDContext, at node: XSDTree? = nil) -> ElementType {
|
||||
let bindings = node.map {
|
||||
PureXML.Schema.XSDParser.namespaceBindingsInScope(of: $0, defaultBindings: context.namespaceBindings)
|
||||
} ?? context.namespaceBindings
|
||||
let local = XSDNode.stripPrefix(typeName)
|
||||
let uri = XSDNode.schemaComponentNamespace(typeName, bindings, targetNamespace: context.targetNamespace)
|
||||
if uri == PureXML.Schema.XSDParser.xsdNamespace {
|
||||
if let builtin = BuiltinType(rawValue: local) {
|
||||
return .simple(SimpleType(base: builtin))
|
||||
}
|
||||
if local == "anySimpleType" {
|
||||
return .simple(SimpleType(base: .string, isAnySimpleType: true))
|
||||
}
|
||||
if local == "anyType" {
|
||||
return .complex(anyType)
|
||||
}
|
||||
if let item = XSDSimpleParser.listBuiltinItem(local) {
|
||||
return .simple(.list(item: SimpleType(base: item), isBuiltinList: true))
|
||||
}
|
||||
}
|
||||
return .typeReference(typeDeclarationKey(local, namespaceURI: uri))
|
||||
}
|
||||
|
||||
private static var anyType: ComplexType {
|
||||
ComplexType(
|
||||
attributeWildcard: Wildcard(processContents: .skip),
|
||||
content: .mixed(Particle(minOccurs: 0, maxOccurs: nil, term: .wildcard(Wildcard(processContents: .skip)))),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -49,11 +49,20 @@ extension PureXML.Schema.XSDSimpleParser {
|
||||
}
|
||||
|
||||
static func applyFacets(_ restriction: XSDTree, into facets: inout PureXML.Schema.Facets) {
|
||||
var stepPatterns: [String] = []
|
||||
for facet in XSDNode.elementChildren(restriction) {
|
||||
guard facet.name?.namespaceURI == PureXML.Schema.XSDParser.xsdNamespace else { continue }
|
||||
let value = XSDNode.attribute(facet, "value")
|
||||
applyStringFacet(XSDNode.localName(facet), value, into: &facets)
|
||||
applyNumericFacet(XSDNode.localName(facet), value, into: &facets)
|
||||
let local = XSDNode.localName(facet)
|
||||
if local == "pattern", let value {
|
||||
stepPatterns.append(value)
|
||||
} else {
|
||||
applyStringFacet(local, value, into: &facets)
|
||||
applyNumericFacet(local, value, into: &facets)
|
||||
}
|
||||
}
|
||||
if !stepPatterns.isEmpty {
|
||||
facets.patternGroups.append(stepPatterns)
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,7 +88,6 @@ extension PureXML.Schema.XSDSimpleParser {
|
||||
|
||||
private static func applyStringFacet(_ name: String?, _ value: String?, into facets: inout PureXML.Schema.Facets) {
|
||||
switch name {
|
||||
case "pattern": if let value { facets.patterns.append(value) }
|
||||
case "enumeration": if let value { facets.enumeration = (facets.enumeration ?? []) + [value] }
|
||||
case "minInclusive": facets.minInclusive = value
|
||||
case "maxInclusive": facets.maxInclusive = value
|
||||
|
||||
@ -36,7 +36,8 @@ extension PureXML.Schema {
|
||||
var facets = baseType.facets
|
||||
// A restriction's own `enumeration` replaces the inherited set (the new
|
||||
// enumeration is the value space, not a union with the base's), whereas
|
||||
// `pattern` accumulates across steps and is ANDed. So drop the inherited
|
||||
// `pattern` accumulates in groups: OR within a restriction step, AND
|
||||
// across derivation (libxml2 `xmlSchemaValidateFacets`). Drop the inherited
|
||||
// enumeration when this step declares its own.
|
||||
if XSDNode.elementChildren(restriction).contains(where: { XSDNode.localName($0) == "enumeration" }) {
|
||||
facets.enumeration = nil
|
||||
@ -80,7 +81,7 @@ extension PureXML.Schema {
|
||||
switch error {
|
||||
case .unbalanced, .danglingQuantifier:
|
||||
true
|
||||
case .empty, .unsupported, .badQuantifier, .badEscape, .badClass:
|
||||
case .unsupported, .badQuantifier, .badEscape, .badClass:
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,8 @@ public extension PureXML.Validation {
|
||||
public let typeBlock: [String: Set<PureXML.Schema.DerivationMethod>]
|
||||
public let elementBlock: [String: Set<PureXML.Schema.DerivationMethod>]
|
||||
public let typeDerivation: [String: PureXML.Schema.TypeDerivation]
|
||||
public let globalAttributes: [String: PureXML.Schema.AttributeUse]
|
||||
public let identityFieldTypes: [String: PureXML.Schema.SimpleType]
|
||||
|
||||
public init(
|
||||
types: [String: PureXML.Schema.ElementType],
|
||||
@ -23,6 +25,8 @@ public extension PureXML.Validation {
|
||||
typeBlock: [String: Set<PureXML.Schema.DerivationMethod>] = [:],
|
||||
elementBlock: [String: Set<PureXML.Schema.DerivationMethod>] = [:],
|
||||
typeDerivation: [String: PureXML.Schema.TypeDerivation] = [:],
|
||||
globalAttributes: [String: PureXML.Schema.AttributeUse] = [:],
|
||||
identityFieldTypes: [String: PureXML.Schema.SimpleType] = [:],
|
||||
) {
|
||||
self.types = types
|
||||
self.constraints = constraints
|
||||
@ -33,10 +37,13 @@ public extension PureXML.Validation {
|
||||
self.typeBlock = typeBlock
|
||||
self.elementBlock = elementBlock
|
||||
self.typeDerivation = typeDerivation
|
||||
self.globalAttributes = globalAttributes
|
||||
self.identityFieldTypes = identityFieldTypes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// XSD validation expressed as composable ``Validation`` values over an
|
||||
public extension PureXML.Validation {
|
||||
/// ``XSDContext``, in the OpenAPIKit idiom: each rule is a named, removable
|
||||
/// ``Validation`` value with a positive description, and the ``validator()``
|
||||
/// composes them; failures are located ``ValidationError``s.
|
||||
@ -63,14 +70,16 @@ public extension PureXML.Validation {
|
||||
guard case let .element(root) = context.subject, let declaration = context.document.rootDeclaration else {
|
||||
return []
|
||||
}
|
||||
let schema = context.document
|
||||
let validator = PureXML.Schema.ComplexValidator(
|
||||
types: context.document.types,
|
||||
nillableElements: context.document.nillableElements,
|
||||
elementConstraints: context.document.elementConstraints,
|
||||
abstractTypes: context.document.abstractTypes,
|
||||
typeBlock: context.document.typeBlock,
|
||||
elementBlock: context.document.elementBlock,
|
||||
typeDerivation: context.document.typeDerivation,
|
||||
types: schema.types,
|
||||
globalAttributes: schema.globalAttributes,
|
||||
nillableElements: schema.nillableElements,
|
||||
elementConstraints: schema.elementConstraints,
|
||||
abstractTypes: schema.abstractTypes,
|
||||
typeBlock: schema.typeBlock,
|
||||
elementBlock: schema.elementBlock,
|
||||
typeDerivation: schema.typeDerivation,
|
||||
)
|
||||
var errors = validator.validate(root, as: declaration, at: [.element(root.name.description)])
|
||||
// Document-scoped ID uniqueness and IDREF resolution, gathered
|
||||
@ -86,10 +95,13 @@ public extension PureXML.Validation {
|
||||
static var identityConstraints: Validation<PureXML.Model.Node, XSDContext> {
|
||||
.init(
|
||||
description: "XSD identity constraints hold",
|
||||
check: { context in
|
||||
check: { context -> [PureXML.Validation.ValidationError] in
|
||||
guard case let .element(root) = context.subject else { return [] }
|
||||
return PureXML.Schema.IdentityValidator(constraints: context.document.constraints)
|
||||
.validate(PureXML.Model.TreeNode(context.subject), at: [.element(root.name.description)])
|
||||
return PureXML.Schema.IdentityValidator(
|
||||
constraints: context.document.constraints,
|
||||
fieldTypes: context.document.identityFieldTypes,
|
||||
types: context.document.types,
|
||||
).validate(PureXML.Model.TreeNode(context.subject), at: [.element(root.name.description)])
|
||||
},
|
||||
when: { $0.codingPath.isEmpty },
|
||||
)
|
||||
|
||||
@ -1,7 +1,35 @@
|
||||
extension PureXML.Schema.XSDParser {
|
||||
/// The `xs:anyAttribute` wildcard declared directly under `node`, if any.
|
||||
static func attributeWildcard(under node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.Wildcard? {
|
||||
PureXML.Schema.XSDNode.firstChild(node, named: "anyAttribute").map { wildcard($0, context) }
|
||||
/// The effective `xs:anyAttribute` wildcard declared under `node`, including
|
||||
/// any `anyAttribute` on nested `attributeGroup` references.
|
||||
static func attributeWildcard(
|
||||
under node: XSDTree,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
visited: Set<String> = [],
|
||||
) -> PureXML.Schema.Wildcard? {
|
||||
var combined: PureXML.Schema.Wildcard?
|
||||
if let direct = PureXML.Schema.XSDNode.firstChild(node, named: "anyAttribute").map({ wildcard($0, context) }) {
|
||||
combined = PureXML.Schema.Wildcard.union(combined, direct)
|
||||
}
|
||||
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
||||
guard PureXML.Schema.XSDNode.localName(child) == "attributeGroup",
|
||||
let ref = PureXML.Schema.XSDNode.attribute(child, "ref")
|
||||
else { continue }
|
||||
let name = PureXML.Schema.XSDNode.stripPrefix(ref)
|
||||
if visited.contains(name) {
|
||||
guard context.redefinedAttributeGroups.contains(name),
|
||||
let base = context.baseAttributeGroups[name],
|
||||
let found = attributeWildcard(under: base, context, visited: visited)
|
||||
else { continue }
|
||||
combined = PureXML.Schema.Wildcard.union(combined, found)
|
||||
continue
|
||||
}
|
||||
guard let group = context.attributeGroups[name] else { continue }
|
||||
let scoped = context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(group))
|
||||
if let found = attributeWildcard(under: group, scoped, visited: visited.union([name])) {
|
||||
combined = PureXML.Schema.Wildcard.union(combined, found)
|
||||
}
|
||||
}
|
||||
return combined
|
||||
}
|
||||
|
||||
/// Parses a wildcard's `namespace` and `processContents` constraints.
|
||||
|
||||
@ -36,6 +36,7 @@ extension PureXML.XPath {
|
||||
_ expression: Expression,
|
||||
over root: PureXML.Model.TreeNode,
|
||||
functions: FunctionTable = FunctionTable(),
|
||||
namespaces: [String: String] = [:],
|
||||
) -> [PureXML.Model.TreeNode] {
|
||||
let context = EvaluationContext(
|
||||
node: .tree(root),
|
||||
@ -43,6 +44,7 @@ extension PureXML.XPath {
|
||||
size: 1,
|
||||
variables: [:],
|
||||
functions: library.merging(functions),
|
||||
namespaces: namespaces,
|
||||
)
|
||||
guard let value = try? eval(expression, context), case let .nodeSet(nodes) = value else {
|
||||
return []
|
||||
|
||||
@ -45,12 +45,20 @@ public extension PureXML.XPath {
|
||||
try Evaluator.value(expression, over: node, variables: variables)
|
||||
}
|
||||
|
||||
/// Evaluates the query over a pre-built tree and returns the matched tree
|
||||
/// nodes in document order. Build the tree once (``PureXML/parseTree(_:)``)
|
||||
/// and query it repeatedly; pair with ``value(at:position:size:variables:)``
|
||||
/// to evaluate further expressions relative to each result.
|
||||
public func nodes(over root: PureXML.Model.TreeNode, namespaces: [String: String] = [:]) -> [PureXML.Model.TreeNode] {
|
||||
Evaluator.nodes(expression, over: root, namespaces: namespaces)
|
||||
}
|
||||
|
||||
/// Evaluates the query over a pre-built tree and returns the matched tree
|
||||
/// nodes in document order. Build the tree once (``PureXML/parseTree(_:)``)
|
||||
/// and query it repeatedly; pair with ``value(at:position:size:variables:)``
|
||||
/// to evaluate further expressions relative to each result.
|
||||
public func nodes(over root: PureXML.Model.TreeNode) -> [PureXML.Model.TreeNode] {
|
||||
Evaluator.nodes(expression, over: root)
|
||||
nodes(over: root, namespaces: [:])
|
||||
}
|
||||
|
||||
/// Evaluates the query against an explicit context: a node already in a
|
||||
|
||||
@ -44,6 +44,16 @@ struct RegexCategoryTests {
|
||||
#expect(try !matches("\\p{IsBasicLatin}+", "abç"))
|
||||
}
|
||||
|
||||
@Test("\\p{IsArabic} tests the Arabic block")
|
||||
func test_arabicBlock() throws {
|
||||
#expect(try matches("\\p{IsArabic}+", "\u{0627}\u{0628}"))
|
||||
// Adjacent Arabic block scalars that Swift fuses into one grapheme cluster.
|
||||
#expect(try matches("\\p{IsArabic}+", "\u{0600}\u{0601}"))
|
||||
// Base letter plus combining mark: two XSD characters, one grapheme.
|
||||
#expect(try matches("\\p{IsArabic}+", "\u{0627}\u{064B}"))
|
||||
#expect(try !matches("\\p{IsArabic}+", "abc"))
|
||||
}
|
||||
|
||||
@Test("An unknown category name is a compile error")
|
||||
func test_unknownCategory() {
|
||||
#expect(throws: PureXML.Regex.RegexError.self) {
|
||||
|
||||
@ -82,6 +82,14 @@ struct RegexTests {
|
||||
#expect(try !matches("\\w", " "))
|
||||
#expect(try matches("a\\sb", "a b"))
|
||||
#expect(try matches("\\D+", "abc"))
|
||||
// XSTS reT51: Tamil digit U+0BE6 was unassigned in Unicode 3.1, so it matches \D.
|
||||
#expect(try matches("\\D", "\u{0BE6}"))
|
||||
// XSTS reZ003v: ceiling brackets were Sm in Unicode 3.1, so they match `\w`.
|
||||
#expect(try matches("\\w", "\u{2308}"))
|
||||
#expect(try matches("[\\w]", "\u{2308}"))
|
||||
#expect(try matches("[\\w]", "\u{2309}"))
|
||||
// XSTS reZ004v: decimal digits from other scripts match `\d` via Nd.
|
||||
#expect(try matches("\\d", "\u{0661}"))
|
||||
}
|
||||
|
||||
@Test("Escaped metacharacters are literal")
|
||||
@ -102,7 +110,14 @@ struct RegexTests {
|
||||
func test_errors() {
|
||||
#expect(throws: PureXML.Regex.RegexError.self) { _ = try PureXML.Regex.Pattern("a(b") }
|
||||
#expect(throws: PureXML.Regex.RegexError.self) { _ = try PureXML.Regex.Pattern("*ab") }
|
||||
#expect(throws: PureXML.Regex.RegexError.self) { _ = try PureXML.Regex.Pattern("") }
|
||||
}
|
||||
|
||||
@Test("The empty pattern is valid and matches only the empty string")
|
||||
func test_emptyPattern() throws {
|
||||
// XSD grammar (Datatypes Appendix F): branch ::= piece*, so a branch
|
||||
// may have zero pieces. The empty regex is valid and matches "".
|
||||
#expect(try matches("", ""))
|
||||
#expect(try !matches("", "a"))
|
||||
}
|
||||
|
||||
@Test("Pathological quantifiers compile bounded instead of exhausting memory")
|
||||
|
||||
117
Tests/SchemaXSTSDiagnosticTests.swift
Normal file
117
Tests/SchemaXSTSDiagnosticTests.swift
Normal file
@ -0,0 +1,117 @@
|
||||
import Foundation
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
/// Opt-in diagnostics for XSTS valid-instance rejections. Set `XSTS_ROOT` and
|
||||
/// `XSTS_DIAG=1` to print validation errors for every remaining #146 failure.
|
||||
@Suite("XSTS diagnostics (opt-in via XSTS_ROOT and XSTS_DIAG)")
|
||||
struct SchemaXSTSDiagnosticTests {
|
||||
private var root: String? {
|
||||
ProcessInfo.processInfo.environment["XSTS_ROOT"]
|
||||
}
|
||||
|
||||
@Test("Print errors for remaining valid-instance rejections")
|
||||
func test_printFailures() throws {
|
||||
guard ProcessInfo.processInfo.environment["XSTS_DIAG"] != nil, let root else { return }
|
||||
let failures = try String(contentsOfFile: "/tmp/xsts-failures.txt", encoding: .utf8)
|
||||
.split(separator: "\n")
|
||||
.compactMap { line -> String? in
|
||||
guard line.contains("valid instance rejected") else { return nil }
|
||||
return String(line.split(separator: ":")[0])
|
||||
}
|
||||
let suite = try PureXML.parse(String(contentsOfFile: root + "/suite.xml", encoding: .utf8), limits: .init(allowDoctype: true))
|
||||
for sample in failures {
|
||||
let errors = try runSample(sample, root: root, suite: suite)
|
||||
if !errors.isEmpty {
|
||||
print("\(sample): \(errors.map(\.description).joined(separator: " | "))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func runSample(_ sample: String, root: String, suite: PureXML.Model.Node) throws -> [PureXML.Validation.ValidationError] {
|
||||
let parts = sample.split(separator: "/", maxSplits: 1)
|
||||
guard parts.count == 2,
|
||||
let (group, dir) = findGroup(named: String(parts[0]), root: root, suite: suite)
|
||||
else { return [] }
|
||||
var schema: PureXML.Schema.Document?
|
||||
for schemaTest in elements(named: "schemaTest", under: .element(group)) {
|
||||
for href in references(in: schemaTest, named: "schemaDocument") {
|
||||
let schemaPath = resolve(href, against: dir)
|
||||
let schemaDirectory = directory(of: schemaPath)
|
||||
let loader: (String) -> String? = { (try? String(contentsOfFile: resolve($0, against: schemaDirectory), encoding: .utf8)) }
|
||||
guard let source = try? String(contentsOfFile: schemaPath, encoding: .utf8),
|
||||
let compiled = try? PureXML.Schema.Document(source, schemaLoader: loader)
|
||||
else { continue }
|
||||
schema = schema.map { $0.merged(with: compiled) } ?? compiled
|
||||
}
|
||||
}
|
||||
guard let schema else { return [] }
|
||||
for instanceTest in elements(named: "instanceTest", under: .element(group)) {
|
||||
guard attribute("name", of: instanceTest) == String(parts[1]) else { continue }
|
||||
guard let href = references(in: instanceTest, named: "instanceDocument").first else { continue }
|
||||
let instancePath = resolve(href, against: dir)
|
||||
guard let xml = try? String(contentsOfFile: instancePath, encoding: .utf8) else { continue }
|
||||
let instanceDirectory = directory(of: instancePath)
|
||||
let loader: (String) -> String? = { (try? String(contentsOfFile: resolve($0, against: instanceDirectory), encoding: .utf8)) }
|
||||
return (try? schema.validate(xml, schemaLoader: loader)) ?? []
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
private func findGroup(named name: String, root: String, suite: PureXML.Model.Node) -> (PureXML.Model.Element, String)? {
|
||||
guard case let .document(suiteChildren) = suite else { return nil }
|
||||
for child in suiteChildren {
|
||||
guard case let .element(suiteElement) = child else { continue }
|
||||
for setHref in references(in: suiteElement, named: "testSetRef") {
|
||||
let setPath = root + "/" + setHref
|
||||
let setDirectory = directory(of: setPath)
|
||||
guard let setText = try? String(contentsOfFile: setPath, encoding: .utf8),
|
||||
let setNode = try? PureXML.parse(setText, limits: .init(allowDoctype: true))
|
||||
else { continue }
|
||||
for group in elements(named: "testGroup", under: setNode) where attribute("name", of: group) == name {
|
||||
return (group, setDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func elements(named name: String, under node: PureXML.Model.Node) -> [PureXML.Model.Element] {
|
||||
var found: [PureXML.Model.Element] = []
|
||||
switch node {
|
||||
case let .document(children): for child in children {
|
||||
found += elements(named: name, under: child)
|
||||
}
|
||||
case let .element(element):
|
||||
if element.name.localName == name { found.append(element) }
|
||||
for child in element.children {
|
||||
found += elements(named: name, under: child)
|
||||
}
|
||||
default: break
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
private func references(in element: PureXML.Model.Element, named name: String) -> [String] {
|
||||
elements(named: name, under: .element(element)).compactMap {
|
||||
$0.attributes.first { $0.name.localName == "href" }?.value
|
||||
}
|
||||
}
|
||||
|
||||
private func attribute(_ name: String, of element: PureXML.Model.Element) -> String? {
|
||||
element.attributes.first { $0.name.localName == name }?.value
|
||||
}
|
||||
|
||||
private func directory(of path: String) -> String {
|
||||
guard let slash = path.lastIndex(of: "/") else { return "." }
|
||||
return String(path[..<slash])
|
||||
}
|
||||
|
||||
private func resolve(_ href: String, against base: String) -> String {
|
||||
var parts = base.split(separator: "/", omittingEmptySubsequences: false).map(String.init)
|
||||
for segment in href.split(separator: "/") {
|
||||
if segment == ".." { if !parts.isEmpty { parts.removeLast() } } else if segment != "." { parts.append(String(segment)) }
|
||||
}
|
||||
return parts.joined(separator: "/")
|
||||
}
|
||||
}
|
||||
183
Tests/SchemaXSTSRegressionTests.swift
Normal file
183
Tests/SchemaXSTSRegressionTests.swift
Normal file
@ -0,0 +1,183 @@
|
||||
import Foundation
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
/// Opt-in regressions drawn from XSTS clusters fixed for #146. Requires
|
||||
/// `XSTS_ROOT` pointing at the 2006-11-06 suite (same as ``XSTSSuiteTests``).
|
||||
@Suite("XSTS instance regressions (opt-in via XSTS_ROOT)")
|
||||
struct SchemaXSTSRegressionTests {
|
||||
private var root: String? {
|
||||
ProcessInfo.processInfo.environment["XSTS_ROOT"]
|
||||
}
|
||||
|
||||
@Test("attgD019: attributeGroup anyAttribute is inherited")
|
||||
func test_attgD019() throws {
|
||||
guard let root else { return }
|
||||
let base = root.appending("/msData/attributeGroup")
|
||||
let mainXSD = try String(contentsOfFile: "\(base)/attgD019.xsd", encoding: .utf8)
|
||||
let otherXSD = try String(contentsOfFile: "\(base)/attgD019a.xsd", encoding: .utf8)
|
||||
let xml = try String(contentsOfFile: "\(base)/attgD019.xml", encoding: .utf8)
|
||||
let doc = try PureXML.Schema.Document(mainXSD)
|
||||
let loader: (String) -> String? = { loc in loc.hasSuffix("attgD019a.xsd") ? otherXSD : nil }
|
||||
#expect(try doc.validate(xml, schemaLoader: loader).isEmpty)
|
||||
}
|
||||
|
||||
@Test("schA1: multi-schema xsi:schemaLocation with distinct ct-A variants")
|
||||
func test_schA1() throws {
|
||||
guard let root else { return }
|
||||
let base = root.appending("/msData/schema")
|
||||
let mainXSD = try String(contentsOfFile: "\(base)/schA1_a.xsd", encoding: .utf8)
|
||||
let xml = try String(contentsOfFile: "\(base)/schA1.xml", encoding: .utf8)
|
||||
let doc = try PureXML.Schema.Document(mainXSD)
|
||||
let loader: (String) -> String? = { loc in
|
||||
if loc.hasSuffix("schA1_b.xsd") {
|
||||
return try? String(contentsOfFile: "\(base)/schA1_b.xsd", encoding: .utf8)
|
||||
}
|
||||
if loc.hasSuffix("schA1_c.xsd") {
|
||||
return try? String(contentsOfFile: "\(base)/schA1_c.xsd", encoding: .utf8)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
#expect(try doc.validate(xml, schemaLoader: loader).isEmpty)
|
||||
}
|
||||
|
||||
@Test("ipo2: imported address schema with default XSD namespace")
|
||||
func test_ipo2() throws {
|
||||
guard let root else { return }
|
||||
let base = root.appending("/boeingData/ipo2")
|
||||
let mainXSD = try String(contentsOfFile: "\(base)/ipo.xsd", encoding: .utf8)
|
||||
let xml = try String(contentsOfFile: "\(base)/ipo_1.xml", encoding: .utf8)
|
||||
let addXSD = try String(contentsOfFile: "\(base)/address.xsd", encoding: .utf8)
|
||||
let doc = try PureXML.Schema.Document(mainXSD, schemaLoader: { loc in loc == "address.xsd" ? addXSD : nil })
|
||||
let loader: (String) -> String? = { loc in
|
||||
loc == "address.xsd" ? addXSD : (loc.hasSuffix("ipo.xsd") ? mainXSD : nil)
|
||||
}
|
||||
#expect(try doc.validate(xml, schemaLoader: loader).isEmpty)
|
||||
}
|
||||
|
||||
@Test("attgC007: redefined attributeGroup self-reference merges base attrs")
|
||||
func test_attgC007() throws {
|
||||
guard let root else { return }
|
||||
let base = root.appending("/msData/attributeGroup")
|
||||
let mainXSD = try String(contentsOfFile: "\(base)/attgC007.xsd", encoding: .utf8)
|
||||
let xml = try String(contentsOfFile: "\(base)/attgC007.xml", encoding: .utf8)
|
||||
let redXSD = try String(contentsOfFile: "\(base)/attgC007vRed.xsd", encoding: .utf8)
|
||||
let loader: (String) -> String? = { loc in loc.hasSuffix("attgC007vRed.xsd") ? redXSD : nil }
|
||||
let doc = try PureXML.Schema.Document(mainXSD, schemaLoader: loader)
|
||||
#expect(try doc.validate(xml, schemaLoader: loader).isEmpty)
|
||||
}
|
||||
|
||||
@Test("idF025: redefined complexType extension keeps base particles")
|
||||
func test_idF025() throws {
|
||||
guard let root else { return }
|
||||
let base = root.appending("/msData/identityConstraint")
|
||||
let mainXSD = try String(contentsOfFile: "\(base)/idF025.xsd", encoding: .utf8)
|
||||
let xml = try String(contentsOfFile: "\(base)/idF025.xml", encoding: .utf8)
|
||||
let redXSD = try String(contentsOfFile: "\(base)/idF025.red", encoding: .utf8)
|
||||
let loader: (String) -> String? = { loc in loc.hasSuffix("idF025.red") ? redXSD : nil }
|
||||
let doc = try PureXML.Schema.Document(mainXSD, schemaLoader: loader)
|
||||
#expect(try doc.validate(xml, schemaLoader: loader).isEmpty)
|
||||
}
|
||||
|
||||
@Test("identity test suite 001: key/keyref decimal value space")
|
||||
func test_identity001() throws {
|
||||
guard let root else { return }
|
||||
let base = root.appending("/sunData/combined/identity/IdentityTestSuite/001")
|
||||
let xsd = try String(contentsOfFile: "\(base)/test.xsd", encoding: .utf8)
|
||||
let xml = try String(contentsOfFile: "\(base)/test.1.v.xml", encoding: .utf8)
|
||||
#expect(try PureXML.Schema.Document(xsd).validate(xml).isEmpty)
|
||||
}
|
||||
|
||||
@Test("ipo3: xsi:type resolves imported complex type")
|
||||
func test_ipo3() throws {
|
||||
guard let root else { return }
|
||||
let base = root.appending("/boeingData/ipo3")
|
||||
let mainXSD = try String(contentsOfFile: "\(base)/ipo.xsd", encoding: .utf8)
|
||||
let xml = try String(contentsOfFile: "\(base)/ipo_1.xml", encoding: .utf8)
|
||||
let addXSD = try String(contentsOfFile: "\(base)/address.xsd", encoding: .utf8)
|
||||
let itemXSD = try String(contentsOfFile: "\(base)/itematt.xsd", encoding: .utf8)
|
||||
let loader: (String) -> String? = { loc in
|
||||
if loc.hasSuffix("address.xsd") { return addXSD }
|
||||
if loc.hasSuffix("itematt.xsd") { return itemXSD }
|
||||
return nil
|
||||
}
|
||||
let doc = try PureXML.Schema.Document(mainXSD, schemaLoader: loader)
|
||||
#expect(try doc.validate(xml, schemaLoader: loader).isEmpty)
|
||||
}
|
||||
|
||||
@Test("ipo4: redefine address type with xsi:type USAddress")
|
||||
func test_ipo4() throws {
|
||||
guard let root else { return }
|
||||
let base = root.appending("/boeingData/ipo4")
|
||||
let mainXSD = try String(contentsOfFile: "\(base)/ipo.xsd", encoding: .utf8)
|
||||
let xml = try String(contentsOfFile: "\(base)/ipo_1.xml", encoding: .utf8)
|
||||
let addXSD = try String(contentsOfFile: "\(base)/address.xsd", encoding: .utf8)
|
||||
let itemXSD = try String(contentsOfFile: "\(base)/itematt.xsd", encoding: .utf8)
|
||||
let loader: (String) -> String? = { loc in
|
||||
if loc.hasSuffix("address.xsd") { return addXSD }
|
||||
if loc.hasSuffix("itematt.xsd") { return itemXSD }
|
||||
return nil
|
||||
}
|
||||
let doc = try PureXML.Schema.Document(mainXSD, schemaLoader: loader)
|
||||
let errors = try doc.validate(xml, schemaLoader: loader)
|
||||
if !errors.isEmpty {
|
||||
print("ipo4 errors:", errors.map(\.description).joined(separator: " | "))
|
||||
}
|
||||
#expect(errors.isEmpty)
|
||||
}
|
||||
|
||||
@Test("identity test suite 002: QName key/keyref value space")
|
||||
func test_identity002() throws {
|
||||
guard let root else { return }
|
||||
let base = root.appending("/sunData/combined/identity/IdentityTestSuite/002")
|
||||
let xsd = try String(contentsOfFile: "\(base)/test.xsd", encoding: .utf8)
|
||||
let xml = try String(contentsOfFile: "\(base)/test.2.v.xml", encoding: .utf8)
|
||||
let errors = try PureXML.Schema.Document(xsd).validate(xml)
|
||||
if !errors.isEmpty {
|
||||
print("identity002 errors:", errors.map(\.description).joined(separator: " | "))
|
||||
}
|
||||
#expect(errors.isEmpty)
|
||||
}
|
||||
|
||||
@Test("identity test suite 003: decimal key/keyref with union field xpath")
|
||||
func test_identity003() throws {
|
||||
guard let root else { return }
|
||||
let base = root.appending("/sunData/combined/identity/IdentityTestSuite/003")
|
||||
let xsd = try String(contentsOfFile: "\(base)/test.xsd", encoding: .utf8)
|
||||
let xml = try String(contentsOfFile: "\(base)/test.1.v.xml", encoding: .utf8)
|
||||
#expect(try PureXML.Schema.Document(xsd).validate(xml).isEmpty)
|
||||
}
|
||||
|
||||
@Test("particlesZ003: imported element ref resolves namespace prefix on node")
|
||||
func test_particlesZ003() throws {
|
||||
guard let root else { return }
|
||||
let base = root.appending("/msData/particles")
|
||||
let mainXSD = try String(contentsOfFile: "\(base)/particlesZ003.xsd", encoding: .utf8)
|
||||
let xml = try String(contentsOfFile: "\(base)/particlesZ003.xml", encoding: .utf8)
|
||||
let impXSD = try String(contentsOfFile: "\(base)/particlesZ003.imp", encoding: .utf8)
|
||||
let loader: (String) -> String? = { loc in loc.hasSuffix("particlesZ003.imp") ? impXSD : nil }
|
||||
let doc = try PureXML.Schema.Document(mainXSD, schemaLoader: loader)
|
||||
#expect(try doc.validate(xml, schemaLoader: loader).isEmpty)
|
||||
}
|
||||
|
||||
@Test("ctE018: chameleon include promotes type to parent target namespace")
|
||||
func test_ctE018() throws {
|
||||
guard let root else { return }
|
||||
let base = root.appending("/msData/complexType")
|
||||
let mainXSD = try String(contentsOfFile: "\(base)/ctE018_a.xsd", encoding: .utf8)
|
||||
let xml = try String(contentsOfFile: "\(base)/ctE018.xml", encoding: .utf8)
|
||||
let incXSD = try String(contentsOfFile: "\(base)/ctE018_b.xsd", encoding: .utf8)
|
||||
let loader: (String) -> String? = { loc in loc.hasSuffix("ctE018_b.xsd") ? incXSD : nil }
|
||||
let doc = try PureXML.Schema.Document(mainXSD, schemaLoader: loader)
|
||||
#expect(try doc.validate(xml, schemaLoader: loader).isEmpty)
|
||||
}
|
||||
|
||||
@Test("Arabic block pattern matches per Unicode scalar")
|
||||
func test_arabicBlockPattern() throws {
|
||||
guard let root else { return }
|
||||
let base = root.appending("/msData/regex")
|
||||
let xsd = try String(contentsOfFile: "\(base)/Arabic.xsd", encoding: .utf8)
|
||||
let xml = try String(contentsOfFile: "\(base)/Arabic.xml", encoding: .utf8)
|
||||
#expect(try PureXML.Schema.Document(xsd).validate(xml).isEmpty)
|
||||
}
|
||||
}
|
||||
@ -95,7 +95,7 @@ struct XSDDatatypeTests {
|
||||
|
||||
@Test("pattern facets use the regex engine")
|
||||
func test_patternFacet() {
|
||||
let type = Schema.SimpleType(base: .string, facets: .init(patterns: ["[A-Z]{3}-\\d{2}"]))
|
||||
let type = Schema.SimpleType(base: .string, facets: .init(patternGroups: [["[A-Z]{3}-\\d{2}"]]))
|
||||
#expect(type.isValid("XYZ-42") && !type.isValid("xyz-42"))
|
||||
}
|
||||
|
||||
|
||||
@ -28,10 +28,30 @@ struct XSTSSuiteTests {
|
||||
/// `swift test` are unaffected; run it with `swift test -c release --filter
|
||||
/// XSTS` (debug is far slower and the corpus is large). Per-case deviations
|
||||
/// are written to /tmp/xsts-failures.txt for the burn-down.
|
||||
///
|
||||
/// Re-measured 2026-06-16 after the harness was corrected to compile and merge
|
||||
/// ALL of a schemaTest's schemaDocuments (it previously loaded only the first,
|
||||
/// under-loading any multi-document schema). That removed a spurious valid-
|
||||
/// instance rejection (wildZ003, whose root element is declared in the second
|
||||
/// document) and exposed one previously-masked invalid-instance acceptance
|
||||
/// (schA5, a schema-collection test the engine wrongly accepts once the full
|
||||
/// collection is loaded): instances rejected 8 → 7, invalid accepted 146 → 147.
|
||||
///
|
||||
/// Pinning the regex repertoire to Unicode 3.2 (the version the corpus is
|
||||
/// consistent at) then cleared reZ003v, whose `\w` instance is all code points
|
||||
/// assigned by 3.2: instances rejected 7 → 6, no other bucket moved.
|
||||
///
|
||||
/// Driving per-child assessment from the matched content-model particle rather
|
||||
/// than a by-name lookup (#180) then cleared isDefault079 (two same-named
|
||||
/// particles with different `fixed` values) and QFE1700c2 (an element matching
|
||||
/// a `skip` wildcard must not be validated against its global declaration):
|
||||
/// instances rejected 6 → 4, no other bucket moved. The remaining four are the
|
||||
/// three Ethiopic-digit `\d` cases (reS17, reS38, reZ004v) and ctZ007 (a
|
||||
/// cross-document substitution group, tracked under #180/#161).
|
||||
private let knownSchemaValidRejected = 1
|
||||
private let knownSchemaInvalidAccepted = 264
|
||||
private let knownInstanceValidRejected = 173
|
||||
private let knownInstanceInvalidAccepted = 155
|
||||
private let knownSchemaInvalidAccepted = 262
|
||||
private let knownInstanceValidRejected = 4
|
||||
private let knownInstanceInvalidAccepted = 147
|
||||
|
||||
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
|
||||
func test_suite() throws {
|
||||
@ -99,18 +119,33 @@ struct XSTSSuiteTests {
|
||||
var schemaExpectedValid = true
|
||||
for schemaTest in elements(named: "schemaTest", under: .element(group)) {
|
||||
guard isAccepted(schemaTest) else { continue }
|
||||
guard let first = references(in: .element(schemaTest), named: "schemaDocument").first else { continue }
|
||||
let hrefs = references(in: .element(schemaTest), named: "schemaDocument")
|
||||
guard !hrefs.isEmpty else { continue }
|
||||
schemaExpectedValid = expectedValidity(schemaTest) != "invalid"
|
||||
let schemaPath = resolve(first, against: directory)
|
||||
let schemaDirectory = self.directory(of: schemaPath)
|
||||
let loader: (String) -> String? = { location in
|
||||
(try? String(contentsOfFile: resolve(location, against: schemaDirectory), encoding: .utf8))
|
||||
// A schemaTest's documents jointly form the schema under test (one is
|
||||
// typically the entry point that imports the others; their order in the
|
||||
// metadata is not significant). Compile each and merge so the schema is
|
||||
// complete regardless of which document declares the root.
|
||||
var compiled: PureXML.Schema.Document?
|
||||
var allCompiled = true
|
||||
for href in hrefs {
|
||||
let schemaPath = resolve(href, against: directory)
|
||||
let schemaDirectory = self.directory(of: schemaPath)
|
||||
let loader: (String) -> String? = { location in
|
||||
(try? String(contentsOfFile: resolve(location, against: schemaDirectory), encoding: .utf8))
|
||||
}
|
||||
guard let source = try? String(contentsOfFile: schemaPath, encoding: .utf8) else {
|
||||
counters.note("\(name): unreadable schema \(href)")
|
||||
allCompiled = false
|
||||
continue
|
||||
}
|
||||
guard let document = try? PureXML.Schema.Document(source, schemaLoader: loader) else {
|
||||
allCompiled = false
|
||||
continue
|
||||
}
|
||||
compiled = compiled.map { $0.merged(with: document) } ?? document
|
||||
}
|
||||
guard let source = try? String(contentsOfFile: schemaPath, encoding: .utf8) else {
|
||||
counters.note("\(name): unreadable schema \(first)")
|
||||
continue
|
||||
}
|
||||
let compiled = try? PureXML.Schema.Document(source, schemaLoader: loader)
|
||||
if !allCompiled { compiled = nil }
|
||||
switch (schemaExpectedValid, compiled == nil) {
|
||||
case (true, true):
|
||||
counters.schemaValidRejected += 1
|
||||
|
||||
@ -18,9 +18,9 @@ Parallel: **#130** (XSLT suite), **#139** (performance epic; tasks [#175](https:
|
||||
| Gate | Now | Issue |
|
||||
|---|---|---|
|
||||
| Valid schemas rejected | 1 | [#148](https://github.com/mihaelamj/PureXML/issues/148) |
|
||||
| Invalid schemas accepted | 264 | [#145](https://github.com/mihaelamj/PureXML/issues/145) |
|
||||
| Valid instances rejected | 173 | [#146](https://github.com/mihaelamj/PureXML/issues/146) |
|
||||
| Invalid instances accepted | 155 | [#147](https://github.com/mihaelamj/PureXML/issues/147) |
|
||||
| Invalid schemas accepted | 261 | [#145](https://github.com/mihaelamj/PureXML/issues/145) |
|
||||
| Valid instances rejected | 99 | [#146](https://github.com/mihaelamj/PureXML/issues/146) |
|
||||
| Invalid instances accepted | 147 | [#147](https://github.com/mihaelamj/PureXML/issues/147) |
|
||||
|
||||
Gates are **metric trackers**, not implementation tasks. Pick up **task** issues; ratchet baselines in `Tests/XSTSSuiteTests.swift` when counts fall.
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user