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).
162 lines
5.0 KiB
Swift
162 lines
5.0 KiB
Swift
@testable import PureYAML
|
|
|
|
enum ValidationCorpusSource {
|
|
case yaml(String)
|
|
case value(PureYAML.Model.Value)
|
|
|
|
func load() throws -> PureYAML.Model.Value {
|
|
switch self {
|
|
case let .yaml(yaml):
|
|
try PureYAML.parse(yaml)
|
|
case let .value(value):
|
|
value
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ValidationCorpusIssueFixture: CustomStringConvertible {
|
|
var name: String
|
|
var source: ValidationCorpusSource
|
|
var validator: PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>
|
|
var strict: Bool
|
|
var expectedIssues: [PureYAML.Validation.ValidationError]
|
|
var forbiddenIssues: [PureYAML.Validation.ValidationError]
|
|
|
|
init(
|
|
name: String,
|
|
source: ValidationCorpusSource,
|
|
validator: PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>,
|
|
strict: Bool = true,
|
|
expectedIssues: [PureYAML.Validation.ValidationError],
|
|
forbiddenIssues: [PureYAML.Validation.ValidationError] = [],
|
|
) {
|
|
self.name = name
|
|
self.source = source
|
|
self.validator = validator
|
|
self.strict = strict
|
|
self.expectedIssues = expectedIssues
|
|
self.forbiddenIssues = forbiddenIssues
|
|
}
|
|
|
|
var description: String {
|
|
name
|
|
}
|
|
}
|
|
|
|
struct ValidationCorpusSuccessFixture: CustomStringConvertible {
|
|
var name: String
|
|
var source: ValidationCorpusSource
|
|
var validator: PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>
|
|
var forbiddenIssues: [PureYAML.Validation.ValidationError]
|
|
|
|
init(
|
|
name: String,
|
|
source: ValidationCorpusSource,
|
|
validator: PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>,
|
|
forbiddenIssues: [PureYAML.Validation.ValidationError] = [],
|
|
) {
|
|
self.name = name
|
|
self.source = source
|
|
self.validator = validator
|
|
self.forbiddenIssues = forbiddenIssues
|
|
}
|
|
|
|
var description: String {
|
|
name
|
|
}
|
|
}
|
|
|
|
func requiredRootStringKeyRule(_ key: String) -> PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value> {
|
|
PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value>(description: "Root requires string key '\(key)'") { context in
|
|
guard case let .mapping(mapping) = context.subject else {
|
|
return []
|
|
}
|
|
|
|
guard let value = mapping[key] else {
|
|
return [
|
|
PureYAML.Validation.ValidationError(
|
|
severity: .error,
|
|
reason: "Required string key '\(key)' is missing",
|
|
path: context.path.appending(.key(key)),
|
|
),
|
|
]
|
|
}
|
|
|
|
guard case .string = value else {
|
|
return [
|
|
PureYAML.Validation.ValidationError(
|
|
severity: .error,
|
|
reason: "Required string key '\(key)' must be a string",
|
|
path: context.path.appending(.key(key)),
|
|
),
|
|
]
|
|
}
|
|
|
|
return []
|
|
} when: { context in
|
|
context.path.isRoot
|
|
}
|
|
}
|
|
|
|
func forbiddenMappingKeyRule(
|
|
_ key: String,
|
|
severity: PureYAML.Validation.Severity,
|
|
) -> PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value> {
|
|
PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value>(description: "Mapping forbids key '\(key)'") { context in
|
|
guard case let .mapping(mapping) = context.subject,
|
|
mapping[key] != nil
|
|
else {
|
|
return []
|
|
}
|
|
|
|
return [
|
|
PureYAML.Validation.ValidationError(
|
|
severity: severity,
|
|
reason: "Forbidden key '\(key)' is present",
|
|
path: context.path.appending(.key(key)),
|
|
),
|
|
]
|
|
} when: { context in
|
|
if case .mapping = context.subject {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
func uniqueRouteNamesRule() -> PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value> {
|
|
PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value>(description: "Route names are unique") { context in
|
|
guard case let .mapping(root) = context.subject,
|
|
case let .sequence(routes)? = root["routes"]
|
|
else {
|
|
return []
|
|
}
|
|
|
|
var seen = Set<String>()
|
|
var issues: [PureYAML.Validation.ValidationError] = []
|
|
for index in routes.indices {
|
|
guard case let .mapping(route) = routes[index],
|
|
case let .string(name)? = route["name"]
|
|
else {
|
|
continue
|
|
}
|
|
|
|
if seen.insert(name).inserted {
|
|
continue
|
|
}
|
|
|
|
issues.append(.init(
|
|
severity: .error,
|
|
reason: "Duplicate route name '\(name)'",
|
|
path: context.path
|
|
.appending(.key("routes"))
|
|
.appending(.index(index))
|
|
.appending(.key("name")),
|
|
))
|
|
}
|
|
return issues
|
|
} when: { context in
|
|
context.path.isRoot
|
|
}
|
|
}
|