PureYAML/Tests/ValidationCheckMachineryTests.swift
Mihaela Mihaljevic bf2dc4ece7 refactor(validation): rename framework to match the validation idiom
The validation module already implemented the composable-rule idiom under
its own names (Check/Issue/SubjectContext) and a value-type Validator with
duplicated traversal machinery per layer. Renamed the core types to the
canonical names (Validation/ValidationError/ValidationErrorCollection/
ValidationContext/BuiltinValidation/AnyValidation), converted Validator to
a reference-type class with the two-tier default-rule split, and genericized
it over both Subject and Document via a new Traversable protocol so the
model and tagged layers share one walk engine instead of two copies. Wired
the JSON-representability rule set into the same shape and closed the two
missing pieces (unwrap combinator, JSON coverage/config-pin tests).
2026-07-12 18:10:00 +02:00

115 lines
5.3 KiB
Swift

@testable import PureYAML
import Testing
/// `Optional<Mapping>` does not conform to `Validatable` in production (an Optional must never
/// satisfy a non-Optional check); this test-only alias and conformance let a machinery test box one
/// into the erased `any Validatable` boundary to prove the runtime `type(of:)` guard rejects it.
private typealias OptionalMapping = PureYAML.Model.Mapping?
extension PureYAML.Model.Mapping?: PureYAML.Validation.Validatable {}
@Suite("Type-directed check machinery")
struct ValidationCheckMachineryTests {
private typealias Check = PureYAML.Validation.Validation<PureYAML.Model.Mapping, PureYAML.Model.Value>
private typealias AnyValidation = PureYAML.Validation.AnyValidation<PureYAML.Model.Value>
private typealias Issue = PureYAML.Validation.ValidationError
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 = AnyValidation(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 = AnyValidation(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("An Optional wrapping the subject type does not satisfy the non-Optional check")
func test_optionalDoesNotSatisfyNonOptionalCheck() {
let erased = AnyValidation(alwaysFails())
let optional: OptionalMapping = mapping
#expect(erased.apply(to: optional, at: .root, in: .null).isEmpty)
// The exact type still fires, so the guard above is not vacuously true.
#expect(erased.apply(to: mapping, at: .root, in: .null).count == 1)
}
@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(AnyValidation(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<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(alwaysFails())
let issues = validator.collect(document).values
#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<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(
PureYAML.Validation.BuiltinValidation.mappingKeysAreUnique,
)
#expect(validator.validationDescriptions == ["Mapping keys are unique"])
#expect(throws: PureYAML.Validation.ValidationErrorCollection.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<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(
PureYAML.Validation.BuiltinValidation.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<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(
PureYAML.Validation.BuiltinValidation.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)
}
}