PureXML/Sources/Schema/XSDValidations.swift
Mihaela Mihaljevic 77db89397b fix(schema): enforce document-scoped xs:ID uniqueness and xs:IDREF resolution
xs:ID, xs:IDREF, and xs:IDREFS were validated only lexically (as NCNames); the
document-wide rules that every ID is unique and every IDREF/IDREFS item matches
some ID were not enforced, so duplicate IDs and dangling references passed. The
tree validator now records ID and reference values during its typed walk over
elements and attributes (a new IDTracker), and reports duplicate IDs and
unresolved references after the whole document is seen, so a forward reference
resolves against an ID declared later.

XSTS: invalid instances accepted 179 to 166. Baseline updated; regression tests
cover unique IDs, a forward reference, a duplicate ID, an unresolved IDREF, and
IDREFS item resolution. The ID/IDREF helpers live in a ComplexValidator
extension to keep the type within the file/body length limits.

Refs #147, #129.
2026-06-13 19:17:34 +02:00

104 lines
5.6 KiB
Swift

public extension PureXML.Validation {
/// The compiled-schema context the XSD validations read: the named-type table
/// a content model resolves against, the identity constraints, and the root
/// element's declared type.
struct XSDContext {
public let types: [String: PureXML.Schema.ElementType]
public let constraints: [String: [PureXML.Schema.IdentityConstraint]]
public let rootDeclaration: PureXML.Schema.ElementType?
public let nillableElements: Set<String>
public let elementConstraints: [String: PureXML.Schema.ValueConstraint]
public let abstractTypes: Set<String>
public let typeBlock: [String: Set<PureXML.Schema.DerivationMethod>]
public let elementBlock: [String: Set<PureXML.Schema.DerivationMethod>]
public let typeDerivation: [String: PureXML.Schema.TypeDerivation]
public init(
types: [String: PureXML.Schema.ElementType],
constraints: [String: [PureXML.Schema.IdentityConstraint]],
rootDeclaration: PureXML.Schema.ElementType?,
nillableElements: Set<String> = [],
elementConstraints: [String: PureXML.Schema.ValueConstraint] = [:],
abstractTypes: Set<String> = [],
typeBlock: [String: Set<PureXML.Schema.DerivationMethod>] = [:],
elementBlock: [String: Set<PureXML.Schema.DerivationMethod>] = [:],
typeDerivation: [String: PureXML.Schema.TypeDerivation] = [:],
) {
self.types = types
self.constraints = constraints
self.rootDeclaration = rootDeclaration
self.nillableElements = nillableElements
self.elementConstraints = elementConstraints
self.abstractTypes = abstractTypes
self.typeBlock = typeBlock
self.elementBlock = elementBlock
self.typeDerivation = typeDerivation
}
}
/// XSD validation expressed as composable ``Validation`` values over an
/// ``XSDContext``, in the OpenAPIKit idiom: each rule is a named, removable
/// ``Validation`` value with a positive description, and the ``validator()``
/// composes them; failures are located ``ValidationError``s.
///
/// Scope note (the #101 decision). XSD content validation is *one* recursive
/// ``Validation`` (``contentValidity``) rather than a separate rule per
/// constraint, because XSD is a recursive type system: a child's type is
/// resolved from its parent's content model and position, then rewritten by
/// `xsi:type` and the derivation controls mid-walk. The constraint categories
/// (attributes, content model, derivation, nillability, fixed values) are
/// interdependent through that resolution, so they cannot be split into
/// independent rules without either changing semantics or duplicating the
/// recursion. This is the sanctioned multi-error form (the rule's `check`
/// returns one located error per violation). Each category is isolation-tested
/// through crafted inputs so it is verifiable on its own, which is the
/// testability the idiom asks for. Identity constraints, which *are*
/// independent, are a separate rule (``identityConstraints``).
enum XSD {
/// The document element is valid against its declared XSD type.
static var contentValidity: Validation<PureXML.Model.Node, XSDContext> {
.init(
description: "The document element is valid against its XSD type",
check: { context in
guard case let .element(root) = context.subject, let declaration = context.document.rootDeclaration else {
return []
}
let validator = PureXML.Schema.ComplexValidator(
types: context.document.types,
nillableElements: context.document.nillableElements,
elementConstraints: context.document.elementConstraints,
abstractTypes: context.document.abstractTypes,
typeBlock: context.document.typeBlock,
elementBlock: context.document.elementBlock,
typeDerivation: context.document.typeDerivation,
)
var errors = validator.validate(root, as: declaration, at: [.element(root.name.description)])
// Document-scoped ID uniqueness and IDREF resolution, gathered
// during the typed walk above, reported once the tree is seen.
errors += validator.idErrors()
return errors
},
when: { $0.codingPath.isEmpty },
)
}
/// The document's XSD identity constraints (`unique`, `key`, `keyref`) hold.
static var identityConstraints: Validation<PureXML.Model.Node, XSDContext> {
.init(
description: "XSD identity constraints hold",
check: { context in
guard case let .element(root) = context.subject else { return [] }
return PureXML.Schema.IdentityValidator(constraints: context.document.constraints)
.validate(PureXML.Model.TreeNode(context.subject), at: [.element(root.name.description)])
},
when: { $0.codingPath.isEmpty },
)
}
/// A validator combining the content-model and identity-constraint rules.
static func validator() -> Validator<XSDContext> {
Validator<XSDContext>.blank.validating(contentValidity, identityConstraints)
}
}
}