PureXML/Sources/Schema/ContentModelEffectiveModel.swift
Mihaela Mihaljevic 380160cc2b refactor(validation): decompose XSD instance validation into composable rules
The two monolithic XSD content/identity rules only satisfied the OpenAPIKit
validation idiom at the wrong granularity: no per-constraint removability,
no isolable failing/near-miss tests. Split them into 32 named rules, each
finding tagged at its origin and cached per XSDContext so the walk still
runs once per document (the #101 decision against duplicating XSD's
recursive type resolution still holds). Wired the streaming path's ID/IDREF
and identity checks through the same framework instead of calling the
validators imperatively, and added the unwrapAndLookup combinator the rule
already named as required.

Also includes the mandated repo-wide swiftformat/swiftlint pass.
2026-07-12 14:12:17 +02:00

65 lines
3.1 KiB
Swift

extension PureXML.Schema.ContentModelDeterminism {
/// The model-group node (`sequence`/`choice`/`all`/`group`) that holds a
/// complex type's content model, reached through `complexContent` and its
/// `restriction`/`extension`, or nil for simple or empty content.
static func contentModelNode(_ complexType: XSDTree) -> XSDTree? {
if let direct = modelGroupChild(complexType) {
return direct
}
guard let complexContent = PureXML.Schema.XSDNode.firstChild(complexType, named: "complexContent") else { return nil }
for derivation in PureXML.Schema.XSDNode.elementChildren(complexContent) {
if let model = modelGroupChild(derivation) {
return model
}
}
return nil
}
private static func modelGroupChild(_ node: XSDTree) -> XSDTree? {
PureXML.Schema.XSDNode.elementChildren(node).first {
["sequence", "choice", "all", "group"].contains(PureXML.Schema.XSDNode.localName($0) ?? "")
}
}
/// The ordered model-group nodes whose concatenation is a complex type's
/// EFFECTIVE content model. For a `complexContent` extension this is the
/// base type's effective content followed by this type's own content (XSD
/// 1.0 3.4.2: a content type derived by extension is the base content type's
/// particle followed by the extension's). A `restriction` or the shorthand
/// form contributes only its own content. The base is followed only when it
/// resolves to a local (target-namespace) complex type whose chain is
/// non-cyclic; a foreign, built-in (e.g. `xs:anyType`), unresolved, or cyclic
/// base cannot be assembled here, so the cross-boundary check stands down to
/// this type's own content, never rejecting a model it cannot fully see (an
/// under-rejection, never a false positive).
static func effectiveModelNodes(
_ complexType: XSDTree,
_ bindings: [String: String],
_ context: PureXML.Schema.XSDContext,
_ visiting: Set<String>,
) -> [XSDTree] {
let own = contentModelNode(complexType).map { [$0] } ?? []
guard let base = extensionBaseName(complexType),
PureXML.Schema.XSDNode.referenceNamespace(base, bindings) == context.targetNamespace
else {
return own
}
let baseName = PureXML.Schema.XSDNode.stripPrefix(base)
guard !visiting.contains(baseName), let baseNode = context.complexTypeNodes[baseName] else {
return own
}
return effectiveModelNodes(baseNode, bindings, context, visiting.union([baseName])) + own
}
/// The `base` of a `complexContent` extension, or nil when the type is not a
/// complexContent extension (a restriction or the shorthand form).
private static func extensionBaseName(_ complexType: XSDTree) -> String? {
guard let content = PureXML.Schema.XSDNode.firstChild(complexType, named: "complexContent"),
let ext = PureXML.Schema.XSDNode.firstChild(content, named: "extension")
else {
return nil
}
return PureXML.Schema.XSDNode.attribute(ext, "base")
}
}