PureYAML/Tests/ValidationCheckMachineryTests.swift
Mihaela M fc1a6db1dc refactor(validation): fold the tagged validator onto one framework
The tagged-node validator carried its own copy of the validation machinery:
a Tagged.Rule cloning Validation's check type and a Tagged.Context cloning
its context, differing only in subject type. That is the per-layer
duplication the validation idiom exists to prevent (principle 10).

Generalise the type-directed framework over both the subject and the
document: SubjectContext, Check, and AnyCheck now take a Document type
parameter for the validated tree's root. The model layer validates with a
Model.Value document, the tagged layer with a Tagged.Node document, each
truthful rather than forcing one root type to stand in for the other.

The tagged layer keeps only what is genuinely its own: the Node document
type, its built-in check (now Tagged.BuiltinCheck, a Check<Node, Node>), and
its traversal. Tagged.Rule and the tagged Context are removed. The validator
surface is unchanged but validating now takes a Check<Node, Node>.

All 399 tests pass; the coverage gate reconciles to 62 public types.
2026-06-28 10:41:10 +02:00

100 lines
4.1 KiB
Swift

@testable import PureYAML
import Testing
@Suite("Type-directed check machinery")
struct ValidationCheckMachineryTests {
private typealias Check = PureYAML.Validation.Check<PureYAML.Model.Mapping, PureYAML.Model.Value>
private typealias AnyCheck = PureYAML.Validation.AnyCheck<PureYAML.Model.Value>
private typealias Issue = PureYAML.Validation.Issue
private var mapping: PureYAML.Model.Mapping {
.init([.init(key: "a", value: .int(1))])
}
private func alwaysFails(description: String = "always fails") -> Check {
Check(description: description) { context in
[Issue(severity: .error, reason: "boom", path: context.path)]
}
}
// MARK: machinery negatives (the dispatch must not fire where it should not)
@Test("A check fires on its subject type and produces its error")
func test_firesOnSubjectType() {
let erased = AnyCheck(alwaysFails())
#expect(erased.apply(to: mapping, at: .root, in: .null).count == 1)
}
@Test("A check does not fire on a value of another type")
func test_doesNotFireOnWrongType() {
let erased = AnyCheck(alwaysFails())
// A Model.Value (even one wrapping a mapping) is not a Model.Mapping, so nothing fires.
#expect(erased.apply(to: PureYAML.Model.Value.mapping(mapping), at: .root, in: .null).isEmpty)
#expect(erased.apply(to: PureYAML.Model.Key.string("a"), at: .root, in: .null).isEmpty)
}
@Test("A false `when` predicate yields no errors")
func test_falsePredicateYieldsNothing() {
let gated = Check(description: "gated", check: { _ in
[Issue(severity: .error, reason: "boom", path: .root)]
}, when: { _ in false })
#expect(AnyCheck(gated).apply(to: mapping, at: .root, in: .null).isEmpty)
}
// MARK: positive control (proving the negatives are not vacuous)
@Test("The same subject type occurring twice yields two errors")
func test_sameTypeTwiceYieldsTwoErrors() {
// A document with two mappings; an always-failing Check<Mapping> must fire on both.
let document = PureYAML.Model.Value.sequence(.init([
.mapping(.init([.init(key: "a", value: .int(1))])),
.mapping(.init([.init(key: "b", value: .int(2))])),
]))
let validator = PureYAML.Validation.Validator.blank.validating(alwaysFails())
let issues = validator.collect(document).issues
#expect(issues.count == 2)
}
// MARK: the type-directed builtin (failing and a near miss)
@Test("The type-directed mappingKeysAreUnique builtin catches duplicate keys")
func test_builtinCatchesDuplicateKeys() {
let validator = PureYAML.Validation.Validator.blank.validating(
PureYAML.Validation.BuiltinCheck.mappingKeysAreUnique,
)
#expect(validator.validationDescriptions == ["Mapping keys are unique"])
#expect(throws: PureYAML.Validation.Issue.Collection.self) {
try validator.validate(.mapping(.init([
.init(key: "a", value: .int(1)),
.init(key: "a", value: .int(2)),
])))
}
}
@Test("The builtin passes a near-miss document whose keys are unique")
func test_builtinPassesUniqueKeys() throws {
let validator = PureYAML.Validation.Validator.blank.validating(
PureYAML.Validation.BuiltinCheck.mappingKeysAreUnique,
)
// The boundary the rule almost trips: two keys that differ by one character.
_ = try validator.validate(.mapping(.init([
.init(key: "a", value: .int(1)),
.init(key: "b", value: .int(1)),
])))
}
@Test("Type-directed dispatch reaches a nested mapping")
func test_dispatchReachesNestedMapping() {
let validator = PureYAML.Validation.Validator.blank.validating(
PureYAML.Validation.BuiltinCheck.mappingKeysAreUnique,
)
let nested = PureYAML.Model.Value.mapping(.init([
.init(key: "outer", value: .mapping(.init([
.init(key: "a", value: .int(1)),
.init(key: "a", value: .int(2)),
]))),
]))
#expect(!validator.collect(nested).errors.isEmpty)
}
}