diff --git a/CHANGELOG.md b/CHANGELOG.md index abc9c90..e8e9dd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- The validator is now configurable by rule identity, per the validation-rules idiom: + `withoutValidating(_ description:)` removes a rule by its description (the removal identity, so + descriptions must be unique and stable), and `validationDescriptions` lists the active rules in + application order. A configuration-pin test asserts the exact descriptions of the blank and + default validators, so adding, removing, or rewording a rule is a deliberate change. - A validation-coverage gate (`scripts/check-validation-coverage.sh`, wired into `scripts/check-all.sh` and CI) enforcing the validation-rules discipline that every public type is either validated or excluded. It enumerates the public surface from the compiler symbol graph (the diff --git a/Sources/Validation/Validator.swift b/Sources/Validation/Validator.swift index 1d524d8..6ea1a40 100644 --- a/Sources/Validation/Validator.swift +++ b/Sources/Validation/Validator.swift @@ -63,6 +63,21 @@ public extension PureYAML.Validation { copy.rules.append(rule) return copy } + + /// Removes every rule whose description matches, the identity the validation idiom uses for + /// removal. Descriptions are therefore required to be unique and stable across the rule set. + public func withoutValidating(_ description: String) -> Self { + var copy = self + copy.rules.removeAll { $0.description == description } + return copy + } + + /// The descriptions of the active rules, in application order: the fixed point a + /// configuration-pin test asserts against, so adding, removing, or rewording a rule changes + /// it deliberately. + public var validationDescriptions: [String] { + rules.map(\.description) + } } } diff --git a/Tests/ValidationConfigurationTests.swift b/Tests/ValidationConfigurationTests.swift new file mode 100644 index 0000000..ac3265e --- /dev/null +++ b/Tests/ValidationConfigurationTests.swift @@ -0,0 +1,39 @@ +@testable import PureYAML +import Testing + +@Suite("Validator configuration") +struct ValidationConfigurationTests { + private var duplicateKeys: PureYAML.Model.Value { + .mapping(.init([ + .init(key: "a", value: .int(1)), + .init(key: "a", value: .int(2)), + ])) + } + + @Test("The blank validator has no rules; the default set is pinned by description") + func test_configurationDescriptionsArePinned() { + #expect(PureYAML.Validation.Validator.blank.validationDescriptions == []) + #expect(PureYAML.Validation.Validator().validationDescriptions == ["Mapping keys are unique"]) + } + + @Test("Default rule descriptions are unique, the identity withoutValidating removes by") + func test_defaultDescriptionsAreUnique() { + let descriptions = PureYAML.Validation.Validator().validationDescriptions + #expect(Set(descriptions).count == descriptions.count) + } + + @Test("withoutValidating removes a rule by its description") + func test_withoutValidatingRemovesByDescription() throws { + let validator = PureYAML.Validation.Validator().withoutValidating("Mapping keys are unique") + #expect(validator.validationDescriptions == []) + // With the rule removed, a document with duplicate keys validates clean. + _ = try validator.validate(duplicateKeys) + } + + @Test("The default validator still catches what its removable rule guards") + func test_defaultStillValidates() { + #expect(throws: PureYAML.Validation.Issue.Collection.self) { + try PureYAML.Validation.Validator().validate(duplicateKeys) + } + } +}