Skip to content

Commit b8f872e

Browse files
committed
feat: add data validation DSL
1 parent fca0ffb commit b8f872e

22 files changed

+1240
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
public extension ValidatorResults {
2+
/// `ValidatorResult` representing
3+
/// validation has failed.
4+
struct Invalid {
5+
public let reason: String
6+
}
7+
}
8+
9+
extension ValidatorResults.Invalid: ValidatorResult {
10+
public var isFailure: Bool {
11+
true
12+
}
13+
14+
public var successDescriptions: [String] {
15+
[]
16+
}
17+
18+
public var failureDescriptions: [String] {
19+
[
20+
"is invalid: \(self.reason)"
21+
]
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
public extension ValidatorResults {
2+
/// `ValidatorResult` representing
3+
/// a group of `ValidatorResult`s.
4+
struct Nested {
5+
public let results: [ValidatorResult]
6+
}
7+
}
8+
9+
extension ValidatorResults.Nested: ValidatorResult {
10+
public var isFailure: Bool {
11+
self.results.first { $0.isFailure } != nil
12+
}
13+
14+
public var successDescriptions: [String] {
15+
self.results.lazy.filter { !$0.isFailure }
16+
.flatMap { $0.successDescriptions }
17+
}
18+
19+
public var failureDescriptions: [String] {
20+
self.results.lazy.filter { $0.isFailure }
21+
.flatMap { $0.failureDescriptions }
22+
}
23+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
public extension ValidatorResults {
2+
/// `ValidatorResult` representing
3+
/// validation is skipped by validator.
4+
struct Skipped {}
5+
}
6+
7+
extension ValidatorResults.Skipped: ValidatorResult {
8+
public var isFailure: Bool {
9+
false
10+
}
11+
12+
public var successDescriptions: [String] {
13+
[]
14+
}
15+
16+
public var failureDescriptions: [String] {
17+
[]
18+
}
19+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/// A name space type containing all default
2+
/// ``ValidatorResult`` provided by this library.
3+
public struct ValidatorResults {}
4+
5+
/// A type representing result of validations
6+
/// performed by ``Validator``.
7+
public protocol ValidatorResult {
8+
/// Whether validation succedded or failed.
9+
var isFailure: Bool { get }
10+
/// Descriptions to use in the event of validation succeeds.
11+
var successDescriptions: [String] { get }
12+
/// Descriptions to use in the event of validation fails.
13+
var failureDescriptions: [String] { get }
14+
}
15+
16+
/// An error type representing validation failure.
17+
public struct ValidationError: Error, CustomStringConvertible {
18+
/// The actual result of validation.
19+
public let result: ValidatorResult
20+
21+
/// A textual representation of this error.
22+
public var description: String {
23+
return result.failureDescriptions.joined(separator: "\n")
24+
}
25+
}
26+
27+
internal extension ValidatorResult {
28+
/// The result success and failure descriptions combined.
29+
var resultDescription: String {
30+
var desc: [String] = []
31+
desc.append("→ Successes")
32+
desc += self.failureDescriptions.indented()
33+
desc.append("→ Failures")
34+
desc += self.failureDescriptions.indented()
35+
return desc.joined(separator: "\n")
36+
}
37+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/// A type capable of being validated.
2+
///
3+
/// While confirming a ``Validator`` needs to be provided for perorming
4+
/// validation and conformance adds a throwing ``validate()`` method.
5+
///
6+
/// ```swift
7+
/// struct User: Validatable {
8+
/// let name: String
9+
/// let email: String
10+
/// let age: Int
11+
///
12+
/// var validator: Validator<Self> {
13+
/// return Validator<Self>
14+
/// .name(!.isEmpty, .alphanumeric)
15+
/// .email(.isEmail)
16+
/// }
17+
/// }
18+
/// ```
19+
public protocol Validatable {
20+
/// The ``Validator`` used to validate data.
21+
var validator: Validator<Self> { get }
22+
}
23+
24+
public extension Validatable {
25+
/// Performs validation on current data
26+
/// using provided ``validator``.
27+
///
28+
/// - Throws: ``ValidationError`` if validation fails.
29+
func validate() throws {
30+
let result = self.validatorResult()
31+
guard result.isFailure else { return }
32+
throw ValidationError(result: result)
33+
}
34+
35+
/// Performs validation on current data and provides result.
36+
///
37+
/// - Returns: The result of validation.
38+
internal func validatorResult() -> ValidatorResult {
39+
return self.validator.validate(self)
40+
}
41+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/// Adds ``Validator``s for property type `U` of parent type `T`.
2+
///
3+
/// Use the `@dynamicCallable` feature to provide ``Validator``s
4+
/// to add for the already provided property that the validation was created with.
5+
@dynamicCallable
6+
public struct Validation<T, U> {
7+
/// The `KeyPath` to property for which current validation added.
8+
internal let keyPath: KeyPath<T, U>
9+
/// The validations store for all the property based
10+
/// validations of parent type.
11+
internal var parent: Validations<T>
12+
13+
/// Creates a new validation for the provided propety.
14+
///
15+
/// - Parameters:
16+
/// - keyPath: The `KeyPath` for the property.
17+
/// - parent: The store for all the property based validations of parent type.
18+
///
19+
/// - Returns: The newly created validation.
20+
internal init(keyPath: KeyPath<T, U>, parent: Validations<T>) {
21+
self.keyPath = keyPath
22+
self.parent = parent
23+
}
24+
25+
/// Adds validators for a property of parent type `T`.
26+
///
27+
/// When validation is performed on parent type data,
28+
/// properties will be validated with the provided validators.
29+
///
30+
/// - Parameter args: The validators to add.
31+
/// - Returns: The ``Validations`` object that stores
32+
/// all property validation of parent type `T`.
33+
public func dynamicallyCall(
34+
withArguments args: [Validator<U>]
35+
) -> Validations<T> {
36+
for arg in args { parent.addValidator(at: keyPath, arg) }
37+
return parent
38+
}
39+
40+
/// Adds validators for a property of parent type `T`.
41+
///
42+
/// When validation is performed on parent type data,
43+
/// properties will be validated with the provided validators.
44+
///
45+
/// - Parameter args: The validators to add.
46+
/// - Returns: The ``Validator`` of parent type `T`
47+
/// containing all the provided validations.
48+
public func dynamicallyCall(
49+
withArguments args: [Validator<U>]
50+
) -> Validator<T> {
51+
for arg in args { parent.addValidator(at: keyPath, arg) }
52+
return parent.validator
53+
}
54+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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

Comments
 (0)