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.
326 lines
17 KiB
Swift
326 lines
17 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 let unionMembers: [String: [String]]
|
|
public let globalAttributes: [String: PureXML.Schema.AttributeUse]
|
|
public let identityFieldTypes: [String: PureXML.Schema.SimpleType]
|
|
public let identityFieldConstraints: [String: PureXML.Schema.ValueConstraint]
|
|
public let identityNonSimpleFields: Set<String>
|
|
/// Memoizes the one tagged content-validity walk and the one tagged
|
|
/// identity-constraint walk for this context, so the decomposed rule set
|
|
/// below (``XSD``) walks the tree once, not once per rule. See
|
|
/// ``XSDFindingsCache``.
|
|
let findingsCache = XSDFindingsCache()
|
|
|
|
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] = [:],
|
|
unionMembers: [String: [String]] = [:],
|
|
globalAttributes: [String: PureXML.Schema.AttributeUse] = [:],
|
|
identityFieldTypes: [String: PureXML.Schema.SimpleType] = [:],
|
|
identityFieldConstraints: [String: PureXML.Schema.ValueConstraint] = [:],
|
|
identityNonSimpleFields: Set<String> = [],
|
|
) {
|
|
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
|
|
self.unionMembers = unionMembers
|
|
self.globalAttributes = globalAttributes
|
|
self.identityFieldTypes = identityFieldTypes
|
|
self.identityFieldConstraints = identityFieldConstraints
|
|
self.identityNonSimpleFields = identityNonSimpleFields
|
|
}
|
|
}
|
|
}
|
|
|
|
public extension PureXML.Validation {
|
|
/// ``XSDContext``, in the OpenAPIKit idiom: each constraint category is a
|
|
/// named, removable ``Validation`` value with a positive description, and
|
|
/// ``validator()`` composes them; failures are located ``ValidationError``s.
|
|
///
|
|
/// Decomposition boundary (the #101 decision, extended). XSD content
|
|
/// validation and identity-constraint validation are each one recursive walk:
|
|
/// 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, and
|
|
/// identity's `key`/`unique`/`keyref` share one scope stack threaded through
|
|
/// one recursive descent. Splitting either walk into independently-INVOKED
|
|
/// rules would force either duplicating that recursion (a correctness risk:
|
|
/// two walks can drift) or fabricating rules that look independently
|
|
/// removable but are not.
|
|
///
|
|
/// Instead, every finding is tagged at its origin with the decomposed rule it
|
|
/// belongs to (``XSDRuleToken``), the ONE walk is memoized per ``XSDContext``
|
|
/// (``XSDFindingsCache``), and each rule below reads the memoized, tagged
|
|
/// result and filters by its own token. The walk still runs exactly once when
|
|
/// the full rule set is registered together; each rule is still independently
|
|
/// named, described, removable via `withoutValidating`, and isolation-tested
|
|
/// (a rule tested alone via `.blank.validating(rule)` still triggers the one
|
|
/// full walk and returns just its own slice, so isolation tests are correct
|
|
/// without any special-casing).
|
|
enum XSD {
|
|
/// Every content-validity finding for the document root, tagged by rule,
|
|
/// computed once and cached on `context.document.findingsCache`.
|
|
private static func contentFindings(for context: ValidationContext<PureXML.Model.Node, XSDContext>) -> [XSDFinding] {
|
|
if let cached = context.document.findingsCache.contentFindings {
|
|
return cached
|
|
}
|
|
guard case let .element(root) = context.subject, let declaration = context.document.rootDeclaration else {
|
|
context.document.findingsCache.contentFindings = []
|
|
return []
|
|
}
|
|
let schema = context.document
|
|
let validator = PureXML.Schema.ComplexValidator(
|
|
types: schema.types,
|
|
globalAttributes: schema.globalAttributes,
|
|
nillableElements: schema.nillableElements,
|
|
elementConstraints: schema.elementConstraints,
|
|
abstractTypes: schema.abstractTypes,
|
|
typeBlock: schema.typeBlock,
|
|
elementBlock: schema.elementBlock,
|
|
typeDerivation: schema.typeDerivation,
|
|
unionMembers: schema.unionMembers,
|
|
)
|
|
var findings = validator.validateTagged(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.
|
|
findings += validator.idErrors().map { XSDFinding(.idIdrefConsistent, $0) }
|
|
context.document.findingsCache.contentFindings = findings
|
|
return findings
|
|
}
|
|
|
|
/// Every identity-constraint finding for the document root, tagged by
|
|
/// rule, computed once and cached on `context.document.findingsCache`.
|
|
private static func identityFindings(for context: ValidationContext<PureXML.Model.Node, XSDContext>) -> [XSDFinding] {
|
|
if let cached = context.document.findingsCache.identityFindings {
|
|
return cached
|
|
}
|
|
guard case let .element(root) = context.subject else {
|
|
context.document.findingsCache.identityFindings = []
|
|
return []
|
|
}
|
|
let findings = PureXML.Schema.IdentityValidator(
|
|
constraints: context.document.constraints,
|
|
fieldTypes: context.document.identityFieldTypes,
|
|
fieldConstraints: context.document.identityFieldConstraints,
|
|
nonSimpleFields: context.document.identityNonSimpleFields,
|
|
types: context.document.types,
|
|
).validateTagged(PureXML.Model.TreeNode(context.subject), at: [.element(root.name.description)])
|
|
context.document.findingsCache.identityFindings = findings
|
|
return findings
|
|
}
|
|
|
|
/// One decomposed content-validity rule: `description` names the correct
|
|
/// state, `token` selects its slice of the one memoized tagged walk.
|
|
private static func contentRule(_ token: XSDRuleToken, _ description: String) -> Validation<PureXML.Model.Node, XSDContext> {
|
|
.init(
|
|
description: description,
|
|
check: { context in
|
|
guard case .element = context.subject else { return [] }
|
|
return contentFindings(for: context).filter { $0.rule == token }.map(\.error)
|
|
},
|
|
when: { $0.codingPath.isEmpty },
|
|
)
|
|
}
|
|
|
|
/// One decomposed identity-constraint rule. See ``contentRule(_:_:)``.
|
|
private static func identityRule(_ token: XSDRuleToken, _ description: String) -> Validation<PureXML.Model.Node, XSDContext> {
|
|
.init(
|
|
description: description,
|
|
check: { context in
|
|
guard case .element = context.subject else { return [] }
|
|
return identityFindings(for: context).filter { $0.rule == token }.map(\.error)
|
|
},
|
|
when: { $0.codingPath.isEmpty },
|
|
)
|
|
}
|
|
|
|
// MARK: Content validity
|
|
|
|
static var nilAttributeRequiresNillable: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.nilAttributeRequiresNillable, "An element carries xsi:nil only when it is declared nillable")
|
|
}
|
|
|
|
static var nilElementMustBeEmpty: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.nilElementMustBeEmpty, "A nilled element has no content")
|
|
}
|
|
|
|
static var nilElementNoFixedConstraint: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.nilElementNoFixedConstraint, "A nilled element carries no fixed value constraint")
|
|
}
|
|
|
|
static var fixedElementNoChildren: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.fixedElementNoChildren, "A fixed element has no element children")
|
|
}
|
|
|
|
static var fixedElementValueMatches: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.fixedElementValueMatches, "A fixed element's content equals its fixed value")
|
|
}
|
|
|
|
static var emptyContentForbidsChildren: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.emptyContentForbidsChildren, "An empty-content element has no element children")
|
|
}
|
|
|
|
static var simpleContentForbidsChildren: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.simpleContentForbidsChildren, "A simple-content element has no element children")
|
|
}
|
|
|
|
static var simpleContentTypeValid: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.simpleContentTypeValid, "A simple-content element's text is valid against its type")
|
|
}
|
|
|
|
static var textForbidden: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.textForbidden, "An element-only or empty-content element carries no text")
|
|
}
|
|
|
|
static var wildcardElementUndeclared: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.wildcardElementUndeclared, "A strict wildcard-matched element has a global declaration")
|
|
}
|
|
|
|
static var simpleTypeDisallowedAttribute: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.simpleTypeDisallowedAttribute, "A simple-typed element carries only schema-instance and namespace attributes")
|
|
}
|
|
|
|
static var simpleTypeForbidsChildren: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.simpleTypeForbidsChildren, "A simple-typed element has no element children")
|
|
}
|
|
|
|
static var simpleTypeValueValid: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.simpleTypeValueValid, "A simple-typed element's value is valid against its type")
|
|
}
|
|
|
|
static var typeReferenceResolves: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.typeReferenceResolves, "A type reference resolves to a known, non-circular type")
|
|
}
|
|
|
|
static var contentModelStructure: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.contentModelStructure, "An element's children conform to its content model")
|
|
}
|
|
|
|
static var attributeTypeValid: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.attributeTypeValid, "A declared attribute's value is valid against its type")
|
|
}
|
|
|
|
static var attributeFixedMatches: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.attributeFixedMatches, "A declared attribute's value equals its fixed value")
|
|
}
|
|
|
|
static var attributeRequiredPresent: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.attributeRequiredPresent, "A required attribute is present")
|
|
}
|
|
|
|
static var attributeSingleID: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.attributeSingleID, "An element carries at most one attribute of type ID")
|
|
}
|
|
|
|
static var attributeDeclaredOrPermitted: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.attributeDeclaredOrPermitted, "An attribute is declared or permitted by a wildcard")
|
|
}
|
|
|
|
static var wildcardAttributeUndeclared: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.wildcardAttributeUndeclared, "A strict wildcard-matched attribute has a global declaration")
|
|
}
|
|
|
|
static var wildcardAttributeTypeValid: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.wildcardAttributeTypeValid, "A wildcard-matched attribute's value is valid against its type")
|
|
}
|
|
|
|
static var wildcardAttributeFixedMatches: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.wildcardAttributeFixedMatches, "A wildcard-matched attribute's value equals its fixed value")
|
|
}
|
|
|
|
static var xsiTypeSubstitutionValid: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.xsiTypeSubstitutionValid, "An xsi:type substitution is validly derived and not blocked")
|
|
}
|
|
|
|
static var xsiTypeOverrideResolves: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.xsiTypeOverrideResolves, "An xsi:type override names a known, validly derived type")
|
|
}
|
|
|
|
static var idIdrefConsistent: Validation<PureXML.Model.Node, XSDContext> {
|
|
contentRule(.idIdrefConsistent, "Every xs:ID is unique and every xs:IDREF resolves to one")
|
|
}
|
|
|
|
/// Every decomposed content-validity rule, in application order.
|
|
static var contentValidityRules: [Validation<PureXML.Model.Node, XSDContext>] {
|
|
[
|
|
nilAttributeRequiresNillable, nilElementMustBeEmpty, nilElementNoFixedConstraint,
|
|
fixedElementNoChildren, fixedElementValueMatches, emptyContentForbidsChildren,
|
|
simpleContentForbidsChildren, simpleContentTypeValid, textForbidden,
|
|
wildcardElementUndeclared, simpleTypeDisallowedAttribute, simpleTypeForbidsChildren,
|
|
simpleTypeValueValid, typeReferenceResolves, contentModelStructure,
|
|
attributeTypeValid, attributeFixedMatches, attributeRequiredPresent,
|
|
attributeSingleID, attributeDeclaredOrPermitted, wildcardAttributeUndeclared,
|
|
wildcardAttributeTypeValid, wildcardAttributeFixedMatches, xsiTypeSubstitutionValid,
|
|
xsiTypeOverrideResolves, idIdrefConsistent,
|
|
]
|
|
}
|
|
|
|
// MARK: Identity constraints
|
|
|
|
static var identityConstraintXPathsValid: Validation<PureXML.Model.Node, XSDContext> {
|
|
identityRule(.identityConstraintXPathsValid, "An identity constraint's selector and field XPaths compile")
|
|
}
|
|
|
|
static var identityKeyFieldPresent: Validation<PureXML.Model.Node, XSDContext> {
|
|
identityRule(.identityKeyFieldPresent, "A key constraint's fields are all present")
|
|
}
|
|
|
|
static var identityValueUnique: Validation<PureXML.Model.Node, XSDContext> {
|
|
identityRule(.identityValueUnique, "A key or unique constraint's values are distinct")
|
|
}
|
|
|
|
static var identityKeyrefResolves: Validation<PureXML.Model.Node, XSDContext> {
|
|
identityRule(.identityKeyrefResolves, "A keyref constraint's value matches a key or unique in scope")
|
|
}
|
|
|
|
static var identityFieldCardinality: Validation<PureXML.Model.Node, XSDContext> {
|
|
identityRule(.identityFieldCardinality, "An identity constraint field selects at most one node")
|
|
}
|
|
|
|
static var identityFieldMustBeSimple: Validation<PureXML.Model.Node, XSDContext> {
|
|
identityRule(.identityFieldMustBeSimple, "An identity constraint field identifies a node with simple content")
|
|
}
|
|
|
|
/// Every decomposed identity-constraint rule, in application order.
|
|
static var identityConstraintRules: [Validation<PureXML.Model.Node, XSDContext>] {
|
|
[
|
|
identityConstraintXPathsValid, identityKeyFieldPresent, identityValueUnique,
|
|
identityKeyrefResolves, identityFieldCardinality, identityFieldMustBeSimple,
|
|
]
|
|
}
|
|
|
|
/// A validator combining every decomposed content-model and
|
|
/// identity-constraint rule.
|
|
static func validator() -> Validator<XSDContext> {
|
|
Validator<XSDContext>.defaults(
|
|
nonReference: contentValidityRules.map { AnyValidation($0) },
|
|
reference: identityConstraintRules.map { AnyValidation($0) },
|
|
)
|
|
}
|
|
}
|
|
}
|