Make XSD validation namespace-aware and enforce the form defaults and xs:all structural constraints that were previously ignored: - targetNamespace is tracked: a schema's global elements are in its target namespace, and an instance's root element must be in it. - elementFormDefault / attributeFormDefault (and per-declaration form) qualify local element and attribute declarations. The content-model matcher already compares namespace URIs, so qualified declarations now match only same-namespace instance nodes; attribute matching became namespace-aware too. - xs:all groups are checked at compile time: every member must be an element occurring at most once, the group may occur at most once, and nested model groups inside an all are rejected (a SchemaError). When a schema has no target namespace the stamping is a no-op, so unqualified schemas validate exactly as before. Cross-namespace type references through xs:import resolve by local name only; full multi-namespace type tables are not modeled. Closes #78
148 lines
6.3 KiB
Swift
148 lines
6.3 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)"
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
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 } }
|
|
}
|
|
}
|