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
141 lines
5.9 KiB
Swift
141 lines
5.9 KiB
Swift
/// 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 } }
|
|
}
|
|
}
|