diff --git a/CHANGELOG.md b/CHANGELOG.md index dd46f37..3ab4028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 fixtures or any fixture for a rule that no longer ships. Each rule is also exercised in isolation, failing its failing fixture and passing its near miss. A planted orphan fixture was confirmed to fail the meta-test. -- Type-directed validation, the core of the validation-rules idiom: a `Check` carries a - positive description, a `when` predicate, and a Bool or multi-error check, and fires only on - values of its `Subject` type wherever they occur in the tree. `AnyCheck` erases the subject type +- Type-directed validation, the core of the validation-rules idiom: a `Check` + carries a positive description, a `when` predicate, and a Bool or multi-error check, and fires only + on values of its `Subject` type wherever they occur in the tree. `AnyCheck` erases the subject type and dispatches by runtime type, guarding against an optional or a wrong type matching. The validator gains a `checks` list and a `validating(_:)` overload for checks, and the traversal offers each node the values it may specialise on (the value, and for a mapping the mapping and @@ -53,6 +53,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 scalar resolution, block-scalar indentation) which parse-level cannot see. This turns "best YAML library" from an assertion into a tracked number. +### Changed + +- The tagged-node validator no longer carries its own copy of the validation machinery. The + type-directed `Check`, its erasure `AnyCheck`, and its `SubjectContext` are now generic over both + the subject and the document (the validated tree's root type), so one framework serves both layers: + the model tree validates with a `Model.Value` document, the tagged tree with a `Tagged.Node` + document. `Tagged.Rule` and the tagged validation `Context` are removed; the tagged validator keeps + only its own document type, its built-in check (`Tagged.BuiltinCheck`), and its traversal. This is + the one-framework, no-per-layer-duplication discipline of the validation-rules idiom (principle 10); + the tagged validator's public surface (`validate`, `collect`, `blank`, `validating`) is unchanged + except that `validating` now takes a `Check` instead of a `Tagged.Rule`. + ### Fixed - Content after a document-end marker (`...`) now begins a new bare document. A stream such as diff --git a/Sources/Tagged/Tagged.Check.Builtins.swift b/Sources/Tagged/Tagged.Check.Builtins.swift new file mode 100644 index 0000000..03388d1 --- /dev/null +++ b/Sources/Tagged/Tagged.Check.Builtins.swift @@ -0,0 +1,88 @@ +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` on the one + /// shared validation framework, not a copy of the machinery (validation-rules.md, principle 10). + enum BuiltinCheck { + /// 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 { + PureYAML.Validation.Check(description: "Built-in tags are supported and kind-consistent") { context in + var issues: [PureYAML.Validation.Issue] = [] + if case let .mapping(mapping) = context.subject { + issues.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 + } + + if let expectedKind = tag.expectedKind, expectedKind != context.subject.kind { + issues.append( + PureYAML.Validation.Issue( + severity: .error, + reason: "Tag '\(tag.rawValue)' expects a \(expectedKind) node, found \(context.subject.kind)", + path: context.path, + ), + ) + return issues + } + + if !tag.isSupportedBuiltInTag { + issues.append( + PureYAML.Validation.Issue( + severity: .error, + reason: "Unsupported built-in tag '\(tag.rawValue)'", + path: context.path, + ), + ) + } + + return issues + } when: { context in + if context.subject.tag?.isBuiltIn == true { + return true + } + if case let .mapping(mapping) = context.subject { + return mapping.pairs.contains { $0.keyTag?.isBuiltIn == true } + } + return false + } + } + + static func keyTagIssue( + for pair: PureYAML.Tagged.Pair, + at path: PureYAML.Validation.Path, + ) -> PureYAML.Validation.Issue? { + guard let tag = pair.keyTag, tag.isBuiltIn else { + return nil + } + if tag == .merge, pair.key == "<<" { + return nil + } + if tag == .merge { + return PureYAML.Validation.Issue( + 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( + severity: .error, + reason: "Tag '\(tag.rawValue)' expects a scalar mapping key", + path: path, + ) + } + guard tag.isSupportedBuiltInTag else { + return PureYAML.Validation.Issue( + severity: .error, + reason: "Unsupported built-in tag '\(tag.rawValue)' on mapping key", + path: path, + ) + } + return nil + } + } +} diff --git a/Sources/Tagged/Tagged.Context.swift b/Sources/Tagged/Tagged.Context.swift deleted file mode 100644 index 9ef955c..0000000 --- a/Sources/Tagged/Tagged.Context.swift +++ /dev/null @@ -1,18 +0,0 @@ -public extension PureYAML.Tagged { - /// Context passed to a tagged-node validation rule. - struct Context: Sendable { - public var root: Node - public var subject: Node - public var path: PureYAML.Validation.Path - - public init( - root: Node, - subject: Node, - path: PureYAML.Validation.Path, - ) { - self.root = root - self.subject = subject - self.path = path - } - } -} diff --git a/Sources/Tagged/Tagged.Rule.Builtins.swift b/Sources/Tagged/Tagged.Rule.Builtins.swift deleted file mode 100644 index 94a93fd..0000000 --- a/Sources/Tagged/Tagged.Rule.Builtins.swift +++ /dev/null @@ -1,82 +0,0 @@ -public extension PureYAML.Tagged.Rule { - /// Reports unsupported built-in tags and built-in tags applied to the wrong node kind. - static var builtInTagsAreSupportedAndKindConsistent: Self { - Self(description: "Built-in tags are supported and kind-consistent") { context in - var issues: [PureYAML.Validation.Issue] = [] - if case let .mapping(mapping) = context.subject { - issues.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 - } - - if let expectedKind = tag.expectedKind, expectedKind != context.subject.kind { - issues.append( - PureYAML.Validation.Issue( - severity: .error, - reason: "Tag '\(tag.rawValue)' expects a \(expectedKind) node, found \(context.subject.kind)", - path: context.path, - ), - ) - return issues - } - - if !tag.isSupportedBuiltInTag { - issues.append( - PureYAML.Validation.Issue( - severity: .error, - reason: "Unsupported built-in tag '\(tag.rawValue)'", - path: context.path, - ), - ) - } - - return issues - } when: { context in - if context.subject.tag?.isBuiltIn == true { - return true - } - if case let .mapping(mapping) = context.subject { - return mapping.pairs.contains { $0.keyTag?.isBuiltIn == true } - } - return false - } - } - - static func keyTagIssue( - for pair: PureYAML.Tagged.Pair, - at path: PureYAML.Validation.Path, - ) -> PureYAML.Validation.Issue? { - guard let tag = pair.keyTag, tag.isBuiltIn else { - return nil - } - if tag == .merge, pair.key == "<<" { - return nil - } - if tag == .merge { - return PureYAML.Validation.Issue( - 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( - severity: .error, - reason: "Tag '\(tag.rawValue)' expects a scalar mapping key", - path: path, - ) - } - guard tag.isSupportedBuiltInTag else { - return PureYAML.Validation.Issue( - severity: .error, - reason: "Unsupported built-in tag '\(tag.rawValue)' on mapping key", - path: path, - ) - } - return nil - } -} diff --git a/Sources/Tagged/Tagged.Rule.swift b/Sources/Tagged/Tagged.Rule.swift deleted file mode 100644 index 54cdebc..0000000 --- a/Sources/Tagged/Tagged.Rule.swift +++ /dev/null @@ -1,48 +0,0 @@ -public extension PureYAML.Tagged { - /// A validation rule for tag-preserving YAML nodes. - struct Rule: Sendable { - public let description: String - public let predicate: @Sendable (Context) -> Bool - public let check: @Sendable (Context) -> [PureYAML.Validation.Issue] - - public init( - description: String, - check: @escaping @Sendable (Context) -> [PureYAML.Validation.Issue], - when predicate: @escaping @Sendable (Context) -> Bool = { _ in true }, - ) { - self.description = description - self.predicate = predicate - self.check = check - } - - public init( - description: String, - severity: PureYAML.Validation.Severity = .error, - check: @escaping @Sendable (Context) -> Bool, - when predicate: @escaping @Sendable (Context) -> Bool = { _ in true }, - ) { - self.init( - description: description, - check: { context in - check(context) - ? [] - : [ - PureYAML.Validation.Issue( - severity: severity, - reason: "Failed to satisfy: \(description)", - path: context.path, - ), - ] - }, - when: predicate, - ) - } - - public func apply(_ context: Context) -> [PureYAML.Validation.Issue] { - guard predicate(context) else { - return [] - } - return check(context) - } - } -} diff --git a/Sources/Tagged/Tagged.Validator.swift b/Sources/Tagged/Tagged.Validator.swift index ef90b16..34aac4f 100644 --- a/Sources/Tagged/Tagged.Validator.swift +++ b/Sources/Tagged/Tagged.Validator.swift @@ -1,18 +1,21 @@ public extension PureYAML.Tagged { - /// Traverses a tag-preserving YAML node tree and applies validation rules. + /// 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 rules: [Rule] + public var checks: [PureYAML.Validation.AnyCheck] - public init(rules: [Rule] = Self.defaultRules) { - self.rules = rules + public init(checks: [PureYAML.Validation.AnyCheck] = Self.defaultChecks) { + self.checks = checks } public static var blank: Self { - Self(rules: []) + Self(checks: []) } - public static var defaultRules: [Rule] { - [.builtInTagsAreSupportedAndKindConsistent] + public static var defaultChecks: [PureYAML.Validation.AnyCheck] { + [PureYAML.Validation.AnyCheck(BuiltinCheck.builtInTagsAreSupportedAndKindConsistent)] } @discardableResult @@ -58,11 +61,18 @@ public extension PureYAML.Tagged { return PureYAML.Stream.Result(issues) } - public func validating(_ rule: Rule) -> Self { + /// 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) -> Self { var copy = self - copy.rules.append(rule) + 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) + } } } @@ -73,9 +83,10 @@ extension PureYAML.Tagged.Validator { path: PureYAML.Validation.Path, issues: inout [PureYAML.Validation.Issue], ) { - let context = PureYAML.Tagged.Context(root: root, subject: subject, path: path) - for rule in rules { - issues.append(contentsOf: rule.apply(context)) + // The document is the whole tagged tree; each check filters by runtime type, so a + // `Check` fires on every node offered here. + for check in checks { + issues.append(contentsOf: check.apply(to: subject, at: path, in: root)) } switch subject { diff --git a/Sources/Validation/AnyCheck.swift b/Sources/Validation/AnyCheck.swift index 5d65cef..92d5d87 100644 --- a/Sources/Validation/AnyCheck.swift +++ b/Sources/Validation/AnyCheck.swift @@ -1,12 +1,13 @@ public extension PureYAML.Validation { - /// A type-erased `Check`, so checks specialised on different subject types live in one list. It - /// filters by runtime type: a `Check` 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: Sendable { + /// A type-erased `Check`, 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` 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: Sendable { public let description: String - private let applyErased: @Sendable (any Validatable, Path, PureYAML.Model.Value) -> [Issue] + private let applyErased: @Sendable (any Validatable, Path, Document) -> [Issue] - public init(_ check: Check) { + public init(_ check: Check) { description = check.description applyErased = { subject, path, document in guard let typed = subject as? Subject, type(of: subject) == Subject.self else { @@ -19,7 +20,7 @@ public extension PureYAML.Validation { public func apply( to subject: any Validatable, at path: Path, - in document: PureYAML.Model.Value, + in document: Document, ) -> [Issue] { applyErased(subject, path, document) } diff --git a/Sources/Validation/Check.Builtins.swift b/Sources/Validation/Check.Builtins.swift index 76a7bae..11826f5 100644 --- a/Sources/Validation/Check.Builtins.swift +++ b/Sources/Validation/Check.Builtins.swift @@ -4,7 +4,7 @@ public extension PureYAML.Validation { /// 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 { + public static var mappingKeysAreUnique: Check { Check(description: "Mapping keys are unique") { context in var seen = Set() var issues: [Issue] = [] diff --git a/Sources/Validation/Check.Combinators.swift b/Sources/Validation/Check.Combinators.swift index db2b3aa..82c0f76 100644 --- a/Sources/Validation/Check.Combinators.swift +++ b/Sources/Validation/Check.Combinators.swift @@ -4,6 +4,10 @@ // 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. +// +// 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 +// (`D == Tagged.Node`). /// A key path is immutable and safe to read concurrently, but the standard library does not declare /// it `Sendable`, so wrap one captured by a `@Sendable` predicate closure. @@ -13,50 +17,50 @@ private struct SendableKeyPath: @unchecked Sendable { // MARK: comparison operators lifting a key path and a literal into a predicate -public func == ( - lhs: KeyPath, U>, +public func == ( + lhs: KeyPath, U>, rhs: U, -) -> @Sendable (PureYAML.Validation.SubjectContext) -> Bool { +) -> @Sendable (PureYAML.Validation.SubjectContext) -> Bool { let boxed = SendableKeyPath(keyPath: lhs) return { $0[keyPath: boxed.keyPath] == rhs } } -public func != ( - lhs: KeyPath, U>, +public func != ( + lhs: KeyPath, U>, rhs: U, -) -> @Sendable (PureYAML.Validation.SubjectContext) -> Bool { +) -> @Sendable (PureYAML.Validation.SubjectContext) -> Bool { let boxed = SendableKeyPath(keyPath: lhs) return { $0[keyPath: boxed.keyPath] != rhs } } -public func > ( - lhs: KeyPath, U>, +public func > ( + lhs: KeyPath, U>, rhs: U, -) -> @Sendable (PureYAML.Validation.SubjectContext) -> Bool { +) -> @Sendable (PureYAML.Validation.SubjectContext) -> Bool { let boxed = SendableKeyPath(keyPath: lhs) return { $0[keyPath: boxed.keyPath] > rhs } } -public func >= ( - lhs: KeyPath, U>, +public func >= ( + lhs: KeyPath, U>, rhs: U, -) -> @Sendable (PureYAML.Validation.SubjectContext) -> Bool { +) -> @Sendable (PureYAML.Validation.SubjectContext) -> Bool { let boxed = SendableKeyPath(keyPath: lhs) return { $0[keyPath: boxed.keyPath] >= rhs } } -public func < ( - lhs: KeyPath, U>, +public func < ( + lhs: KeyPath, U>, rhs: U, -) -> @Sendable (PureYAML.Validation.SubjectContext) -> Bool { +) -> @Sendable (PureYAML.Validation.SubjectContext) -> Bool { let boxed = SendableKeyPath(keyPath: lhs) return { $0[keyPath: boxed.keyPath] < rhs } } -public func <= ( - lhs: KeyPath, U>, +public func <= ( + lhs: KeyPath, U>, rhs: U, -) -> @Sendable (PureYAML.Validation.SubjectContext) -> Bool { +) -> @Sendable (PureYAML.Validation.SubjectContext) -> Bool { let boxed = SendableKeyPath(keyPath: lhs) return { $0[keyPath: boxed.keyPath] <= rhs } } @@ -64,25 +68,25 @@ public func <= ( 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( - _ keyPath: KeyPath, U>, + static func take( + _ keyPath: KeyPath, U>, _ predicate: @escaping @Sendable (U) -> Bool, - ) -> @Sendable (SubjectContext) -> Bool { + ) -> @Sendable (SubjectContext) -> Bool { let boxed = SendableKeyPath(keyPath: keyPath) return { predicate($0[keyPath: boxed.keyPath]) } } /// `take` rooted through the subject, so an author can write `take(\.someChild) { ... }`. - static func take( + static func take( _ keyPath: KeyPath, _ predicate: @escaping @Sendable (U) -> Bool, - ) -> @Sendable (SubjectContext) -> Bool { + ) -> @Sendable (SubjectContext) -> 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(_ checks: Check...) -> Check { + static func all(_ checks: Check...) -> Check { let list = checks return Check(description: "all of: \(list.map(\.description).joined(separator: ", "))") { context in list.flatMap { $0.apply(context) } @@ -91,10 +95,10 @@ public extension PureYAML.Validation { /// Runs child-typed checks against a value reached through a key path, keeping the parent's path /// and document. - static func lift( + static func lift( _ keyPath: KeyPath, - into checks: Check..., - ) -> Check { + into checks: Check..., + ) -> Check { let list = checks let boxed = SendableKeyPath(keyPath: keyPath) return Check(description: "child: \(list.map(\.description).joined(separator: ", "))") { context in @@ -110,19 +114,19 @@ public extension PureYAML.Validation { // MARK: composing two checks (&& concatenates errors, || short-circuits) -public func && ( - lhs: PureYAML.Validation.Check, - rhs: PureYAML.Validation.Check, -) -> PureYAML.Validation.Check { +public func && ( + lhs: PureYAML.Validation.Check, + rhs: PureYAML.Validation.Check, +) -> PureYAML.Validation.Check { PureYAML.Validation.Check(description: "\(lhs.description) and \(rhs.description)") { context in lhs.apply(context) + rhs.apply(context) } } -public func || ( - lhs: PureYAML.Validation.Check, - rhs: PureYAML.Validation.Check, -) -> PureYAML.Validation.Check { +public func || ( + lhs: PureYAML.Validation.Check, + rhs: PureYAML.Validation.Check, +) -> PureYAML.Validation.Check { PureYAML.Validation.Check(description: "\(lhs.description) or \(rhs.description)") { context in let left = lhs.apply(context) guard !left.isEmpty else { diff --git a/Sources/Validation/Check.swift b/Sources/Validation/Check.swift index a3aedd7..31ab740 100644 --- a/Sources/Validation/Check.swift +++ b/Sources/Validation/Check.swift @@ -1,17 +1,18 @@ public extension PureYAML.Validation { /// A type-directed validation value (the `Validation` 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. - struct Check: Sendable { + /// 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: Sendable { public let description: String - public let predicate: @Sendable (SubjectContext) -> Bool - public let check: @Sendable (SubjectContext) -> [Issue] + public let predicate: @Sendable (SubjectContext) -> Bool + public let check: @Sendable (SubjectContext) -> [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) -> [Issue], - when predicate: @escaping @Sendable (SubjectContext) -> Bool = { _ in true }, + check: @escaping @Sendable (SubjectContext) -> [Issue], + when predicate: @escaping @Sendable (SubjectContext) -> Bool = { _ in true }, ) { self.description = description self.predicate = predicate @@ -23,8 +24,8 @@ public extension PureYAML.Validation { public init( description: String, severity: Severity = .error, - check: @escaping @Sendable (SubjectContext) -> Bool, - when predicate: @escaping @Sendable (SubjectContext) -> Bool = { _ in true }, + check: @escaping @Sendable (SubjectContext) -> Bool, + when predicate: @escaping @Sendable (SubjectContext) -> Bool = { _ in true }, ) { self.init( description: description, @@ -38,7 +39,7 @@ public extension PureYAML.Validation { } /// Gates on the predicate, then runs the check. - public func apply(_ context: SubjectContext) -> [Issue] { + public func apply(_ context: SubjectContext) -> [Issue] { guard predicate(context) else { return [] } diff --git a/Sources/Validation/SubjectContext.swift b/Sources/Validation/SubjectContext.swift index 2a3c0dc..fc25021 100644 --- a/Sources/Validation/SubjectContext.swift +++ b/Sources/Validation/SubjectContext.swift @@ -1,13 +1,15 @@ public extension PureYAML.Validation { /// The read-only bundle a type-directed check receives: the whole `document` (for cross-cutting - /// checks), the current `subject` of the check's type, and the `path` to where it sits. - struct SubjectContext: Sendable { - public var document: PureYAML.Model.Value + /// 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: Sendable { + public var document: Document public var subject: Subject public var path: Path public init( - document: PureYAML.Model.Value, + document: Document, subject: Subject, path: Path, ) { diff --git a/Sources/Validation/Validatable.swift b/Sources/Validation/Validatable.swift index 8d55c79..b428641 100644 --- a/Sources/Validation/Validatable.swift +++ b/Sources/Validation/Validatable.swift @@ -8,3 +8,4 @@ public extension PureYAML.Validation { extension PureYAML.Model.Value: PureYAML.Validation.Validatable {} extension PureYAML.Model.Mapping: PureYAML.Validation.Validatable {} extension PureYAML.Model.Key: PureYAML.Validation.Validatable {} +extension PureYAML.Tagged.Node: PureYAML.Validation.Validatable {} diff --git a/Sources/Validation/Validator.swift b/Sources/Validation/Validator.swift index ca8fbd7..92aaee0 100644 --- a/Sources/Validation/Validator.swift +++ b/Sources/Validation/Validator.swift @@ -2,9 +2,9 @@ public extension PureYAML.Validation { /// Traverses a YAML value tree and applies validation rules at each node. struct Validator: Sendable { public var rules: [Rule] - public var checks: [AnyCheck] + public var checks: [AnyCheck] - public init(rules: [Rule] = Self.defaultRules, checks: [AnyCheck] = []) { + public init(rules: [Rule] = Self.defaultRules, checks: [AnyCheck] = []) { self.rules = rules self.checks = checks } @@ -68,7 +68,7 @@ public extension PureYAML.Validation { /// 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) -> Self { + public func validating(_ check: Check) -> Self { var copy = self copy.checks.append(AnyCheck(check)) return copy diff --git a/Tests/TaggedCompatibilityTests.swift b/Tests/TaggedCompatibilityTests.swift index 6b8af20..a849c0b 100644 --- a/Tests/TaggedCompatibilityTests.swift +++ b/Tests/TaggedCompatibilityTests.swift @@ -210,7 +210,9 @@ 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.Tagged.Rule(description: "Local tags are warnings") { context in + let localTagsAreWarnings = PureYAML.Validation.Check( + description: "Local tags are warnings", + ) { context in guard context.subject.tag == .init("!Local") else { return [] } diff --git a/Tests/ValidationCheckMachineryTests.swift b/Tests/ValidationCheckMachineryTests.swift index 241db40..2c26b35 100644 --- a/Tests/ValidationCheckMachineryTests.swift +++ b/Tests/ValidationCheckMachineryTests.swift @@ -3,8 +3,8 @@ import Testing @Suite("Type-directed check machinery") struct ValidationCheckMachineryTests { - private typealias Check = PureYAML.Validation.Check - private typealias AnyCheck = PureYAML.Validation.AnyCheck + private typealias Check = PureYAML.Validation.Check + private typealias AnyCheck = PureYAML.Validation.AnyCheck private typealias Issue = PureYAML.Validation.Issue private var mapping: PureYAML.Model.Mapping { diff --git a/Tests/ValidationCombinatorTests.swift b/Tests/ValidationCombinatorTests.swift index 5c8f14a..930007d 100644 --- a/Tests/ValidationCombinatorTests.swift +++ b/Tests/ValidationCombinatorTests.swift @@ -13,19 +13,19 @@ private struct Person: PureYAML.Validation.Validatable { @Suite("Validation combinators") struct ValidationCombinatorTests { - private typealias Context = PureYAML.Validation.SubjectContext + private typealias Context = PureYAML.Validation.SubjectContext 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 failing(_ description: String) -> PureYAML.Validation.Check { + private func failing(_ description: String) -> PureYAML.Validation.Check { .init(description: description) { ctx in [PureYAML.Validation.Issue(severity: .error, reason: description, path: ctx.path)] } } - private func passing(_ description: String) -> PureYAML.Validation.Check { + private func passing(_ description: String) -> PureYAML.Validation.Check { .init(description: description) { _ in [] } } @@ -41,14 +41,17 @@ struct ValidationCombinatorTests { @Test("take digs to a value and runs arbitrary logic") func test_take() { - let cityIsSet = PureYAML.Validation.take(\Person.address) { !$0.city.isEmpty } + let cityIsSet: @Sendable (Context) -> Bool = PureYAML.Validation.take(\Person.address) { !$0.city.isEmpty } #expect(cityIsSet(context(city: "Lovelace")) == true) #expect(cityIsSet(context(city: "")) == false) } @Test("A Bool check built from an operator auto-formats its failure") func test_boolCheckFromOperator() { - let adult = PureYAML.Validation.Check(description: "Person is an adult", check: (\Context.subject.age) >= 18) + let adult = PureYAML.Validation.Check( + description: "Person is an adult", + check: (\Context.subject.age) >= 18, + ) #expect(adult.apply(context(age: 30)).isEmpty) let issues = adult.apply(context(age: 10)) #expect(issues.count == 1) @@ -77,7 +80,10 @@ struct ValidationCombinatorTests { @Test("lift runs child-typed checks against a value reached by key path") func test_lift() { - let cityNotEmpty = PureYAML.Validation.Check
(description: "City is set", check: { !$0.subject.city.isEmpty }) + let cityNotEmpty = PureYAML.Validation.Check( + description: "City is set", + check: { !$0.subject.city.isEmpty }, + ) let lifted = PureYAML.Validation.lift(\Person.address, into: cityNotEmpty) #expect(lifted.apply(context(city: "Lovelace")).isEmpty) #expect(lifted.apply(context(city: "")).count == 1) diff --git a/scripts/validation-coverage.tsv b/scripts/validation-coverage.tsv index 626b77f..f118fe7 100644 --- a/scripts/validation-coverage.tsv +++ b/scripts/validation-coverage.tsv @@ -56,12 +56,11 @@ PureYAML.Tagged.Sequence excluded data-transfer ordered nodes PureYAML.Tagged.Tag excluded data-transfer tag identifier PureYAML.Tagged.NodeKind excluded closed enum; illegal states unrepresentable PureYAML.Tagged.Document excluded wraps a validated Tagged.Node -PureYAML.Tagged.Context excluded construction context data-transfer type PureYAML.Tagged.ConstructionContext excluded construction context data-transfer type PureYAML.Tagged.ConstructionError excluded error enum; data-transfer PureYAML.Tagged.Constructor excluded tag-construction engine; not a validated subject -PureYAML.Tagged.Rule excluded validation machinery; duplicates Validation.Rule, slated for de-dup (principle 10) -PureYAML.Tagged.Validator excluded validation machinery; duplicates Validation.Validator, slated for de-dup (principle 10) +PureYAML.Tagged.BuiltinCheck excluded caseless namespace of the tagged layer's built-in checks +PureYAML.Tagged.Validator excluded tagged-tree traversal over the shared Check framework; not a validated subject # --- validation framework's own types --- PureYAML.Validation.Validator excluded validation machinery; not a validated subject