/// 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] { PureXML.Validation.PathKey.steps(forChildNames: children.map(\.name.description)) } /// 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 } } /// The per-particle `default`/`fixed` value constraint of each named element in a /// content model, keyed by namespaced identity. The streaming validator uses this /// to apply a child's particle constraint (not the by-local-name `elementConstraints` /// map, which collides across same-local-name elements in different types). static func elementValueConstraints(in term: PureXML.Schema.Term) -> [String: PureXML.Schema.ValueConstraint] { var result: [String: PureXML.Schema.ValueConstraint] = [:] collectValueConstraints(term, into: &result) return result } fileprivate static func collectValueConstraints(_ term: XSDTerm, into result: inout [String: PureXML.Schema.ValueConstraint]) { switch term { case let .element(name, _, _, valueConstraint, _, _): if let valueConstraint { result[key(name)] = valueConstraint } case let .group(group): for member in group.particles { collectValueConstraints(member.term, into: &result) } case .wildcard: break } } static func key(_ name: PureXML.Model.QualifiedName) -> String { "{\(name.namespaceURI ?? "")}\(name.localName)" } /// Whether two names match by namespace and local name. Used to match an /// instance attribute against a declared attribute use, so attributes that /// share a local name in different namespaces are kept distinct. static func sameName(_ lhs: PureXML.Model.QualifiedName, _ rhs: PureXML.Model.QualifiedName) -> Bool { 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. The /// value is a QName resolved in the instance's namespace context: a prefix /// binds through that prefix, an unprefixed name through the in-scope default /// namespace. With no default namespace an unprefixed name is in no namespace, /// so it keys against the no-namespace type, NOT a namespaced type of the same /// local name (XSTS targetNS00101m: `xsi:type="Test"` with no default namespace /// does not name `{targetNS}Test`). 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) } return PureXML.Schema.XSDParser.typeDeclarationKey(local, namespaceURI: namespaceBindings[""]) } /// 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). static func textContent(_ element: PureXML.Model.Element) -> String { rawTextContent(element).trimmingXMLWhitespace() } /// The element's character content verbatim. Simple-content validation uses /// this so the type's own `whiteSpace` facet decides normalization: a /// `preserve` type (xs:string) keeps leading/trailing whitespace, while a /// `collapse` type still trims and collapses through `process`. static func rawTextContent(_ element: PureXML.Model.Element) -> String { element.children.reduce(into: "") { result, child in switch child { case let .text(value), let .cdata(value): result += value default: break } } } /// Whether `complex` is the ur-type `xsd:anyType`: no declared attributes, a /// `lax` attribute wildcard, and mixed content of a single unbounded `lax` /// element wildcard (XSD 1.0 §3.4.7). The ur-type admits any `xsi:type` /// substitution. A `skip` wildcard is also accepted so any legacy construction /// of the ur-type still reads as the ur-type. static func isUrType(_ complex: PureXML.Schema.ComplexType) -> Bool { guard complex.attributes.isEmpty, complex.attributeWildcard?.processContents != .strict, case let .mixed(particle) = complex.content, particle.minOccurs == 0, particle.maxOccurs == nil, case let .wildcard(wildcard) = particle.term, wildcard.processContents != .strict else { return false } return true } 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? { xsiTypeAttributeValue(element).map { $0.split(separator: ":").last.map(String.init) ?? $0 } } } extension PureXML.Schema.ComplexValidator { /// Whether the element carries `xsi:nil="true"`. static func isNil(_ element: PureXML.Model.Element) -> Bool { nilAttributeValue(element) == "true" } /// The value of the `xsi:nil` attribute if present (any value), or nil if the /// element carries no `xsi:nil`. Used for the cvc-elt.3.1 rule that a /// non-nillable element must carry no `xsi:nil` attribute at all. static func nilAttributeValue(_ element: PureXML.Model.Element) -> String? { element.attributes.first { attribute in attribute.name.localName == "nil" && (attribute.name.namespaceURI == "http://www.w3.org/2001/XMLSchema-instance" || attribute.name.prefix == "xsi") }?.value } /// 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 } } } } public extension PureXML.Schema { /// The outcome of following a `typeReference` chain: the underlying simple or /// complex type, or the failure to report (an unknown name, or a circular /// chain that can never resolve). enum TypeResolution { case resolved(ElementType) case unknown(String) case circular(String) } } extension PureXML.Schema.ComplexValidator { /// Follows a `typeReference` chain through `types` with cycle detection, the /// one shared resolver behind the tree validator, the streaming validator, and /// completions, so unknown names and circular chains are reported identically /// everywhere instead of being silently truncated. static func resolveReference( _ type: PureXML.Schema.ElementType, in types: [String: PureXML.Schema.ElementType], ) -> PureXML.Schema.TypeResolution { var current = type var visited: Set = [] while case let .typeReference(name) = current { guard visited.insert(name).inserted else { return .circular(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(Self.urComplexType) } 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(urComplexType) } return nil } /// The ur-type `xsd:anyType`. Per XSD 1.0 §3.4.7 its element and attribute /// wildcards are `lax` (not skip): declared children and attributes are /// validated against their global declarations, undeclared content is admitted. static let urComplexType = PureXML.Schema.ComplexType( attributeWildcard: PureXML.Schema.Wildcard(processContents: .lax), content: .mixed(PureXML.Schema.Particle( minOccurs: 0, maxOccurs: nil, term: .wildcard(PureXML.Schema.Wildcard(processContents: .lax)), )), ) /// The instance resolver over this validator's type table. func resolveReference(_ type: PureXML.Schema.ElementType) -> PureXML.Schema.TypeResolution { Self.resolveReference(type, in: types) } }