|
| 1 | +/// Stores property validations of data type `T` specialized with. |
| 2 | +/// |
| 3 | +/// Use the `@dynamicMemberLookup` |
| 4 | +/// feature to add validations based on property. |
| 5 | +@dynamicMemberLookup |
| 6 | +public class Validations<T> { |
| 7 | + /// `ValidatorResult` of a validator that validates |
| 8 | + /// all the properties and groups them with their `KeyPath`. |
| 9 | + public struct Property: ValidatorResult { |
| 10 | + /// The result of property validations associated with property `KeyPath`. |
| 11 | + public internal(set) var results: [PartialKeyPath<T>: ValidatorResult] = |
| 12 | + [:] |
| 13 | + |
| 14 | + public var isFailure: Bool { |
| 15 | + return self.results.first(where: \.value.isFailure) != nil |
| 16 | + } |
| 17 | + |
| 18 | + public var successDescriptions: [String] { |
| 19 | + var desc: [String] = [] |
| 20 | + for (keyPath, result) in self.results where !result.isFailure { |
| 21 | + desc.append("→ \(keyPath)") |
| 22 | + desc += result.successDescriptions.indented() |
| 23 | + } |
| 24 | + return desc |
| 25 | + } |
| 26 | + |
| 27 | + public var failureDescriptions: [String] { |
| 28 | + var desc: [String] = [] |
| 29 | + for (keyPath, result) in self.results where result.isFailure { |
| 30 | + desc.append("→ \(keyPath)") |
| 31 | + desc += result.failureDescriptions.indented() |
| 32 | + } |
| 33 | + return desc |
| 34 | + } |
| 35 | + } |
| 36 | + |
| 37 | + /// Stores all property based validations associated with the property `KeyPath`. |
| 38 | + private var storage: [PartialKeyPath<T>: [(T) -> ValidatorResult]] = [:] |
| 39 | + |
| 40 | + /// The parent type `T` data validator |
| 41 | + /// with all the property based validations. |
| 42 | + internal var validator: Validator<T> { |
| 43 | + return .init { self.validate($0) } |
| 44 | + } |
| 45 | + |
| 46 | + /// Stores provided property validator and `KeyPath`. |
| 47 | + /// |
| 48 | + /// - Parameters: |
| 49 | + /// - keyPath: The `KeyPath` for the property. |
| 50 | + /// - validator: The validator to add. |
| 51 | + @usableFromInline |
| 52 | + internal func addValidator<U>( |
| 53 | + at keyPath: KeyPath<T, U>, |
| 54 | + _ validator: Validator<U> |
| 55 | + ) { |
| 56 | + let validation: (T) -> ValidatorResult = { |
| 57 | + let data = $0[keyPath: keyPath] |
| 58 | + return validator.validate(data) |
| 59 | + } |
| 60 | + |
| 61 | + if storage[keyPath] == nil { |
| 62 | + storage[keyPath] = [validation] |
| 63 | + } else { |
| 64 | + storage[keyPath]!.append(validation) |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + /// Stores the validator associated with provided property `KeyPath`. |
| 69 | + /// |
| 70 | + /// - Parameter keyPath: The `KeyPath` for the property. |
| 71 | + @inlinable |
| 72 | + internal func addValidator<U: Validatable>(at keyPath: KeyPath<T, U>) { |
| 73 | + addValidator(at: keyPath, .init { $0.validator.validate($0) }) |
| 74 | + } |
| 75 | + |
| 76 | + /// Validates properties of data of type `T` with stored validators |
| 77 | + /// and returns the result. |
| 78 | + /// |
| 79 | + /// - Parameter data: The data to validate. |
| 80 | + /// - Returns: The result of validation. |
| 81 | + internal func validate(_ data: T) -> ValidatorResult { |
| 82 | + guard !storage.isEmpty else { return ValidatorResults.Skipped() } |
| 83 | + var result = Property() |
| 84 | + for (keyPath, validations) in storage where !validations.isEmpty { |
| 85 | + let results = validations.map { $0(data) } |
| 86 | + result.results[keyPath] = ValidatorResults.Nested(results: results) |
| 87 | + } |
| 88 | + return result |
| 89 | + } |
| 90 | + |
| 91 | + /// Exposes property of the specialized data type `T` to add validation on. |
| 92 | + /// |
| 93 | + /// Provide validator(s) to the returned ``Validation`` |
| 94 | + /// to validate that property with the validators. |
| 95 | + /// |
| 96 | + /// - Parameter keyPath: The `KeyPath` for the property. |
| 97 | + /// - Returns: ``Validation`` for the property. |
| 98 | + public subscript<U>( |
| 99 | + dynamicMember keyPath: KeyPath<T, U> |
| 100 | + ) -> Validation<T, U> { |
| 101 | + let validation = Validation<T, U>(keyPath: keyPath, parent: self) |
| 102 | + return validation |
| 103 | + } |
| 104 | + |
| 105 | + /// Exposes property of the specialized data type `T` to add validation on. |
| 106 | + /// |
| 107 | + /// Adds the provided property ``Validatable/validator`` |
| 108 | + /// along with any provided validator(s) to the returned |
| 109 | + /// ``Validation`` to validate that property. |
| 110 | + /// |
| 111 | + /// - Parameter keyPath: The `KeyPath` for the property. |
| 112 | + /// - Returns: ``Validation`` for the property. |
| 113 | + public subscript<U: Validatable>( |
| 114 | + dynamicMember keyPath: KeyPath<T, U> |
| 115 | + ) -> Validation<T, U> { |
| 116 | + addValidator(at: keyPath) |
| 117 | + let validation = Validation<T, U>(keyPath: keyPath, parent: self) |
| 118 | + return validation |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +internal extension Array where Element == String { |
| 123 | + /// Indents provided array of `String`. |
| 124 | + /// |
| 125 | + /// - Returns: The indented `String`s. |
| 126 | + func indented() -> [String] { |
| 127 | + return self.map { " " + $0 } |
| 128 | + } |
| 129 | +} |
0 commit comments