From 4fad61228116191bed8f5878adbcb2ada62049b7 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Mon, 8 Jun 2026 14:14:23 +0200 Subject: [PATCH] feat(schema): XSD derivation control (abstract, block, final, redefine) Read and enforce the XSD 1.0 derivation controls that were previously ignored: - abstract elements may not appear in an instance directly, only through a substitution member; an element of an abstract complex type must supply an xsi:type naming a concrete derived type. - block on a type forbids the disallowed xsi:type substitutions; block on an element drops substitution-group members that derive by a blocked method. - final on a type rejects, at compile time, any type that derives from it by a final method. - xs:redefine requires the redefined type to derive from itself. Track each named complex type's base and derivation method as the backbone these checks walk. Also compose complexContent extension and restriction content models, which were previously dropped: an extension now inherits its base's attributes and content (base first, then the extension, in sequence). Complex-type restriction is not verified to be a structurally valid subset of its base (Particle Valid (Restriction)); that limit is documented at its sites. Closes #76 --- Sources/Schema/ComplexValidator.swift | 160 ++----------- .../Schema/ComplexValidatorDerivation.swift | 39 ++++ Sources/Schema/ComplexValidatorHelpers.swift | 140 ++++++++++++ Sources/Schema/DerivationMethod.swift | 10 + Sources/Schema/SchemaDocument.swift | 28 ++- Sources/Schema/TypeDerivation.swift | 17 ++ Sources/Schema/XSDComplexContent.swift | 77 +++++++ Sources/Schema/XSDContext.swift | 26 +++ Sources/Schema/XSDDerivation.swift | 168 ++++++++++++++ Sources/Schema/XSDParser.swift | 122 +++++----- Sources/Schema/XSDValidations.swift | 12 + Sources/Schema/XSDWildcardParser.swift | 33 +++ Tests/XSDDerivationControlTests.swift | 210 ++++++++++++++++++ Tests/XSDImportTests.swift | 2 +- 14 files changed, 836 insertions(+), 208 deletions(-) create mode 100644 Sources/Schema/ComplexValidatorDerivation.swift create mode 100644 Sources/Schema/ComplexValidatorHelpers.swift create mode 100644 Sources/Schema/DerivationMethod.swift create mode 100644 Sources/Schema/TypeDerivation.swift create mode 100644 Sources/Schema/XSDComplexContent.swift create mode 100644 Sources/Schema/XSDDerivation.swift create mode 100644 Sources/Schema/XSDWildcardParser.swift create mode 100644 Tests/XSDDerivationControlTests.swift diff --git a/Sources/Schema/ComplexValidator.swift b/Sources/Schema/ComplexValidator.swift index ca9eba2..cfdf203 100644 --- a/Sources/Schema/ComplexValidator.swift +++ b/Sources/Schema/ComplexValidator.swift @@ -16,20 +16,32 @@ public extension PureXML.Schema { /// path, and the results are ``PureXML/Validation/ValidationError`` values. struct ComplexValidator { /// The named type table that `ElementType.typeReference` resolves against. - private let types: [String: ElementType] + let types: [String: ElementType] /// Local names of elements declared `nillable="true"`. private let nillableElements: Set /// The `default`/`fixed` value constraint declared on each element name. private let elementConstraints: [String: ValueConstraint] + /// Local names of complex types declared `abstract="true"`. + let abstractTypes: Set + /// Derivation methods each named type forbids through `xsi:type`. + let typeBlock: [String: Set] + /// Each named complex type's base and derivation method. + let typeDerivation: [String: TypeDerivation] public init( types: [String: ElementType] = [:], nillableElements: Set = [], elementConstraints: [String: ValueConstraint] = [:], + abstractTypes: Set = [], + typeBlock: [String: Set] = [:], + typeDerivation: [String: TypeDerivation] = [:], ) { self.types = types self.nillableElements = nillableElements self.elementConstraints = elementConstraints + self.abstractTypes = abstractTypes + self.typeBlock = typeBlock + self.typeDerivation = typeDerivation } /// Validates `element` against `type` at `path`, one error per violation. @@ -225,7 +237,17 @@ public extension PureXML.Schema { ) { // An instance `xsi:type` overrides the declared type, provided the named // type exists in the schema. Resolve to the type itself rather than a - // reference so re-entry does not re-read the same xsi:type. + // reference so re-entry does not re-read the same xsi:type. A `block` on + // the declared type can forbid the substitution; an abstract declared + // type requires one. + if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path) { + errors.append(blocked) + return + } + if case let .typeReference(name) = declared, let missing = abstractTypeError(named: name, child: child, at: path) { + errors.append(missing) + return + } var declared = declared if let overriding = Self.xsiTypeName(child), let resolved = types[overriding] { declared = resolved @@ -255,137 +277,3 @@ public extension PureXML.Schema { } } } - -extension PureXML.Schema.ComplexValidator { - // MARK: Helpers - - /// The coding-path step for each child: its name, with a sibling index only - /// when more than one child shares that name. - static func childSteps(_ children: [PureXML.Model.Element]) -> [PureXML.Validation.PathKey] { - var totals: [String: Int] = [:] - for child in children { - totals[child.name.description, default: 0] += 1 - } - var seen: [String: Int] = [:] - return children.map { child in - let name = child.name.description - let index = (seen[name] ?? 0) + 1 - seen[name] = index - return (totals[name] ?? 0) > 1 ? .element(name, index: index) : .element(name) - } - } - - /// Validates an `all` group order-independently: every child matches a - /// member within its occurrence bounds, and each member meets its minimum. - fileprivate static func matchesAll(_ group: XSDGroup, names: [PureXML.Model.QualifiedName]) -> Bool { - var counts = [Int](repeating: 0, count: group.particles.count) - for name in names { - guard let index = group.particles.indices.first(where: { position in - let member = group.particles[position] - let room = member.maxOccurs.map { counts[position] < $0 } ?? true - return room && label(of: member.term).matches(name) - }) else { return false } - counts[index] += 1 - } - for (index, member) in group.particles.enumerated() where counts[index] < member.minOccurs { - return false - } - return true - } - - fileprivate static func label(of term: XSDTerm) -> XSDTermLabel { - switch term { - case let .element(name, _): .name(name) - case let .wildcard(wildcard): .wildcard(wildcard) - case .group: .wildcard(XSDWildcard()) - } - } - - /// The `processContents` of a wildcard reachable in `term`, if the content - /// model contains one. - fileprivate static func wildcard(in term: XSDTerm) -> XSDProcessContents? { - switch term { - case let .wildcard(wildcard): - return wildcard.processContents - case let .group(group): - for member in group.particles { - if let found = wildcard(in: member.term) { return found } - } - return nil - case .element: - return nil - } - } - - fileprivate static func elementTypes(in term: XSDTerm) -> [String: XSDElementType] { - var result: [String: XSDElementType] = [:] - collectTypes(term, into: &result) - return result - } - - fileprivate static func collectTypes(_ term: XSDTerm, into result: inout [String: XSDElementType]) { - switch term { - case let .element(name, type): - if let type { result[key(name)] = type } - case let .group(group): - for member in group.particles { - collectTypes(member.term, into: &result) - } - case .wildcard: - break - } - } - - static func key(_ name: PureXML.Model.QualifiedName) -> String { - "{\(name.namespaceURI ?? "")}\(name.localName)" - } - - static func textContent(_ element: PureXML.Model.Element) -> String { - let text = element.children.reduce(into: "") { result, child in - switch child { - case let .text(value), let .cdata(value): result += value - default: break - } - } - return text.trimmingXMLWhitespace() - } - - static func isNamespaceDeclaration(_ attribute: PureXML.Model.Attribute) -> Bool { - attribute.name.prefix == "xmlns" || (attribute.name.prefix == nil && attribute.name.localName == "xmlns") - } - - /// Whether an attribute belongs to the XML Schema instance namespace - /// (`xsi:type`, `xsi:nil`, and the schema-location hints), which are - /// processing directives rather than declared attributes. - static func isSchemaInstanceAttribute(_ attribute: PureXML.Model.Attribute) -> Bool { - attribute.name.namespaceURI == "http://www.w3.org/2001/XMLSchema-instance" || attribute.name.prefix == "xsi" - } - - /// The local name of an element's `xsi:type` attribute value, or nil when it - /// 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 } - } -} - -extension PureXML.Schema.ComplexValidator { - /// Whether the element carries `xsi:nil="true"`. - static func isNil(_ element: PureXML.Model.Element) -> Bool { - element.attributes.contains { attribute in - attribute.name.localName == "nil" - && (attribute.name.namespaceURI == "http://www.w3.org/2001/XMLSchema-instance" || attribute.name.prefix == "xsi") - && attribute.value == "true" - } - } - - /// Whether the element has any child element or non-whitespace text. - static func hasContent(_ element: PureXML.Model.Element) -> Bool { - !textContent(element).isEmpty || element.children.contains { if case .element = $0 { true } else { false } } - } -} diff --git a/Sources/Schema/ComplexValidatorDerivation.swift b/Sources/Schema/ComplexValidatorDerivation.swift new file mode 100644 index 0000000..64e5550 --- /dev/null +++ b/Sources/Schema/ComplexValidatorDerivation.swift @@ -0,0 +1,39 @@ +extension PureXML.Schema.ComplexValidator { + /// The error when an element's declared type is an abstract complex type and + /// the element supplies no `xsi:type` to name a concrete derived type. An + /// abstract type cannot itself be the type of an instance element. + func abstractTypeError( + named name: String, + child: PureXML.Model.Element, + at path: [PureXML.Validation.PathKey], + ) -> PureXML.Validation.ValidationError? { + guard abstractTypes.contains(name), Self.xsiTypeName(child) == nil else { return nil } + return PureXML.Validation.ValidationError( + reason: "element '\(child.name.localName)' has abstract type '\(name)' and requires an xsi:type naming a concrete derived type", + at: path, + ) + } + + /// The error when an `xsi:type` substitution is forbidden by `block` on the + /// declared type: the substituted type reaches the declared type by a method + /// the declared type lists in `block`. Returns nil when there is no named + /// declared type, no `block`, or the substitution is permitted. + func blockedSubstitutionError( + declared: PureXML.Schema.ElementType, + child: PureXML.Model.Element, + at path: [PureXML.Validation.PathKey], + ) -> PureXML.Validation.ValidationError? { + guard case let .typeReference(declaredName) = declared, + let substitute = Self.xsiTypeName(child), types[substitute] != nil, + let blocked = typeBlock[declaredName], + let methods = PureXML.Schema.XSDParser.derivationMethods(from: substitute, to: declaredName, typeDerivation), + !methods.isDisjoint(with: blocked) + else { + return nil + } + return PureXML.Validation.ValidationError( + reason: "xsi:type '\(substitute)' is blocked: type '\(declaredName)' disallows substitution by this derivation", + at: path, + ) + } +} diff --git a/Sources/Schema/ComplexValidatorHelpers.swift b/Sources/Schema/ComplexValidatorHelpers.swift new file mode 100644 index 0000000..06698e9 --- /dev/null +++ b/Sources/Schema/ComplexValidatorHelpers.swift @@ -0,0 +1,140 @@ +/// File-scope aliases for the complex-type validator's helpers, kept out of the +/// namespace to avoid nesting a type two levels deep in a dotted extension. +private typealias XSDTerm = PureXML.Schema.Term +private typealias XSDTermLabel = PureXML.Schema.TermLabel +private typealias XSDElementType = PureXML.Schema.ElementType +private typealias XSDWildcard = PureXML.Schema.Wildcard + +extension PureXML.Schema.ComplexValidator { + // MARK: Helpers + + /// The coding-path step for each child: its name, with a sibling index only + /// when more than one child shares that name. + static func childSteps(_ children: [PureXML.Model.Element]) -> [PureXML.Validation.PathKey] { + var totals: [String: Int] = [:] + for child in children { + totals[child.name.description, default: 0] += 1 + } + var seen: [String: Int] = [:] + return children.map { child in + let name = child.name.description + let index = (seen[name] ?? 0) + 1 + seen[name] = index + return (totals[name] ?? 0) > 1 ? .element(name, index: index) : .element(name) + } + } + + /// Validates an `all` group order-independently: every child matches a + /// member within its occurrence bounds, and each member meets its minimum. + static func matchesAll(_ group: PureXML.Schema.Group, names: [PureXML.Model.QualifiedName]) -> Bool { + var counts = [Int](repeating: 0, count: group.particles.count) + for name in names { + guard let index = group.particles.indices.first(where: { position in + let member = group.particles[position] + let room = member.maxOccurs.map { counts[position] < $0 } ?? true + return room && label(of: member.term).matches(name) + }) else { return false } + counts[index] += 1 + } + for (index, member) in group.particles.enumerated() where counts[index] < member.minOccurs { + return false + } + return true + } + + fileprivate static func label(of term: XSDTerm) -> XSDTermLabel { + switch term { + case let .element(name, _): .name(name) + case let .wildcard(wildcard): .wildcard(wildcard) + case .group: .wildcard(XSDWildcard()) + } + } + + /// The `processContents` of a wildcard reachable in `term`, if the content + /// model contains one. + static func wildcard(in term: PureXML.Schema.Term) -> PureXML.Schema.ProcessContents? { + switch term { + case let .wildcard(wildcard): + return wildcard.processContents + case let .group(group): + for member in group.particles { + if let found = wildcard(in: member.term) { return found } + } + return nil + case .element: + return nil + } + } + + static func elementTypes(in term: PureXML.Schema.Term) -> [String: PureXML.Schema.ElementType] { + var result: [String: XSDElementType] = [:] + collectTypes(term, into: &result) + return result + } + + fileprivate static func collectTypes(_ term: XSDTerm, into result: inout [String: XSDElementType]) { + switch term { + case let .element(name, type): + if let type { result[key(name)] = type } + case let .group(group): + for member in group.particles { + collectTypes(member.term, into: &result) + } + case .wildcard: + break + } + } + + static func key(_ name: PureXML.Model.QualifiedName) -> String { + "{\(name.namespaceURI ?? "")}\(name.localName)" + } + + static func textContent(_ element: PureXML.Model.Element) -> String { + let text = element.children.reduce(into: "") { result, child in + switch child { + case let .text(value), let .cdata(value): result += value + default: break + } + } + return text.trimmingXMLWhitespace() + } + + static func isNamespaceDeclaration(_ attribute: PureXML.Model.Attribute) -> Bool { + attribute.name.prefix == "xmlns" || (attribute.name.prefix == nil && attribute.name.localName == "xmlns") + } + + /// Whether an attribute belongs to the XML Schema instance namespace + /// (`xsi:type`, `xsi:nil`, and the schema-location hints), which are + /// processing directives rather than declared attributes. + static func isSchemaInstanceAttribute(_ attribute: PureXML.Model.Attribute) -> Bool { + attribute.name.namespaceURI == "http://www.w3.org/2001/XMLSchema-instance" || attribute.name.prefix == "xsi" + } + + /// The local name of an element's `xsi:type` attribute value, or nil when it + /// 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 } + } +} + +extension PureXML.Schema.ComplexValidator { + /// Whether the element carries `xsi:nil="true"`. + static func isNil(_ element: PureXML.Model.Element) -> Bool { + element.attributes.contains { attribute in + attribute.name.localName == "nil" + && (attribute.name.namespaceURI == "http://www.w3.org/2001/XMLSchema-instance" || attribute.name.prefix == "xsi") + && attribute.value == "true" + } + } + + /// Whether the element has any child element or non-whitespace text. + static func hasContent(_ element: PureXML.Model.Element) -> Bool { + !textContent(element).isEmpty || element.children.contains { if case .element = $0 { true } else { false } } + } +} diff --git a/Sources/Schema/DerivationMethod.swift b/Sources/Schema/DerivationMethod.swift new file mode 100644 index 0000000..09847f6 --- /dev/null +++ b/Sources/Schema/DerivationMethod.swift @@ -0,0 +1,10 @@ +public extension PureXML.Schema { + /// A way one type is derived from another, or one element substitutes for + /// another: by extension, by restriction, or (for elements) by substitution + /// group membership. Used by the `block` and `final` derivation controls. + enum DerivationMethod: Sendable, Equatable, Hashable { + case `extension` + case restriction + case substitution + } +} diff --git a/Sources/Schema/SchemaDocument.swift b/Sources/Schema/SchemaDocument.swift index 931aae2..bb03acc 100644 --- a/Sources/Schema/SchemaDocument.swift +++ b/Sources/Schema/SchemaDocument.swift @@ -2,10 +2,20 @@ public extension PureXML.Schema { /// An error compiling or applying a schema. enum SchemaError: Swift.Error, Equatable, Sendable, CustomStringConvertible { case notASchema + /// A type derives from a base that declares that derivation method `final`. + case finalViolation(type: String, base: String, method: String) + /// A type inside `xs:redefine` does not redefine itself: its restriction or + /// extension base must name the type it redefines. + case redefineIncompatible(type: String) public var description: String { switch self { - case .notASchema: "the document is not an xs:schema" + case .notASchema: + "the document is not an xs:schema" + case let .finalViolation(type, base, method): + "type '\(type)' derives from '\(base)' by \(method), which '\(base)' declares final" + case let .redefineIncompatible(type): + "redefined type '\(type)' must derive from itself" } } } @@ -18,6 +28,10 @@ public extension PureXML.Schema { private let constraints: [String: [IdentityConstraint]] private let nillableElements: Set private let elementConstraints: [String: ValueConstraint] + private let abstractTypes: Set + private let abstractElements: Set + private let typeBlock: [String: Set] + private let typeDerivation: [String: TypeDerivation] /// Compiles a schema document. `schemaLoader` resolves the /// `schemaLocation` of `xs:include`, `xs:import`, and `xs:redefine` to @@ -31,6 +45,10 @@ public extension PureXML.Schema { constraints = compiled.constraints nillableElements = compiled.nillableElements elementConstraints = compiled.elementConstraints + abstractTypes = compiled.abstractTypes + abstractElements = compiled.abstractElements + typeBlock = compiled.typeBlock + typeDerivation = compiled.typeDerivation } /// Validates an instance document against the schema, returning one located @@ -49,12 +67,20 @@ public extension PureXML.Schema { guard let declaration = elements[root.name.localName] else { return [.init(reason: "no element declaration for '\(root.name.localName)'", at: [])] } + // An abstract element may not appear in an instance directly; only a + // concrete substitution-group member may stand in its place. + if abstractElements.contains(root.name.localName) { + return [.init(reason: "abstract element '\(root.name.localName)' must not appear in an instance", at: [.element(root.name.description)])] + } let context = PureXML.Validation.XSDContext( types: types, constraints: constraints, rootDeclaration: declaration, nillableElements: nillableElements, elementConstraints: elementConstraints, + abstractTypes: abstractTypes, + typeBlock: typeBlock, + typeDerivation: typeDerivation, ) return PureXML.Validation.XSD.validator().errors(for: .element(root), in: context) } diff --git a/Sources/Schema/TypeDerivation.swift b/Sources/Schema/TypeDerivation.swift new file mode 100644 index 0000000..f5bb89c --- /dev/null +++ b/Sources/Schema/TypeDerivation.swift @@ -0,0 +1,17 @@ +public extension PureXML.Schema { + /// How a named complex type is derived from its base: the base type's local + /// name and whether the derivation is an extension or a restriction. This is + /// the backbone the `block`, `final`, and `xsi:type`-substitution checks walk + /// to decide whether a derivation is permitted. + struct TypeDerivation: Sendable, Equatable { + /// The local name of the base type this type derives from. + public var base: String + /// Whether this type extends or restricts its base. + public var method: DerivationMethod + + public init(base: String, method: DerivationMethod) { + self.base = base + self.method = method + } + } +} diff --git a/Sources/Schema/XSDComplexContent.swift b/Sources/Schema/XSDComplexContent.swift new file mode 100644 index 0000000..03e514e --- /dev/null +++ b/Sources/Schema/XSDComplexContent.swift @@ -0,0 +1,77 @@ +private typealias XSDComplexType = PureXML.Schema.ComplexType +private typealias XSDContentType = PureXML.Schema.ContentType +private typealias XSDParticle = PureXML.Schema.Particle +private typealias XSDGroup = PureXML.Schema.Group +private typealias XSDContentNode = PureXML.Schema.XSDNode + +extension PureXML.Schema.XSDParser { + /// 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 + /// restriction states the full restricted model and replaces the base. + /// + /// Note: a restriction's model is taken as given. It is NOT verified to be a + /// structurally valid subset of the base ("Particle Valid (Restriction)" in + /// XSD 1.0), which is a much larger algorithm; an unfaithful restriction is + /// accepted. + static func complexContentType(_ derivationNode: XSDTree, mixed: Bool, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.ComplexType { + let ownAttributes = attributeUses(under: derivationNode, context) + let ownWildcard = attributeWildcard(under: derivationNode, context) + let ownParticle = modelGroup(in: derivationNode, context) + guard XSDContentNode.localName(derivationNode) == "extension", let base = resolvedBase(derivationNode, context) else { + return XSDComplexType(attributes: ownAttributes, attributeWildcard: ownWildcard, content: content(ownParticle, mixed: mixed)) + } + return XSDComplexType( + attributes: base.attributes + ownAttributes, + attributeWildcard: ownWildcard ?? base.attributeWildcard, + content: extendedContent(base: base.content, own: ownParticle, mixed: mixed), + ) + } + + /// The base complex type a derivation extends, parsed from its definition + /// 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 { + return nil + } + return complexType(baseNode, context.visitingType(baseName)) + } + + /// A particle wrapped as element-only or mixed content, or empty content when + /// there is no particle. + private static func content(_ particle: XSDParticle?, mixed: Bool) -> XSDContentType { + guard let particle else { return mixed ? .mixed(emptyParticle) : .empty } + return mixed ? .mixed(particle) : .elementOnly(particle) + } + + /// The content of an extension: the base content's particle followed by the + /// extension's own particle, in a sequence. Mixed if either side is mixed. + private static func extendedContent(base: XSDContentType, own: XSDParticle?, mixed: Bool) -> XSDContentType { + let baseMixed = if case .mixed = base { true } else { false } + let combined = combine(particle(of: base), own) + return content(combined, mixed: mixed || baseMixed) + } + + private static func particle(of content: XSDContentType) -> XSDParticle? { + switch content { + case let .elementOnly(particle), let .mixed(particle): particle + case .empty, .simpleContent: nil + } + } + + private static func combine(_ first: XSDParticle?, _ second: XSDParticle?) -> XSDParticle? { + switch (first, second) { + case let (first?, second?): + XSDParticle(term: .group(XSDGroup(compositor: .sequence, particles: [first, second]))) + case let (first?, nil): first + case let (nil, second): second + } + } + + private static var emptyParticle: XSDParticle { + XSDParticle(minOccurs: 0, maxOccurs: 1, term: .group(XSDGroup(compositor: .sequence, particles: []))) + } +} diff --git a/Sources/Schema/XSDContext.swift b/Sources/Schema/XSDContext.swift index 41d6bf8..045bc7b 100644 --- a/Sources/Schema/XSDContext.swift +++ b/Sources/Schema/XSDContext.swift @@ -105,6 +105,17 @@ extension PureXML.Schema { var nillableElements: Set = [] /// The `default`/`fixed` value constraint declared on each element name. var elementConstraints: [String: ValueConstraint] = [:] + /// Local names of complex types declared `abstract="true"`: an element of + /// such a type must supply an `xsi:type` naming a concrete derived type. + var abstractTypes: Set = [] + /// Local names of element declarations declared `abstract="true"`: they may + /// not appear in an instance directly, only through a substitution member. + var abstractElements: Set = [] + /// Derivation methods the named type forbids when used through `xsi:type`. + var typeBlock: [String: Set] = [:] + /// Each named complex type's base type and derivation method, the backbone + /// the `block` check walks from an `xsi:type` to its declared type. + var typeDerivation: [String: TypeDerivation] = [:] } /// The parsing context: the named simple types resolved so far, plus the @@ -114,18 +125,33 @@ extension PureXML.Schema { var simpleTypes: [String: SimpleType] var attributeGroups: [String: XSDTree] var groups: [String: XSDTree] + /// 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] = [:] /// The schema's target namespace, for resolving `##other`/`##targetNamespace` /// in wildcard constraints. var targetNamespace: String? /// 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]] = [:] + /// 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 = [] var visitingGroups: Set = [] + /// Named complex types being resolved up the current `complexContent` + /// derivation chain, a guard against a cyclic base reference. + var visitingTypes: Set = [] func visiting(_ group: String) -> XSDContext { var copy = self copy.visitingGroups.insert(group) return copy } + + func visitingType(_ type: String) -> XSDContext { + var copy = self + copy.visitingTypes.insert(type) + return copy + } } } diff --git a/Sources/Schema/XSDDerivation.swift b/Sources/Schema/XSDDerivation.swift new file mode 100644 index 0000000..9f0918b --- /dev/null +++ b/Sources/Schema/XSDDerivation.swift @@ -0,0 +1,168 @@ +private typealias SchemaFault = PureXML.Schema.SchemaError +private typealias XSDDerivNode = PureXML.Schema.XSDNode + +extension PureXML.Schema.XSDParser { + /// The derivation-control facts gathered from a schema's definition + /// containers: which types and elements are abstract, the `block`/`final` + /// derivation methods each names, every named complex type's base and + /// derivation method, and each element's declared type name (needed to test a + /// substitution member's derivation against its head's `block`). + /// + /// Note: this records the derivation *backbone* (base + method) and enforces + /// `block`, `final`, and abstract usage. It does NOT verify that a complex-type + /// restriction is a structurally valid subset of its base ("Particle Valid + /// (Restriction)" in XSD 1.0), which is a much larger algorithm; a structurally + /// unfaithful restriction is accepted here. + struct DerivationTables { + var typeDerivation: [String: PureXML.Schema.TypeDerivation] = [:] + var abstractTypes: Set = [] + var typeBlock: [String: Set] = [:] + var typeFinal: [String: Set] = [:] + var abstractElements: Set = [] + var elementBlock: [String: Set] = [:] + var elementTypeNames: [String: String] = [:] + } + + static func derivationTables(_ containers: [XSDTree]) -> DerivationTables { + var tables = DerivationTables() + for container in containers { + for type in descendants(container, named: "complexType") { + gatherType(type, into: &tables) + } + for element in descendants(container, named: "element") { + gatherElement(element, into: &tables) + } + } + return tables + } + + private static func gatherType(_ type: XSDTree, into tables: inout DerivationTables) { + guard let name = XSDDerivNode.attribute(type, "name") else { return } + if XSDDerivNode.attribute(type, "abstract") == "true" { tables.abstractTypes.insert(name) } + let block = methodSet(XSDDerivNode.attribute(type, "block")) + if !block.isEmpty { tables.typeBlock[name] = block } + let finalSet = methodSet(XSDDerivNode.attribute(type, "final")) + if !finalSet.isEmpty { tables.typeFinal[name] = finalSet } + if let derivation = derivationInfo(type) { tables.typeDerivation[name] = derivation } + } + + private static func gatherElement(_ element: XSDTree, into tables: inout DerivationTables) { + guard let name = XSDDerivNode.attribute(element, "name") else { return } + if XSDDerivNode.attribute(element, "abstract") == "true" { tables.abstractElements.insert(name) } + let block = methodSet(XSDDerivNode.attribute(element, "block")) + if !block.isEmpty { tables.elementBlock[name] = block } + if let type = XSDDerivNode.attribute(element, "type") { + tables.elementTypeNames[name] = XSDDerivNode.stripPrefix(type) + } + } + + /// Parses a `block`/`final`/`substitutionGroup`-style value: `#all` for every + /// method, or a whitespace-separated subset of `extension`/`restriction`/ + /// `substitution`. + static func methodSet(_ raw: String?) -> Set { + guard let raw, !raw.isEmpty else { return [] } + var set: Set = [] + for token in raw.split(whereSeparator: \.isWhitespace) { + switch token { + case "#all": return [.extension, .restriction, .substitution] + case "extension": set.insert(.extension) + case "restriction": set.insert(.restriction) + case "substitution": set.insert(.substitution) + default: break + } + } + return set + } + + /// The base type and derivation method a named complex type declares through + /// its `complexContent`/`simpleContent` `extension` or `restriction`. + static func derivationInfo(_ type: XSDTree) -> PureXML.Schema.TypeDerivation? { + let container = XSDDerivNode.firstChild(type, named: "complexContent") + ?? XSDDerivNode.firstChild(type, named: "simpleContent") + guard let container else { return nil } + if let ext = XSDDerivNode.firstChild(container, named: "extension"), let base = XSDDerivNode.attribute(ext, "base") { + return PureXML.Schema.TypeDerivation(base: XSDDerivNode.stripPrefix(base), method: .extension) + } + if let res = XSDDerivNode.firstChild(container, named: "restriction"), let base = XSDDerivNode.attribute(res, "base") { + return PureXML.Schema.TypeDerivation(base: XSDDerivNode.stripPrefix(base), method: .restriction) + } + return nil + } + + /// The derivation methods along the chain from `derived` up to `base`, or nil + /// when `derived` does not derive from `base`. An empty set means they are the + /// same type. Cycle-guarded so a malformed schema cannot loop. + static func derivationMethods(from derived: String, to base: String, _ table: [String: PureXML.Schema.TypeDerivation]) -> Set? { + var methods: Set = [] + var current = derived + var visited: Set = [derived] + while current != base { + guard let step = table[current] else { return nil } + methods.insert(step.method) + guard visited.insert(step.base).inserted else { return nil } + current = step.base + } + return methods + } + + /// Throws when any type derives from a base by a method that base declares + /// `final`. + static func checkFinal(_ tables: DerivationTables) throws { + for (name, derivation) in tables.typeDerivation { + guard tables.typeFinal[derivation.base]?.contains(derivation.method) == true else { continue } + throw SchemaFault.finalViolation(type: name, base: derivation.base, method: methodName(derivation.method)) + } + } + + /// Throws when a type inside an `xs:redefine` does not derive from itself: a + /// redefinition's `base` must name the type being redefined. + static func checkRedefine(_ containers: [XSDTree]) throws { + for container in containers where XSDDerivNode.localName(container) == "redefine" { + for type in XSDDerivNode.children(container, named: "complexType") { + guard let name = XSDDerivNode.attribute(type, "name") else { continue } + if derivationInfo(type)?.base != name { throw SchemaFault.redefineIncompatible(type: name) } + } + for type in XSDDerivNode.children(container, named: "simpleType") { + try checkRedefinedSimpleType(type) + } + } + } + + private static func checkRedefinedSimpleType(_ type: XSDTree) throws { + guard let name = XSDDerivNode.attribute(type, "name") else { return } + let base = XSDDerivNode.firstChild(type, named: "restriction") + .flatMap { XSDDerivNode.attribute($0, "base") } + .map(XSDDerivNode.stripPrefix) + if base != name { throw SchemaFault.redefineIncompatible(type: name) } + } + + /// Drops from each substitution group the members a head's `block` forbids: a + /// member whose type derives from the head's type by a blocked method may not + /// substitute. + static func filterSubstitutions(_ subs: [String: [String]], _ tables: DerivationTables) -> [String: [String]] { + var result: [String: [String]] = [:] + for (head, members) in subs { + guard let blocked = tables.elementBlock[head], let headType = tables.elementTypeNames[head] else { + result[head] = members + continue + } + result[head] = members.filter { member in + guard let memberType = tables.elementTypeNames[member], + let methods = derivationMethods(from: memberType, to: headType, tables.typeDerivation) + else { + return true + } + return methods.isDisjoint(with: blocked) + } + } + return result + } + + private static func methodName(_ method: PureXML.Schema.DerivationMethod) -> String { + switch method { + case .extension: "extension" + case .restriction: "restriction" + case .substitution: "substitution" + } + } +} diff --git a/Sources/Schema/XSDParser.swift b/Sources/Schema/XSDParser.swift index 6b6cb6f..911988c 100644 --- a/Sources/Schema/XSDParser.swift +++ b/Sources/Schema/XSDParser.swift @@ -14,25 +14,19 @@ extension PureXML.Schema { } var visited: Set = [] let containers = XSDNode.collectContainers(schema, loader, &visited) + let derivation = derivationTables(containers) + try checkFinal(derivation) + try checkRedefine(containers) var context = XSDContext( simpleTypes: [:], attributeGroups: indexByName(allChildren(containers, named: "attributeGroup")), groups: indexByName(allChildren(containers, named: "group")), targetNamespace: XSDNode.attribute(schema, "targetNamespace"), - substitutions: XSDNode.substitutionMembers(containers), + substitutions: filterSubstitutions(XSDNode.substitutionMembers(containers), derivation), + abstractElements: derivation.abstractElements, ) - // Named simple types may restrict or list one another regardless of - // document order, so resolve them over repeated passes: each pass sees - // the types resolved by the previous one. A chain of length n settles - // in at most n passes. - let simpleNodes = allChildren(containers, named: "simpleType").filter { XSDNode.attribute($0, "name") != nil } - for _ in simpleNodes.indices { - for node in simpleNodes { - if let name = XSDNode.attribute(node, "name") { - context.simpleTypes[name] = XSDSimpleParser.simpleType(node, context) - } - } - } + context.complexTypeNodes = indexByName(allChildren(containers, named: "complexType")) + 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") { @@ -40,16 +34,15 @@ extension PureXML.Schema { } } var elements: [String: ElementType] = [:] - for node in allChildren(containers, named: "element") { - if let name = XSDNode.attribute(node, "name") { - let type = elementType(node, context) - elements[name] = type - // An `xs:element ref="name"` resolves to this declaration's type - // through a reserved key (a colon keeps it out of the NCName-only - // type namespace), letting forward and recursive refs resolve at - // validation time. - types[elementKey(name)] = type - } + 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 + // An `xs:element ref="name"` resolves to this declaration's type + // through a reserved key (a colon keeps it out of the NCName-only + // type namespace), letting forward and recursive refs resolve at + // validation time. + types[elementKey(name)] = type } let (nillable, elementConstraints) = elementMetadata(containers) return XSDCompiled( @@ -58,6 +51,10 @@ extension PureXML.Schema { constraints: identityConstraints(containers), nillableElements: nillable, elementConstraints: elementConstraints, + abstractTypes: derivation.abstractTypes, + abstractElements: derivation.abstractElements, + typeBlock: derivation.typeBlock, + typeDerivation: derivation.typeDerivation, ) } @@ -65,6 +62,21 @@ extension PureXML.Schema { containers.flatMap { XSDNode.children($0, named: name) } } + /// 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 + /// dependency chain of length n settles in at most n passes. + private static func resolveSimpleTypes(_ nodes: [XSDTree], into context: inout XSDContext) { + let named = nodes.filter { XSDNode.attribute($0, "name") != nil } + for _ in named.indices { + for node in named { + if let name = XSDNode.attribute(node, "name") { + context.simpleTypes[name] = XSDSimpleParser.simpleType(node, context) + } + } + } + } + static func elementKey(_ name: String) -> String { "element:\(name)" } @@ -112,19 +124,20 @@ extension PureXML.Schema { // MARK: Complex types - private static func complexType(_ node: XSDTree, _ context: XSDContext) -> ComplexType { + static func complexType(_ node: XSDTree, _ context: XSDContext) -> ComplexType { let mixed = XSDNode.attribute(node, "mixed") == "true" - var attributes = attributeUses(under: node, context) + let attributes = attributeUses(under: node, context) if let simpleContent = XSDNode.firstChild(node, named: "simpleContent") { let inner = derivation(simpleContent) - attributes += inner.map { attributeUses(under: $0, context) } ?? [] let wildcard = inner.flatMap { attributeWildcard(under: $0, context) } - return ComplexType(attributes: attributes, attributeWildcard: wildcard, content: .simpleContent(simpleContentType(simpleContent, context))) + let extra = inner.map { attributeUses(under: $0, context) } ?? [] + return ComplexType(attributes: attributes + extra, attributeWildcard: wildcard, content: .simpleContent(simpleContentType(simpleContent, context))) } - let container = XSDNode.firstChild(node, named: "complexContent").flatMap(derivation) ?? node - attributes += container === node ? [] : attributeUses(under: container, context) - let wildcard = attributeWildcard(under: container, context) - guard let particle = modelGroup(in: container, context) else { + if let complexContent = XSDNode.firstChild(node, named: "complexContent"), let inner = derivation(complexContent) { + return complexContentType(inner, mixed: mixed || XSDNode.attribute(complexContent, "mixed") == "true", context) + } + let wildcard = attributeWildcard(under: node, context) + guard let particle = modelGroup(in: node, context) else { return ComplexType(attributes: attributes, attributeWildcard: wildcard, content: .empty) } return ComplexType(attributes: attributes, attributeWildcard: wildcard, content: mixed ? .mixed(particle) : .elementOnly(particle)) @@ -145,7 +158,7 @@ extension PureXML.Schema { // MARK: Attribute uses and groups - private static func attributeUses(under node: XSDTree, _ context: XSDContext, visited: Set = []) -> [AttributeUse] { + static func attributeUses(under node: XSDTree, _ context: XSDContext, visited: Set = []) -> [AttributeUse] { var uses: [AttributeUse] = [] for child in XSDNode.elementChildren(node) { switch XSDNode.localName(child) { @@ -191,7 +204,7 @@ extension PureXML.Schema { // MARK: Model groups - private static func modelGroup(in node: XSDTree, _ context: XSDContext) -> Particle? { + static func modelGroup(in node: XSDTree, _ context: XSDContext) -> 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) @@ -243,9 +256,12 @@ extension PureXML.Schema { private static func elementParticle(_ node: XSDTree, _ minimum: Int, _ maximum: Int?, _ context: XSDContext) -> Particle { if let ref = XSDNode.attribute(node, "ref") { let name = XSDNode.stripPrefix(ref) - let alternatives = [name] + (context.substitutions[name] ?? []) + // An abstract head may not appear itself, only its members; a + // concrete head appears alongside them. + let head = context.abstractElements.contains(name) ? [] : [name] + let alternatives = head + (context.substitutions[name] ?? []) if alternatives.count == 1 { - return Particle(minOccurs: minimum, maxOccurs: maximum, term: elementReferenceTerm(name)) + return Particle(minOccurs: minimum, maxOccurs: maximum, term: elementReferenceTerm(alternatives[0])) } // The reference admits its substitution-group members, so expand it // to a choice over the head and every member, each carrying its own @@ -321,7 +337,7 @@ extension PureXML.Schema.XSDParser { return PureXML.Schema.IdentityConstraint(name: PureXML.Schema.XSDNode.attribute(node, "name") ?? "", kind: kind, selector: selector, fields: fields) } - private static func descendants(_ node: XSDTree, named name: String) -> [XSDTree] { + static func descendants(_ node: XSDTree, named name: String) -> [XSDTree] { var result: [XSDTree] = [] for child in PureXML.Schema.XSDNode.elementChildren(node) { if PureXML.Schema.XSDNode.localName(child) == name { result.append(child) } @@ -330,37 +346,3 @@ extension PureXML.Schema.XSDParser { return result } } - -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) } - } - - /// Parses a wildcard's `namespace` and `processContents` constraints. - static func wildcard(_ node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.Wildcard { - let processContents: PureXML.Schema.ProcessContents = switch PureXML.Schema.XSDNode.attribute(node, "processContents") { - case "skip": .skip - case "lax": .lax - default: .strict - } - let namespace = wildcardNamespace(PureXML.Schema.XSDNode.attribute(node, "namespace"), context) - return PureXML.Schema.Wildcard(namespace: namespace, processContents: processContents, targetNamespace: context.targetNamespace) - } - - private static func wildcardNamespace(_ value: String?, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.WildcardNamespace { - switch value { - case nil, "##any": return .any - case "##other": return .other - default: - let uris = (value ?? "").split(whereSeparator: \.isWhitespace).map { token -> String in - switch token { - case "##targetNamespace": context.targetNamespace ?? "" - case "##local": "" - default: String(token) - } - } - return .enumerated(Set(uris)) - } - } -} diff --git a/Sources/Schema/XSDValidations.swift b/Sources/Schema/XSDValidations.swift index 1bffc54..d03e871 100644 --- a/Sources/Schema/XSDValidations.swift +++ b/Sources/Schema/XSDValidations.swift @@ -8,6 +8,9 @@ public extension PureXML.Validation { public let rootDeclaration: PureXML.Schema.ElementType? public let nillableElements: Set public let elementConstraints: [String: PureXML.Schema.ValueConstraint] + public let abstractTypes: Set + public let typeBlock: [String: Set] + public let typeDerivation: [String: PureXML.Schema.TypeDerivation] public init( types: [String: PureXML.Schema.ElementType], @@ -15,12 +18,18 @@ public extension PureXML.Validation { rootDeclaration: PureXML.Schema.ElementType?, nillableElements: Set = [], elementConstraints: [String: PureXML.Schema.ValueConstraint] = [:], + abstractTypes: Set = [], + typeBlock: [String: Set] = [:], + typeDerivation: [String: PureXML.Schema.TypeDerivation] = [:], ) { self.types = types self.constraints = constraints self.rootDeclaration = rootDeclaration self.nillableElements = nillableElements self.elementConstraints = elementConstraints + self.abstractTypes = abstractTypes + self.typeBlock = typeBlock + self.typeDerivation = typeDerivation } } @@ -42,6 +51,9 @@ public extension PureXML.Validation { types: context.document.types, nillableElements: context.document.nillableElements, elementConstraints: context.document.elementConstraints, + abstractTypes: context.document.abstractTypes, + typeBlock: context.document.typeBlock, + typeDerivation: context.document.typeDerivation, ) .validate(root, as: declaration, at: [.element(root.name.description)]) }, diff --git a/Sources/Schema/XSDWildcardParser.swift b/Sources/Schema/XSDWildcardParser.swift new file mode 100644 index 0000000..cc75665 --- /dev/null +++ b/Sources/Schema/XSDWildcardParser.swift @@ -0,0 +1,33 @@ +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) } + } + + /// Parses a wildcard's `namespace` and `processContents` constraints. + static func wildcard(_ node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.Wildcard { + let processContents: PureXML.Schema.ProcessContents = switch PureXML.Schema.XSDNode.attribute(node, "processContents") { + case "skip": .skip + case "lax": .lax + default: .strict + } + let namespace = wildcardNamespace(PureXML.Schema.XSDNode.attribute(node, "namespace"), context) + return PureXML.Schema.Wildcard(namespace: namespace, processContents: processContents, targetNamespace: context.targetNamespace) + } + + private static func wildcardNamespace(_ value: String?, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.WildcardNamespace { + switch value { + case nil, "##any": return .any + case "##other": return .other + default: + let uris = (value ?? "").split(whereSeparator: \.isWhitespace).map { token -> String in + switch token { + case "##targetNamespace": context.targetNamespace ?? "" + case "##local": "" + default: String(token) + } + } + return .enumerated(Set(uris)) + } + } +} diff --git a/Tests/XSDDerivationControlTests.swift b/Tests/XSDDerivationControlTests.swift new file mode 100644 index 0000000..08a8588 --- /dev/null +++ b/Tests/XSDDerivationControlTests.swift @@ -0,0 +1,210 @@ +@testable import PureXML +import Testing + +@Suite("XSD derivation control") +struct XSDDerivationControlTests { + private func validate(_ xsd: String, _ xml: String) throws -> [PureXML.Validation.ValidationError] { + try PureXML.Schema.Document(xsd).validate(xml) + } + + /// A schema with an abstract `shape` head, a concrete `circle` member, and a + /// `figure` element whose content references the head. + private let substitution = """ + + + + + + + + + + + + """ + + @Test("An abstract element may not appear at the document root") + func test_abstractRootRejected() throws { + let xsd = """ + + + + """ + #expect(try !validate(xsd, "x").isEmpty) + } + + @Test("An abstract head is not admitted in content, only its members") + func test_abstractHeadInContent() throws { + #expect(try !validate(substitution, "
x
").isEmpty) + #expect(try validate(substitution, "
x
").isEmpty) + } + + @Test("An element of abstract type requires xsi:type naming a concrete type") + func test_abstractTypeNeedsXsiType() throws { + let xsd = """ + + + + + + + + + + + + + + """ + #expect(try !validate(xsd, "a").isEmpty) + #expect(try validate(xsd, "a1").isEmpty) + } + + @Test("block=extension on a type forbids xsi:type substitution by extension") + func test_blockExtensionXsiType() throws { + let xsd = """ + + + + + + + + + + + + + + + + + + + + + """ + // Substitution by extension is blocked. + #expect(try !validate(xsd, "a1").isEmpty) + // Substitution by restriction is still permitted. + #expect(try validate(xsd, "a").isEmpty) + } + + @Test("final=extension forbids deriving a new type by extension") + func test_finalExtension() throws { + let xsd = """ + + + + + + + + + + + + + + """ + #expect(throws: PureXML.Schema.SchemaError.self) { + _ = try PureXML.Schema.Document(xsd) + } + } + + @Test("final=#all forbids both extension and restriction") + func test_finalAll() throws { + let xsd = """ + + + + + + + + + + + + + + """ + #expect(throws: PureXML.Schema.SchemaError.self) { + _ = try PureXML.Schema.Document(xsd) + } + } + + @Test("xs:redefine requires the redefined type to derive from itself") + func test_redefineSelfReference() throws { + let base = """ + + + + + + """ + let bad = """ + + + + + + + + + + + + + """ + #expect(throws: PureXML.Schema.SchemaError.self) { + _ = try PureXML.Schema.Document(bad, schemaLoader: { _ in base }) + } + } + + @Test("xs:redefine compiles when the type derives from itself") + func test_redefineCompiles() throws { + let base = """ + + + + + + """ + let good = """ + + + + + + + + + + + + + """ + _ = try PureXML.Schema.Document(good, schemaLoader: { _ in base }) + } + + @Test("A concrete element with a non-blocked xsi:type still validates") + func test_xsiTypeStillWorks() throws { + let xsd = """ + + + + + + + + + + + + + + """ + #expect(try validate(xsd, "a1").isEmpty) + } +} diff --git a/Tests/XSDImportTests.swift b/Tests/XSDImportTests.swift index b85856e..ba7f7eb 100644 --- a/Tests/XSDImportTests.swift +++ b/Tests/XSDImportTests.swift @@ -44,7 +44,7 @@ struct XSDImportTests { - +