Skip to content

Commit

Permalink
feat: add data validation DSL
Browse files Browse the repository at this point in the history
  • Loading branch information
soumyamahunt committed Nov 6, 2022
1 parent fca0ffb commit b8f872e
Show file tree
Hide file tree
Showing 22 changed files with 1,240 additions and 0 deletions.
23 changes: 23 additions & 0 deletions Sources/ValidatableKit/Results/Invalid.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
public extension ValidatorResults {
/// `ValidatorResult` representing
/// validation has failed.
struct Invalid {
public let reason: String
}
}

extension ValidatorResults.Invalid: ValidatorResult {
public var isFailure: Bool {
true
}

public var successDescriptions: [String] {
[]
}

public var failureDescriptions: [String] {
[
"is invalid: \(self.reason)"
]
}
}
23 changes: 23 additions & 0 deletions Sources/ValidatableKit/Results/Nested.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
public extension ValidatorResults {
/// `ValidatorResult` representing
/// a group of `ValidatorResult`s.
struct Nested {
public let results: [ValidatorResult]
}
}

extension ValidatorResults.Nested: ValidatorResult {
public var isFailure: Bool {
self.results.first { $0.isFailure } != nil
}

public var successDescriptions: [String] {
self.results.lazy.filter { !$0.isFailure }
.flatMap { $0.successDescriptions }
}

public var failureDescriptions: [String] {
self.results.lazy.filter { $0.isFailure }
.flatMap { $0.failureDescriptions }
}
}
19 changes: 19 additions & 0 deletions Sources/ValidatableKit/Results/Skipped.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
public extension ValidatorResults {
/// `ValidatorResult` representing
/// validation is skipped by validator.
struct Skipped {}
}

extension ValidatorResults.Skipped: ValidatorResult {
public var isFailure: Bool {
false
}

public var successDescriptions: [String] {
[]
}

public var failureDescriptions: [String] {
[]
}
}
37 changes: 37 additions & 0 deletions Sources/ValidatableKit/Results/ValidatorResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/// A name space type containing all default
/// ``ValidatorResult`` provided by this library.
public struct ValidatorResults {}

/// A type representing result of validations
/// performed by ``Validator``.
public protocol ValidatorResult {
/// Whether validation succedded or failed.
var isFailure: Bool { get }
/// Descriptions to use in the event of validation succeeds.
var successDescriptions: [String] { get }
/// Descriptions to use in the event of validation fails.
var failureDescriptions: [String] { get }
}

/// An error type representing validation failure.
public struct ValidationError: Error, CustomStringConvertible {
/// The actual result of validation.
public let result: ValidatorResult

/// A textual representation of this error.
public var description: String {
return result.failureDescriptions.joined(separator: "\n")
}
}

internal extension ValidatorResult {
/// The result success and failure descriptions combined.
var resultDescription: String {
var desc: [String] = []
desc.append("→ Successes")
desc += self.failureDescriptions.indented()
desc.append("→ Failures")
desc += self.failureDescriptions.indented()
return desc.joined(separator: "\n")
}
}
41 changes: 41 additions & 0 deletions Sources/ValidatableKit/Validatable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/// A type capable of being validated.
///
/// While confirming a ``Validator`` needs to be provided for perorming
/// validation and conformance adds a throwing ``validate()`` method.
///
/// ```swift
/// struct User: Validatable {
/// let name: String
/// let email: String
/// let age: Int
///
/// var validator: Validator<Self> {
/// return Validator<Self>
/// .name(!.isEmpty, .alphanumeric)
/// .email(.isEmail)
/// }
/// }
/// ```
public protocol Validatable {
/// The ``Validator`` used to validate data.
var validator: Validator<Self> { get }
}

