A Rule was exactly a Check<Model.Value, Model.Value>: it fired on every node, switched on the value itself, and carried the root. It was therefore a second copy of the one check machinery, the duplication the validation idiom exists to prevent (principle 10), now also resolved for the tagged layer. Remove Rule and its Context. The model validator holds only checks; its default is the type-directed BuiltinCheck.mappingKeysAreUnique; validating takes a Check. Custom whole-tree rules become Check<Model.Value, Model.Value> with the former context.root now context.document. Behaviour is byte-identical: the mapping-key builtin reports the same issues at the same paths in the same DFS order, so the public validate/collect/blank surface is unchanged. All 399 tests pass; the coverage gate reconciles to 60 public types.
280 lines
10 KiB
Swift
280 lines
10 KiB
Swift
@testable import PureYAML
|
|
import Testing
|
|
|
|
@Suite("Validation Modes")
|
|
struct ValidationModeTests {
|
|
@Test("Custom validation rules report exact errors")
|
|
func test_customValidationRuleErrors() throws {
|
|
let value = try PureYAML.parse(
|
|
"""
|
|
mode: legacy
|
|
nested:
|
|
mode: legacy
|
|
""",
|
|
)
|
|
let validator = PureYAML.Validation.Validator.blank.validating(legacyModeRule(severity: .error))
|
|
|
|
expectValidationError(value, using: validator) { collection in
|
|
#expect(collection.issues == [
|
|
.init(
|
|
severity: .error,
|
|
reason: "Legacy mode is not allowed",
|
|
path: .init([.key("mode")]),
|
|
),
|
|
.init(
|
|
severity: .error,
|
|
reason: "Legacy mode is not allowed",
|
|
path: .init([.key("nested"), .key("mode")]),
|
|
),
|
|
])
|
|
}
|
|
}
|
|
|
|
@Test("Strict validation treats warnings as failures")
|
|
func test_strictValidationTreatsWarningsAsFailures() throws {
|
|
let value = try PureYAML.parse("mode: legacy")
|
|
let validator = PureYAML.Validation.Validator.blank.validating(legacyModeRule(severity: .warning))
|
|
|
|
expectValidationError(value, using: validator) { collection in
|
|
#expect(collection.issues == [
|
|
.init(
|
|
severity: .warning,
|
|
reason: "Legacy mode is not allowed",
|
|
path: .init([.key("mode")]),
|
|
),
|
|
])
|
|
}
|
|
}
|
|
|
|
@Test("Non-strict validation returns warnings")
|
|
func test_nonStrictValidationReturnsWarnings() throws {
|
|
let value = try PureYAML.parse("mode: legacy")
|
|
let validator = PureYAML.Validation.Validator.blank.validating(legacyModeRule(severity: .warning))
|
|
|
|
let warnings = try PureYAML.validate(value, using: validator, strict: false)
|
|
|
|
#expect(warnings == [
|
|
PureYAML.Validation.Issue(
|
|
severity: .warning,
|
|
reason: "Legacy mode is not allowed",
|
|
path: .init([.key("mode")]),
|
|
),
|
|
])
|
|
}
|
|
|
|
@Test("Non-strict validation throws only errors when errors and warnings are mixed")
|
|
func test_nonStrictValidationThrowsOnlyErrorsWhenErrorsAndWarningsAreMixed() throws {
|
|
let value = try PureYAML.parse(
|
|
"""
|
|
title: First
|
|
title: Second
|
|
mode: legacy
|
|
""",
|
|
)
|
|
let validator = PureYAML.Validation.Validator()
|
|
.validating(legacyModeRule(severity: .warning))
|
|
|
|
expectValidationError(value, using: validator, strict: false) { collection in
|
|
#expect(collection.issues == [
|
|
.init(
|
|
severity: .error,
|
|
reason: "Duplicate mapping key 'title'",
|
|
path: .init([.key("title")]),
|
|
),
|
|
])
|
|
#expect(!collection.issues.contains(.init(
|
|
severity: .warning,
|
|
reason: "Legacy mode is not allowed",
|
|
path: .init([.key("mode")]),
|
|
)))
|
|
}
|
|
}
|
|
|
|
@Test("Validation traversal preserves rule order before child paths")
|
|
func test_validationTraversalPreservesRuleOrderBeforeChildPaths() throws {
|
|
let value = try PureYAML.parse(
|
|
"""
|
|
root:
|
|
mode: legacy
|
|
list:
|
|
- mode: legacy
|
|
""",
|
|
)
|
|
let validator = PureYAML.Validation.Validator.blank
|
|
.validating(traceRule(label: "first"))
|
|
.validating(traceRule(label: "second"))
|
|
.validating(legacyModeRule(severity: .warning))
|
|
|
|
let result = validator.collect(value)
|
|
|
|
#expect(result.issues.map(\.description) == [
|
|
"warning: first at root at root",
|
|
"warning: second at root at root",
|
|
"warning: first at $.root at $.root",
|
|
"warning: second at $.root at $.root",
|
|
"warning: first at $.root.mode at $.root.mode",
|
|
"warning: second at $.root.mode at $.root.mode",
|
|
"warning: Legacy mode is not allowed at $.root.mode",
|
|
"warning: first at $.list at $.list",
|
|
"warning: second at $.list at $.list",
|
|
"warning: first at $.list[0] at $.list[0]",
|
|
"warning: second at $.list[0] at $.list[0]",
|
|
"warning: first at $.list[0].mode at $.list[0].mode",
|
|
"warning: second at $.list[0].mode at $.list[0].mode",
|
|
"warning: Legacy mode is not allowed at $.list[0].mode",
|
|
])
|
|
}
|
|
|
|
@Test("Validation rule predicate prevents rule from running")
|
|
func test_validationRulePredicatePreventsRuleFromRunning() throws {
|
|
let value = try PureYAML.parse("mode: legacy")
|
|
let validator = PureYAML.Validation.Validator.blank.validating(
|
|
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(
|
|
description: "Never runs",
|
|
check: { context in
|
|
[
|
|
PureYAML.Validation.Issue(
|
|
severity: .error,
|
|
reason: "Should not appear",
|
|
path: context.path,
|
|
),
|
|
]
|
|
},
|
|
when: { _ in false },
|
|
),
|
|
)
|
|
|
|
let result = validator.collect(value)
|
|
|
|
#expect(result == PureYAML.Validation.Result())
|
|
#expect(result.isValid)
|
|
}
|
|
|
|
@Test("Validation rule predicate filters applied subjects")
|
|
func test_validationRulePredicateFiltersAppliedSubjects() throws {
|
|
let value = try PureYAML.parse(
|
|
"""
|
|
mode: modern
|
|
nested:
|
|
mode: legacy
|
|
label: legacy
|
|
""",
|
|
)
|
|
let validator = PureYAML.Validation.Validator.blank.validating(
|
|
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(
|
|
description: "Legacy mode is not allowed",
|
|
check: { context in
|
|
[
|
|
PureYAML.Validation.Issue(
|
|
severity: .error,
|
|
reason: "Legacy mode is not allowed",
|
|
path: context.path,
|
|
),
|
|
]
|
|
},
|
|
when: { context in
|
|
context.path.components.last == .key("mode")
|
|
&& context.subject == .string("legacy")
|
|
},
|
|
),
|
|
)
|
|
|
|
let result = validator.collect(value)
|
|
|
|
#expect(result.issues == [
|
|
.init(
|
|
severity: .error,
|
|
reason: "Legacy mode is not allowed",
|
|
path: .init([.key("nested"), .key("mode")]),
|
|
),
|
|
])
|
|
#expect(!result.issues.contains(.init(
|
|
severity: .error,
|
|
reason: "Legacy mode is not allowed",
|
|
path: .init([.key("nested"), .key("label")]),
|
|
)))
|
|
}
|
|
|
|
@Test("Validation rules can inspect root subject and path deterministically")
|
|
func test_validationRulesCanInspectRootSubjectAndPathDeterministically() throws {
|
|
let value = try PureYAML.parse(
|
|
"""
|
|
required_prefix: prod
|
|
routes:
|
|
- name: prod-users
|
|
- name: test-status
|
|
- label: test-ignored
|
|
""",
|
|
)
|
|
let validator = PureYAML.Validation.Validator.blank.validating(
|
|
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(
|
|
description: "Route names use the root prefix",
|
|
check: { context in
|
|
guard case let .mapping(root) = context.document,
|
|
case let .string(prefix)? = root["required_prefix"],
|
|
case let .string(name) = context.subject,
|
|
name.hasPrefix("\(prefix)-")
|
|
else {
|
|
return [
|
|
PureYAML.Validation.Issue(
|
|
severity: .error,
|
|
reason: "Route name does not use root prefix",
|
|
path: context.path,
|
|
),
|
|
]
|
|
}
|
|
return []
|
|
},
|
|
when: { context in
|
|
context.path.components.last == .key("name")
|
|
},
|
|
),
|
|
)
|
|
|
|
let result = validator.collect(value)
|
|
|
|
#expect(result.issues == [
|
|
.init(
|
|
severity: .error,
|
|
reason: "Route name does not use root prefix",
|
|
path: .init([.key("routes"), .index(1), .key("name")]),
|
|
),
|
|
])
|
|
#expect(!result.issues.contains(.init(
|
|
severity: .error,
|
|
reason: "Route name does not use root prefix",
|
|
path: .init([.key("routes"), .index(2), .key("label")]),
|
|
)))
|
|
}
|
|
|
|
@Test("Boolean validation rule reports exact default failure reason")
|
|
func test_booleanValidationRuleReportsExactDefaultFailureReason() throws {
|
|
let value = try PureYAML.parse(
|
|
"""
|
|
title: Example
|
|
state: draft
|
|
""",
|
|
)
|
|
let validator = PureYAML.Validation.Validator.blank.validating(
|
|
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(
|
|
description: "State is published",
|
|
check: { context in context.subject == .string("published") },
|
|
when: { context in context.path.components.last == .key("state") },
|
|
),
|
|
)
|
|
|
|
expectValidationError(value, using: validator) { collection in
|
|
#expect(collection.issues == [
|
|
.init(
|
|
severity: .error,
|
|
reason: "Failed to satisfy: State is published",
|
|
path: .init([.key("state")]),
|
|
),
|
|
])
|
|
}
|
|
|
|
let valid = try PureYAML.parse("state: published")
|
|
#expect(try PureYAML.validate(valid, using: validator).isEmpty)
|
|
}
|
|
}
|