// The declarative combinator algebra for ``PureXML/Validation/Validation``. // Free functions returning closures, so validations compose by value rather than // through imperative control flow. Generic over both the subject type and the // document, per the one-framework-parameterized-over-the-document rule. typealias Validatable = PureXML.Validation.Validatable typealias VContext = PureXML.Validation.ValidationContext typealias VError = PureXML.Validation.ValidationError typealias VRule = PureXML.Validation.Validation // MARK: Context -> error array func && ( lhs: @escaping (VContext) -> [VError], rhs: @escaping (VContext) -> [VError], ) -> (VContext) -> [VError] { { context in lhs(context) + rhs(context) } } func || ( lhs: @escaping (VContext) -> [VError], rhs: @escaping (VContext) -> [VError], ) -> (VContext) -> [VError] { { context in let left = lhs(context) if left.isEmpty { return [] } let right = rhs(context) return right.isEmpty ? [] : left + right } } // MARK: Context -> Bool func && ( lhs: @escaping (VContext) -> Bool, rhs: @escaping (VContext) -> Bool, ) -> (VContext) -> Bool { { context in lhs(context) && rhs(context) } } func || ( lhs: @escaping (VContext) -> Bool, rhs: @escaping (VContext) -> Bool, ) -> (VContext) -> Bool { { context in lhs(context) || rhs(context) } } // MARK: Subject KeyPath -> Bool func == (lhs: KeyPath, rhs: U) -> (VContext) -> Bool { { $0.subject[keyPath: lhs] == rhs } } func != (lhs: KeyPath, rhs: U) -> (VContext) -> Bool { { $0.subject[keyPath: lhs] != rhs } } func > (lhs: KeyPath, rhs: U) -> (VContext) -> Bool { { $0.subject[keyPath: lhs] > rhs } } func >= (lhs: KeyPath, rhs: U) -> (VContext) -> Bool { { $0.subject[keyPath: lhs] >= rhs } } func < (lhs: KeyPath, rhs: U) -> (VContext) -> Bool { { $0.subject[keyPath: lhs] < rhs } } func <= (lhs: KeyPath, rhs: U) -> (VContext) -> Bool { { $0.subject[keyPath: lhs] <= rhs } } // MARK: Digging and lifting /// Dig to a value by KeyPath and run arbitrary logic on it. func take(_ path: KeyPath, check: @escaping (U) -> Bool) -> (VContext) -> Bool { { check($0.subject[keyPath: path]) } } /// Run child-typed validations against a lifted value, keeping the parent path /// and document. func lift( _ path: KeyPath, into validations: VRule..., ) -> (VContext) -> [VError] { { context in validations.flatMap { $0.apply(to: context.subject[keyPath: path], at: context.codingPath, in: context.document) } } } /// Unwrap an optional, erroring if nil, else run child validations. func unwrap( _ path: KeyPath, into validations: VRule..., description: String? = nil, ) -> (VContext) -> [VError] { { context in guard let subject = context.subject[keyPath: path] else { let reason = description.map { "Tried to unwrap but found nil: \($0)" } ?? "Tried to unwrap an optional and found nil" return [VError(reason: reason, at: context.codingPath)] } return validations.flatMap { $0.apply(to: subject, at: context.codingPath, in: context.document) } } } /// Apply many validations to the same context (`lift` with `\.self`). func all(_ validations: VRule...) -> (VContext) -> [VError] { { context in validations.flatMap { $0.apply(to: context.subject, at: context.codingPath, in: context.document) } } } /// Resolve a named value from the document, error if missing, else validate the resolved value. func lookup( _ path: KeyPath, name: KeyPath, missing: @escaping (String) -> String, into validations: VRule..., ) -> (VContext) -> [VError] { { context in let key = context.subject[keyPath: name] guard let resolved = context.document[keyPath: path][key] else { return [VError(reason: missing(key), at: context.codingPath)] } return validations.flatMap { $0.apply(to: resolved, at: context.codingPath, in: context.document) } } } /// Resolve an optional named reference from the document: a nil reference is /// not an error, a named-but-unresolved reference errors, else validate the /// resolved value. func unwrapAndLookup( _ path: KeyPath, name: KeyPath, missing: @escaping (String) -> String, into validations: VRule..., ) -> (VContext) -> [VError] { { context in guard let key = context.subject[keyPath: name] else { return [] } guard let resolved = context.document[keyPath: path][key] else { return [VError(reason: missing(key), at: context.codingPath)] } return validations.flatMap { $0.apply(to: resolved, at: context.codingPath, in: context.document) } } }