diff --git a/CHANGELOG.md b/CHANGELOG.md index de205ee..67a437f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- XSTS invalid-schemas-accepted (#145): a particle restriction whose restricting element has a union-typed base element now applies Type Derivation OK (Simple) (`cos-st-derived-ok`) rather than structural member subsetting. The NameAndTypeOK type clause (ยง3.9.6) requires the restricting element's type to be validly derived from the base element's type; PureXML accepted a *separately declared* union whenever its member types were a subset of the base union's members, which is not the spec's derivation relation. The restricting type is now valid over a union base only if it is the base type, derives from it through a declared restriction chain, or (clause 2.2.4) validly derives from one of the union's member types. Catches particlesIk011 (a fresh `union(integer)` is not a restriction of `union(integer, string)`): invalid-schemas-accepted 9 to 8, with valid-schemas-rejected held at 0 (verified over the full XSTS; the change is scoped to the element-restriction path, so no attribute or simple-type derivation check moved). Disclosed divergence: particlesIk025/Ik027, where the restricting type derives from a union *member*, are accepted per `cos-st-derived-ok` clause 2.2.4, a contested reading their XSTS `invalid` expectation does not share. + ## [0.4.8] - 2026-06-27 ### Removed diff --git a/Sources/Schema/ParticleRestrictionElement.swift b/Sources/Schema/ParticleRestrictionElement.swift index 2be6d89..8a96b39 100644 --- a/Sources/Schema/ParticleRestrictionElement.swift +++ b/Sources/Schema/ParticleRestrictionElement.swift @@ -41,8 +41,17 @@ extension PureXML.Schema.ParticleRestriction { if let baseResolved = resolveType(baseTypeName, types), let derivedResolved = resolveType(restrictedTypeName, types) { switch (derivedResolved, baseResolved) { case let (.simple(derivedSimple), .simple(baseSimple)): - if case .union = baseSimple.variety { - return PureXML.Schema.XSDParser.isSimpleTypeRestrictionOK(derived: derivedSimple, base: baseSimple) + if case let .union(baseMembers) = baseSimple.variety { + // cos-st-derived-ok with a union base: the restricting type + // validly derives iff it is the base type, derives from it + // through a declared restriction chain (typeDerivesByRestriction + // returns true for an equal name), or (clause 2.2.4) validly + // derives from one of the union's member types. A separately + // declared union whose members merely subset the base union's is + // NOT a derivation (particlesIk011); structural member subsetting + // is not the spec's derivation relation. + if typeDerivesByRestriction(restrictedTypeName, baseTypeName, derivation, types) { return true } + return baseMembers.contains { PureXML.Schema.XSDParser.isSimpleTypeRestrictionOK(derived: derivedSimple, base: $0) } } case (.complex, .simple): return false diff --git a/Tests/XSDParticleRestrictionTests.swift b/Tests/XSDParticleRestrictionTests.swift index 4001ae6..2776e0e 100644 --- a/Tests/XSDParticleRestrictionTests.swift +++ b/Tests/XSDParticleRestrictionTests.swift @@ -303,14 +303,18 @@ struct XSDParticleRestrictionTests { // a union member (xs:string) validly restricts the union-typed base element. #expect(doc("xs:string", base: "u", types: userTypes) != nil) - // a union restricting another union (subset members): valid. let unions = """ """ - #expect(doc("U1", base: "U2", types: unions) != nil) - // a union restricting another union but adding/having unmatching members: invalid. + // A separately-declared union whose members merely subset the base union's + // is NOT a valid derivation: cos-st-derived-ok requires the restricting + // type to be the base, derive from it through a declared restriction chain, + // or derive from one of its members. Structural member subsetting is not + // the spec's derivation relation (cf. XSTS particlesIk011). + #expect(doc("U1", base: "U2", types: unions) == nil) + // The reverse (U2 has a member U1 lacks) is invalid for the same reason. #expect(doc("U2", base: "U1", types: unions) == nil) // a list type restricting a union type: invalid. #expect(doc("L1", base: "U1", types: unions) == nil) diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 2327b9a..c99f623 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -44,7 +44,13 @@ struct XSTSSuiteTests { /// a base `required` attribute may not become `optional`) now also runs over a /// `simpleContent` restriction, not only `complexContent` (particlesZ030_d, /// which libxml2 also rejects). - private let knownSchemaInvalidAccepted = 9 + /// 9 -> 8, NameAndTypeOK over a union base now applies cos-st-derived-ok: a + /// separately-declared union whose members merely subset the base union's is + /// not a valid derivation (particlesIk011); a genuine restriction of the union + /// or of one of its members still passes. (Ik025/Ik027, where the restricting + /// type derives from a union member, are accepted per cos-st-derived-ok 2.2.4, + /// a disclosed divergence from their contested XSTS `invalid` expectation.) + private let knownSchemaInvalidAccepted = 8 private let knownInstanceValidRejected = 0 /// Instance bucket (133 -> 22), per-step deltas in CHANGELOG.md: xsi:type must derive from the declared /// type; anyType cannot stand in for anySimpleType; an untyped substitutionGroup member inherits its head's