PureYAML/Tests/ValidationModeTests.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

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<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(legacyModeRule(severity: .error))
expectValidationError(value, using: validator) { collection in
#expect(collection.values == [
.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<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(legacyModeRule(severity: .warning))
expectValidationError(value, using: validator) { collection in
#expect(collection.values == [
.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<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(legacyModeRule(severity: .warning))
let warnings = try PureYAML.validate(value, using: validator, strict: false)
#expect(warnings == [
PureYAML.Validation.ValidationError(
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<PureYAML.Model.Value, PureYAML.Model.Value>()
.validating(legacyModeRule(severity: .warning))
expectValidationError(value, using: validator, strict: false) { collection in
#expect(collection.values == [
.init(
severity: .error,
reason: "Duplicate mapping key 'title'",
path: .init([.key("title")]),
),
])
#expect(!collection.values.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<PureYAML.Model.Value, PureYAML.Model.Value>.blank
.validating(traceRule(label: "first"))
.validating(traceRule(label: "second"))
.validating(legacyModeRule(severity: .warning))
let result = validator.collect(value)
#expect(result.values.map(\.description) == [
"first at root at root of document",
"second at root at root of document",
"first at $.root at path: $.root",
"second at $.root at path: $.root",
"first at $.root.mode at path: $.root.mode",
"second at $.root.mode at path: $.root.mode",
"Legacy mode is not allowed at path: $.root.mode",
"first at $.list at path: $.list",
"second at $.list at path: $.list",
"first at $.list[0] at path: $.list[0]",
"second at $.list[0] at path: $.list[0]",
"first at $.list[0].mode at path: $.list[0].mode",
"second at $.list[0].mode at path: $.list[0].mode",
"Legacy mode is not allowed at path: $.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<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(
PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value>(
description: "Never runs",
check: { context in
[
PureYAML.Validation.ValidationError(
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<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(
PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value>(
description: "Legacy mode is not allowed",
check: { context in
[
PureYAML.Validation.ValidationError(
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.values == [
.init(
severity: .error,
reason: "Legacy mode is not allowed",
path: .init([.key("nested"), .key("mode")]),
),
])
#expect(!result.values.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<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(
PureYAML.Validation.Validation<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.ValidationError(
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.values == [
.init(
severity: .error,
reason: "Route name does not use root prefix",
path: .init([.key("routes"), .index(1), .key("name")]),
),
])
#expect(!result.values.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<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(
PureYAML.Validation.Validation<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.values == [
.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)
}
}