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).
201 lines
6.7 KiB
Swift
201 lines
6.7 KiB
Swift
@testable import PureYAML
|
|
import Testing
|
|
|
|
@Suite("Validation Rules")
|
|
struct ValidationTests {
|
|
@Test("Valid documents produce no diagnostics", arguments: [
|
|
ValidationValidDocumentCase(
|
|
name: "flat mapping",
|
|
yaml: """
|
|
title: Example
|
|
active: true
|
|
retries: 3
|
|
""",
|
|
),
|
|
ValidationValidDocumentCase(
|
|
name: "nested mapping and sequence",
|
|
yaml: """
|
|
routes:
|
|
- name: Users
|
|
methods:
|
|
- GET
|
|
- POST
|
|
- name: Status
|
|
methods:
|
|
- GET
|
|
""",
|
|
),
|
|
ValidationValidDocumentCase(
|
|
name: "flow collections",
|
|
yaml: """
|
|
values: [one, 2, false, {name: Example}]
|
|
""",
|
|
),
|
|
ValidationValidDocumentCase(
|
|
name: "anchors aliases and block scalars",
|
|
yaml: """
|
|
shared: &shared {title: Shared, active: true}
|
|
first: *shared
|
|
second:
|
|
title: Local
|
|
body: |
|
|
one
|
|
two
|
|
""",
|
|
),
|
|
])
|
|
func test_validDocumentsProduceNoDiagnostics(testCase: ValidationValidDocumentCase) throws {
|
|
let value = try PureYAML.parse(testCase.yaml)
|
|
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>()
|
|
let result = validator.collect(value)
|
|
|
|
#expect(result == PureYAML.Validation.Result())
|
|
#expect(result.isValid)
|
|
#expect(result.errors.isEmpty)
|
|
#expect(result.warnings.isEmpty)
|
|
#expect(try PureYAML.validate(value).isEmpty)
|
|
}
|
|
|
|
@Test("Reports duplicate mapping keys with exact diagnostics", arguments: [
|
|
ValidationDuplicateKeyCase(
|
|
name: "root duplicate key",
|
|
yaml: """
|
|
title: First
|
|
title: Second
|
|
""",
|
|
expectedIssues: [
|
|
.init(
|
|
severity: .error,
|
|
reason: "Duplicate mapping key 'title'",
|
|
path: .init([.key("title")]),
|
|
),
|
|
],
|
|
),
|
|
ValidationDuplicateKeyCase(
|
|
name: "nested duplicate key",
|
|
yaml: """
|
|
routes:
|
|
- name: Users
|
|
name: People
|
|
""",
|
|
expectedIssues: [
|
|
.init(
|
|
severity: .error,
|
|
reason: "Duplicate mapping key 'name'",
|
|
path: .init([.key("routes"), .index(0), .key("name")]),
|
|
),
|
|
],
|
|
),
|
|
ValidationDuplicateKeyCase(
|
|
name: "non-adjacent duplicate key",
|
|
yaml: """
|
|
title: First
|
|
name: Example
|
|
title: Second
|
|
""",
|
|
expectedIssues: [
|
|
.init(
|
|
severity: .error,
|
|
reason: "Duplicate mapping key 'title'",
|
|
path: .init([.key("title")]),
|
|
),
|
|
],
|
|
),
|
|
ValidationDuplicateKeyCase(
|
|
name: "flow mapping duplicate key",
|
|
yaml: """
|
|
root: {name: First, name: Second}
|
|
""",
|
|
expectedIssues: [
|
|
.init(
|
|
severity: .error,
|
|
reason: "Duplicate mapping key 'name'",
|
|
path: .init([.key("root"), .key("name")]),
|
|
),
|
|
],
|
|
),
|
|
ValidationDuplicateKeyCase(
|
|
name: "multiple duplicate keys preserve traversal order",
|
|
yaml: """
|
|
root:
|
|
title: First
|
|
title: Second
|
|
routes:
|
|
- name: Users
|
|
name: People
|
|
- method: GET
|
|
method: POST
|
|
""",
|
|
expectedIssues: [
|
|
.init(
|
|
severity: .error,
|
|
reason: "Duplicate mapping key 'title'",
|
|
path: .init([.key("root"), .key("title")]),
|
|
),
|
|
.init(
|
|
severity: .error,
|
|
reason: "Duplicate mapping key 'name'",
|
|
path: .init([.key("routes"), .index(0), .key("name")]),
|
|
),
|
|
.init(
|
|
severity: .error,
|
|
reason: "Duplicate mapping key 'method'",
|
|
path: .init([.key("routes"), .index(1), .key("method")]),
|
|
),
|
|
],
|
|
),
|
|
ValidationDuplicateKeyCase(
|
|
name: "third repeated key reports each repeated occurrence",
|
|
yaml: """
|
|
title: First
|
|
title: Second
|
|
title: Third
|
|
""",
|
|
expectedIssues: [
|
|
.init(
|
|
severity: .error,
|
|
reason: "Duplicate mapping key 'title'",
|
|
path: .init([.key("title")]),
|
|
),
|
|
.init(
|
|
severity: .error,
|
|
reason: "Duplicate mapping key 'title'",
|
|
path: .init([.key("title")]),
|
|
),
|
|
],
|
|
),
|
|
])
|
|
func test_duplicateMappingKeyDiagnostics(testCase: ValidationDuplicateKeyCase) throws {
|
|
let value = try PureYAML.parse(testCase.yaml)
|
|
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value)
|
|
|
|
#expect(result.values == testCase.expectedIssues)
|
|
#expect(result.errors == testCase.expectedIssues)
|
|
#expect(result.warnings.isEmpty)
|
|
expectValidationError(value) { collection in
|
|
#expect(collection.values == testCase.expectedIssues)
|
|
#expect(collection.description == testCase.expectedIssues.map(\.description).joined(separator: "\n"))
|
|
}
|
|
}
|
|
|
|
@Test("Blank validator disables default rules")
|
|
func test_blankValidatorDisablesDefaultRules() throws {
|
|
let value = PureYAML.Model.Value.mapping(.init([
|
|
.init(key: "title", value: .string("First")),
|
|
.init(key: "title", value: .string("Second")),
|
|
]))
|
|
|
|
#expect(try PureYAML.validate(value, using: .blank).isEmpty)
|
|
}
|
|
|
|
@Test("Default validator exposes exact built-in check descriptions")
|
|
func test_defaultValidatorExposesExactBuiltInCheckDescriptions() {
|
|
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.defaultValidations.map(\.description) == [
|
|
"Mapping keys are unique",
|
|
])
|
|
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().validationDescriptions == [
|
|
"Mapping keys are unique",
|
|
])
|
|
}
|
|
}
|