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).
This commit is contained in:
parent
e1c08260c4
commit
bf2dc4ece7
@ -50,9 +50,9 @@ public extension PureYAML {
|
||||
@discardableResult
|
||||
static func validate(
|
||||
_ value: Model.Value,
|
||||
using validator: Validation.Validator = .init(),
|
||||
using validator: Validation.Validator<Model.Value, Model.Value> = .init(),
|
||||
strict: Bool = true,
|
||||
) throws -> [Validation.Issue] {
|
||||
) throws -> [Validation.ValidationError] {
|
||||
try validator.validate(value, strict: strict)
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ public extension PureYAML {
|
||||
@discardableResult
|
||||
static func validate(
|
||||
_ documents: [Stream.Document],
|
||||
using validator: Validation.Validator = .init(),
|
||||
using validator: Validation.Validator<Model.Value, Model.Value> = .init(),
|
||||
strict: Bool = true,
|
||||
) throws -> [Stream.Issue] {
|
||||
try validator.validate(documents, strict: strict)
|
||||
|
||||
@ -79,20 +79,20 @@ extension PureYAML.Decoding {
|
||||
_ value: PureYAML.Model.Value,
|
||||
codingPath: [any CodingKey],
|
||||
) throws {
|
||||
let result = PureYAML.Validation.Validator().collect(value)
|
||||
guard !result.issues.isEmpty else {
|
||||
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value)
|
||||
guard !result.values.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let pathPrefix = PureYAML.Validation.Path(codingPath: codingPath)
|
||||
let issues = result.issues.map { issue in
|
||||
PureYAML.Validation.Issue(
|
||||
severity: issue.severity,
|
||||
reason: issue.reason,
|
||||
path: .init(pathPrefix.components + issue.path.components),
|
||||
let errors = result.values.map { error in
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: error.severity,
|
||||
reason: error.reason,
|
||||
path: .init(pathPrefix.components + error.path.components),
|
||||
)
|
||||
}
|
||||
throw PureYAML.Validation.Issue.Collection(issues)
|
||||
throw PureYAML.Validation.ValidationErrorCollection(errors)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
26
Sources/Model/Value.Traversable.swift
Normal file
26
Sources/Model/Value.Traversable.swift
Normal file
@ -0,0 +1,26 @@
|
||||
extension PureYAML.Model.Value: PureYAML.Validation.Traversable {
|
||||
/// For a mapping: the mapping itself (a `Validation<Mapping, _>` fires on this), and each pair's
|
||||
/// key node at its own path (so a key-typed check can inspect it, e.g. "is this key a string").
|
||||
/// Nothing extra for any other case.
|
||||
public var extraOffers: [(component: PureYAML.Validation.Path.Component?, value: any PureYAML.Validation.Validatable)] {
|
||||
guard case let .mapping(mapping) = self else {
|
||||
return []
|
||||
}
|
||||
return [(nil, mapping)] + mapping.pairs.map { pair in
|
||||
(component: pair.keyNode.pathComponent, value: pair.keyNode)
|
||||
}
|
||||
}
|
||||
|
||||
/// A mapping's children are its pairs' values, keyed by their key node's path component; a
|
||||
/// sequence's children are its values, keyed by index. A scalar has none.
|
||||
public var children: [(component: PureYAML.Validation.Path.Component, value: PureYAML.Model.Value)] {
|
||||
switch self {
|
||||
case let .mapping(mapping):
|
||||
mapping.pairs.map { pair in (component: pair.keyNode.pathComponent, value: pair.value) }
|
||||
case let .sequence(values):
|
||||
values.indices.map { index in (component: .index(index), value: values[index]) }
|
||||
case .null, .bool, .int, .double, .string:
|
||||
[]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,17 @@
|
||||
import PureYAML
|
||||
|
||||
public extension PureYAML.Validation.BuiltinCheck {
|
||||
public extension PureYAML.Validation.BuiltinValidation {
|
||||
/// Reports any mapping key that is not a string. JSON object names must be
|
||||
/// strings, so a YAML complex key (a sequence or mapping used as a key) has no
|
||||
/// JSON form. PureYAML core already ships ``mappingKeysAreUnique``; this check and
|
||||
/// ``numbersAreFinite`` complete the JSON-representability set.
|
||||
static var mappingKeysAreStrings: PureYAML.Validation.Check<PureYAML.Model.Mapping, PureYAML.Model.Value> {
|
||||
PureYAML.Validation.Check(description: "Mapping keys are strings") { context in
|
||||
static var mappingKeysAreStrings: PureYAML.Validation.Validation<PureYAML.Model.Mapping, PureYAML.Model.Value> {
|
||||
PureYAML.Validation.Validation(description: "Mapping keys are strings") { context in
|
||||
context.subject.pairs.compactMap { pair in
|
||||
guard pair.keyNode.stringValue == nil else {
|
||||
return nil
|
||||
}
|
||||
return PureYAML.Validation.Issue(
|
||||
return PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Mapping key '\(pair.keyNode.description)' is not a string",
|
||||
path: context.path.appending(.complexKey(pair.keyNode)),
|
||||
@ -22,13 +22,13 @@ public extension PureYAML.Validation.BuiltinCheck {
|
||||
|
||||
/// Reports any non-finite number (NaN or infinity). JSON has no literal for
|
||||
/// these, so a value tree carrying one is not representable as JSON.
|
||||
static var numbersAreFinite: PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value> {
|
||||
PureYAML.Validation.Check(description: "Numbers are finite") { context in
|
||||
static var numbersAreFinite: PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value> {
|
||||
PureYAML.Validation.Validation(description: "Numbers are finite") { context in
|
||||
guard case let .double(number) = context.subject, !number.isFinite else {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Number '\(number)' is not finite",
|
||||
path: context.path,
|
||||
@ -6,7 +6,7 @@ public extension PureYAML.JSON.Convert {
|
||||
/// The source text did not parse.
|
||||
case sourceInvalid(String)
|
||||
/// The value cannot be represented in the target format.
|
||||
case notRepresentable([PureYAML.Validation.Issue])
|
||||
case notRepresentable([PureYAML.Validation.ValidationError])
|
||||
/// The emitted output did not parse back to the source value.
|
||||
case roundTripMismatch(PureYAML.JSON.Format)
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ public extension PureYAML.JSON {
|
||||
/// representability first; the round-trip check is not run here (use
|
||||
/// ``convert(_:to:pretty:)`` for the fully validated path).
|
||||
public static func emit(_ value: PureYAML.Model.Value, as target: Format, pretty: Bool = true) throws -> String {
|
||||
let issues = validator(for: target).collect(value).issues
|
||||
let issues = validator(for: target).collect(value).values
|
||||
guard issues.isEmpty else {
|
||||
throw Failure.notRepresentable(issues)
|
||||
}
|
||||
@ -44,7 +44,7 @@ public extension PureYAML.JSON {
|
||||
|
||||
/// The validator for a target format. JSON additionally requires string keys
|
||||
/// and finite numbers; YAML allows complex keys but still forbids duplicates.
|
||||
public static func validator(for target: Format) -> PureYAML.Validation.Validator {
|
||||
public static func validator(for target: Format) -> PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value> {
|
||||
switch target {
|
||||
case .json:
|
||||
PureYAML.JSON.Validator.representability
|
||||
|
||||
@ -9,22 +9,25 @@ public extension PureYAML.JSON {
|
||||
///
|
||||
/// JSON the format tolerates duplicate object names, but a tree that carries two
|
||||
/// pairs with the same key emits an ambiguous document, so the representability
|
||||
/// validator rejects duplicates for determinism. Drop ``unique`` from the rule
|
||||
/// set if you want the permissive reading.
|
||||
/// validator rejects duplicates for determinism. Drop ``PureYAML/Validation/BuiltinValidation/mappingKeysAreUnique``
|
||||
/// from the rule set if you want the permissive reading.
|
||||
enum Validator {
|
||||
/// The full JSON-representability rule set: unique keys, string keys, finite
|
||||
/// numbers. Suitable to pass straight to `PureYAML.JSON.Convert`.
|
||||
public static var representability: PureYAML.Validation.Validator {
|
||||
PureYAML.Validation.Validator(checks: [
|
||||
PureYAML.Validation.AnyCheck(PureYAML.Validation.BuiltinCheck.mappingKeysAreUnique),
|
||||
PureYAML.Validation.AnyCheck(PureYAML.Validation.BuiltinCheck.mappingKeysAreStrings),
|
||||
PureYAML.Validation.AnyCheck(PureYAML.Validation.BuiltinCheck.numbersAreFinite),
|
||||
/// numbers. Suitable to pass straight to `PureYAML.JSON.Convert`. Built with
|
||||
/// the same tiered `Validator` shape every other layer uses, so it carries the
|
||||
/// same `.blank`/`.validating`/`.withoutValidating`/`.validationDescriptions`
|
||||
/// machinery (validation-rules.md, principle 10).
|
||||
public static var representability: PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value> {
|
||||
PureYAML.Validation.Validator(nonReferenceDefaultValidations: [
|
||||
PureYAML.Validation.AnyValidation(PureYAML.Validation.BuiltinValidation.mappingKeysAreUnique),
|
||||
PureYAML.Validation.AnyValidation(PureYAML.Validation.BuiltinValidation.mappingKeysAreStrings),
|
||||
PureYAML.Validation.AnyValidation(PureYAML.Validation.BuiltinValidation.numbersAreFinite),
|
||||
])
|
||||
}
|
||||
|
||||
/// Every reason the value cannot be emitted as strict JSON, in document order.
|
||||
public static func issues(for value: PureYAML.Model.Value) -> [PureYAML.Validation.Issue] {
|
||||
representability.collect(value).issues
|
||||
public static func issues(for value: PureYAML.Model.Value) -> [PureYAML.Validation.ValidationError] {
|
||||
representability.collect(value).values
|
||||
}
|
||||
|
||||
/// Whether the value can be emitted as strict JSON.
|
||||
|
||||
@ -2,11 +2,11 @@ public extension PureYAML.Stream {
|
||||
/// Validation issue with the document index preserved.
|
||||
struct Issue: Swift.Error, Equatable, Sendable, CustomStringConvertible {
|
||||
public var documentIndex: Int
|
||||
public var issue: PureYAML.Validation.Issue
|
||||
public var issue: PureYAML.Validation.ValidationError
|
||||
|
||||
public init(
|
||||
documentIndex: Int,
|
||||
issue: PureYAML.Validation.Issue,
|
||||
issue: PureYAML.Validation.ValidationError,
|
||||
) {
|
||||
self.documentIndex = documentIndex
|
||||
self.issue = issue
|
||||
|
||||
@ -1,55 +1,55 @@
|
||||
public extension PureYAML.Tagged {
|
||||
/// The caseless namespace of built-in type-directed checks for the tagged-node tree, the tagged
|
||||
/// layer's counterpart of `Validation.BuiltinCheck`. Each is a `Check<Node, Node>` on the one
|
||||
/// layer's counterpart of `Validation.BuiltinValidation`. Each is a `Validation<_, Node>` on the one
|
||||
/// shared validation framework, not a copy of the machinery (validation-rules.md, principle 10).
|
||||
enum BuiltinCheck {
|
||||
enum BuiltinValidation {
|
||||
/// Reports each duplicate key within a single tagged mapping. The tagged-layer counterpart of
|
||||
/// `Validation.BuiltinCheck.mappingKeysAreUnique`; duplicate keys are as invalid in a
|
||||
/// `Validation.BuiltinValidation.mappingKeysAreUnique`; duplicate keys are as invalid in a
|
||||
/// tag-preserving tree as in the plain model, and `Tagged.Mapping` keeps every parsed pair.
|
||||
public static var mappingKeysAreUnique: PureYAML.Validation.Check<Mapping, Node> {
|
||||
PureYAML.Validation.Check(description: "Mapping keys are unique") { context in
|
||||
public static var mappingKeysAreUnique: PureYAML.Validation.Validation<Mapping, Node> {
|
||||
PureYAML.Validation.Validation(description: "Mapping keys are unique") { context in
|
||||
var seen = Set<String>()
|
||||
var issues: [PureYAML.Validation.Issue] = []
|
||||
var errors: [PureYAML.Validation.ValidationError] = []
|
||||
for pair in context.subject.pairs where !seen.insert(pair.key).inserted {
|
||||
issues.append(PureYAML.Validation.Issue(
|
||||
errors.append(PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key '\(pair.key)'",
|
||||
path: context.path.appending(.key(pair.key)),
|
||||
))
|
||||
}
|
||||
return issues
|
||||
return errors
|
||||
}
|
||||
}
|
||||
|
||||
/// Reports unsupported built-in tags and built-in tags applied to the wrong node kind, on the
|
||||
/// node itself and on each mapping key.
|
||||
public static var builtInTagsAreSupportedAndKindConsistent: PureYAML.Validation.Check<Node, Node> {
|
||||
PureYAML.Validation.Check(description: "Built-in tags are supported and kind-consistent") { context in
|
||||
var issues: [PureYAML.Validation.Issue] = []
|
||||
public static var builtInTagsAreSupportedAndKindConsistent: PureYAML.Validation.Validation<Node, Node> {
|
||||
PureYAML.Validation.Validation(description: "Built-in tags are supported and kind-consistent") { context in
|
||||
var errors: [PureYAML.Validation.ValidationError] = []
|
||||
if case let .mapping(mapping) = context.subject {
|
||||
issues.append(contentsOf: mapping.pairs.compactMap { pair in
|
||||
errors.append(contentsOf: mapping.pairs.compactMap { pair in
|
||||
keyTagIssue(for: pair, at: context.path.appending(.key(pair.key)))
|
||||
})
|
||||
}
|
||||
|
||||
guard let tag = context.subject.tag, tag.isBuiltIn else {
|
||||
return issues
|
||||
return errors
|
||||
}
|
||||
|
||||
if let expectedKind = tag.expectedKind, expectedKind != context.subject.kind {
|
||||
issues.append(
|
||||
PureYAML.Validation.Issue(
|
||||
errors.append(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Tag '\(tag.rawValue)' expects a \(expectedKind) node, found \(context.subject.kind)",
|
||||
path: context.path,
|
||||
),
|
||||
)
|
||||
return issues
|
||||
return errors
|
||||
}
|
||||
|
||||
if !tag.isSupportedBuiltInTag {
|
||||
issues.append(
|
||||
PureYAML.Validation.Issue(
|
||||
errors.append(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Unsupported built-in tag '\(tag.rawValue)'",
|
||||
path: context.path,
|
||||
@ -57,7 +57,7 @@ public extension PureYAML.Tagged {
|
||||
)
|
||||
}
|
||||
|
||||
return issues
|
||||
return errors
|
||||
} when: { context in
|
||||
if context.subject.tag?.isBuiltIn == true {
|
||||
return true
|
||||
@ -72,7 +72,7 @@ public extension PureYAML.Tagged {
|
||||
static func keyTagIssue(
|
||||
for pair: PureYAML.Tagged.Pair,
|
||||
at path: PureYAML.Validation.Path,
|
||||
) -> PureYAML.Validation.Issue? {
|
||||
) -> PureYAML.Validation.ValidationError? {
|
||||
guard let tag = pair.keyTag, tag.isBuiltIn else {
|
||||
return nil
|
||||
}
|
||||
@ -80,21 +80,21 @@ public extension PureYAML.Tagged {
|
||||
return nil
|
||||
}
|
||||
if tag == .merge {
|
||||
return PureYAML.Validation.Issue(
|
||||
return PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Tag '\(tag.rawValue)' is only supported on '<<' mapping keys",
|
||||
path: path,
|
||||
)
|
||||
}
|
||||
if let expectedKind = tag.expectedKind, expectedKind != .scalar {
|
||||
return PureYAML.Validation.Issue(
|
||||
return PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Tag '\(tag.rawValue)' expects a scalar mapping key",
|
||||
path: path,
|
||||
)
|
||||
}
|
||||
guard tag.isSupportedBuiltInTag else {
|
||||
return PureYAML.Validation.Issue(
|
||||
return PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Unsupported built-in tag '\(tag.rawValue)' on mapping key",
|
||||
path: path,
|
||||
26
Sources/Tagged/Tagged.Node.Traversable.swift
Normal file
26
Sources/Tagged/Tagged.Node.Traversable.swift
Normal file
@ -0,0 +1,26 @@
|
||||
extension PureYAML.Tagged.Node: PureYAML.Validation.Traversable {
|
||||
/// For a mapping: the mapping itself (a `Validation<Mapping, _>` fires on this). Unlike the model
|
||||
/// layer, key nodes are not offered separately: a tagged mapping's keys are plain `String`, not a
|
||||
/// `Validatable` key type of their own.
|
||||
public var extraOffers: [(component: PureYAML.Validation.Path.Component?, value: any PureYAML.Validation.Validatable)] {
|
||||
guard case let .mapping(mapping) = self else {
|
||||
return []
|
||||
}
|
||||
return [(nil, mapping)]
|
||||
}
|
||||
|
||||
/// A mapping's children are its pairs' values, keyed by their string key; a sequence's children are
|
||||
/// its values, keyed by index. An alias's aliased subtree was already walked at its anchor's
|
||||
/// definition site, so recursing here would report every error in that subtree once per reference;
|
||||
/// a scalar has no children either.
|
||||
public var children: [(component: PureYAML.Validation.Path.Component, value: PureYAML.Tagged.Node)] {
|
||||
switch self {
|
||||
case let .mapping(mapping):
|
||||
mapping.pairs.map { pair in (component: .key(pair.key), value: pair.value) }
|
||||
case let .sequence(sequence):
|
||||
sequence.values.indices.map { index in (component: .index(index), value: sequence.values[index]) }
|
||||
case .scalar, .alias:
|
||||
[]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,136 +1,44 @@
|
||||
public extension PureYAML.Tagged {
|
||||
/// Traverses a tag-preserving YAML node tree and applies type-directed checks. The checks, their
|
||||
/// erasure, and their context all come from the one validation framework: this layer adds only its
|
||||
/// own document type (`Node`), its built-in check, and its traversal (validation-rules.md,
|
||||
/// principle 10).
|
||||
struct Validator: Sendable {
|
||||
public var checks: [PureYAML.Validation.AnyCheck<Node>]
|
||||
|
||||
public init(checks: [PureYAML.Validation.AnyCheck<Node>] = Self.defaultChecks) {
|
||||
self.checks = checks
|
||||
}
|
||||
|
||||
public static var blank: Self {
|
||||
Self(checks: [])
|
||||
}
|
||||
|
||||
public static var defaultChecks: [PureYAML.Validation.AnyCheck<Node>] {
|
||||
[
|
||||
PureYAML.Validation.AnyCheck(BuiltinCheck.builtInTagsAreSupportedAndKindConsistent),
|
||||
PureYAML.Validation.AnyCheck(BuiltinCheck.mappingKeysAreUnique),
|
||||
]
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func validate(
|
||||
_ node: Node,
|
||||
strict: Bool = true,
|
||||
) throws -> [PureYAML.Validation.Issue] {
|
||||
let result = collect(node)
|
||||
let failures = strict ? result.issues : result.errors
|
||||
if !failures.isEmpty {
|
||||
throw PureYAML.Validation.Issue.Collection(failures)
|
||||
}
|
||||
return strict ? [] : result.warnings
|
||||
}
|
||||
|
||||
public func collect(_ node: Node) -> PureYAML.Validation.Result {
|
||||
var issues: [PureYAML.Validation.Issue] = []
|
||||
walk(root: node, subject: node, path: .root, issues: &issues)
|
||||
return PureYAML.Validation.Result(issues)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func validate(
|
||||
_ documents: [Document],
|
||||
strict: Bool = true,
|
||||
) throws -> [PureYAML.Stream.Issue] {
|
||||
let result = collect(documents)
|
||||
let failures = strict ? result.issues : result.errors
|
||||
if !failures.isEmpty {
|
||||
throw PureYAML.Stream.Issue.Collection(failures)
|
||||
}
|
||||
return strict ? [] : result.warnings
|
||||
}
|
||||
|
||||
public func collect(_ documents: [Document]) -> PureYAML.Stream.Result {
|
||||
var issues: [PureYAML.Stream.Issue] = []
|
||||
for document in documents {
|
||||
let result = collect(document.node)
|
||||
issues.append(contentsOf: result.issues.map { issue in
|
||||
PureYAML.Stream.Issue(documentIndex: document.index, issue: issue)
|
||||
})
|
||||
}
|
||||
return PureYAML.Stream.Result(issues)
|
||||
}
|
||||
|
||||
/// Adds a type-directed check, which fires only on the `Node` values it specialises on
|
||||
/// wherever they occur in the tree.
|
||||
public func validating(_ check: PureYAML.Validation.Check<Node, Node>) -> Self {
|
||||
var copy = self
|
||||
copy.checks.append(PureYAML.Validation.AnyCheck(check))
|
||||
return copy
|
||||
}
|
||||
|
||||
/// The descriptions of the active checks, in application order: the configuration fixed point.
|
||||
public var validationDescriptions: [String] {
|
||||
checks.map(\.description)
|
||||
}
|
||||
}
|
||||
/// The tagged layer's validator: the same shared `PureYAML.Validation.Validator` traversal engine
|
||||
/// as every other layer (validation-rules.md, principle 10), specialised on `Node` for both the
|
||||
/// walked subject and the cross-cutting document.
|
||||
typealias Validator = PureYAML.Validation.Validator<Node, Node>
|
||||
}
|
||||
|
||||
extension PureYAML.Tagged.Validator {
|
||||
func walk(
|
||||
root: PureYAML.Tagged.Node,
|
||||
subject: PureYAML.Tagged.Node,
|
||||
path: PureYAML.Validation.Path,
|
||||
issues: inout [PureYAML.Validation.Issue],
|
||||
) {
|
||||
// The document is the whole tagged tree; each check filters by runtime type. Offer the node,
|
||||
// and for a mapping the mapping too, so a `Check<Node, Node>` and a `Check<Mapping, Node>`
|
||||
// each fire on what they specialise on.
|
||||
offer(subject, at: path, root: root, issues: &issues)
|
||||
if case let .mapping(mapping) = subject {
|
||||
offer(mapping, at: path, root: root, issues: &issues)
|
||||
}
|
||||
|
||||
switch subject {
|
||||
case let .mapping(mapping):
|
||||
for pair in mapping.pairs {
|
||||
walk(
|
||||
root: root,
|
||||
subject: pair.value,
|
||||
path: path.appending(.key(pair.key)),
|
||||
issues: &issues,
|
||||
)
|
||||
}
|
||||
case let .sequence(sequence):
|
||||
for index in sequence.values.indices {
|
||||
walk(
|
||||
root: root,
|
||||
subject: sequence.values[index],
|
||||
path: path.appending(.index(index)),
|
||||
issues: &issues,
|
||||
)
|
||||
}
|
||||
case .scalar:
|
||||
break
|
||||
case .alias:
|
||||
// The aliased subtree was already walked at its anchor's
|
||||
// definition site; recursing here would report every issue in
|
||||
// that subtree once more per reference.
|
||||
break
|
||||
}
|
||||
public extension PureYAML.Validation.Validator where Subject == PureYAML.Tagged.Node, Document == PureYAML.Tagged.Node {
|
||||
/// The tagged layer's default checks.
|
||||
static var defaultValidations: [PureYAML.Validation.AnyValidation<PureYAML.Tagged.Node>] {
|
||||
[
|
||||
PureYAML.Validation.AnyValidation(PureYAML.Tagged.BuiltinValidation.builtInTagsAreSupportedAndKindConsistent),
|
||||
PureYAML.Validation.AnyValidation(PureYAML.Tagged.BuiltinValidation.mappingKeysAreUnique),
|
||||
]
|
||||
}
|
||||
|
||||
func offer(
|
||||
_ subject: any PureYAML.Validation.Validatable,
|
||||
at path: PureYAML.Validation.Path,
|
||||
root: PureYAML.Tagged.Node,
|
||||
issues: inout [PureYAML.Validation.Issue],
|
||||
) {
|
||||
for check in checks {
|
||||
issues.append(contentsOf: check.apply(to: subject, at: path, in: root))
|
||||
convenience init() {
|
||||
self.init(nonReferenceDefaultValidations: Self.defaultValidations)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func validate(
|
||||
_ documents: [PureYAML.Tagged.Document],
|
||||
strict: Bool = true,
|
||||
) throws -> [PureYAML.Stream.Issue] {
|
||||
let result = collect(documents)
|
||||
let failures = strict ? result.issues : result.errors
|
||||
if !failures.isEmpty {
|
||||
throw PureYAML.Stream.Issue.Collection(failures)
|
||||
}
|
||||
return strict ? [] : result.warnings
|
||||
}
|
||||
|
||||
func collect(_ documents: [PureYAML.Tagged.Document]) -> PureYAML.Stream.Result {
|
||||
var issues: [PureYAML.Stream.Issue] = []
|
||||
for document in documents {
|
||||
let result = collect(document.node)
|
||||
issues.append(contentsOf: result.values.map { error in
|
||||
PureYAML.Stream.Issue(documentIndex: document.index, issue: error)
|
||||
})
|
||||
}
|
||||
return PureYAML.Stream.Result(issues)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// A type-erased `Check`, so checks specialised on different subject types live in one list, all
|
||||
/// A type-erased `Validation`, so checks specialised on different subject types live in one list, all
|
||||
/// sharing one `Document` (the validated tree's root type). It filters by runtime type: a
|
||||
/// `Check<Subject, Document>` fires only on a `Subject` value, never on an optional of it (the
|
||||
/// `Validation<Subject, Document>` fires only on a `Subject` value, never on an optional of it (the
|
||||
/// `type(of:)` guard) or a value of any other type.
|
||||
struct AnyCheck<Document: Sendable>: Sendable {
|
||||
struct AnyValidation<Document: Sendable>: Sendable {
|
||||
public let description: String
|
||||
private let applyErased: @Sendable (any Validatable, Path, Document) -> [Issue]
|
||||
private let applyErased: @Sendable (any Validatable, Path, Document) -> [ValidationError]
|
||||
|
||||
public init<Subject: Validatable>(_ check: Check<Subject, Document>) {
|
||||
description = check.description
|
||||
public init<Subject: Validatable>(_ validation: Validation<Subject, Document>) {
|
||||
description = validation.description
|
||||
applyErased = { subject, path, document in
|
||||
guard let typed = subject as? Subject, type(of: subject) == Subject.self else {
|
||||
return []
|
||||
}
|
||||
return check.apply(SubjectContext(document: document, subject: typed, path: path))
|
||||
return validation.apply(ValidationContext(document: document, subject: typed, path: path))
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ public extension PureYAML.Validation {
|
||||
to subject: any Validatable,
|
||||
at path: Path,
|
||||
in document: Document,
|
||||
) -> [Issue] {
|
||||
) -> [ValidationError] {
|
||||
applyErased(subject, path, document)
|
||||
}
|
||||
}
|
||||
@ -1,28 +1,28 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// The caseless namespace of built-in type-directed checks, referenceable by name.
|
||||
enum BuiltinCheck {
|
||||
enum BuiltinValidation {
|
||||
/// Reports each duplicate key within a single mapping. The type-directed counterpart of the
|
||||
/// legacy `Rule.mappingKeysAreUnique`; fires on `Model.Mapping` values directly rather than
|
||||
/// switching on a `Model.Value`.
|
||||
public static var mappingKeysAreUnique: Check<PureYAML.Model.Mapping, PureYAML.Model.Value> {
|
||||
Check(description: "Mapping keys are unique") { context in
|
||||
public static var mappingKeysAreUnique: Validation<PureYAML.Model.Mapping, PureYAML.Model.Value> {
|
||||
Validation(description: "Mapping keys are unique") { context in
|
||||
var seen = Set<PureYAML.Model.Key>()
|
||||
var issues: [Issue] = []
|
||||
var errors: [ValidationError] = []
|
||||
for pair in context.subject.pairs where !seen.insert(pair.keyNode).inserted {
|
||||
issues.append(Issue(
|
||||
errors.append(ValidationError(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key '\(pair.keyNode.description)'",
|
||||
path: context.path.appending(pair.keyNode.pathComponent),
|
||||
))
|
||||
}
|
||||
return issues
|
||||
return errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PureYAML.Model.Key {
|
||||
/// The path component a duplicate-key issue points at: a plain `.key` for a string key, a
|
||||
/// The path component a duplicate-key error points at: a plain `.key` for a string key, a
|
||||
/// `.complexKey` for a sequence or mapping key (which has no string spelling).
|
||||
var pathComponent: PureYAML.Validation.Path.Component {
|
||||
switch self {
|
||||
@ -1,49 +0,0 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// A type-directed validation value (the `Validation<Subject>` of the validation idiom): a
|
||||
/// positive `description`, a `predicate` that gates application, and a multi-error `check`. It is
|
||||
/// applied only to values of type `Subject`, anywhere they occur in the traversed tree. `Document`
|
||||
/// is the type of the validated tree's root, the same for every check a given validator runs.
|
||||
struct Check<Subject: Validatable, Document: Sendable>: Sendable {
|
||||
public let description: String
|
||||
public let predicate: @Sendable (SubjectContext<Subject, Document>) -> Bool
|
||||
public let check: @Sendable (SubjectContext<Subject, Document>) -> [Issue]
|
||||
|
||||
/// Multi-error form: return one issue per problem. Use when a subject can fail several ways.
|
||||
public init(
|
||||
description: String,
|
||||
check: @escaping @Sendable (SubjectContext<Subject, Document>) -> [Issue],
|
||||
when predicate: @escaping @Sendable (SubjectContext<Subject, Document>) -> Bool = { _ in true },
|
||||
) {
|
||||
self.description = description
|
||||
self.predicate = predicate
|
||||
self.check = check
|
||||
}
|
||||
|
||||
/// Single-error Bool form: a positive description plus a check whose `false` auto-produces the
|
||||
/// one issue `"Failed to satisfy: <description>"`. The dominant form.
|
||||
public init(
|
||||
description: String,
|
||||
severity: Severity = .error,
|
||||
check: @escaping @Sendable (SubjectContext<Subject, Document>) -> Bool,
|
||||
when predicate: @escaping @Sendable (SubjectContext<Subject, Document>) -> Bool = { _ in true },
|
||||
) {
|
||||
self.init(
|
||||
description: description,
|
||||
check: { context in
|
||||
check(context)
|
||||
? []
|
||||
: [Issue(severity: severity, reason: "Failed to satisfy: \(description)", path: context.path)]
|
||||
},
|
||||
when: predicate,
|
||||
)
|
||||
}
|
||||
|
||||
/// Gates on the predicate, then runs the check.
|
||||
public func apply(_ context: SubjectContext<Subject, Document>) -> [Issue] {
|
||||
guard predicate(context) else {
|
||||
return []
|
||||
}
|
||||
return check(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ public extension PureYAML {
|
||||
static func diagnosticValidationReport(
|
||||
_ yaml: String,
|
||||
file: String? = nil,
|
||||
using validator: Validation.Validator = .init(),
|
||||
using validator: Validation.Validator<Model.Value, Model.Value> = .init(),
|
||||
preflightScanner: Validation.PreflightScanner = .init(),
|
||||
) -> Validation.Report {
|
||||
diagnosticValidationResult(
|
||||
@ -17,7 +17,7 @@ public extension PureYAML {
|
||||
/// Validates many YAML inputs with preflight diagnostics for damaged input.
|
||||
static func diagnosticValidationReports(
|
||||
_ sources: [Validation.Source],
|
||||
using validator: Validation.Validator = .init(),
|
||||
using validator: Validation.Validator<Model.Value, Model.Value> = .init(),
|
||||
failOnWarnings: Bool = false,
|
||||
preflightScanner: Validation.PreflightScanner = .init(),
|
||||
) -> Validation.BatchReport {
|
||||
@ -41,7 +41,7 @@ public extension PureYAML {
|
||||
static func parseValidated(
|
||||
_ yaml: String,
|
||||
file: String? = nil,
|
||||
using validator: Validation.Validator = .init(),
|
||||
using validator: Validation.Validator<Model.Value, Model.Value> = .init(),
|
||||
preflightScanner: Validation.PreflightScanner = .init(),
|
||||
) throws -> Model.Value {
|
||||
let documents = try parseValidatedStream(
|
||||
@ -57,7 +57,7 @@ public extension PureYAML {
|
||||
static func parseValidatedStream(
|
||||
_ yaml: String,
|
||||
file: String? = nil,
|
||||
using validator: Validation.Validator = .init(),
|
||||
using validator: Validation.Validator<Model.Value, Model.Value> = .init(),
|
||||
preflightScanner: Validation.PreflightScanner = .init(),
|
||||
) throws -> [Stream.Document] {
|
||||
let result = diagnosticValidationResult(
|
||||
@ -82,7 +82,7 @@ private extension PureYAML {
|
||||
static func diagnosticValidationResult(
|
||||
_ yaml: String,
|
||||
file: String?,
|
||||
using validator: Validation.Validator,
|
||||
using validator: Validation.Validator<Model.Value, Model.Value>,
|
||||
preflightScanner: Validation.PreflightScanner,
|
||||
) -> DiagnosticValidationResult {
|
||||
var diagnostics = preflightScanner.diagnostics(in: yaml, file: file)
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
public extension PureYAML.Validation.Issue {
|
||||
/// Error thrown when validation reports failing issues.
|
||||
struct Collection: Swift.Error, Equatable, Sendable, CustomStringConvertible {
|
||||
public var issues: [PureYAML.Validation.Issue]
|
||||
|
||||
public init(_ issues: [PureYAML.Validation.Issue]) {
|
||||
self.issues = issues
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
issues.map(\.description).joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// Path-aware validation issue.
|
||||
struct Issue: Swift.Error, Equatable, Sendable, CustomStringConvertible {
|
||||
public var severity: Severity
|
||||
public var reason: String
|
||||
public var path: Path
|
||||
|
||||
public init(
|
||||
severity: Severity,
|
||||
reason: String,
|
||||
path: Path = .root,
|
||||
) {
|
||||
self.severity = severity
|
||||
self.reason = reason
|
||||
self.path = path
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
let location = path.isRoot ? "root" : path.description
|
||||
return "\(severity.description): \(reason) at \(location)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,19 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// Collected validation issues.
|
||||
/// Collected validation errors, the non-throwing outcome accessor beside the throwing
|
||||
/// `Validator.validate(_:strict:)` gate.
|
||||
struct Result: Equatable, Sendable {
|
||||
public var issues: [Issue]
|
||||
public var values: [ValidationError]
|
||||
|
||||
public init(_ issues: [Issue] = []) {
|
||||
self.issues = issues
|
||||
public init(_ values: [ValidationError] = []) {
|
||||
self.values = values
|
||||
}
|
||||
|
||||
public var errors: [Issue] {
|
||||
issues.filter { $0.severity == .error }
|
||||
public var errors: [ValidationError] {
|
||||
values.filter { $0.severity == .error }
|
||||
}
|
||||
|
||||
public var warnings: [Issue] {
|
||||
issues.filter { $0.severity == .warning }
|
||||
public var warnings: [ValidationError] {
|
||||
values.filter { $0.severity == .warning }
|
||||
}
|
||||
|
||||
public var isValid: Bool {
|
||||
|
||||
15
Sources/Validation/Traversable.swift
Normal file
15
Sources/Validation/Traversable.swift
Normal file
@ -0,0 +1,15 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// A tree node a `Validator` can walk. `Validator`'s traversal engine is shared by every layer
|
||||
/// (validation-rules.md, principle 10); each `Traversable` conformance supplies only that layer's
|
||||
/// own traversal shape: "each layer defines only its own document, its rules, and its traversal".
|
||||
protocol Traversable: Validatable {
|
||||
/// Extra values to offer to type-directed checks at this node, beyond the node itself (e.g. a
|
||||
/// mapping's own type, or a mapping's key nodes). A `nil` path component offers at this node's
|
||||
/// own path; a non-nil one offers at that child's path (e.g. a mapping key's location).
|
||||
var extraOffers: [(component: Path.Component?, value: any Validatable)] { get }
|
||||
|
||||
/// This node's children to recurse into, each paired with the path component that reaches it
|
||||
/// from this node.
|
||||
var children: [(component: Path.Component, value: Self)] { get }
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// Marker for a value a type-directed validation can be applied to. A `Check<Subject>` fires on
|
||||
/// Marker for a value a type-directed validation can be applied to. A `Validation<Subject>` fires on
|
||||
/// `Subject` values wherever they appear in the traversed tree; this protocol constrains the
|
||||
/// subjects so the erased dispatch can filter by runtime type.
|
||||
protocol Validatable: Sendable {}
|
||||
|
||||
@ -198,7 +198,7 @@ public extension PureYAML {
|
||||
static func validationReport(
|
||||
_ yaml: String,
|
||||
file: String? = nil,
|
||||
using validator: Validation.Validator = .init(),
|
||||
using validator: Validation.Validator<Model.Value, Model.Value> = .init(),
|
||||
) -> Validation.Report {
|
||||
do {
|
||||
let documents = try parseStream(yaml)
|
||||
@ -241,7 +241,7 @@ public extension PureYAML {
|
||||
/// Validates many YAML inputs without throwing or stopping at the first failure.
|
||||
static func validationReports(
|
||||
_ sources: [Validation.Source],
|
||||
using validator: Validation.Validator = .init(),
|
||||
using validator: Validation.Validator<Model.Value, Model.Value> = .init(),
|
||||
failOnWarnings: Bool = false,
|
||||
) -> Validation.BatchReport {
|
||||
let reports = sources.map { source in
|
||||
|
||||
49
Sources/Validation/Validation.swift
Normal file
49
Sources/Validation/Validation.swift
Normal file
@ -0,0 +1,49 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// A type-directed validation value: a positive `description`, a `predicate` that gates
|
||||
/// application, and a multi-error `check`. It is applied only to values of type `Subject`, anywhere
|
||||
/// they occur in the traversed tree. `Document` is the type of the validated tree's root, the same
|
||||
/// for every check a given validator runs.
|
||||
struct Validation<Subject: Validatable, Document: Sendable>: Sendable {
|
||||
public let description: String
|
||||
public let predicate: @Sendable (ValidationContext<Subject, Document>) -> Bool
|
||||
public let check: @Sendable (ValidationContext<Subject, Document>) -> [ValidationError]
|
||||
|
||||
/// Multi-error form: return one error per problem. Use when a subject can fail several ways.
|
||||
public init(
|
||||
description: String,
|
||||
check: @escaping @Sendable (ValidationContext<Subject, Document>) -> [ValidationError],
|
||||
when predicate: @escaping @Sendable (ValidationContext<Subject, Document>) -> Bool = { _ in true },
|
||||
) {
|
||||
self.description = description
|
||||
self.predicate = predicate
|
||||
self.check = check
|
||||
}
|
||||
|
||||
/// Single-error Bool form: a positive description plus a check whose `false` auto-produces the
|
||||
/// one error `"Failed to satisfy: <description>"`. The dominant form.
|
||||
public init(
|
||||
description: String,
|
||||
severity: Severity = .error,
|
||||
check: @escaping @Sendable (ValidationContext<Subject, Document>) -> Bool,
|
||||
when predicate: @escaping @Sendable (ValidationContext<Subject, Document>) -> Bool = { _ in true },
|
||||
) {
|
||||
self.init(
|
||||
description: description,
|
||||
check: { context in
|
||||
check(context)
|
||||
? []
|
||||
: [ValidationError(severity: severity, reason: "Failed to satisfy: \(description)", path: context.path)]
|
||||
},
|
||||
when: predicate,
|
||||
)
|
||||
}
|
||||
|
||||
/// Gates on the predicate, then runs the check.
|
||||
public func apply(_ context: ValidationContext<Subject, Document>) -> [ValidationError] {
|
||||
guard predicate(context) else {
|
||||
return []
|
||||
}
|
||||
return check(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
// The combinator algebra for type-directed checks (validation-rules.md, "The combinator algebra").
|
||||
// Declarative building blocks composed by value: comparison operators and `take` lift a subject or
|
||||
// context value into a Bool predicate; `&&` / `||` compose checks (concatenating or short-circuiting
|
||||
// their errors); `all` and `lift` apply many checks to one or a child value. `lookup` is omitted: it
|
||||
// resolves references against a component store, which the generic model does not carry; a consumer
|
||||
// with references supplies it over its own document.
|
||||
// their errors); `all` and `lift` apply many checks to one or a child value; `unwrap` errors on a nil
|
||||
// optional, else runs child checks. `lookup` is omitted: it resolves references against a component
|
||||
// store, which the generic model does not carry; a consumer with references supplies it over its own
|
||||
// document.
|
||||
//
|
||||
// Each combinator is generic over both the subject `S` and the document `D` (the validated tree's
|
||||
// root type), so the same algebra serves the model layer (`D == Model.Value`) and the tagged layer
|
||||
@ -18,49 +19,49 @@ private struct SendableKeyPath<Root, Value>: @unchecked Sendable {
|
||||
// MARK: comparison operators lifting a key path and a literal into a predicate
|
||||
|
||||
public func == <S, D, U: Equatable & Sendable>(
|
||||
lhs: KeyPath<PureYAML.Validation.SubjectContext<S, D>, U>,
|
||||
lhs: KeyPath<PureYAML.Validation.ValidationContext<S, D>, U>,
|
||||
rhs: U,
|
||||
) -> @Sendable (PureYAML.Validation.SubjectContext<S, D>) -> Bool {
|
||||
) -> @Sendable (PureYAML.Validation.ValidationContext<S, D>) -> Bool {
|
||||
let boxed = SendableKeyPath(keyPath: lhs)
|
||||
return { $0[keyPath: boxed.keyPath] == rhs }
|
||||
}
|
||||
|
||||
public func != <S, D, U: Equatable & Sendable>(
|
||||
lhs: KeyPath<PureYAML.Validation.SubjectContext<S, D>, U>,
|
||||
lhs: KeyPath<PureYAML.Validation.ValidationContext<S, D>, U>,
|
||||
rhs: U,
|
||||
) -> @Sendable (PureYAML.Validation.SubjectContext<S, D>) -> Bool {
|
||||
) -> @Sendable (PureYAML.Validation.ValidationContext<S, D>) -> Bool {
|
||||
let boxed = SendableKeyPath(keyPath: lhs)
|
||||
return { $0[keyPath: boxed.keyPath] != rhs }
|
||||
}
|
||||
|
||||
public func > <S, D, U: Comparable & Sendable>(
|
||||
lhs: KeyPath<PureYAML.Validation.SubjectContext<S, D>, U>,
|
||||
lhs: KeyPath<PureYAML.Validation.ValidationContext<S, D>, U>,
|
||||
rhs: U,
|
||||
) -> @Sendable (PureYAML.Validation.SubjectContext<S, D>) -> Bool {
|
||||
) -> @Sendable (PureYAML.Validation.ValidationContext<S, D>) -> Bool {
|
||||
let boxed = SendableKeyPath(keyPath: lhs)
|
||||
return { $0[keyPath: boxed.keyPath] > rhs }
|
||||
}
|
||||
|
||||
public func >= <S, D, U: Comparable & Sendable>(
|
||||
lhs: KeyPath<PureYAML.Validation.SubjectContext<S, D>, U>,
|
||||
lhs: KeyPath<PureYAML.Validation.ValidationContext<S, D>, U>,
|
||||
rhs: U,
|
||||
) -> @Sendable (PureYAML.Validation.SubjectContext<S, D>) -> Bool {
|
||||
) -> @Sendable (PureYAML.Validation.ValidationContext<S, D>) -> Bool {
|
||||
let boxed = SendableKeyPath(keyPath: lhs)
|
||||
return { $0[keyPath: boxed.keyPath] >= rhs }
|
||||
}
|
||||
|
||||
public func < <S, D, U: Comparable & Sendable>(
|
||||
lhs: KeyPath<PureYAML.Validation.SubjectContext<S, D>, U>,
|
||||
lhs: KeyPath<PureYAML.Validation.ValidationContext<S, D>, U>,
|
||||
rhs: U,
|
||||
) -> @Sendable (PureYAML.Validation.SubjectContext<S, D>) -> Bool {
|
||||
) -> @Sendable (PureYAML.Validation.ValidationContext<S, D>) -> Bool {
|
||||
let boxed = SendableKeyPath(keyPath: lhs)
|
||||
return { $0[keyPath: boxed.keyPath] < rhs }
|
||||
}
|
||||
|
||||
public func <= <S, D, U: Comparable & Sendable>(
|
||||
lhs: KeyPath<PureYAML.Validation.SubjectContext<S, D>, U>,
|
||||
lhs: KeyPath<PureYAML.Validation.ValidationContext<S, D>, U>,
|
||||
rhs: U,
|
||||
) -> @Sendable (PureYAML.Validation.SubjectContext<S, D>) -> Bool {
|
||||
) -> @Sendable (PureYAML.Validation.ValidationContext<S, D>) -> Bool {
|
||||
let boxed = SendableKeyPath(keyPath: lhs)
|
||||
return { $0[keyPath: boxed.keyPath] <= rhs }
|
||||
}
|
||||
@ -69,9 +70,9 @@ public extension PureYAML.Validation {
|
||||
/// Digs to a value through a key path on the context and runs arbitrary logic, the escape hatch
|
||||
/// beyond equality.
|
||||
static func take<S, D, U: Sendable>(
|
||||
_ keyPath: KeyPath<SubjectContext<S, D>, U>,
|
||||
_ keyPath: KeyPath<ValidationContext<S, D>, U>,
|
||||
_ predicate: @escaping @Sendable (U) -> Bool,
|
||||
) -> @Sendable (SubjectContext<S, D>) -> Bool {
|
||||
) -> @Sendable (ValidationContext<S, D>) -> Bool {
|
||||
let boxed = SendableKeyPath(keyPath: keyPath)
|
||||
return { predicate($0[keyPath: boxed.keyPath]) }
|
||||
}
|
||||
@ -80,15 +81,15 @@ public extension PureYAML.Validation {
|
||||
static func take<S, D, U: Sendable>(
|
||||
_ keyPath: KeyPath<S, U>,
|
||||
_ predicate: @escaping @Sendable (U) -> Bool,
|
||||
) -> @Sendable (SubjectContext<S, D>) -> Bool {
|
||||
) -> @Sendable (ValidationContext<S, D>) -> Bool {
|
||||
let boxed = SendableKeyPath(keyPath: keyPath)
|
||||
return { predicate($0.subject[keyPath: boxed.keyPath]) }
|
||||
}
|
||||
|
||||
/// Applies every check to the same context, concatenating their errors.
|
||||
static func all<S, D>(_ checks: Check<S, D>...) -> Check<S, D> {
|
||||
let list = checks
|
||||
return Check(description: "all of: \(list.map(\.description).joined(separator: ", "))") { context in
|
||||
static func all<S, D>(_ validations: Validation<S, D>...) -> Validation<S, D> {
|
||||
let list = validations
|
||||
return Validation(description: "all of: \(list.map(\.description).joined(separator: ", "))") { context in
|
||||
list.flatMap { $0.apply(context) }
|
||||
}
|
||||
}
|
||||
@ -97,12 +98,12 @@ public extension PureYAML.Validation {
|
||||
/// and document.
|
||||
static func lift<S, Child: Validatable, D>(
|
||||
_ keyPath: KeyPath<S, Child>,
|
||||
into checks: Check<Child, D>...,
|
||||
) -> Check<S, D> {
|
||||
let list = checks
|
||||
into validations: Validation<Child, D>...,
|
||||
) -> Validation<S, D> {
|
||||
let list = validations
|
||||
let boxed = SendableKeyPath(keyPath: keyPath)
|
||||
return Check(description: "child: \(list.map(\.description).joined(separator: ", "))") { context in
|
||||
let childContext = SubjectContext(
|
||||
return Validation(description: "child: \(list.map(\.description).joined(separator: ", "))") { context in
|
||||
let childContext = ValidationContext(
|
||||
document: context.document,
|
||||
subject: context.subject[keyPath: boxed.keyPath],
|
||||
path: context.path,
|
||||
@ -110,24 +111,43 @@ public extension PureYAML.Validation {
|
||||
return list.flatMap { $0.apply(childContext) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Unwraps an optional reached through a key path, erroring if nil, else running child checks
|
||||
/// against the unwrapped value.
|
||||
static func unwrap<S, Child: Validatable, D>(
|
||||
_ keyPath: KeyPath<S, Child?>,
|
||||
into validations: Validation<Child, D>...,
|
||||
description: String? = nil,
|
||||
) -> Validation<S, D> {
|
||||
let list = validations
|
||||
let boxed = SendableKeyPath(keyPath: keyPath)
|
||||
return Validation(description: description ?? "unwraps: \(list.map(\.description).joined(separator: ", "))") { context in
|
||||
guard let child = context.subject[keyPath: boxed.keyPath] else {
|
||||
let reason = description.map { "Tried to unwrap but found nil: \($0)" } ?? "Tried to unwrap an optional and found nil"
|
||||
return [ValidationError(severity: .error, reason: reason, path: context.path)]
|
||||
}
|
||||
let childContext = ValidationContext(document: context.document, subject: child, path: context.path)
|
||||
return list.flatMap { $0.apply(childContext) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: composing two checks (&& concatenates errors, || short-circuits)
|
||||
|
||||
public func && <S, D>(
|
||||
lhs: PureYAML.Validation.Check<S, D>,
|
||||
rhs: PureYAML.Validation.Check<S, D>,
|
||||
) -> PureYAML.Validation.Check<S, D> {
|
||||
PureYAML.Validation.Check(description: "\(lhs.description) and \(rhs.description)") { context in
|
||||
lhs: PureYAML.Validation.Validation<S, D>,
|
||||
rhs: PureYAML.Validation.Validation<S, D>,
|
||||
) -> PureYAML.Validation.Validation<S, D> {
|
||||
PureYAML.Validation.Validation(description: "\(lhs.description) and \(rhs.description)") { context in
|
||||
lhs.apply(context) + rhs.apply(context)
|
||||
}
|
||||
}
|
||||
|
||||
public func || <S, D>(
|
||||
lhs: PureYAML.Validation.Check<S, D>,
|
||||
rhs: PureYAML.Validation.Check<S, D>,
|
||||
) -> PureYAML.Validation.Check<S, D> {
|
||||
PureYAML.Validation.Check(description: "\(lhs.description) or \(rhs.description)") { context in
|
||||
lhs: PureYAML.Validation.Validation<S, D>,
|
||||
rhs: PureYAML.Validation.Validation<S, D>,
|
||||
) -> PureYAML.Validation.Validation<S, D> {
|
||||
PureYAML.Validation.Validation(description: "\(lhs.description) or \(rhs.description)") { context in
|
||||
let left = lhs.apply(context)
|
||||
guard !left.isEmpty else {
|
||||
return []
|
||||
@ -3,7 +3,7 @@ public extension PureYAML.Validation {
|
||||
/// checks), the current `subject` of the check's type, and the `path` to where it sits. `Document`
|
||||
/// is the type of the validated tree's root, so each layer carries a truthful document: `Model.Value`
|
||||
/// for the model tree, `Tagged.Node` for the tagged tree.
|
||||
struct SubjectContext<Subject: Validatable, Document: Sendable>: Sendable {
|
||||
struct ValidationContext<Subject: Validatable, Document: Sendable>: Sendable {
|
||||
public var document: Document
|
||||
public var subject: Subject
|
||||
public var path: Path
|
||||
26
Sources/Validation/ValidationError.swift
Normal file
26
Sources/Validation/ValidationError.swift
Normal file
@ -0,0 +1,26 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// Path-aware validation error.
|
||||
struct ValidationError: Swift.Error, Equatable, Sendable, CustomStringConvertible {
|
||||
public var severity: Severity
|
||||
public var reason: String
|
||||
public var path: Path
|
||||
|
||||
public init(
|
||||
severity: Severity,
|
||||
reason: String,
|
||||
path: Path = .root,
|
||||
) {
|
||||
self.severity = severity
|
||||
self.reason = reason
|
||||
self.path = path
|
||||
}
|
||||
|
||||
/// Strips a trailing period from `reason` and renders the location consistently:
|
||||
/// `"<reason> at root of document"` at the root, `"<reason> at path: <path>"` elsewhere.
|
||||
/// `severity` is a separate stored property (see `Result.errors`/`.warnings`), not rendered here.
|
||||
public var description: String {
|
||||
let reasonText = reason.hasSuffix(".") ? String(reason.dropLast()) : reason
|
||||
return path.isRoot ? "\(reasonText) at root of document" : "\(reasonText) at path: \(path.description)"
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Sources/Validation/ValidationErrorCollection.swift
Normal file
14
Sources/Validation/ValidationErrorCollection.swift
Normal file
@ -0,0 +1,14 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// Error thrown when validation reports failing errors.
|
||||
struct ValidationErrorCollection: Swift.Error, Equatable, Sendable, CustomStringConvertible {
|
||||
public var values: [ValidationError]
|
||||
|
||||
public init(_ values: [ValidationError]) {
|
||||
self.values = values
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
values.map(\.description).joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,142 +1,166 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// Traverses a YAML value tree and applies type-directed checks at each node. The check value, its
|
||||
/// erasure, and its context all come from the one validation framework (validation-rules.md,
|
||||
/// principle 10); this layer adds only its document type (`Model.Value`), its built-in checks, and
|
||||
/// its traversal.
|
||||
struct Validator: Sendable {
|
||||
public var checks: [AnyCheck<PureYAML.Model.Value>]
|
||||
/// Traverses a `Subject` tree and applies type-directed checks at each node. The validation value,
|
||||
/// its erasure, its context, the combinators, and this traversal engine are all the one shared
|
||||
/// framework (validation-rules.md, principle 10); each layer supplies only its own `Subject`/
|
||||
/// `Document` types (through `Traversable`), its built-in checks, and its own traversal shape.
|
||||
///
|
||||
/// `@unchecked Sendable`: immutable after `init` (every stored validation list is a `let`), so
|
||||
/// concurrent reads race on nothing; the erased `AnyValidation` closures are simply not provably
|
||||
/// `Sendable` to the compiler.
|
||||
final class Validator<Subject: Traversable, Document: Sendable>: @unchecked Sendable {
|
||||
fileprivate let nonReferenceDefaultValidations: [AnyValidation<Document>]
|
||||
fileprivate let referenceDefaultValidations: [AnyValidation<Document>]
|
||||
fileprivate let customValidations: [AnyValidation<Document>]
|
||||
|
||||
public init(checks: [AnyCheck<PureYAML.Model.Value>] = Self.defaultChecks) {
|
||||
self.checks = checks
|
||||
public init(
|
||||
nonReferenceDefaultValidations: [AnyValidation<Document>] = [],
|
||||
referenceDefaultValidations: [AnyValidation<Document>] = [],
|
||||
customValidations: [AnyValidation<Document>] = [],
|
||||
) {
|
||||
self.nonReferenceDefaultValidations = nonReferenceDefaultValidations
|
||||
self.referenceDefaultValidations = referenceDefaultValidations
|
||||
self.customValidations = customValidations
|
||||
}
|
||||
|
||||
public static var blank: Self {
|
||||
Self(checks: [])
|
||||
/// A validator with no checks; add your own with `validating(_:)`.
|
||||
public static var blank: Validator {
|
||||
Validator()
|
||||
}
|
||||
|
||||
public static var defaultChecks: [AnyCheck<PureYAML.Model.Value>] {
|
||||
[AnyCheck(BuiltinCheck.mappingKeysAreUnique)]
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func validate(
|
||||
_ value: PureYAML.Model.Value,
|
||||
strict: Bool = true,
|
||||
) throws -> [Issue] {
|
||||
let result = collect(value)
|
||||
let failures = strict ? result.issues : result.errors
|
||||
if !failures.isEmpty {
|
||||
throw Issue.Collection(failures)
|
||||
}
|
||||
return strict ? [] : result.warnings
|
||||
}
|
||||
|
||||
public func collect(_ value: PureYAML.Model.Value) -> Result {
|
||||
var issues: [Issue] = []
|
||||
walk(root: value, subject: value, path: .root, issues: &issues)
|
||||
return Result(issues)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func validate(
|
||||
_ documents: [PureYAML.Stream.Document],
|
||||
strict: Bool = true,
|
||||
) throws -> [PureYAML.Stream.Issue] {
|
||||
let result = collect(documents)
|
||||
let failures = strict ? result.issues : result.errors
|
||||
if !failures.isEmpty {
|
||||
throw PureYAML.Stream.Issue.Collection(failures)
|
||||
}
|
||||
return strict ? [] : result.warnings
|
||||
}
|
||||
|
||||
public func collect(_ documents: [PureYAML.Stream.Document]) -> PureYAML.Stream.Result {
|
||||
var issues: [PureYAML.Stream.Issue] = []
|
||||
for document in documents {
|
||||
let result = collect(document.value)
|
||||
issues.append(contentsOf: result.issues.map { issue in
|
||||
PureYAML.Stream.Issue(documentIndex: document.index, issue: issue)
|
||||
})
|
||||
}
|
||||
return PureYAML.Stream.Result(issues)
|
||||
}
|
||||
|
||||
/// Adds a type-directed check, which fires only on values of its `Subject` type wherever they
|
||||
/// occur in the tree.
|
||||
public func validating(_ check: Check<some Validatable, PureYAML.Model.Value>) -> Self {
|
||||
var copy = self
|
||||
copy.checks.append(AnyCheck(check))
|
||||
return copy
|
||||
}
|
||||
|
||||
/// Removes every check whose description matches, the identity the validation idiom uses for
|
||||
/// removal. Descriptions are therefore required to be unique and stable.
|
||||
public func withoutValidating(_ description: String) -> Self {
|
||||
var copy = self
|
||||
copy.checks.removeAll { $0.description == description }
|
||||
return copy
|
||||
}
|
||||
|
||||
/// The descriptions of the active checks, in application order: the fixed point a
|
||||
/// configuration-pin test asserts against, so adding, removing, or rewording one changes it
|
||||
/// deliberately.
|
||||
/// The descriptions of every active check, in application order.
|
||||
public var validationDescriptions: [String] {
|
||||
checks.map(\.description)
|
||||
validations.map(\.description)
|
||||
}
|
||||
|
||||
/// Descriptions of the non-reference default tier, in order.
|
||||
public var nonReferenceValidationDescriptions: [String] {
|
||||
nonReferenceDefaultValidations.map(\.description)
|
||||
}
|
||||
|
||||
/// Descriptions of the reference default tier, in order.
|
||||
public var referenceValidationDescriptions: [String] {
|
||||
referenceDefaultValidations.map(\.description)
|
||||
}
|
||||
|
||||
/// Descriptions of the custom tier, in order.
|
||||
public var customValidationDescriptions: [String] {
|
||||
customValidations.map(\.description)
|
||||
}
|
||||
|
||||
fileprivate var validations: [AnyValidation<Document>] {
|
||||
nonReferenceDefaultValidations + referenceDefaultValidations + customValidations
|
||||
}
|
||||
|
||||
/// Returns a validator with `validation` appended to the custom tier.
|
||||
@discardableResult
|
||||
public func validating(_ validation: Validation<some Validatable, Document>) -> Validator {
|
||||
Validator(
|
||||
nonReferenceDefaultValidations: nonReferenceDefaultValidations,
|
||||
referenceDefaultValidations: referenceDefaultValidations,
|
||||
customValidations: customValidations + [AnyValidation(validation)],
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a validator with the checks matching `descriptions` removed from every tier, the
|
||||
/// identity the validation idiom uses for removal. Descriptions are therefore required to be
|
||||
/// unique and stable.
|
||||
@discardableResult
|
||||
public func withoutValidating(_ descriptions: String...) -> Validator {
|
||||
let drop = Set(descriptions)
|
||||
func filter(_ tier: [AnyValidation<Document>]) -> [AnyValidation<Document>] {
|
||||
tier.filter { !drop.contains($0.description) }
|
||||
}
|
||||
return Validator(
|
||||
nonReferenceDefaultValidations: filter(nonReferenceDefaultValidations),
|
||||
referenceDefaultValidations: filter(referenceDefaultValidations),
|
||||
customValidations: filter(customValidations),
|
||||
)
|
||||
}
|
||||
|
||||
public func collect(_ subject: Subject, in document: Document) -> Result {
|
||||
var errors: [ValidationError] = []
|
||||
walk(subject, at: .root, in: document, into: &errors)
|
||||
return Result(errors)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func validate(
|
||||
_ subject: Subject,
|
||||
in document: Document,
|
||||
strict: Bool = true,
|
||||
) throws -> [ValidationError] {
|
||||
let result = collect(subject, in: document)
|
||||
let failures = strict ? result.values : result.errors
|
||||
if !failures.isEmpty {
|
||||
throw ValidationErrorCollection(failures)
|
||||
}
|
||||
return strict ? [] : result.warnings
|
||||
}
|
||||
|
||||
private func walk(_ subject: Subject, at path: Path, in document: Document, into errors: inout [ValidationError]) {
|
||||
// Offer the node itself, then any extra values this layer's Traversable conformance wants
|
||||
// offered here (a mapping's own type, its key nodes), then recurse into its children.
|
||||
offer(subject, at: path, in: document, into: &errors)
|
||||
for (component, value) in subject.extraOffers {
|
||||
offer(value, at: component.map(path.appending) ?? path, in: document, into: &errors)
|
||||
}
|
||||
for (component, child) in subject.children {
|
||||
walk(child, at: path.appending(component), in: document, into: &errors)
|
||||
}
|
||||
}
|
||||
|
||||
private func offer(_ value: any Validatable, at path: Path, in document: Document, into errors: inout [ValidationError]) {
|
||||
for validation in validations {
|
||||
errors.append(contentsOf: validation.apply(to: value, at: path, in: document))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PureYAML.Validation.Validator {
|
||||
func walk(
|
||||
root: PureYAML.Model.Value,
|
||||
subject: PureYAML.Model.Value,
|
||||
path: PureYAML.Validation.Path,
|
||||
issues: inout [PureYAML.Validation.Issue],
|
||||
) {
|
||||
// Offer each type-directed check the values it may specialise on at this node: the value
|
||||
// itself, and, for a mapping, the mapping and each of its keys. The erased check filters by
|
||||
// runtime type, so a `Check<Model.Mapping>` ignores the value and key offers, and a
|
||||
// `Check<Model.Value>` (a whole-tree rule) fires on the value offer.
|
||||
offer(subject, at: path, root: root, issues: &issues)
|
||||
if case let .mapping(mapping) = subject {
|
||||
offer(mapping, at: path, root: root, issues: &issues)
|
||||
for pair in mapping.pairs {
|
||||
offer(pair.keyNode, at: path.appending(pair.keyNode.pathComponent), root: root, issues: &issues)
|
||||
}
|
||||
}
|
||||
|
||||
switch subject {
|
||||
case let .mapping(mapping):
|
||||
for pair in mapping.pairs {
|
||||
walk(
|
||||
root: root,
|
||||
subject: pair.value,
|
||||
path: path.appending(pair.keyNode.pathComponent),
|
||||
issues: &issues,
|
||||
)
|
||||
}
|
||||
case let .sequence(values):
|
||||
for index in values.indices {
|
||||
walk(
|
||||
root: root,
|
||||
subject: values[index],
|
||||
path: path.appending(.index(index)),
|
||||
issues: &issues,
|
||||
)
|
||||
}
|
||||
case .null, .bool, .int, .double, .string:
|
||||
break
|
||||
}
|
||||
public extension PureYAML.Validation.Validator where Subject == Document {
|
||||
/// Convenience for the common case where the walked tree's root doubles as the cross-cutting
|
||||
/// document (every PureYAML layer, today).
|
||||
func collect(_ subject: Subject) -> PureYAML.Validation.Result {
|
||||
collect(subject, in: subject)
|
||||
}
|
||||
|
||||
func offer(
|
||||
_ subject: any PureYAML.Validation.Validatable,
|
||||
at path: PureYAML.Validation.Path,
|
||||
root: PureYAML.Model.Value,
|
||||
issues: inout [PureYAML.Validation.Issue],
|
||||
) {
|
||||
for check in checks {
|
||||
issues.append(contentsOf: check.apply(to: subject, at: path, in: root))
|
||||
}
|
||||
@discardableResult
|
||||
func validate(_ subject: Subject, strict: Bool = true) throws -> [PureYAML.Validation.ValidationError] {
|
||||
try validate(subject, in: subject, strict: strict)
|
||||
}
|
||||
}
|
||||
|
||||
public extension PureYAML.Validation.Validator where Subject == PureYAML.Model.Value, Document == PureYAML.Model.Value {
|
||||
/// The model layer's default checks: schema-independent structural rules.
|
||||
static var defaultValidations: [PureYAML.Validation.AnyValidation<PureYAML.Model.Value>] {
|
||||
[PureYAML.Validation.AnyValidation(PureYAML.Validation.BuiltinValidation.mappingKeysAreUnique)]
|
||||
}
|
||||
|
||||
convenience init() {
|
||||
self.init(nonReferenceDefaultValidations: Self.defaultValidations)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func validate(
|
||||
_ documents: [PureYAML.Stream.Document],
|
||||
strict: Bool = true,
|
||||
) throws -> [PureYAML.Stream.Issue] {
|
||||
let result = collect(documents)
|
||||
let failures = strict ? result.issues : result.errors
|
||||
if !failures.isEmpty {
|
||||
throw PureYAML.Stream.Issue.Collection(failures)
|
||||
}
|
||||
return strict ? [] : result.warnings
|
||||
}
|
||||
|
||||
func collect(_ documents: [PureYAML.Stream.Document]) -> PureYAML.Stream.Result {
|
||||
var issues: [PureYAML.Stream.Issue] = []
|
||||
for document in documents {
|
||||
let result = collect(document.value)
|
||||
issues.append(contentsOf: result.values.map { error in
|
||||
PureYAML.Stream.Issue(documentIndex: document.index, issue: error)
|
||||
})
|
||||
}
|
||||
return PureYAML.Stream.Result(issues)
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,12 +20,12 @@ struct ArbitraryAnyDecisionTests {
|
||||
mapping.pairs.map { ($0.key, $0.value) },
|
||||
uniquingKeysWith: { _, second in second },
|
||||
)
|
||||
let validationResult = PureYAML.Validation.Validator().collect(value)
|
||||
let validationResult = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value)
|
||||
|
||||
#expect(mapping.pairs.map(\.key) == ["title", "title", "slug"])
|
||||
#expect(projectedDictionary.keys.sorted() == ["slug", "title"])
|
||||
#expect(projectedDictionary["title"] == .string("Second"))
|
||||
#expect(validationResult.issues == [
|
||||
#expect(validationResult.values == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
|
||||
@ -102,14 +102,14 @@ struct CodableCompatibilityTests {
|
||||
.init(key: "/users", value: .int(2)),
|
||||
])))
|
||||
} check: { collection in
|
||||
#expect(collection.issues == [
|
||||
#expect(collection.values == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key '/users'",
|
||||
path: .init([.key("/users")]),
|
||||
),
|
||||
])
|
||||
#expect(collection.description == "error: Duplicate mapping key '/users' at $[\"/users\"]")
|
||||
#expect(collection.description == "Duplicate mapping key '/users' at path: $[\"/users\"]")
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,12 +225,12 @@ private func expectEncodingError(
|
||||
|
||||
private func expectValidationError(
|
||||
operation: () throws -> some Any,
|
||||
check: (PureYAML.Validation.Issue.Collection) -> Void,
|
||||
check: (PureYAML.Validation.ValidationErrorCollection) -> Void,
|
||||
) {
|
||||
do {
|
||||
_ = try operation()
|
||||
recordIssue("expected validation error")
|
||||
} catch let collection as PureYAML.Validation.Issue.Collection {
|
||||
} catch let collection as PureYAML.Validation.ValidationErrorCollection {
|
||||
check(collection)
|
||||
} catch {
|
||||
recordIssue("unexpected error \(error)")
|
||||
|
||||
@ -38,7 +38,7 @@ enum CollectionCompatibilityFixtures {
|
||||
struct ValidationCase: CustomStringConvertible {
|
||||
var name: String
|
||||
var yaml: String
|
||||
var expectedIssues: [PureYAML.Validation.Issue]
|
||||
var expectedIssues: [PureYAML.Validation.ValidationError]
|
||||
|
||||
var description: String {
|
||||
name
|
||||
|
||||
@ -10,20 +10,20 @@ struct ComplexMappingKeyValidationTests {
|
||||
.init(keyNode: key, value: .string("first")),
|
||||
.init(keyNode: key, value: .string("second")),
|
||||
]))
|
||||
let expectedIssue = PureYAML.Validation.Issue(
|
||||
let expectedIssue = PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key '[\"a\", \"b\"]'",
|
||||
path: .init([.complexKey(key)]),
|
||||
)
|
||||
|
||||
let result = PureYAML.Validation.Validator().collect(value)
|
||||
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value)
|
||||
|
||||
#expect(result.issues == [expectedIssue])
|
||||
#expect(result.issues.map(\.description) == [
|
||||
"error: Duplicate mapping key '[\"a\", \"b\"]' at $[?[\"a\", \"b\"]]",
|
||||
#expect(result.values == [expectedIssue])
|
||||
#expect(result.values.map(\.description) == [
|
||||
"Duplicate mapping key '[\"a\", \"b\"]' at path: $[?[\"a\", \"b\"]]",
|
||||
])
|
||||
expectValidationError(value) { collection in
|
||||
#expect(collection.issues == [expectedIssue])
|
||||
#expect(collection.values == [expectedIssue])
|
||||
#expect(collection.description == expectedIssue.description)
|
||||
}
|
||||
}
|
||||
@ -37,16 +37,16 @@ struct ComplexMappingKeyValidationTests {
|
||||
.init(keyNode: key, value: .string("first")),
|
||||
.init(keyNode: key, value: .string("second")),
|
||||
]))
|
||||
let expectedIssue = PureYAML.Validation.Issue(
|
||||
let expectedIssue = PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key '{\"name\": \"service\"}'",
|
||||
path: .init([.complexKey(key)]),
|
||||
)
|
||||
|
||||
expectValidationError(value) { collection in
|
||||
#expect(collection.issues == [expectedIssue])
|
||||
#expect(collection.values == [expectedIssue])
|
||||
#expect(collection.description == """
|
||||
error: Duplicate mapping key '{"name": "service"}' at $[?{"name": "service"}]
|
||||
Duplicate mapping key '{"name": "service"}' at path: $[?{"name": "service"}]
|
||||
""")
|
||||
}
|
||||
}
|
||||
@ -58,7 +58,7 @@ struct ComplexMappingKeyValidationTests {
|
||||
.init(keyNode: key, value: .string("first")),
|
||||
.init(keyNode: key, value: .string("second")),
|
||||
]))
|
||||
let expectedIssue = PureYAML.Validation.Issue(
|
||||
let expectedIssue = PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key '[nan]'",
|
||||
path: .init([.complexKey(key)]),
|
||||
@ -66,8 +66,8 @@ struct ComplexMappingKeyValidationTests {
|
||||
|
||||
#expect(PureYAML.Model.Value.double(.nan) == .double(.nan))
|
||||
expectValidationError(value) { collection in
|
||||
#expect(collection.issues == [expectedIssue])
|
||||
#expect(collection.description == "error: Duplicate mapping key '[nan]' at $[?[nan]]")
|
||||
#expect(collection.values == [expectedIssue])
|
||||
#expect(collection.description == "Duplicate mapping key '[nan]' at path: $[?[nan]]")
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ struct ComplexMappingKeyValidationTests {
|
||||
let issues = try PureYAML.validate(value, strict: false)
|
||||
|
||||
#expect(issues.isEmpty)
|
||||
#expect(PureYAML.Validation.Validator().collect(value).issues.isEmpty)
|
||||
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value).values.isEmpty)
|
||||
#expect(value.rootMapping?["[\"a\"]"] == .string("scalar"))
|
||||
#expect(value.rootMapping?[sequenceKey] == .string("complex"))
|
||||
#expect(value.rootMapping?["a"] == nil)
|
||||
@ -98,19 +98,19 @@ struct ComplexMappingKeyValidationTests {
|
||||
.init(key: "mode", value: .string("legacy")),
|
||||
]))),
|
||||
]))
|
||||
let expectedIssue = PureYAML.Validation.Issue(
|
||||
let expectedIssue = PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Legacy mode is not allowed",
|
||||
path: .init([.complexKey(key), .key("mode")]),
|
||||
)
|
||||
|
||||
let result = PureYAML.Validation.Validator.blank
|
||||
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank
|
||||
.validating(legacyModeRule(severity: .error))
|
||||
.collect(value)
|
||||
|
||||
#expect(result.issues == [expectedIssue])
|
||||
#expect(result.issues.first?.description == """
|
||||
error: Legacy mode is not allowed at $[?{"name": "service"}].mode
|
||||
#expect(result.values == [expectedIssue])
|
||||
#expect(result.values.first?.description == """
|
||||
Legacy mode is not allowed at path: $[?{"name": "service"}].mode
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,14 +110,14 @@ struct DocumentationExampleTests {
|
||||
)
|
||||
let tagIssues = PureYAML.Tagged.Validator().collect(tagged)
|
||||
let expectedIssues = [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Unsupported built-in tag 'tag:yaml.org,2002:timestamp'",
|
||||
path: .init([.key("value")]),
|
||||
),
|
||||
]
|
||||
|
||||
#expect(tagIssues.issues == expectedIssues)
|
||||
#expect(tagIssues.values == expectedIssues)
|
||||
}
|
||||
|
||||
@Test("README tagged constructor example stays executable")
|
||||
@ -180,20 +180,20 @@ struct DocumentationExampleTests {
|
||||
.init(key: "title", value: .string("Second")),
|
||||
]))
|
||||
let expectedIssues = [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
path: .init([.key("title")]),
|
||||
),
|
||||
]
|
||||
let result = PureYAML.Validation.Validator().collect(document)
|
||||
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(document)
|
||||
|
||||
#expect(try PureYAML.validate(validDocument, strict: false).isEmpty)
|
||||
#expect(result.errors == expectedIssues)
|
||||
#expect(result.warnings.isEmpty)
|
||||
expectValidationFailure(document) { collection in
|
||||
#expect(collection.issues == expectedIssues)
|
||||
#expect(collection.description == "error: Duplicate mapping key 'title' at $.title")
|
||||
#expect(collection.values == expectedIssues)
|
||||
#expect(collection.description == "Duplicate mapping key 'title' at path: $.title")
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,12 +210,12 @@ struct DocumentationExampleTests {
|
||||
|
||||
func expectValidationFailure(
|
||||
_ value: PureYAML.Model.Value,
|
||||
check: (PureYAML.Validation.Issue.Collection) -> Void,
|
||||
check: (PureYAML.Validation.ValidationErrorCollection) -> Void,
|
||||
) {
|
||||
do {
|
||||
_ = try PureYAML.validate(value)
|
||||
recordIssue("expected validation failure")
|
||||
} catch let collection as PureYAML.Validation.Issue.Collection {
|
||||
} catch let collection as PureYAML.Validation.ValidationErrorCollection {
|
||||
check(collection)
|
||||
} catch {
|
||||
recordIssue("unexpected error \(error)")
|
||||
|
||||
@ -116,7 +116,7 @@ struct DownstreamDocumentTests {
|
||||
#expect(api?["name"] == .string("API"))
|
||||
#expect(api?["retries"] == .int(3))
|
||||
#expect(api?["timeoutSeconds"] == .int(30))
|
||||
#expect(PureYAML.Validation.Validator().collect(.mapping(root)) == .init())
|
||||
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(.mapping(root)) == .init())
|
||||
}
|
||||
|
||||
func requireRootMapping(_ yaml: String) throws -> PureYAML.Model.Mapping {
|
||||
|
||||
@ -36,11 +36,11 @@ struct EventStreamingParserTests {
|
||||
flow: {id: one, id: two}
|
||||
""")
|
||||
|
||||
let issues = PureYAML.Validation.Validator().collect(value).issues
|
||||
let issues = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value).values
|
||||
|
||||
#expect(issues.map(\.description) == [
|
||||
"error: Duplicate mapping key 'title' at $.root.title",
|
||||
"error: Duplicate mapping key 'id' at $.flow.id",
|
||||
"Duplicate mapping key 'title' at path: $.root.title",
|
||||
"Duplicate mapping key 'id' at path: $.flow.id",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,7 +73,7 @@ struct KeyedCodingTests {
|
||||
.init(key: "title", value: .string("Second")),
|
||||
]))
|
||||
let expectedIssues = [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
path: .init([.key("title")]),
|
||||
@ -81,8 +81,8 @@ struct KeyedCodingTests {
|
||||
]
|
||||
|
||||
expectValidationError(value) { collection in
|
||||
#expect(collection.issues == expectedIssues)
|
||||
#expect(collection.description == "error: Duplicate mapping key 'title' at $.title")
|
||||
#expect(collection.values == expectedIssues)
|
||||
#expect(collection.description == "Duplicate mapping key 'title' at path: $.title")
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ struct KeyedCodingTests {
|
||||
]))
|
||||
|
||||
expectValidationError(value) { collection in
|
||||
#expect(collection.issues == [
|
||||
#expect(collection.values == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
@ -114,14 +114,14 @@ struct KeyedCodingTests {
|
||||
]))
|
||||
|
||||
expectValidationError(value) { collection in
|
||||
#expect(collection.issues == [
|
||||
#expect(collection.values == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
path: .init([.key("metadata"), .key("title")]),
|
||||
),
|
||||
])
|
||||
#expect(collection.description == "error: Duplicate mapping key 'title' at $.metadata.title")
|
||||
#expect(collection.description == "Duplicate mapping key 'title' at path: $.metadata.title")
|
||||
} operation: {
|
||||
_ = try TitleOnly(from: PureYAML.Decoding.Decoder(
|
||||
value: value,
|
||||
@ -312,13 +312,13 @@ private func expectEncodingError(
|
||||
|
||||
private func expectValidationError(
|
||||
_: PureYAML.Model.Value,
|
||||
check: (PureYAML.Validation.Issue.Collection) -> Void,
|
||||
check: (PureYAML.Validation.ValidationErrorCollection) -> Void,
|
||||
operation: () throws -> some Any,
|
||||
) {
|
||||
do {
|
||||
_ = try operation()
|
||||
recordIssue("expected validation error")
|
||||
} catch let collection as PureYAML.Validation.Issue.Collection {
|
||||
} catch let collection as PureYAML.Validation.ValidationErrorCollection {
|
||||
check(collection)
|
||||
} catch {
|
||||
recordIssue("unexpected error \(error)")
|
||||
@ -327,7 +327,7 @@ private func expectValidationError(
|
||||
|
||||
private func expectValidationError(
|
||||
_ value: PureYAML.Model.Value,
|
||||
check: (PureYAML.Validation.Issue.Collection) -> Void,
|
||||
check: (PureYAML.Validation.ValidationErrorCollection) -> Void,
|
||||
) {
|
||||
expectValidationError(value, check: check) {
|
||||
try PureYAML.decode(TitleOnly.self, from: value)
|
||||
|
||||
@ -24,7 +24,7 @@ struct MergeKeyCompatibilityTests {
|
||||
#expect(service?["retries"] == .int(3))
|
||||
#expect(service?["name"] == .string("API"))
|
||||
#expect(service?["<<"] == nil)
|
||||
#expect(PureYAML.Validation.Validator().collect(.mapping(root ?? .init())) == .init())
|
||||
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(.mapping(root ?? .init())) == .init())
|
||||
}
|
||||
|
||||
@Test("Expands merge sequence values with first source precedence and local overrides")
|
||||
@ -86,20 +86,20 @@ struct MergeKeyCompatibilityTests {
|
||||
name: Backend
|
||||
""",
|
||||
)
|
||||
let result = PureYAML.Validation.Validator().collect(value)
|
||||
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value)
|
||||
let expectedIssues = [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'name'",
|
||||
path: .init([.key("service"), .key("name")]),
|
||||
),
|
||||
]
|
||||
|
||||
#expect(result.issues == expectedIssues)
|
||||
#expect(result.values == expectedIssues)
|
||||
#expect(result.errors == expectedIssues)
|
||||
expectValidationError(value) { collection in
|
||||
#expect(collection.issues == expectedIssues)
|
||||
#expect(collection.description == "error: Duplicate mapping key 'name' at $.service.name")
|
||||
#expect(collection.values == expectedIssues)
|
||||
#expect(collection.description == "Duplicate mapping key 'name' at path: $.service.name")
|
||||
}
|
||||
|
||||
let root = requireMapping(value, "expected root mapping")
|
||||
|
||||
@ -12,7 +12,7 @@ struct ParsingCompatibilityTests {
|
||||
|
||||
#expect(value == testCase.expected)
|
||||
expectAbsentRootKeys(testCase.absentRootKeys, in: value)
|
||||
#expect(PureYAML.Validation.Validator().collect(value) == PureYAML.Validation.Result())
|
||||
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value) == PureYAML.Validation.Result())
|
||||
}
|
||||
|
||||
@Test(
|
||||
@ -30,13 +30,13 @@ struct ParsingCompatibilityTests {
|
||||
)
|
||||
func test_collectionDuplicateKeyValidation(testCase: CollectionCompatibilityFixtures.ValidationCase) throws {
|
||||
let value = try PureYAML.parse(testCase.yaml)
|
||||
let result = PureYAML.Validation.Validator().collect(value)
|
||||
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value)
|
||||
|
||||
#expect(result.issues == testCase.expectedIssues)
|
||||
#expect(result.values == testCase.expectedIssues)
|
||||
#expect(result.errors == testCase.expectedIssues)
|
||||
#expect(result.warnings.isEmpty)
|
||||
expectValidationError(value) { collection in
|
||||
#expect(collection.issues == testCase.expectedIssues)
|
||||
#expect(collection.values == testCase.expectedIssues)
|
||||
#expect(collection.description == testCase.expectedIssues.map(\.description).joined(separator: "\n"))
|
||||
}
|
||||
}
|
||||
@ -59,13 +59,13 @@ struct ParsingCompatibilityTests {
|
||||
|
||||
#expect(value == testCase.expected)
|
||||
expectAbsentRootKeys(testCase.absentRootKeys, in: value)
|
||||
#expect(PureYAML.Validation.Validator().collect(value) == PureYAML.Validation.Result())
|
||||
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value) == PureYAML.Validation.Result())
|
||||
}
|
||||
|
||||
@Test("Validates direct values for gaps ordinary loaders may collapse")
|
||||
func test_directUnsupportedGapValueValidation() {
|
||||
let value = UnsupportedYAMLGapsFixtures.directPreservedValue
|
||||
let result = PureYAML.Validation.Validator().collect(value)
|
||||
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value)
|
||||
|
||||
guard case let .mapping(mapping) = value else {
|
||||
recordIssue("expected direct mapping value")
|
||||
@ -76,13 +76,13 @@ struct ParsingCompatibilityTests {
|
||||
#expect(mapping["<<"] == .mapping(.init([
|
||||
.init(key: "enabled", value: .bool(true)),
|
||||
])))
|
||||
#expect(result.issues == UnsupportedYAMLGapsFixtures.directPreservedIssues)
|
||||
#expect(result.issues.map(\.description) == [
|
||||
"error: Duplicate mapping key 'title' at $.title",
|
||||
#expect(result.values == UnsupportedYAMLGapsFixtures.directPreservedIssues)
|
||||
#expect(result.values.map(\.description) == [
|
||||
"Duplicate mapping key 'title' at path: $.title",
|
||||
])
|
||||
expectValidationError(value) { collection in
|
||||
#expect(collection.issues == UnsupportedYAMLGapsFixtures.directPreservedIssues)
|
||||
#expect(collection.description == "error: Duplicate mapping key 'title' at $.title")
|
||||
#expect(collection.values == UnsupportedYAMLGapsFixtures.directPreservedIssues)
|
||||
#expect(collection.description == "Duplicate mapping key 'title' at path: $.title")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
69
Tests/PureYAMLJSONTests/JSONCoverageMetaTests.swift
Normal file
69
Tests/PureYAMLJSONTests/JSONCoverageMetaTests.swift
Normal file
@ -0,0 +1,69 @@
|
||||
import PureYAML
|
||||
@testable import PureYAMLJSON
|
||||
import Testing
|
||||
|
||||
/// "Coverage is itself a tested law" (validation-rules.md), applied to the JSON-representability
|
||||
/// validator, the same discipline `ValidationCoverageMetaTests` in the core target applies to the model
|
||||
/// and tagged validators.
|
||||
@Suite("JSON representability finding-coverage law")
|
||||
struct JSONCoverageMetaTests {
|
||||
private struct Fixture {
|
||||
let check: PureYAML.Validation.AnyValidation<PureYAML.Model.Value>
|
||||
let failing: PureYAML.Model.Value
|
||||
let nearMiss: PureYAML.Model.Value
|
||||
}
|
||||
|
||||
private static let fixtures: [Fixture] = [
|
||||
.init(
|
||||
check: .init(PureYAML.Validation.BuiltinValidation.mappingKeysAreUnique),
|
||||
failing: .mapping(.init([
|
||||
.init(key: "a", value: .int(1)),
|
||||
.init(key: "a", value: .int(2)),
|
||||
])),
|
||||
nearMiss: .mapping(.init([
|
||||
.init(key: "a", value: .int(1)),
|
||||
.init(key: "b", value: .int(2)),
|
||||
])),
|
||||
),
|
||||
.init(
|
||||
check: .init(PureYAML.Validation.BuiltinValidation.mappingKeysAreStrings),
|
||||
failing: .mapping(.init([.init(keyNode: .sequence([.int(1)]), value: .int(2))])),
|
||||
// The boundary: a string key, the only key shape JSON permits.
|
||||
nearMiss: .mapping(.init([.init(key: "a", value: .int(2))])),
|
||||
),
|
||||
.init(
|
||||
check: .init(PureYAML.Validation.BuiltinValidation.numbersAreFinite),
|
||||
failing: .mapping(.init([.init(key: "x", value: .double(.infinity))])),
|
||||
// The boundary: the largest finite double, one step from non-finite.
|
||||
nearMiss: .mapping(.init([.init(key: "x", value: .double(.greatestFiniteMagnitude))])),
|
||||
),
|
||||
]
|
||||
|
||||
@Test("The JSON representability validator's descriptions are pinned")
|
||||
func test_representabilityDescriptionsArePinned() {
|
||||
#expect(PureYAML.JSON.Validator.representability.validationDescriptions == [
|
||||
"Mapping keys are unique",
|
||||
"Mapping keys are strings",
|
||||
"Numbers are finite",
|
||||
])
|
||||
}
|
||||
|
||||
@Test("JSON fixtures cover exactly the representability validator's default checks")
|
||||
func test_fixturesCoverDefaults() {
|
||||
let ruled = Set(PureYAML.JSON.Validator.representability.validationDescriptions)
|
||||
let covered = Set(Self.fixtures.map(\.check.description))
|
||||
#expect(ruled.subtracting(covered).isEmpty, "JSON default checks without fixtures: \(ruled.subtracting(covered).sorted())")
|
||||
#expect(covered.subtracting(ruled).isEmpty, "JSON fixtures for checks that no longer ship: \(covered.subtracting(ruled).sorted())")
|
||||
}
|
||||
|
||||
@Test("Each JSON check fails its failing fixture and passes its near miss in isolation")
|
||||
func test_checksFailAndNearMiss() throws {
|
||||
for fixture in Self.fixtures {
|
||||
let isolated = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>(nonReferenceDefaultValidations: [fixture.check])
|
||||
#expect(throws: PureYAML.Validation.ValidationErrorCollection.self, "check '\(fixture.check.description)' did not fail its failing fixture") {
|
||||
try isolated.validate(fixture.failing)
|
||||
}
|
||||
_ = try isolated.validate(fixture.nearMiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,12 @@ struct JSONRuleTests {
|
||||
@Test("mappingKeysAreStrings fails on a complex key")
|
||||
func test_mappingKeysAreStrings_fails() {
|
||||
let document = mapping([.init(keyNode: .sequence([.int(1)]), value: .int(2))])
|
||||
let issues = PureYAML.Validation.Validator(checks: [PureYAML.Validation.AnyCheck(PureYAML.Validation.BuiltinCheck.mappingKeysAreStrings)]).collect(document).issues
|
||||
let issues = PureYAML.Validation
|
||||
.Validator<
|
||||
PureYAML.Model.Value,
|
||||
PureYAML.Model.Value
|
||||
>(nonReferenceDefaultValidations: [PureYAML.Validation.AnyValidation(PureYAML.Validation.BuiltinValidation.mappingKeysAreStrings)])
|
||||
.collect(document).values
|
||||
#expect(issues.count == 1)
|
||||
#expect(issues.first?.reason == "Mapping key '[1]' is not a string")
|
||||
}
|
||||
@ -19,14 +24,24 @@ struct JSONRuleTests {
|
||||
@Test("mappingKeysAreStrings passes when every key is a string")
|
||||
func test_mappingKeysAreStrings_passes() {
|
||||
let document = mapping([.init(key: "a", value: .int(1))])
|
||||
let issues = PureYAML.Validation.Validator(checks: [PureYAML.Validation.AnyCheck(PureYAML.Validation.BuiltinCheck.mappingKeysAreStrings)]).collect(document).issues
|
||||
let issues = PureYAML.Validation
|
||||
.Validator<
|
||||
PureYAML.Model.Value,
|
||||
PureYAML.Model.Value
|
||||
>(nonReferenceDefaultValidations: [PureYAML.Validation.AnyValidation(PureYAML.Validation.BuiltinValidation.mappingKeysAreStrings)])
|
||||
.collect(document).values
|
||||
#expect(issues.isEmpty)
|
||||
}
|
||||
|
||||
@Test("numbersAreFinite fails on infinity at the value's path")
|
||||
func test_numbersAreFinite_fails() {
|
||||
let document = mapping([.init(key: "x", value: .double(.infinity))])
|
||||
let issues = PureYAML.Validation.Validator(checks: [PureYAML.Validation.AnyCheck(PureYAML.Validation.BuiltinCheck.numbersAreFinite)]).collect(document).issues
|
||||
let issues = PureYAML.Validation
|
||||
.Validator<
|
||||
PureYAML.Model.Value,
|
||||
PureYAML.Model.Value
|
||||
>(nonReferenceDefaultValidations: [PureYAML.Validation.AnyValidation(PureYAML.Validation.BuiltinValidation.numbersAreFinite)]).collect(document)
|
||||
.values
|
||||
#expect(issues.count == 1)
|
||||
#expect(issues.first?.reason == "Number 'inf' is not finite")
|
||||
}
|
||||
@ -34,7 +49,12 @@ struct JSONRuleTests {
|
||||
@Test("numbersAreFinite passes on a large finite double")
|
||||
func test_numbersAreFinite_passes() {
|
||||
let document = mapping([.init(key: "x", value: .double(.greatestFiniteMagnitude))])
|
||||
let issues = PureYAML.Validation.Validator(checks: [PureYAML.Validation.AnyCheck(PureYAML.Validation.BuiltinCheck.numbersAreFinite)]).collect(document).issues
|
||||
let issues = PureYAML.Validation
|
||||
.Validator<
|
||||
PureYAML.Model.Value,
|
||||
PureYAML.Model.Value
|
||||
>(nonReferenceDefaultValidations: [PureYAML.Validation.AnyValidation(PureYAML.Validation.BuiltinValidation.numbersAreFinite)]).collect(document)
|
||||
.values
|
||||
#expect(issues.isEmpty)
|
||||
}
|
||||
|
||||
|
||||
@ -41,7 +41,7 @@ struct RealYAMLCorpusTests {
|
||||
continue
|
||||
}
|
||||
|
||||
let issues = PureYAML.Validation.Validator().collect(documents)
|
||||
let issues = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(documents)
|
||||
let issueDescriptions = issues.issues.map(\.description).joined(separator: "\n")
|
||||
#expect(issues == PureYAML.Stream.Result(), "\(seed.id): \(issueDescriptions)")
|
||||
guard seed.expectedRoundTrip == "success" else {
|
||||
|
||||
@ -24,7 +24,7 @@ struct RealYAMLFixtureTests {
|
||||
let value = try PureYAML.parse(fixture.load())
|
||||
|
||||
fixture.expectRepresentativeValues(in: value)
|
||||
#expect(PureYAML.Validation.Validator().collect(value) == PureYAML.Validation.Result())
|
||||
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value) == PureYAML.Validation.Result())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -232,7 +232,7 @@ struct StreamValidationTests {
|
||||
),
|
||||
]
|
||||
|
||||
let result = PureYAML.Validation.Validator().collect(documents)
|
||||
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(documents)
|
||||
#expect(result.issues == expectedIssues)
|
||||
#expect(result.errors == expectedIssues)
|
||||
#expect(result.warnings.isEmpty)
|
||||
@ -240,8 +240,8 @@ struct StreamValidationTests {
|
||||
expectStreamValidationError(documents) { collection in
|
||||
#expect(collection.issues == expectedIssues)
|
||||
#expect(collection.description == """
|
||||
document[0]: error: Duplicate mapping key 'title' at $.title
|
||||
document[1]: error: Duplicate mapping key 'name' at $.routes[0].name
|
||||
document[0]: Duplicate mapping key 'title' at path: $.title
|
||||
document[1]: Duplicate mapping key 'name' at path: $.routes[0].name
|
||||
""")
|
||||
}
|
||||
}
|
||||
@ -258,7 +258,7 @@ struct StreamValidationTests {
|
||||
""",
|
||||
)
|
||||
|
||||
let result = PureYAML.Validation.Validator().collect(documents)
|
||||
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(documents)
|
||||
#expect(result == PureYAML.Stream.Result())
|
||||
#expect(result.isValid)
|
||||
#expect(result.errors.isEmpty)
|
||||
@ -272,7 +272,7 @@ struct StreamValidationTests {
|
||||
PureYAML.Stream.Document(index: 4, value: .string("legacy")),
|
||||
PureYAML.Stream.Document(index: 5, value: .string("modern")),
|
||||
]
|
||||
let validator = PureYAML.Validation.Validator.blank
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank
|
||||
.validating(legacyModeRule(severity: .warning))
|
||||
let expectedIssues = [
|
||||
PureYAML.Stream.Issue(
|
||||
@ -292,7 +292,7 @@ struct StreamValidationTests {
|
||||
#expect(warnings == expectedIssues)
|
||||
|
||||
let modernOnly = [PureYAML.Stream.Document(index: 6, value: .string("modern"))]
|
||||
#expect(PureYAML.Validation.Validator.blank
|
||||
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank
|
||||
.validating(legacyModeRule(severity: .warning))
|
||||
.collect(modernOnly)
|
||||
.issues
|
||||
@ -318,7 +318,7 @@ private func expectStreamParseError(
|
||||
|
||||
private func expectStreamValidationError(
|
||||
_ documents: [PureYAML.Stream.Document],
|
||||
using validator: PureYAML.Validation.Validator = .init(),
|
||||
using validator: PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value> = .init(),
|
||||
check: (PureYAML.Stream.Issue.Collection) -> Void,
|
||||
) {
|
||||
do {
|
||||
|
||||
@ -61,27 +61,27 @@ struct TaggedValidationCompatibilityTests {
|
||||
""",
|
||||
)
|
||||
let expectedIssues = [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Unsupported built-in tag 'tag:yaml.org,2002:timestamp'",
|
||||
path: .init([.key("date")]),
|
||||
),
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Unsupported built-in tag 'tag:yaml.org,2002:binary'",
|
||||
path: .init([.key("payload")]),
|
||||
),
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Unsupported built-in tag 'tag:yaml.org,2002:set'",
|
||||
path: .init([.key("set")]),
|
||||
),
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Unsupported built-in tag 'tag:yaml.org,2002:omap'",
|
||||
path: .init([.key("omap")]),
|
||||
),
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Unsupported built-in tag 'tag:yaml.org,2002:pairs'",
|
||||
path: .init([.key("pairs")]),
|
||||
@ -89,10 +89,10 @@ struct TaggedValidationCompatibilityTests {
|
||||
]
|
||||
let result = PureYAML.Tagged.Validator().collect(node)
|
||||
|
||||
#expect(result.issues == expectedIssues)
|
||||
#expect(result.values == expectedIssues)
|
||||
#expect(result.errors == expectedIssues)
|
||||
expectTaggedValidationError(node) { collection in
|
||||
#expect(collection.issues == expectedIssues)
|
||||
#expect(collection.values == expectedIssues)
|
||||
#expect(collection.description == expectedIssues.map(\.description).joined(separator: "\n"))
|
||||
}
|
||||
}
|
||||
@ -107,24 +107,24 @@ struct TaggedValidationCompatibilityTests {
|
||||
""",
|
||||
)
|
||||
let expectedIssues = [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Tag 'tag:yaml.org,2002:seq' expects a sequence node, found mapping",
|
||||
path: .init([.key("seq")]),
|
||||
),
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Tag 'tag:yaml.org,2002:map' expects a mapping node, found sequence",
|
||||
path: .init([.key("map")]),
|
||||
),
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Tag 'tag:yaml.org,2002:str' expects a scalar node, found sequence",
|
||||
path: .init([.key("scalar")]),
|
||||
),
|
||||
]
|
||||
|
||||
#expect(PureYAML.Tagged.Validator().collect(node).issues == expectedIssues)
|
||||
#expect(PureYAML.Tagged.Validator().collect(node).values == expectedIssues)
|
||||
}
|
||||
|
||||
@Test("Validates unsupported and inconsistent mapping key tags exactly")
|
||||
@ -142,17 +142,17 @@ struct TaggedValidationCompatibilityTests {
|
||||
""",
|
||||
)
|
||||
let expectedIssues = [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Unsupported built-in tag 'tag:yaml.org,2002:timestamp' on mapping key",
|
||||
path: .init([.key("2001-01-23")]),
|
||||
),
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Tag 'tag:yaml.org,2002:seq' expects a scalar mapping key",
|
||||
path: .init([.key("key")]),
|
||||
),
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Tag 'tag:yaml.org,2002:merge' is only supported on '<<' mapping keys",
|
||||
path: .init([.key("not-merge")]),
|
||||
@ -161,7 +161,7 @@ struct TaggedValidationCompatibilityTests {
|
||||
let mapping = try requireTaggedMapping(node)
|
||||
|
||||
#expect(mapping?.pairs.map(\.key) == ["2001-01-23", "key", "not-merge", "<<"])
|
||||
#expect(PureYAML.Tagged.Validator().collect(node).issues == expectedIssues)
|
||||
#expect(PureYAML.Tagged.Validator().collect(node).values == expectedIssues)
|
||||
}
|
||||
|
||||
@Test("Validates mapping node tags and mapping key tags in one deterministic pass")
|
||||
@ -177,19 +177,19 @@ struct TaggedValidationCompatibilityTests {
|
||||
tag: .set,
|
||||
))
|
||||
let expectedIssues = [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Unsupported built-in tag 'tag:yaml.org,2002:timestamp' on mapping key",
|
||||
path: .init([.key("2001-01-23")]),
|
||||
),
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Unsupported built-in tag 'tag:yaml.org,2002:set'",
|
||||
path: .root,
|
||||
),
|
||||
]
|
||||
|
||||
#expect(PureYAML.Tagged.Validator().collect(node).issues == expectedIssues)
|
||||
#expect(PureYAML.Tagged.Validator().collect(node).values == expectedIssues)
|
||||
}
|
||||
|
||||
@Test("Allows project-specific tags for application validation")
|
||||
@ -212,14 +212,14 @@ struct TaggedValidationCompatibilityTests {
|
||||
@Test("Custom tagged validation rules inspect application tags exactly")
|
||||
func test_customTaggedValidationRulesInspectApplicationTagsExactly() throws {
|
||||
let node = try PureYAML.parseTagged("value: !Local one")
|
||||
let localTagsAreWarnings = PureYAML.Validation.Check<PureYAML.Tagged.Node, PureYAML.Tagged.Node>(
|
||||
let localTagsAreWarnings = PureYAML.Validation.Validation<PureYAML.Tagged.Node, PureYAML.Tagged.Node>(
|
||||
description: "Local tags are warnings",
|
||||
) { context in
|
||||
guard context.subject.tag == .init("!Local") else {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .warning,
|
||||
reason: "Local application tag requires caller handling",
|
||||
path: context.path,
|
||||
@ -227,7 +227,7 @@ struct TaggedValidationCompatibilityTests {
|
||||
]
|
||||
}
|
||||
let expectedIssues = [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .warning,
|
||||
reason: "Local application tag requires caller handling",
|
||||
path: .init([.key("value")]),
|
||||
@ -236,10 +236,10 @@ struct TaggedValidationCompatibilityTests {
|
||||
let validator = PureYAML.Tagged.Validator.blank.validating(localTagsAreWarnings)
|
||||
|
||||
#expect(PureYAML.Tagged.Validator.blank.collect(node) == .init())
|
||||
#expect(validator.collect(node).issues == expectedIssues)
|
||||
#expect(validator.collect(node).values == expectedIssues)
|
||||
#expect(try validator.validate(node, strict: false) == expectedIssues)
|
||||
expectTaggedValidationError(node, using: validator) { collection in
|
||||
#expect(collection.issues == expectedIssues)
|
||||
#expect(collection.values == expectedIssues)
|
||||
}
|
||||
}
|
||||
|
||||
@ -319,12 +319,12 @@ private func requireTaggedScalar(
|
||||
private func expectTaggedValidationError(
|
||||
_ node: PureYAML.Tagged.Node,
|
||||
using validator: PureYAML.Tagged.Validator = .init(),
|
||||
body: (PureYAML.Validation.Issue.Collection) -> Void,
|
||||
body: (PureYAML.Validation.ValidationErrorCollection) -> Void,
|
||||
) {
|
||||
do {
|
||||
try validator.validate(node)
|
||||
recordIssue("expected tagged validation to fail")
|
||||
} catch let collection as PureYAML.Validation.Issue.Collection {
|
||||
} catch let collection as PureYAML.Validation.ValidationErrorCollection {
|
||||
body(collection)
|
||||
} catch {
|
||||
recordIssue("expected validation issue collection, got \(error)")
|
||||
|
||||
@ -183,7 +183,7 @@ struct TaggedConstructorTests {
|
||||
.construct(node)
|
||||
let mapping = requireMapping(value)
|
||||
let expectedIssues = [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
path: .init([.key("title")]),
|
||||
@ -193,7 +193,7 @@ struct TaggedConstructorTests {
|
||||
#expect(mapping?.pairs.map(\.key) == ["title", "title"])
|
||||
#expect(mapping?.pairs.map(\.value) == [.string("one"), .string("two")])
|
||||
#expect(value == node.modelValueErasingTags)
|
||||
#expect(PureYAML.Validation.Validator().collect(value).issues == expectedIssues)
|
||||
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value).values == expectedIssues)
|
||||
}
|
||||
|
||||
@Test("Constructors do not run tagged validation implicitly")
|
||||
@ -203,7 +203,7 @@ struct TaggedConstructorTests {
|
||||
.modelValueErasingTags
|
||||
.construct(node)
|
||||
let expectedIssues = [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Unsupported built-in tag 'tag:yaml.org,2002:timestamp'",
|
||||
path: .root,
|
||||
@ -211,7 +211,7 @@ struct TaggedConstructorTests {
|
||||
]
|
||||
|
||||
#expect(value == .string("2001-01-23"))
|
||||
#expect(PureYAML.Tagged.Validator().collect(node).issues == expectedIssues)
|
||||
#expect(PureYAML.Tagged.Validator().collect(node).values == expectedIssues)
|
||||
}
|
||||
|
||||
@Test("Tagged construction keeps complex mapping keys out of the tagged layer")
|
||||
|
||||
@ -9,14 +9,14 @@ struct TaggedKeyUniquenessTests {
|
||||
@Test("A duplicate key in a tagged mapping is reported at the key's path")
|
||||
func test_reportsDuplicateTaggedKey() throws {
|
||||
let node = try PureYAML.parseTagged("a: 1\na: 2")
|
||||
#expect(PureYAML.Tagged.Validator().collect(node).issues == [
|
||||
#expect(PureYAML.Tagged.Validator().collect(node).values == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'a'",
|
||||
path: .init([.key("a")]),
|
||||
),
|
||||
])
|
||||
#expect(throws: PureYAML.Validation.Issue.Collection.self) {
|
||||
#expect(throws: PureYAML.Validation.ValidationErrorCollection.self) {
|
||||
try PureYAML.Tagged.Validator().validate(node)
|
||||
}
|
||||
}
|
||||
@ -30,7 +30,7 @@ struct TaggedKeyUniquenessTests {
|
||||
@Test("A duplicate key nested under a tag is reported at its nested path")
|
||||
func test_reportsNestedDuplicateTaggedKey() throws {
|
||||
let node = try PureYAML.parseTagged("outer:\n a: 1\n a: 2")
|
||||
#expect(PureYAML.Tagged.Validator().collect(node).issues == [
|
||||
#expect(PureYAML.Tagged.Validator().collect(node).values == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'a'",
|
||||
|
||||
@ -202,6 +202,6 @@ struct TaggedNodeRoundTripTests {
|
||||
let source = "a: &dup\n k: 1\n k: 2\nb: *dup\nc: *dup\n"
|
||||
let node = try PureYAML.parseTagged(source)
|
||||
let result = PureYAML.Tagged.Validator().collect(node)
|
||||
#expect(result.issues.count == 1)
|
||||
#expect(result.values.count == 1)
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,14 +101,14 @@ struct UnkeyedCodingTests {
|
||||
expectValidationError {
|
||||
_ = try PureYAML.decode([TitleOnlyArticle].self, from: value)
|
||||
} check: { collection in
|
||||
#expect(collection.issues == [
|
||||
#expect(collection.values == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
path: .init([.index(0), .key("title")]),
|
||||
),
|
||||
])
|
||||
#expect(collection.description == "error: Duplicate mapping key 'title' at $[0].title")
|
||||
#expect(collection.description == "Duplicate mapping key 'title' at path: $[0].title")
|
||||
}
|
||||
}
|
||||
|
||||
@ -311,12 +311,12 @@ private func expectEncodingError(
|
||||
|
||||
private func expectValidationError(
|
||||
operation: () throws -> some Any,
|
||||
check: (PureYAML.Validation.Issue.Collection) -> Void,
|
||||
check: (PureYAML.Validation.ValidationErrorCollection) -> Void,
|
||||
) {
|
||||
do {
|
||||
_ = try operation()
|
||||
recordIssue("expected validation error")
|
||||
} catch let collection as PureYAML.Validation.Issue.Collection {
|
||||
} catch let collection as PureYAML.Validation.ValidationErrorCollection {
|
||||
check(collection)
|
||||
} catch {
|
||||
recordIssue("unexpected error \(error)")
|
||||
|
||||
@ -11,7 +11,7 @@ struct SequenceFailureFixture: CustomStringConvertible {
|
||||
|
||||
enum Expected {
|
||||
case decoding(PureYAML.Decoding.Error)
|
||||
case validation([PureYAML.Validation.Issue], String)
|
||||
case validation([PureYAML.Validation.ValidationError], String)
|
||||
}
|
||||
|
||||
var name: String
|
||||
@ -43,12 +43,12 @@ struct SequenceFailureFixture: CustomStringConvertible {
|
||||
}
|
||||
#expect(error == expected)
|
||||
#expect(error.description == expected.description)
|
||||
} catch let collection as PureYAML.Validation.Issue.Collection {
|
||||
} catch let collection as PureYAML.Validation.ValidationErrorCollection {
|
||||
guard case let .validation(issues, description) = expected else {
|
||||
recordIssue("unexpected validation error \(collection)")
|
||||
return
|
||||
}
|
||||
#expect(collection.issues == issues)
|
||||
#expect(collection.values == issues)
|
||||
#expect(collection.description == description)
|
||||
} catch {
|
||||
recordIssue("unexpected error \(error)")
|
||||
@ -131,7 +131,7 @@ let sequenceFailureFixtures: [SequenceFailureFixture] = [
|
||||
path: .init([.index(0), .key("title")]),
|
||||
),
|
||||
],
|
||||
"error: Duplicate mapping key 'title' at $[0].title",
|
||||
"Duplicate mapping key 'title' at path: $[0].title",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@ -143,7 +143,7 @@ enum UnsupportedYAMLGapsFixtures {
|
||||
.init(key: "title", value: .string("Second")),
|
||||
]))
|
||||
|
||||
static let directPreservedIssues: [PureYAML.Validation.Issue] = [
|
||||
static let directPreservedIssues: [PureYAML.Validation.ValidationError] = [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
|
||||
@ -5,38 +5,38 @@ import Testing
|
||||
struct ValidationBuiltinRuleTests {
|
||||
@Test("Validates preserved value trees that ordinary YAML loaders may collapse")
|
||||
func test_validatesPreservedValueTreesThatOrdinaryYAMLLoadersMayCollapse() {
|
||||
let result = PureYAML.Validation.Validator().collect(preservedDuplicateKeyValue)
|
||||
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(preservedDuplicateKeyValue)
|
||||
|
||||
#expect(result.issues == expectedIssues)
|
||||
#expect(result.issues.map(\.description) == [
|
||||
"error: Duplicate mapping key 'title' at $.title",
|
||||
"error: Duplicate mapping key 'items' at $.items",
|
||||
"error: Duplicate mapping key '/users' at $.paths[\"/users\"]",
|
||||
"error: Duplicate mapping key 'name.with.dot' at $.items[0][\"name.with.dot\"]",
|
||||
#expect(result.values == expectedIssues)
|
||||
#expect(result.values.map(\.description) == [
|
||||
"Duplicate mapping key 'title' at path: $.title",
|
||||
"Duplicate mapping key 'items' at path: $.items",
|
||||
"Duplicate mapping key '/users' at path: $.paths[\"/users\"]",
|
||||
"Duplicate mapping key 'name.with.dot' at path: $.items[0][\"name.with.dot\"]",
|
||||
])
|
||||
#expect(!result.issues.contains(.init(
|
||||
#expect(!result.values.contains(.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'paths'",
|
||||
path: .init([.key("paths")]),
|
||||
)))
|
||||
expectValidationError(preservedDuplicateKeyValue) { collection in
|
||||
#expect(collection.issues == expectedIssues)
|
||||
#expect(collection.values == expectedIssues)
|
||||
#expect(collection.description == expectedIssues.map(\.description).joined(separator: "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Mapping key uniqueness is scoped to mappings by type-directed dispatch")
|
||||
func test_mappingKeyUniquenessIsScopedToMappings() {
|
||||
let validator = PureYAML.Validation.Validator()
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>()
|
||||
|
||||
// A scalar value carries no mapping, so the mapping-keyed check never fires on it.
|
||||
#expect(validator.collect(.string("subject")).issues.isEmpty)
|
||||
#expect(validator.collect(.string("subject")).values.isEmpty)
|
||||
|
||||
// A mapping with a duplicate key reports exactly one issue, at the key's path.
|
||||
#expect(validator.collect(.mapping(.init([
|
||||
.init(key: "title", value: .string("one")),
|
||||
.init(key: "title", value: .string("two")),
|
||||
]))).issues == [
|
||||
]))).values == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
@ -62,7 +62,7 @@ private let preservedDuplicateKeyValue = PureYAML.Model.Value.mapping(.init([
|
||||
.init(key: "items", value: .sequence([])),
|
||||
]))
|
||||
|
||||
private let expectedIssues: [PureYAML.Validation.Issue] = [
|
||||
private let expectedIssues: [PureYAML.Validation.ValidationError] = [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
/// `Optional<Mapping>` does not conform to `Validatable` in production (an Optional must never
|
||||
/// satisfy a non-Optional check); this test-only alias and conformance let a machinery test box one
|
||||
/// into the erased `any Validatable` boundary to prove the runtime `type(of:)` guard rejects it.
|
||||
private typealias OptionalMapping = PureYAML.Model.Mapping?
|
||||
extension PureYAML.Model.Mapping?: PureYAML.Validation.Validatable {}
|
||||
|
||||
@Suite("Type-directed check machinery")
|
||||
struct ValidationCheckMachineryTests {
|
||||
private typealias Check = PureYAML.Validation.Check<PureYAML.Model.Mapping, PureYAML.Model.Value>
|
||||
private typealias AnyCheck = PureYAML.Validation.AnyCheck<PureYAML.Model.Value>
|
||||
private typealias Issue = PureYAML.Validation.Issue
|
||||
private typealias Check = PureYAML.Validation.Validation<PureYAML.Model.Mapping, PureYAML.Model.Value>
|
||||
private typealias AnyValidation = PureYAML.Validation.AnyValidation<PureYAML.Model.Value>
|
||||
private typealias Issue = PureYAML.Validation.ValidationError
|
||||
|
||||
private var mapping: PureYAML.Model.Mapping {
|
||||
.init([.init(key: "a", value: .int(1))])
|
||||
@ -21,24 +27,33 @@ struct ValidationCheckMachineryTests {
|
||||
|
||||
@Test("A check fires on its subject type and produces its error")
|
||||
func test_firesOnSubjectType() {
|
||||
let erased = AnyCheck(alwaysFails())
|
||||
let erased = AnyValidation(alwaysFails())
|
||||
#expect(erased.apply(to: mapping, at: .root, in: .null).count == 1)
|
||||
}
|
||||
|
||||
@Test("A check does not fire on a value of another type")
|
||||
func test_doesNotFireOnWrongType() {
|
||||
let erased = AnyCheck(alwaysFails())
|
||||
let erased = AnyValidation(alwaysFails())
|
||||
// A Model.Value (even one wrapping a mapping) is not a Model.Mapping, so nothing fires.
|
||||
#expect(erased.apply(to: PureYAML.Model.Value.mapping(mapping), at: .root, in: .null).isEmpty)
|
||||
#expect(erased.apply(to: PureYAML.Model.Key.string("a"), at: .root, in: .null).isEmpty)
|
||||
}
|
||||
|
||||
@Test("An Optional wrapping the subject type does not satisfy the non-Optional check")
|
||||
func test_optionalDoesNotSatisfyNonOptionalCheck() {
|
||||
let erased = AnyValidation(alwaysFails())
|
||||
let optional: OptionalMapping = mapping
|
||||
#expect(erased.apply(to: optional, at: .root, in: .null).isEmpty)
|
||||
// The exact type still fires, so the guard above is not vacuously true.
|
||||
#expect(erased.apply(to: mapping, at: .root, in: .null).count == 1)
|
||||
}
|
||||
|
||||
@Test("A false `when` predicate yields no errors")
|
||||
func test_falsePredicateYieldsNothing() {
|
||||
let gated = Check(description: "gated", check: { _ in
|
||||
[Issue(severity: .error, reason: "boom", path: .root)]
|
||||
}, when: { _ in false })
|
||||
#expect(AnyCheck(gated).apply(to: mapping, at: .root, in: .null).isEmpty)
|
||||
#expect(AnyValidation(gated).apply(to: mapping, at: .root, in: .null).isEmpty)
|
||||
}
|
||||
|
||||
// MARK: positive control (proving the negatives are not vacuous)
|
||||
@ -50,8 +65,8 @@ struct ValidationCheckMachineryTests {
|
||||
.mapping(.init([.init(key: "a", value: .int(1))])),
|
||||
.mapping(.init([.init(key: "b", value: .int(2))])),
|
||||
]))
|
||||
let validator = PureYAML.Validation.Validator.blank.validating(alwaysFails())
|
||||
let issues = validator.collect(document).issues
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(alwaysFails())
|
||||
let issues = validator.collect(document).values
|
||||
#expect(issues.count == 2)
|
||||
}
|
||||
|
||||
@ -59,11 +74,11 @@ struct ValidationCheckMachineryTests {
|
||||
|
||||
@Test("The type-directed mappingKeysAreUnique builtin catches duplicate keys")
|
||||
func test_builtinCatchesDuplicateKeys() {
|
||||
let validator = PureYAML.Validation.Validator.blank.validating(
|
||||
PureYAML.Validation.BuiltinCheck.mappingKeysAreUnique,
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(
|
||||
PureYAML.Validation.BuiltinValidation.mappingKeysAreUnique,
|
||||
)
|
||||
#expect(validator.validationDescriptions == ["Mapping keys are unique"])
|
||||
#expect(throws: PureYAML.Validation.Issue.Collection.self) {
|
||||
#expect(throws: PureYAML.Validation.ValidationErrorCollection.self) {
|
||||
try validator.validate(.mapping(.init([
|
||||
.init(key: "a", value: .int(1)),
|
||||
.init(key: "a", value: .int(2)),
|
||||
@ -73,8 +88,8 @@ struct ValidationCheckMachineryTests {
|
||||
|
||||
@Test("The builtin passes a near-miss document whose keys are unique")
|
||||
func test_builtinPassesUniqueKeys() throws {
|
||||
let validator = PureYAML.Validation.Validator.blank.validating(
|
||||
PureYAML.Validation.BuiltinCheck.mappingKeysAreUnique,
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(
|
||||
PureYAML.Validation.BuiltinValidation.mappingKeysAreUnique,
|
||||
)
|
||||
// The boundary the rule almost trips: two keys that differ by one character.
|
||||
_ = try validator.validate(.mapping(.init([
|
||||
@ -85,8 +100,8 @@ struct ValidationCheckMachineryTests {
|
||||
|
||||
@Test("Type-directed dispatch reaches a nested mapping")
|
||||
func test_dispatchReachesNestedMapping() {
|
||||
let validator = PureYAML.Validation.Validator.blank.validating(
|
||||
PureYAML.Validation.BuiltinCheck.mappingKeysAreUnique,
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(
|
||||
PureYAML.Validation.BuiltinValidation.mappingKeysAreUnique,
|
||||
)
|
||||
let nested = PureYAML.Model.Value.mapping(.init([
|
||||
.init(key: "outer", value: .mapping(.init([
|
||||
|
||||
@ -5,27 +5,36 @@ private struct Address: PureYAML.Validation.Validatable {
|
||||
var city: String
|
||||
}
|
||||
|
||||
private struct Nickname: PureYAML.Validation.Validatable {
|
||||
var value: String
|
||||
}
|
||||
|
||||
private struct Person: PureYAML.Validation.Validatable {
|
||||
var name: String
|
||||
var age: Int
|
||||
var address: Address
|
||||
var nickname: Nickname?
|
||||
}
|
||||
|
||||
@Suite("Validation combinators")
|
||||
struct ValidationCombinatorTests {
|
||||
private typealias Context = PureYAML.Validation.SubjectContext<Person, PureYAML.Model.Value>
|
||||
private typealias Context = PureYAML.Validation.ValidationContext<Person, PureYAML.Model.Value>
|
||||
|
||||
private func context(name: String = "Ada", age: Int = 30, city: String = "Lovelace") -> Context {
|
||||
.init(document: .null, subject: Person(name: name, age: age, address: Address(city: city)), path: .root)
|
||||
private func context(name: String = "Ada", age: Int = 30, city: String = "Lovelace", nickname: String? = nil) -> Context {
|
||||
.init(
|
||||
document: .null,
|
||||
subject: Person(name: name, age: age, address: Address(city: city), nickname: nickname.map(Nickname.init)),
|
||||
path: .root,
|
||||
)
|
||||
}
|
||||
|
||||
private func failing(_ description: String) -> PureYAML.Validation.Check<Person, PureYAML.Model.Value> {
|
||||
private func failing(_ description: String) -> PureYAML.Validation.Validation<Person, PureYAML.Model.Value> {
|
||||
.init(description: description) { ctx in
|
||||
[PureYAML.Validation.Issue(severity: .error, reason: description, path: ctx.path)]
|
||||
[PureYAML.Validation.ValidationError(severity: .error, reason: description, path: ctx.path)]
|
||||
}
|
||||
}
|
||||
|
||||
private func passing(_ description: String) -> PureYAML.Validation.Check<Person, PureYAML.Model.Value> {
|
||||
private func passing(_ description: String) -> PureYAML.Validation.Validation<Person, PureYAML.Model.Value> {
|
||||
.init(description: description) { _ in [] }
|
||||
}
|
||||
|
||||
@ -48,7 +57,7 @@ struct ValidationCombinatorTests {
|
||||
|
||||
@Test("A Bool check built from an operator auto-formats its failure")
|
||||
func test_boolCheckFromOperator() {
|
||||
let adult = PureYAML.Validation.Check<Person, PureYAML.Model.Value>(
|
||||
let adult = PureYAML.Validation.Validation<Person, PureYAML.Model.Value>(
|
||||
description: "Person is an adult",
|
||||
check: (\Context.subject.age) >= 18,
|
||||
)
|
||||
@ -80,7 +89,7 @@ struct ValidationCombinatorTests {
|
||||
|
||||
@Test("lift runs child-typed checks against a value reached by key path")
|
||||
func test_lift() {
|
||||
let cityNotEmpty = PureYAML.Validation.Check<Address, PureYAML.Model.Value>(
|
||||
let cityNotEmpty = PureYAML.Validation.Validation<Address, PureYAML.Model.Value>(
|
||||
description: "City is set",
|
||||
check: { !$0.subject.city.isEmpty },
|
||||
)
|
||||
@ -88,4 +97,24 @@ struct ValidationCombinatorTests {
|
||||
#expect(lifted.apply(context(city: "Lovelace")).isEmpty)
|
||||
#expect(lifted.apply(context(city: "")).count == 1)
|
||||
}
|
||||
|
||||
@Test("unwrap errors on nil and otherwise runs child checks against the unwrapped value")
|
||||
func test_unwrap() {
|
||||
let notEmpty = PureYAML.Validation.Validation<Nickname, PureYAML.Model.Value>(
|
||||
description: "Nickname is not empty",
|
||||
check: { !$0.subject.value.isEmpty },
|
||||
)
|
||||
let unwrapped = PureYAML.Validation.unwrap(\Person.nickname, into: notEmpty)
|
||||
|
||||
let onNil = unwrapped.apply(context(nickname: nil))
|
||||
#expect(onNil.count == 1)
|
||||
#expect(onNil.first?.reason.contains("nil") == true)
|
||||
|
||||
let onValid = unwrapped.apply(context(nickname: "Ada"))
|
||||
#expect(onValid.isEmpty)
|
||||
|
||||
let onInvalid = unwrapped.apply(context(nickname: ""))
|
||||
#expect(onInvalid.count == 1)
|
||||
#expect(onInvalid.first?.reason == "Failed to satisfy: Nickname is not empty")
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,19 +12,19 @@ struct ValidationConfigurationTests {
|
||||
|
||||
@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"])
|
||||
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validationDescriptions == [])
|
||||
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().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
|
||||
let descriptions = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().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")
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().withoutValidating("Mapping keys are unique")
|
||||
#expect(validator.validationDescriptions == [])
|
||||
// With the rule removed, a document with duplicate keys validates clean.
|
||||
_ = try validator.validate(duplicateKeys)
|
||||
@ -32,8 +32,8 @@ struct ValidationConfigurationTests {
|
||||
|
||||
@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)
|
||||
#expect(throws: PureYAML.Validation.ValidationErrorCollection.self) {
|
||||
try PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().validate(duplicateKeys)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,18 +17,18 @@ enum ValidationCorpusSource {
|
||||
struct ValidationCorpusIssueFixture: CustomStringConvertible {
|
||||
var name: String
|
||||
var source: ValidationCorpusSource
|
||||
var validator: PureYAML.Validation.Validator
|
||||
var validator: PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>
|
||||
var strict: Bool
|
||||
var expectedIssues: [PureYAML.Validation.Issue]
|
||||
var forbiddenIssues: [PureYAML.Validation.Issue]
|
||||
var expectedIssues: [PureYAML.Validation.ValidationError]
|
||||
var forbiddenIssues: [PureYAML.Validation.ValidationError]
|
||||
|
||||
init(
|
||||
name: String,
|
||||
source: ValidationCorpusSource,
|
||||
validator: PureYAML.Validation.Validator,
|
||||
validator: PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>,
|
||||
strict: Bool = true,
|
||||
expectedIssues: [PureYAML.Validation.Issue],
|
||||
forbiddenIssues: [PureYAML.Validation.Issue] = [],
|
||||
expectedIssues: [PureYAML.Validation.ValidationError],
|
||||
forbiddenIssues: [PureYAML.Validation.ValidationError] = [],
|
||||
) {
|
||||
self.name = name
|
||||
self.source = source
|
||||
@ -46,14 +46,14 @@ struct ValidationCorpusIssueFixture: CustomStringConvertible {
|
||||
struct ValidationCorpusSuccessFixture: CustomStringConvertible {
|
||||
var name: String
|
||||
var source: ValidationCorpusSource
|
||||
var validator: PureYAML.Validation.Validator
|
||||
var forbiddenIssues: [PureYAML.Validation.Issue]
|
||||
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,
|
||||
forbiddenIssues: [PureYAML.Validation.Issue] = [],
|
||||
validator: PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>,
|
||||
forbiddenIssues: [PureYAML.Validation.ValidationError] = [],
|
||||
) {
|
||||
self.name = name
|
||||
self.source = source
|
||||
@ -66,15 +66,15 @@ struct ValidationCorpusSuccessFixture: CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
func requiredRootStringKeyRule(_ key: String) -> PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value> {
|
||||
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(description: "Root requires string key '\(key)'") { context in
|
||||
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.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Required string key '\(key)' is missing",
|
||||
path: context.path.appending(.key(key)),
|
||||
@ -84,7 +84,7 @@ func requiredRootStringKeyRule(_ key: String) -> PureYAML.Validation.Check<PureY
|
||||
|
||||
guard case .string = value else {
|
||||
return [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Required string key '\(key)' must be a string",
|
||||
path: context.path.appending(.key(key)),
|
||||
@ -101,8 +101,8 @@ func requiredRootStringKeyRule(_ key: String) -> PureYAML.Validation.Check<PureY
|
||||
func forbiddenMappingKeyRule(
|
||||
_ key: String,
|
||||
severity: PureYAML.Validation.Severity,
|
||||
) -> PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value> {
|
||||
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(description: "Mapping forbids key '\(key)'") { context in
|
||||
) -> 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 {
|
||||
@ -110,7 +110,7 @@ func forbiddenMappingKeyRule(
|
||||
}
|
||||
|
||||
return [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: severity,
|
||||
reason: "Forbidden key '\(key)' is present",
|
||||
path: context.path.appending(.key(key)),
|
||||
@ -124,8 +124,8 @@ func forbiddenMappingKeyRule(
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueRouteNamesRule() -> PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value> {
|
||||
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(description: "Route names are unique") { context in
|
||||
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 {
|
||||
@ -133,7 +133,7 @@ func uniqueRouteNamesRule() -> PureYAML.Validation.Check<PureYAML.Model.Value, P
|
||||
}
|
||||
|
||||
var seen = Set<String>()
|
||||
var issues: [PureYAML.Validation.Issue] = []
|
||||
var issues: [PureYAML.Validation.ValidationError] = []
|
||||
for index in routes.indices {
|
||||
guard case let .mapping(route) = routes[index],
|
||||
case let .string(name)? = route["name"]
|
||||
|
||||
@ -15,7 +15,7 @@ struct ValidationCorpusTests {
|
||||
#expect(try PureYAML.validate(value, using: testCase.validator).isEmpty)
|
||||
|
||||
for forbiddenIssue in testCase.forbiddenIssues {
|
||||
#expect(!result.issues.contains(forbiddenIssue))
|
||||
#expect(!result.values.contains(forbiddenIssue))
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,15 +24,15 @@ struct ValidationCorpusTests {
|
||||
let value = try testCase.source.load()
|
||||
let result = testCase.validator.collect(value)
|
||||
|
||||
#expect(result.issues == testCase.expectedIssues)
|
||||
#expect(result.issues.map(\.description) == testCase.expectedIssues.map(\.description))
|
||||
#expect(result.values == testCase.expectedIssues)
|
||||
#expect(result.values.map(\.description) == testCase.expectedIssues.map(\.description))
|
||||
|
||||
for forbiddenIssue in testCase.forbiddenIssues {
|
||||
#expect(!result.issues.contains(forbiddenIssue))
|
||||
#expect(!result.values.contains(forbiddenIssue))
|
||||
}
|
||||
|
||||
expectValidationError(value, using: testCase.validator, strict: testCase.strict) { collection in
|
||||
#expect(collection.issues == testCase.expectedIssues)
|
||||
#expect(collection.values == testCase.expectedIssues)
|
||||
#expect(collection.description == testCase.expectedIssues.map(\.description).joined(separator: "\n"))
|
||||
}
|
||||
}
|
||||
@ -66,24 +66,24 @@ struct ValidationCorpusTests {
|
||||
- mode: legacy
|
||||
""",
|
||||
)
|
||||
let validator = PureYAML.Validation.Validator.blank
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank
|
||||
.validating(traceRule(label: "first"))
|
||||
.validating(forbiddenMappingKeyRule("mode", severity: .warning))
|
||||
.validating(legacyModeRule(severity: .error))
|
||||
|
||||
let result = validator.collect(value)
|
||||
|
||||
#expect(result.issues.map(\.description) == [
|
||||
"warning: first at root at root",
|
||||
"warning: first at $.root at $.root",
|
||||
"warning: Forbidden key 'mode' is present at $.root.mode",
|
||||
"warning: first at $.root.mode at $.root.mode",
|
||||
"error: Legacy mode is not allowed at $.root.mode",
|
||||
"warning: first at $.list at $.list",
|
||||
"warning: first at $.list[0] at $.list[0]",
|
||||
"warning: Forbidden key 'mode' is present at $.list[0].mode",
|
||||
"warning: first at $.list[0].mode at $.list[0].mode",
|
||||
"error: Legacy mode is not allowed at $.list[0].mode",
|
||||
#expect(result.values.map(\.description) == [
|
||||
"first at root at root of document",
|
||||
"first at $.root at path: $.root",
|
||||
"Forbidden key 'mode' is present at path: $.root.mode",
|
||||
"first at $.root.mode at path: $.root.mode",
|
||||
"Legacy mode is not allowed at path: $.root.mode",
|
||||
"first at $.list at path: $.list",
|
||||
"first at $.list[0] at path: $.list[0]",
|
||||
"Forbidden key 'mode' is present at path: $.list[0].mode",
|
||||
"first at $.list[0].mode at path: $.list[0].mode",
|
||||
"Legacy mode is not allowed at path: $.list[0].mode",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,14 +10,14 @@ import Testing
|
||||
@Suite("Validation finding-coverage law")
|
||||
struct ValidationCoverageMetaTests {
|
||||
private struct ModelFixture {
|
||||
let check: PureYAML.Validation.AnyCheck<PureYAML.Model.Value>
|
||||
let check: PureYAML.Validation.AnyValidation<PureYAML.Model.Value>
|
||||
let failing: PureYAML.Model.Value
|
||||
let nearMiss: PureYAML.Model.Value
|
||||
}
|
||||
|
||||
private static let modelFixtures: [ModelFixture] = [
|
||||
.init(
|
||||
check: .init(PureYAML.Validation.BuiltinCheck.mappingKeysAreUnique),
|
||||
check: .init(PureYAML.Validation.BuiltinValidation.mappingKeysAreUnique),
|
||||
failing: .mapping(.init([
|
||||
.init(key: "a", value: .int(1)),
|
||||
.init(key: "a", value: .int(2)),
|
||||
@ -31,14 +31,14 @@ struct ValidationCoverageMetaTests {
|
||||
]
|
||||
|
||||
private struct TaggedFixture {
|
||||
let check: PureYAML.Validation.AnyCheck<PureYAML.Tagged.Node>
|
||||
let check: PureYAML.Validation.AnyValidation<PureYAML.Tagged.Node>
|
||||
let failing: PureYAML.Tagged.Node
|
||||
let nearMiss: PureYAML.Tagged.Node
|
||||
}
|
||||
|
||||
private static let taggedFixtures: [TaggedFixture] = [
|
||||
.init(
|
||||
check: .init(PureYAML.Tagged.BuiltinCheck.mappingKeysAreUnique),
|
||||
check: .init(PureYAML.Tagged.BuiltinValidation.mappingKeysAreUnique),
|
||||
failing: .mapping(.init(pairs: [
|
||||
.init(key: "a", value: .scalar(.init(rawValue: "1", value: .int(1)))),
|
||||
.init(key: "a", value: .scalar(.init(rawValue: "2", value: .int(2)))),
|
||||
@ -49,7 +49,7 @@ struct ValidationCoverageMetaTests {
|
||||
])),
|
||||
),
|
||||
.init(
|
||||
check: .init(PureYAML.Tagged.BuiltinCheck.builtInTagsAreSupportedAndKindConsistent),
|
||||
check: .init(PureYAML.Tagged.BuiltinValidation.builtInTagsAreSupportedAndKindConsistent),
|
||||
// A built-in tag on the wrong node kind: !!map applied to a scalar.
|
||||
failing: .scalar(.init(rawValue: "x", value: .string("x"), tag: .mapping)),
|
||||
// The boundary: a built-in tag that is supported and kind-consistent, !!str on a scalar.
|
||||
@ -59,7 +59,7 @@ struct ValidationCoverageMetaTests {
|
||||
|
||||
@Test("Model fixtures cover exactly the model validator's default checks")
|
||||
func test_modelFixturesCoverDefaults() {
|
||||
let ruled = Set(PureYAML.Validation.Validator().validationDescriptions)
|
||||
let ruled = Set(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().validationDescriptions)
|
||||
let covered = Set(Self.modelFixtures.map(\.check.description))
|
||||
#expect(ruled.subtracting(covered).isEmpty, "model default checks without fixtures: \(ruled.subtracting(covered).sorted())")
|
||||
#expect(covered.subtracting(ruled).isEmpty, "model fixtures for checks that no longer ship: \(covered.subtracting(ruled).sorted())")
|
||||
@ -68,8 +68,8 @@ struct ValidationCoverageMetaTests {
|
||||
@Test("Each model check fails its failing fixture and passes its near miss in isolation")
|
||||
func test_modelChecksFailAndNearMiss() throws {
|
||||
for fixture in Self.modelFixtures {
|
||||
let isolated = PureYAML.Validation.Validator(checks: [fixture.check])
|
||||
#expect(throws: PureYAML.Validation.Issue.Collection.self, "check '\(fixture.check.description)' did not fail its failing fixture") {
|
||||
let isolated = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>(nonReferenceDefaultValidations: [fixture.check])
|
||||
#expect(throws: PureYAML.Validation.ValidationErrorCollection.self, "check '\(fixture.check.description)' did not fail its failing fixture") {
|
||||
try isolated.validate(fixture.failing)
|
||||
}
|
||||
_ = try isolated.validate(fixture.nearMiss)
|
||||
@ -87,8 +87,8 @@ struct ValidationCoverageMetaTests {
|
||||
@Test("Each tagged check fails its failing fixture and passes its near miss in isolation")
|
||||
func test_taggedChecksFailAndNearMiss() throws {
|
||||
for fixture in Self.taggedFixtures {
|
||||
let isolated = PureYAML.Tagged.Validator(checks: [fixture.check])
|
||||
#expect(throws: PureYAML.Validation.Issue.Collection.self, "check '\(fixture.check.description)' did not fail its failing fixture") {
|
||||
let isolated = PureYAML.Tagged.Validator(nonReferenceDefaultValidations: [fixture.check])
|
||||
#expect(throws: PureYAML.Validation.ValidationErrorCollection.self, "check '\(fixture.check.description)' did not fail its failing fixture") {
|
||||
try isolated.validate(fixture.failing)
|
||||
}
|
||||
_ = try isolated.validate(fixture.nearMiss)
|
||||
|
||||
@ -9,13 +9,13 @@ struct ValidationFailureFixtureTests {
|
||||
let value = try testCase.loadValue()
|
||||
let result = testCase.validator.collect(value)
|
||||
|
||||
#expect(result.issues == testCase.expectedIssues)
|
||||
#expect(result.issues.map(\.description) == testCase.expectedDescriptions)
|
||||
#expect(result.values == testCase.expectedIssues)
|
||||
#expect(result.values.map(\.description) == testCase.expectedDescriptions)
|
||||
#expect(result.descriptionLines == testCase.expectedDescriptions)
|
||||
#expect(!result.isValid == testCase.expectedIssues.contains { $0.severity == .error })
|
||||
|
||||
expectValidationError(value, using: testCase.validator) { collection in
|
||||
#expect(collection.issues == testCase.expectedIssues)
|
||||
#expect(collection.values == testCase.expectedIssues)
|
||||
#expect(collection.description == testCase.expectedDescriptions.joined(separator: "\n"))
|
||||
}
|
||||
}
|
||||
@ -55,8 +55,8 @@ let validationFailureFixtures: [ValidationFailureFixture] = [
|
||||
),
|
||||
],
|
||||
expectedDescriptions: [
|
||||
"error: Duplicate mapping key 'title' at $.title",
|
||||
"error: Duplicate mapping key '/users' at $.paths[\"/users\"]",
|
||||
"Duplicate mapping key 'title' at path: $.title",
|
||||
"Duplicate mapping key '/users' at path: $.paths[\"/users\"]",
|
||||
],
|
||||
),
|
||||
.init(
|
||||
@ -78,8 +78,8 @@ let validationFailureFixtures: [ValidationFailureFixture] = [
|
||||
),
|
||||
],
|
||||
expectedDescriptions: [
|
||||
"error: Required string key 'title' is missing at $.title",
|
||||
"error: Forbidden key 'draft' is present at $.metadata.draft",
|
||||
"Required string key 'title' is missing at path: $.title",
|
||||
"Forbidden key 'draft' is present at path: $.metadata.draft",
|
||||
],
|
||||
),
|
||||
.init(
|
||||
@ -99,8 +99,8 @@ let validationFailureFixtures: [ValidationFailureFixture] = [
|
||||
),
|
||||
],
|
||||
expectedDescriptions: [
|
||||
"warning: Forbidden key 'deprecated' is present at $.deprecated",
|
||||
"warning: Forbidden key 'deprecated' is present at $.nested.deprecated",
|
||||
"Forbidden key 'deprecated' is present at path: $.deprecated",
|
||||
"Forbidden key 'deprecated' is present at path: $.nested.deprecated",
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -108,8 +108,8 @@ let validationFailureFixtures: [ValidationFailureFixture] = [
|
||||
struct ValidationFailureFixture: CustomStringConvertible {
|
||||
var name: String
|
||||
var fileName: String
|
||||
var validator: PureYAML.Validation.Validator
|
||||
var expectedIssues: [PureYAML.Validation.Issue]
|
||||
var validator: PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>
|
||||
var expectedIssues: [PureYAML.Validation.ValidationError]
|
||||
var expectedDescriptions: [String]
|
||||
|
||||
var description: String {
|
||||
@ -129,6 +129,6 @@ struct ValidationFailureFixture: CustomStringConvertible {
|
||||
|
||||
private extension PureYAML.Validation.Result {
|
||||
var descriptionLines: [String] {
|
||||
issues.map(\.description)
|
||||
values.map(\.description)
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,10 +12,10 @@ struct ValidationModeTests {
|
||||
mode: legacy
|
||||
""",
|
||||
)
|
||||
let validator = PureYAML.Validation.Validator.blank.validating(legacyModeRule(severity: .error))
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(legacyModeRule(severity: .error))
|
||||
|
||||
expectValidationError(value, using: validator) { collection in
|
||||
#expect(collection.issues == [
|
||||
#expect(collection.values == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Legacy mode is not allowed",
|
||||
@ -33,10 +33,10 @@ struct ValidationModeTests {
|
||||
@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))
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(legacyModeRule(severity: .warning))
|
||||
|
||||
expectValidationError(value, using: validator) { collection in
|
||||
#expect(collection.issues == [
|
||||
#expect(collection.values == [
|
||||
.init(
|
||||
severity: .warning,
|
||||
reason: "Legacy mode is not allowed",
|
||||
@ -49,12 +49,12 @@ struct ValidationModeTests {
|
||||
@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 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.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .warning,
|
||||
reason: "Legacy mode is not allowed",
|
||||
path: .init([.key("mode")]),
|
||||
@ -71,18 +71,18 @@ struct ValidationModeTests {
|
||||
mode: legacy
|
||||
""",
|
||||
)
|
||||
let validator = PureYAML.Validation.Validator()
|
||||
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.issues == [
|
||||
#expect(collection.values == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
path: .init([.key("title")]),
|
||||
),
|
||||
])
|
||||
#expect(!collection.issues.contains(.init(
|
||||
#expect(!collection.values.contains(.init(
|
||||
severity: .warning,
|
||||
reason: "Legacy mode is not allowed",
|
||||
path: .init([.key("mode")]),
|
||||
@ -100,40 +100,40 @@ struct ValidationModeTests {
|
||||
- mode: legacy
|
||||
""",
|
||||
)
|
||||
let validator = PureYAML.Validation.Validator.blank
|
||||
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.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",
|
||||
#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.blank.validating(
|
||||
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(
|
||||
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.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Should not appear",
|
||||
path: context.path,
|
||||
@ -160,12 +160,12 @@ struct ValidationModeTests {
|
||||
label: legacy
|
||||
""",
|
||||
)
|
||||
let validator = PureYAML.Validation.Validator.blank.validating(
|
||||
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(
|
||||
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.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Legacy mode is not allowed",
|
||||
path: context.path,
|
||||
@ -181,14 +181,14 @@ struct ValidationModeTests {
|
||||
|
||||
let result = validator.collect(value)
|
||||
|
||||
#expect(result.issues == [
|
||||
#expect(result.values == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Legacy mode is not allowed",
|
||||
path: .init([.key("nested"), .key("mode")]),
|
||||
),
|
||||
])
|
||||
#expect(!result.issues.contains(.init(
|
||||
#expect(!result.values.contains(.init(
|
||||
severity: .error,
|
||||
reason: "Legacy mode is not allowed",
|
||||
path: .init([.key("nested"), .key("label")]),
|
||||
@ -206,8 +206,8 @@ struct ValidationModeTests {
|
||||
- label: test-ignored
|
||||
""",
|
||||
)
|
||||
let validator = PureYAML.Validation.Validator.blank.validating(
|
||||
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(
|
||||
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,
|
||||
@ -216,7 +216,7 @@ struct ValidationModeTests {
|
||||
name.hasPrefix("\(prefix)-")
|
||||
else {
|
||||
return [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Route name does not use root prefix",
|
||||
path: context.path,
|
||||
@ -233,14 +233,14 @@ struct ValidationModeTests {
|
||||
|
||||
let result = validator.collect(value)
|
||||
|
||||
#expect(result.issues == [
|
||||
#expect(result.values == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Route name does not use root prefix",
|
||||
path: .init([.key("routes"), .index(1), .key("name")]),
|
||||
),
|
||||
])
|
||||
#expect(!result.issues.contains(.init(
|
||||
#expect(!result.values.contains(.init(
|
||||
severity: .error,
|
||||
reason: "Route name does not use root prefix",
|
||||
path: .init([.key("routes"), .index(2), .key("label")]),
|
||||
@ -255,8 +255,8 @@ struct ValidationModeTests {
|
||||
state: draft
|
||||
""",
|
||||
)
|
||||
let validator = PureYAML.Validation.Validator.blank.validating(
|
||||
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(
|
||||
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") },
|
||||
@ -264,7 +264,7 @@ struct ValidationModeTests {
|
||||
)
|
||||
|
||||
expectValidationError(value, using: validator) { collection in
|
||||
#expect(collection.issues == [
|
||||
#expect(collection.values == [
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Failed to satisfy: State is published",
|
||||
|
||||
@ -5,8 +5,8 @@ import Testing
|
||||
struct ValidationModelTests {
|
||||
@Test("Validation result separates errors and warnings")
|
||||
func test_validationResultSeparatesErrorsAndWarnings() {
|
||||
let error = PureYAML.Validation.Issue(severity: .error, reason: "error")
|
||||
let warning = PureYAML.Validation.Issue(severity: .warning, reason: "warning")
|
||||
let error = PureYAML.Validation.ValidationError(severity: .error, reason: "error")
|
||||
let warning = PureYAML.Validation.ValidationError(severity: .warning, reason: "warning")
|
||||
let result = PureYAML.Validation.Result([error, warning])
|
||||
|
||||
#expect(result.isValid == false)
|
||||
@ -67,27 +67,27 @@ struct ValidationModelTests {
|
||||
|
||||
@Test("Validation issue descriptions include severity reason and path")
|
||||
func test_validationIssueDescriptionsIncludeSeverityReasonAndPath() {
|
||||
let issue = PureYAML.Validation.Issue(
|
||||
let issue = PureYAML.Validation.ValidationError(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
path: .init([.key("title")]),
|
||||
)
|
||||
|
||||
#expect(issue.description == "error: Duplicate mapping key 'title' at $.title")
|
||||
#expect(issue.description == "Duplicate mapping key 'title' at path: $.title")
|
||||
}
|
||||
|
||||
@Test("Validation issue collection describes all failures")
|
||||
func test_validationIssueCollectionDescription() {
|
||||
let collection = PureYAML.Validation.Issue.Collection([
|
||||
let collection = PureYAML.Validation.ValidationErrorCollection([
|
||||
.init(severity: .error, reason: "first", path: .root),
|
||||
.init(severity: .warning, reason: "second", path: .init([.key("name")])),
|
||||
])
|
||||
|
||||
#expect(collection.description == """
|
||||
error: first at root
|
||||
warning: second at $.name
|
||||
first at root of document
|
||||
second at path: $.name
|
||||
""")
|
||||
#expect(collection.description.contains("error: first"))
|
||||
#expect(collection.description.contains("first"))
|
||||
#expect(!collection.description.contains("info:"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,8 +56,8 @@ struct ValidationReportTests {
|
||||
|
||||
@Test("Warning reports stay visible without making the report invalid")
|
||||
func test_warningReportsStayVisibleWithoutMakingReportInvalid() {
|
||||
let validator = PureYAML.Validation.Validator.blank.validating(
|
||||
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(
|
||||
PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value>(
|
||||
description: "Mode is not legacy",
|
||||
severity: .warning,
|
||||
check: { context in context.subject != .string("legacy") },
|
||||
@ -84,8 +84,8 @@ struct ValidationReportTests {
|
||||
|
||||
@Test("Batch reports collect every source without stopping at the first failure")
|
||||
func test_batchReportsCollectEverySourceWithoutStoppingAtFirstFailure() {
|
||||
let validator = PureYAML.Validation.Validator().validating(
|
||||
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().validating(
|
||||
PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value>(
|
||||
description: "Mode is not legacy",
|
||||
severity: .warning,
|
||||
check: { context in context.subject != .string("legacy") },
|
||||
@ -130,8 +130,8 @@ struct ValidationReportTests {
|
||||
|
||||
@Test("Batch reports can treat warnings as failures")
|
||||
func test_batchReportsCanTreatWarningsAsFailures() {
|
||||
let validator = PureYAML.Validation.Validator.blank.validating(
|
||||
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.blank.validating(
|
||||
PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value>(
|
||||
description: "Mode is not legacy",
|
||||
severity: .warning,
|
||||
check: { context in context.subject != .string("legacy") },
|
||||
|
||||
@ -13,7 +13,7 @@ struct ValidationValidDocumentCase: CustomStringConvertible {
|
||||
struct ValidationDuplicateKeyCase: CustomStringConvertible {
|
||||
var name: String
|
||||
var yaml: String
|
||||
var expectedIssues: [PureYAML.Validation.Issue]
|
||||
var expectedIssues: [PureYAML.Validation.ValidationError]
|
||||
|
||||
var description: String {
|
||||
name
|
||||
@ -31,13 +31,13 @@ struct ValidationPathCase: CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
func legacyModeRule(severity: PureYAML.Validation.Severity) -> PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value> {
|
||||
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(description: "Mode must not be legacy") { context in
|
||||
func legacyModeRule(severity: PureYAML.Validation.Severity) -> PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value> {
|
||||
PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value>(description: "Mode must not be legacy") { context in
|
||||
guard case .string("legacy") = context.subject else {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: severity,
|
||||
reason: "Legacy mode is not allowed",
|
||||
path: context.path,
|
||||
@ -46,10 +46,10 @@ func legacyModeRule(severity: PureYAML.Validation.Severity) -> PureYAML.Validati
|
||||
}
|
||||
}
|
||||
|
||||
func traceRule(label: String) -> PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value> {
|
||||
PureYAML.Validation.Check<PureYAML.Model.Value, PureYAML.Model.Value>(description: "Trace \(label)") { context in
|
||||
func traceRule(label: String) -> PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value> {
|
||||
PureYAML.Validation.Validation<PureYAML.Model.Value, PureYAML.Model.Value>(description: "Trace \(label)") { context in
|
||||
[
|
||||
PureYAML.Validation.Issue(
|
||||
PureYAML.Validation.ValidationError(
|
||||
severity: .warning,
|
||||
reason: "\(label) at \(context.path.isRoot ? "root" : context.path.description)",
|
||||
path: context.path,
|
||||
@ -60,14 +60,14 @@ func traceRule(label: String) -> PureYAML.Validation.Check<PureYAML.Model.Value,
|
||||
|
||||
func expectValidationError(
|
||||
_ value: PureYAML.Model.Value,
|
||||
using validator: PureYAML.Validation.Validator = .init(),
|
||||
using validator: PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value> = .init(),
|
||||
strict: Bool = true,
|
||||
check: (PureYAML.Validation.Issue.Collection) -> Void,
|
||||
check: (PureYAML.Validation.ValidationErrorCollection) -> Void,
|
||||
) {
|
||||
do {
|
||||
try PureYAML.validate(value, using: validator, strict: strict)
|
||||
recordIssue("expected validation failure")
|
||||
} catch let collection as PureYAML.Validation.Issue.Collection {
|
||||
} catch let collection as PureYAML.Validation.ValidationErrorCollection {
|
||||
check(collection)
|
||||
} catch {
|
||||
recordIssue("unexpected error \(error)")
|
||||
|
||||
@ -46,7 +46,7 @@ struct ValidationTests {
|
||||
])
|
||||
func test_validDocumentsProduceNoDiagnostics(testCase: ValidationValidDocumentCase) throws {
|
||||
let value = try PureYAML.parse(testCase.yaml)
|
||||
let validator = PureYAML.Validation.Validator()
|
||||
let validator = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>()
|
||||
let result = validator.collect(value)
|
||||
|
||||
#expect(result == PureYAML.Validation.Result())
|
||||
@ -167,13 +167,13 @@ struct ValidationTests {
|
||||
])
|
||||
func test_duplicateMappingKeyDiagnostics(testCase: ValidationDuplicateKeyCase) throws {
|
||||
let value = try PureYAML.parse(testCase.yaml)
|
||||
let result = PureYAML.Validation.Validator().collect(value)
|
||||
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value)
|
||||
|
||||
#expect(result.issues == testCase.expectedIssues)
|
||||
#expect(result.values == testCase.expectedIssues)
|
||||
#expect(result.errors == testCase.expectedIssues)
|
||||
#expect(result.warnings.isEmpty)
|
||||
expectValidationError(value) { collection in
|
||||
#expect(collection.issues == testCase.expectedIssues)
|
||||
#expect(collection.values == testCase.expectedIssues)
|
||||
#expect(collection.description == testCase.expectedIssues.map(\.description).joined(separator: "\n"))
|
||||
}
|
||||
}
|
||||
@ -190,10 +190,10 @@ struct ValidationTests {
|
||||
|
||||
@Test("Default validator exposes exact built-in check descriptions")
|
||||
func test_defaultValidatorExposesExactBuiltInCheckDescriptions() {
|
||||
#expect(PureYAML.Validation.Validator.defaultChecks.map(\.description) == [
|
||||
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>.defaultValidations.map(\.description) == [
|
||||
"Mapping keys are unique",
|
||||
])
|
||||
#expect(PureYAML.Validation.Validator().validationDescriptions == [
|
||||
#expect(PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().validationDescriptions == [
|
||||
"Mapping keys are unique",
|
||||
])
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user