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] public init(checks: [AnyCheck] = Self.defaultChecks) { self.checks = checks } public static var blank: Self { Self(checks: []) } public static var defaultChecks: [AnyCheck] { [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) -> 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. public var validationDescriptions: [String] { checks.map(\.description) } } } 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` ignores the value and key offers, and a // `Check` (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 } } 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)) } } }