PureXML/Sources/Validation/StructuralValidations.swift
Mihaela Mihaljevic 1fbe359ada feat(validation): composable validation framework + structural rule
Slice 1 of #92. Introduce the OpenAPIKit-idiom validation framework, replacing
the imperative if-tree structural validator:

- ValidationError (reason + coding path) renders "<reason> at path: root/row/cell";
  ValidationErrorCollection gathers every failure and is thrown once.
- Validation<Subject, Document> is the atomic, composable rule value, with the
  multi-error and single-error Bool inits (the latter auto-forms "Failed to
  satisfy: <description>"). Predicate-gated, type-directed via an erased
  AnyValidation that guards an optional from matching a non-optional rule.
- The framework is parameterized over both Subject and Document so the structural,
  DTD, XSD, and Schematron layers share one machinery (Document = Void for rules
  that need only their subject).
- Validator<Document> walks the node tree, building the coding path, with blank /
  default factories and fluent validating / withoutValidating.
- The combinator algebra (&&, ||, comparison lifts, take, lift, unwrap, all) as
  free functions returning closures.

The structural duplicate-attribute rule is reborn as Validation<Element, Void>.
Combinators and the erased wrapper are unit-tested as pure functions per the rule.

The legacy Validation.Issue stays only for the not-yet-migrated schema validators
and is retired in a later slice.
2026-06-08 12:00:34 +02:00

48 lines
1.9 KiB
Swift

public extension PureXML.Validation {
/// The schema-independent structural validations: well-formedness rules that
/// hold for any document regardless of a DTD or schema.
enum Structural {
/// An element must not declare the same attribute name twice. Reports one
/// error per duplicate.
public static var uniqueAttributes: Validation<PureXML.Model.Element, Void> {
.init(description: "Element attribute names are unique") { context in
var seen: Set<String> = []
var errors: [ValidationError] = []
for attribute in context.subject.attributes {
let key = attribute.name.description
if !seen.insert(key).inserted {
errors.append(ValidationError(
reason: "Duplicate attribute '\(key)' on <\(context.subject.name.description)>",
at: context.codingPath,
))
}
}
return errors
}
}
/// The default structural rule set.
static var defaults: [Validation<PureXML.Model.Element, Void>] {
[uniqueAttributes]
}
}
}
public extension PureXML.Validation.Validator where Document == Void {
/// The default structural validator: schema-independent well-formedness rules.
init() {
self.init(validations: PureXML.Validation.Structural.defaults.map { PureXML.Validation.AnyValidation($0) })
}
/// Validates a node with the structural rules, throwing a
/// ``PureXML/Validation/ValidationErrorCollection`` on any failure.
func validate(_ node: PureXML.Model.Node) throws {
try validate(node, in: ())
}
/// The structural errors for a node, in document order; empty when valid.
func errors(for node: PureXML.Model.Node) -> [PureXML.Validation.ValidationError] {
errors(for: node, in: ())
}
}