public extension Validatable {
/// Performs validation on current data
/// using provided ``validator``.
///
/// - Throws: ``ValidationError`` if validation fails.
func validate() throws {
let result = self.validatorResult()
guard result.isFailure else { return }
throw ValidationError(result: result)
}

/// Performs validation on current data and provides result.
///
/// - Returns: The result of validation.
internal func validatorResult() -> ValidatorResult {
return self.validator.validate(self)
}
}
54 changes: 54 additions & 0 deletions Sources/ValidatableKit/Validation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/// Adds ``Validator``s for property type `U` of parent type `T`.
///
/// Use the `@dynamicCallable` feature to provide ``Validator``s
/// to add for the already provided property that the validation was created with.
@dynamicCallable
public struct Validation<T, U> {
/// The `KeyPath` to property for which current validation added.
internal let keyPath: KeyPath<T, U>
/// The validations store for all the property based
/// validations of parent type.
internal var parent: Validations<T>

/// Creates a new validation for the provided propety.
///
/// - Parameters:
/// - keyPath: The `KeyPath` for the property.
/// - parent: The store for all the property based validations of parent type.
///
/// - Returns: The newly created validation.
internal init(keyPath: KeyPath<T, U>, parent: Validations<T>) {
self.keyPath = keyPath
self.parent = parent
}

/// Adds validators for a property of parent type `T`.
///
/// When validation is performed on parent type data,
/// properties will be validated with the provided validators.
///
/// - Parameter args: The validators to add.
/// - Returns: The ``Validations`` object that stores
/// all property validation of parent type `T`.
public func dynamicallyCall(
withArguments args: [Validator<U>]
) -> Validations<T> {
for arg in args { parent.addValidator(at: keyPath, arg) }
return parent
}

/// Adds validators for a property of parent type `T`.
///
/// When validation is performed on parent type data,
/// properties will be validated with the provided validators.
///
/// - Parameter args: The validators to add.
/// - Returns: The ``Validator`` of parent type `T`
/// containing all the provided validations.
public func dynamicallyCall(
withArguments args: [Validator<U>]
) -> Validator<T> {
for arg in args { parent.addValidator(at: keyPath, arg) }
return parent.validator
}
}
129 changes: 129 additions & 0 deletions Sources/ValidatableKit/Validations.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/// Stores property validations of data type `T` specialized with.
///
/// Use the `@dynamicMemberLookup`
/// feature to add validations based on property.
@dynamicMemberLookup
public class Validations<T> {
/// `ValidatorResult` of a validator that validates
/// all the properties and groups them with their `KeyPath`.
public struct Property: ValidatorResult {
/// The result of property validations associated with property `KeyPath`.
public internal(set) var results: [PartialKeyPath<T>: ValidatorResult] =
[:]

public var isFailure: Bool {
return self.results.first(where: \.value.isFailure) != nil
}

public var successDescriptions: [String] {
var desc: [String] = []
for (keyPath, result) in self.results where !result.isFailure {
desc.append("\(keyPath)")
desc += result.successDescriptions.indented()
}
return desc
}

public var failureDescriptions: [String] {
var desc: [String] = []
for (keyPath, result) in self.results where result.isFailure {
desc.append("\(keyPath)")
desc += result.failureDescriptions.indented()
}
return desc
}
}

/// Stores all property based validations associated with the property `KeyPath`.
private var storage: [PartialKeyPath<T>: [(T) -> ValidatorResult]] = [:]

/// The parent type `T` data validator
/// with all the property based validations.
internal var validator: Validator<T> {
return .init { self.validate($0) }
}

/// Stores provided property validator and `KeyPath`.
///
/// - Parameters:
/// - keyPath: The `KeyPath` for the property.
/// - validator: The validator to add.
@usableFromInline
internal func addValidator<U>(
at keyPath: KeyPath<T, U>,
_ validator: Validator<U>
) {
let validation: (T) -> ValidatorResult = {
let data = $0[keyPath: keyPath]
return validator.validate(data)
}

if storage[keyPath] == nil {
storage[keyPath] = [validation]
} else {
storage[keyPath]!.append(validation)
}
}

/// Stores the validator associated with provided property `KeyPath`.
///
/// - Parameter keyPath: The `KeyPath` for the property.
@inlinable
internal func addValidator<U: Validatable>(at keyPath: KeyPath<T, U>) {
addValidator(at: keyPath, .init { $0.validator.validate($0) })
}

/// Validates properties of data of type `T` with stored validators
/// and returns the result.
///
/// - Parameter data: The data to validate.
/// - Returns: The result of validation.
internal func validate(_ data: T) -> ValidatorResult {
guard !storage.isEmpty else { return ValidatorResults.Skipped() }
var result = Property()
for (keyPath, validations) in storage where !validations.isEmpty {
let results = validations.map { $0(data) }
result.results[keyPath] = ValidatorResults.Nested(results: results)
}
return result
}

/// Exposes property of the specialized data type `T` to add validation on.
///
/// Provide validator(s) to the returned ``Validation``
/// to validate that property with the validators.
///
/// - Parameter keyPath: The `KeyPath` for the property.
/// - Returns: ``Validation`` for the property.
public subscript<U>(
dynamicMember keyPath: KeyPath<T, U>
) -> Validation<T, U> {
let validation = Validation<T, U>(keyPath: keyPath, parent: self)
return validation
}

/// Exposes property of the specialized data type `T` to add validation on.
///
/// Adds the provided property ``Validatable/validator``
/// along with any provided validator(s) to the returned
/// ``Validation`` to validate that property.
///
/// - Parameter keyPath: The `KeyPath` for the property.
/// - Returns: ``Validation`` for the property.
public subscript<U: Validatable>(
dynamicMember keyPath: KeyPath<T, U>
) -> Validation<T, U> {
addValidator(at: keyPath)
let validation = Validation<T, U>(keyPath: keyPath, parent: self)
return validation
}
}

internal extension Array where Element == String {
/// Indents provided array of `String`.
///
/// - Returns: The indented `String`s.
func indented() -> [String] {
return self.map { " " + $0 }
}
}
Loading

0 comments on commit b8f872e

Please sign in to comment.