PureXML/Sources/Validation/DTDStandaloneValidity.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

82 lines
4.0 KiB
Swift

/// The standalone validity constraints (2.9): a document declaring
/// `standalone='yes'` must not depend on markup declarations from the external
/// subset. Three dependencies are checked per element: an attribute supplied
/// by an externally-declared default, an attribute whose value changes under
/// the tokenized normalization an external declaration calls for, and
/// whitespace in element content whose model is externally declared.
extension PureXML.Validation.DTD {
/// Externally-declared attribute defaults and normalization must not
/// affect a standalone document.
static var standaloneAttributes: PureXML.Validation.Validation<DTDElement, PureXML.Validation.DTDSchema> {
.init(description: "Standalone documents do not depend on external attribute declarations") { context in
let schema = context.document
guard schema.standalone,
let declarations = schema.externalAttributes[context.subject.name.description]
else { return [] }
return declarations.compactMap { declaration in
standaloneAttributeViolation(declaration, on: context.subject, at: context.codingPath)
}
}
}
/// Whitespace in element content is a dependency on the external subset
/// when the content model is externally declared (a standalone processor
/// could not know the whitespace is ignorable).
static var standaloneElementWhitespace: PureXML.Validation.Validation<DTDElement, PureXML.Validation.DTDSchema> {
.init(description: "Standalone documents have no whitespace in externally-declared element content") { context in
let schema = context.document
let name = context.subject.name.description
guard schema.standalone, schema.externalElementModels.contains(name),
case .children = schema.models[name],
elementContentWhitespace(in: context.subject)
else { return [] }
let reason = "standalone document has whitespace in the externally-declared element content of <\(name)>"
return [DTDFailure(reason: reason, at: context.codingPath)]
}
}
private static func standaloneAttributeViolation(
_ declaration: PureXML.Validation.AttributeDeclaration,
on element: DTDElement,
at path: DTDPath,
) -> DTDFailure? {
let supplied = element.attributes.first {
$0.name.description == declaration.name || $0.name.localName == declaration.name
}
guard let supplied else {
// Absent attribute with an external default: the default would be
// supplied from the external subset.
let hasDefault = switch declaration.defaultDecl {
case .fixed, .value: true
case .required, .implied: false
}
guard hasDefault else { return nil }
return DTDFailure(
reason: "standalone document depends on the externally-declared default for attribute '\(declaration.name)' on <\(element.name.description)>",
at: path,
)
}
// Present attribute whose externally-declared non-CDATA type calls for
// normalization the standalone processor would not perform.
if case .cdata = declaration.type {
return nil
}
guard supplied.value != normalize(supplied.value, for: declaration.type) else { return nil }
return DTDFailure(
reason: "standalone document depends on the externally-declared normalization of attribute '\(declaration.name)' on <\(element.name.description)>",
at: path,
)
}
/// Whether the element directly contains whitespace-only text.
private static func elementContentWhitespace(in element: DTDElement) -> Bool {
element.children.contains { child in
if case let .text(value) = child {
!value.isEmpty && value.allSatisfy(\.isWhitespace)
} else {
false
}
}
}
}