PureXML/Sources/Schema/XSDParser.swift
Mihaela Mihaljevic 2b7138ab61 feat(schema): XSD namespace qualification and xs:all constraints
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
2026-06-08 14:24:17 +02:00

370 lines
19 KiB
Swift

extension PureXML.Schema {
/// Parses an XSD schema document into its global element declarations and its
/// named-type table. Matches the schema vocabulary by local name, so the XML
/// Schema namespace prefix may be anything. Supports global and local element
/// declarations, named and inline simple and complex types, `sequence`,
/// `choice`, and `all` model groups with occurrence, attribute uses, simple
/// content, the full facet set, `list` and `union` simple types, named
/// attribute groups and model groups, and element and group references.
enum XSDParser {
static func parse(_ xsd: String, loader: (String) -> String? = { _ in nil }) throws -> XSDCompiled {
let root = try PureXML.parseTree(xsd)
guard let schema = XSDNode.elementChildren(root).first(where: { XSDNode.localName($0) == "schema" }) else {
throw PureXML.Schema.SchemaError.notASchema
}
var visited: Set<String> = []
let containers = XSDNode.collectContainers(schema, loader, &visited)
let derivation = derivationTables(containers)
try checkFinal(derivation)
try checkRedefine(containers)
try checkAllGroups(containers)
var context = XSDContext(
simpleTypes: [:],
attributeGroups: indexByName(allChildren(containers, named: "attributeGroup")),
groups: indexByName(allChildren(containers, named: "group")),
targetNamespace: XSDNode.attribute(schema, "targetNamespace"),
substitutions: filterSubstitutions(XSDNode.substitutionMembers(containers), derivation),
abstractElements: derivation.abstractElements,
)
context.elementFormQualified = XSDNode.attribute(schema, "elementFormDefault") == "qualified"
context.attributeFormQualified = XSDNode.attribute(schema, "attributeFormDefault") == "qualified"
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") {
types[name] = .complex(complexType(node, context))
}
}
var elements: [String: ElementType] = [:]
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(
elements: elements,
types: types,
constraints: identityConstraints(containers),
nillableElements: nillable,
elementConstraints: elementConstraints,
abstractTypes: derivation.abstractTypes,
abstractElements: derivation.abstractElements,
typeBlock: derivation.typeBlock,
typeDerivation: derivation.typeDerivation,
targetNamespace: context.targetNamespace,
)
}
private static func allChildren(_ containers: [XSDTree], named name: String) -> [XSDTree] {
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)"
}
private static func indexByName(_ nodes: [XSDTree]) -> [String: XSDTree] {
var index: [String: XSDTree] = [:]
for node in nodes {
if let name = XSDNode.attribute(node, "name") { index[name] = node }
}
return index
}
// MARK: Element and type references
private static func elementType(_ node: XSDTree, _ context: XSDContext) -> ElementType {
if let typeName = XSDNode.attribute(node, "type") {
return typeReference(typeName)
}
if let inline = XSDNode.firstChild(node, named: "simpleType") {
return .simple(XSDSimpleParser.simpleType(inline, context))
}
if let inline = XSDNode.firstChild(node, named: "complexType") {
return .complex(complexType(inline, context))
}
return .complex(anyType)
}
private static func typeReference(_ typeName: String) -> ElementType {
let local = XSDNode.stripPrefix(typeName)
if let builtin = BuiltinType(rawValue: local) {
return .simple(SimpleType(base: builtin))
}
if let item = XSDSimpleParser.listBuiltinItem(local) {
return .simple(.list(item: SimpleType(base: item)))
}
return .typeReference(local)
}
private static var anyType: ComplexType {
ComplexType(
attributeWildcard: Wildcard(processContents: .skip),
content: .mixed(Particle(minOccurs: 0, maxOccurs: nil, term: .wildcard(Wildcard(processContents: .skip)))),
)
}
// MARK: Complex types
static func complexType(_ node: XSDTree, _ context: XSDContext) -> ComplexType {
let mixed = XSDNode.attribute(node, "mixed") == "true"
let attributes = attributeUses(under: node, context)
if let simpleContent = XSDNode.firstChild(node, named: "simpleContent") {
let inner = derivation(simpleContent)
let wildcard = inner.flatMap { attributeWildcard(under: $0, context) }
let extra = inner.map { attributeUses(under: $0, context) } ?? []
return ComplexType(attributes: attributes + extra, attributeWildcard: wildcard, content: .simpleContent(simpleContentType(simpleContent, context)))
}
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))
}
private static func derivation(_ node: XSDTree) -> XSDTree? {
XSDNode.firstChild(node, named: "restriction") ?? XSDNode.firstChild(node, named: "extension")
}
private static func simpleContentType(_ node: XSDTree, _ context: XSDContext) -> SimpleType {
guard let inner = derivation(node) else { return SimpleType(base: .string) }
let baseName = XSDNode.stripPrefix(XSDNode.attribute(inner, "base") ?? "string")
let base = BuiltinType(rawValue: baseName) ?? context.simpleTypes[baseName]?.base ?? .string
var facets = context.simpleTypes[baseName]?.facets ?? Facets()
XSDSimpleParser.applyFacets(inner, into: &facets)
return SimpleType(base: base, facets: facets)
}
// MARK: Attribute uses and groups
static func attributeUses(under node: XSDTree, _ context: XSDContext, visited: Set<String> = []) -> [AttributeUse] {
var uses: [AttributeUse] = []
for child in XSDNode.elementChildren(node) {
switch XSDNode.localName(child) {
case "attribute":
if let use = attributeUse(child, context) { uses.append(use) }
case "attributeGroup":
guard let ref = XSDNode.attribute(child, "ref") else { break }
let name = XSDNode.stripPrefix(ref)
guard !visited.contains(name), let group = context.attributeGroups[name] else { break }
uses += attributeUses(under: group, context, visited: visited.union([name]))
default:
break
}
}
return uses
}
private static func attributeUse(_ node: XSDTree, _ context: XSDContext) -> AttributeUse? {
guard let name = XSDNode.attribute(node, "name") else { return nil }
let required = XSDNode.attribute(node, "use") == "required"
let type: SimpleType = if let typeName = XSDNode.attribute(node, "type") {
XSDSimpleParser.simpleTypeReference(typeName, context)
} else if let inline = XSDNode.firstChild(node, named: "simpleType") {
XSDSimpleParser.simpleType(inline, context)
} else {
SimpleType(base: .string)
}
let qualified = XSDNode.attribute(node, "form") == "qualified"
|| (XSDNode.attribute(node, "form") == nil && context.attributeFormQualified)
return AttributeUse(
name: PureXML.Model.QualifiedName(localName: name, namespaceURI: qualified ? context.targetNamespace : nil),
type: type,
required: required,
valueConstraint: valueConstraint(of: node),
)
}
/// The `default` or `fixed` value constraint declared on an attribute or
/// element node, if any (`fixed` takes precedence).
static func valueConstraint(of node: XSDTree) -> ValueConstraint? {
if let fixed = XSDNode.attribute(node, "fixed") { return .fixed(fixed) }
if let value = XSDNode.attribute(node, "default") { return .default(value) }
return nil
}
// MARK: Model groups
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)
}
}
if let groupRef = XSDNode.firstChild(node, named: "group") {
return particle(groupRef, context)
}
return nil
}
private static func groupParticle(
_ node: XSDTree,
_ compositor: Compositor,
_ context: XSDContext,
) -> Particle {
var particles: [Particle] = []
for child in XSDNode.elementChildren(node) {
if let member = particle(child, context) { particles.append(member) }
}
let (minimum, maximum) = XSDNode.occurrence(node)
return Particle(
minOccurs: minimum,
maxOccurs: maximum,
term: .group(Group(compositor: compositor, particles: particles)),
)
}
private static func particle(_ node: XSDTree, _ context: XSDContext) -> Particle? {
let (minimum, maximum) = XSDNode.occurrence(node)
switch XSDNode.localName(node) {
case "element":
return elementParticle(node, minimum, maximum, context)
case "sequence":
return groupParticle(node, .sequence, context)
case "choice":
return groupParticle(node, .choice, context)
case "all":
return groupParticle(node, .all, context)
case "group":
return groupReferenceParticle(node, minimum, maximum, context)
case "any":
return Particle(minOccurs: minimum, maxOccurs: maximum, term: .wildcard(wildcard(node, context)))
default:
return nil
}
}
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)
// 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(alternatives[0], context))
}
// The reference admits its substitution-group members, so expand it
// to a choice over the head and every member, each carrying its own
// declared type.
let members = alternatives.map { Particle(term: elementReferenceTerm($0, context)) }
return Particle(minOccurs: minimum, maxOccurs: maximum, term: .group(Group(compositor: .choice, particles: members)))
}
let name = XSDNode.attribute(node, "name") ?? ""
return Particle(
minOccurs: minimum,
maxOccurs: maximum,
term: .element(name: localElementName(name, XSDNode.attribute(node, "form"), context), type: elementType(node, context)),
)
}
private static func groupReferenceParticle(_ node: XSDTree, _ minimum: Int, _ maximum: Int?, _ context: XSDContext) -> Particle? {
guard let ref = XSDNode.attribute(node, "ref") else { return nil }
let name = XSDNode.stripPrefix(ref)
guard !context.visitingGroups.contains(name), let definition = context.groups[name],
let inner = modelGroup(in: definition, context.visiting(name))
else {
return nil
}
return Particle(minOccurs: minimum, maxOccurs: maximum, term: inner.term)
}
}
}
extension PureXML.Schema.XSDParser {
/// The qualified name of a reference to a global element: globals are always in
/// the schema's target namespace.
static func elementReferenceTerm(_ name: String, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.Term {
.element(
name: PureXML.Model.QualifiedName(localName: name, namespaceURI: context.targetNamespace),
type: .typeReference(elementKey(name)),
)
}
/// The qualified name of a local element declaration: in the target namespace
/// when `elementFormDefault` (or the element's own `form`) is qualified,
/// otherwise in no namespace.
static func localElementName(_ name: String, _ form: String?, _ context: PureXML.Schema.XSDContext) -> PureXML.Model.QualifiedName {
let qualified = form == "qualified" || (form == nil && context.elementFormQualified)
return PureXML.Model.QualifiedName(localName: name, namespaceURI: qualified ? context.targetNamespace : nil)
}
}
extension PureXML.Schema.XSDParser {
/// Gathers `nillable` and `default`/`fixed` value constraints from every
/// element declaration at any depth, keyed by the element's name.
static func elementMetadata(_ containers: [XSDTree]) -> (Set<String>, [String: PureXML.Schema.ValueConstraint]) {
var nillable: Set<String> = []
var constraints: [String: PureXML.Schema.ValueConstraint] = [:]
for container in containers {
for element in descendants(container, named: "element") {
guard let name = PureXML.Schema.XSDNode.attribute(element, "name") else { continue }
if PureXML.Schema.XSDNode.attribute(element, "nillable") == "true" { nillable.insert(name) }
if let constraint = valueConstraint(of: element) { constraints[name] = constraint }
}
}
return (nillable, constraints)
}
/// Gathers identity constraints (`unique`, `key`, `keyref`) declared on any
/// element at any depth, keyed by the element's name.
static func identityConstraints(_ containers: [XSDTree]) -> [String: [PureXML.Schema.IdentityConstraint]] {
var map: [String: [PureXML.Schema.IdentityConstraint]] = [:]
for container in containers {
for element in descendants(container, named: "element") {
guard let name = PureXML.Schema.XSDNode.attribute(element, "name") else { continue }
let constraints = PureXML.Schema.XSDNode.elementChildren(element).compactMap(constraint)
if !constraints.isEmpty { map[name, default: []] += constraints }
}
}
return map
}
private static func constraint(_ node: XSDTree) -> PureXML.Schema.IdentityConstraint? {
let kind: PureXML.Schema.IdentityConstraintKind
switch PureXML.Schema.XSDNode.localName(node) {
case "unique": kind = .unique
case "key": kind = .key
case "keyref": kind = .keyref(refer: PureXML.Schema.XSDNode.stripPrefix(PureXML.Schema.XSDNode.attribute(node, "refer") ?? ""))
default: return nil
}
let selector = PureXML.Schema.XSDNode.firstChild(node, named: "selector").flatMap { PureXML.Schema.XSDNode.attribute($0, "xpath") } ?? ""
let fields = PureXML.Schema.XSDNode.children(node, named: "field").compactMap { PureXML.Schema.XSDNode.attribute($0, "xpath") }
return PureXML.Schema.IdentityConstraint(name: PureXML.Schema.XSDNode.attribute(node, "name") ?? "", kind: kind, selector: selector, fields: fields)
}
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) }
result += descendants(child, named: name)
}
return result
}
}