PureXML/Tests/ValidationRuleTestSupport.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

72 lines
3.0 KiB
Swift

import Testing
@testable import PureXML
enum ValidationRuleTestSupport {
typealias DTDSchema = PureXML.Validation.DTDSchema
typealias XSDContext = PureXML.Validation.XSDContext
/// `xmlns:xsi` namespace declaration, spliced into instance fixtures that need
/// `xsi:nil`/`xsi:type`.
static let xsiNS = "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
/// Compiles `schema`, parses `instance`, and runs `rule` alone (isolated via
/// `.blank.validating(rule)`) against the root element declared `rootName`.
/// Shared by every `ValidationRuleXSD*Tests` suite.
static func xsdErrors(
_ rule: PureXML.Validation.Validation<PureXML.Model.Node, XSDContext>,
schema: String,
instance: String,
rootName: String,
) throws -> [PureXML.Validation.ValidationError] {
let compiled = try PureXML.Schema.XSDParser.parse(schema)
let node = try PureXML.parse(instance)
guard case let .document(children) = node, let root = children.compactMap(\.element).first else {
Issue.record("no root element")
return []
}
let context = XSDContext(
types: compiled.types,
constraints: compiled.constraints,
rootDeclaration: compiled.elements[rootName],
nillableElements: compiled.nillableElements,
elementConstraints: compiled.elementConstraints,
abstractTypes: compiled.nsAbstractTypes,
typeBlock: compiled.nsTypeBlock,
elementBlock: compiled.nsElementBlock,
typeDerivation: compiled.nsTypeDerivation,
unionMembers: compiled.nsUnionMembers,
globalAttributes: compiled.globalAttributes,
identityFieldTypes: compiled.identityFieldTypes,
identityFieldConstraints: compiled.identityFieldConstraints,
identityNonSimpleFields: compiled.identityNonSimpleFields,
)
return PureXML.Validation.Validator<XSDContext>.blank
.validating(rule)
.errors(for: .element(root), in: context)
}
static func dtd(_ xml: String, resolver: PureXML.Parsing.EntityResolver = .refusing) throws -> (PureXML.Model.Node, DTDSchema) {
let parsed = try PureXML.Parsing.Parser().parseWithDocumentType(xml, limits: .init(allowDoctype: true), resolver: resolver)
let standalone = parsed.declaration?.standalone == true
return (parsed.node, DTDSchema(parsed.documentType, standalone: standalone))
}
static let standaloneExternalDTD = """
<!ELEMENT root (child)*>
<!ELEMENT child (#PCDATA)>
<!ATTLIST root token (a|b|c) "a" id ID #IMPLIED>
"""
static func standaloneResolver() -> PureXML.Parsing.EntityResolver {
let dtd = standaloneExternalDTD
return PureXML.Parsing.EntityResolver(
resolveEntity: { _, _ in nil },
resolveExternalSubset: { _ in dtd },
)
}
static func path(_ error: PureXML.Validation.ValidationError) -> [String] {
error.codingPath.map(\.stringValue)
}
}