A flexible state validation solution.
- Function builder composition API
- Easy composition of small validators into more complex ones.
- Easily extensible
- Documented here
Create simple validators from predicates
let validateEmail = Verify<String>.that({ $0.contains("@") }, otherwise: .myError)
You can easily create validators on any type via extensions:
extension Verify where Subject == Int {
public static func greaterThanZero(otherwise error: Error) -> Validator_<Subject> {
Verify<Int>.that({ $0 > 0}, otherwise: error)
}
}
extension Verify where Subject == String {
public static func minLength(_ value: Int, otherwise error: Error) -> Validator_<Subject> {
Verify.that({ (string: String) in string.count >= value }, otherwise: error)
}
}
Having created these extensions they will become avaiable like this:
Verify<String>.minLength(10, otherwise: .myError)
Verify<Int>.greaterThanZero(otherwise: .myOtherError)
Verify has two flavors of composition, a senquenced or in order composition, or a parallel composition.
Sequenced composition
In sequence composition only one a validator is ran at a time and will accumulate at most one error since the next validator in the chain will only be applied when the previous succeeds.
let emailValidator = Verify<String>.inOrder {
Verify.that({ $0.contains("@")}, otherwise: invalidEmail)
Verify.minLength(5, otherwise: invalidEmail)
}
let input = "1"
emailValidator.errors(input).count == 1
Notice that even the input "1" fails both the validations only one error will be accumulated. This is usually the desired behavour since we want to validate one condition at a time.
Also can be written as:
let emailValidator = Verify<String>
.that({ $0.contains("@")}, otherwise: invalidEmail)
.andThat({ $0.count >= 5}, otherwise: invalidEmail)
let input = "1"
emailValidator.errors(input).count == 1
Parallel composition
In parallel composition we run all validators at once and accumulate all errors.
let emailValidator = Verify<String>.atOnce {
Verify.that({ $0.contains("@")}, otherwise: invalidEmail)
Verify.minLength(5, otherwise: invalidEmail)
}
let input = "1"
emailValidator.errors(input).count == 2
The previous example will acumulate both errors.
Factories
Method | Signature | Description |
---|---|---|
Verify.that | (Predicate<S>) -> Validator<S> |
Validates with predicate |
Verify.at | (KeyPath<S, P>, Predicate<P>) -> Validator<S> |
Validates the property focused by keypath |
Verify.error | (Error) -> Validator<S> |
Always fails with specified error |
Composition
Method | Signature | Accumulates errors |
---|---|---|
andThen | (Validator<S>) -> Validator<S> |
No |
andThat / thenCheck | (Predicate) -> Validator |
No |
add | (Validator<S>) -> Validator<S> |
Yes |
addCheck | (Predicate<S>) -> Validator<S> |
Yes |
Utilities
Method | Signature | Description |
---|---|---|
ignore | (Predicate<S>) -> Validator<S> |
Bypass validator when the provided predicate holds true |
Given a model, for instance a UserRegistration struct:
struct UserRegistration {
let email: String
let password: String
let passwordConfirmation: String
}
we can apply validation to specific properties with keypaths.
let invalidEmail = UserRegistrationError.invalidEmail
let invalidPassword = UserRegistrationError.invalidPassword
let emailValidator = Verify<String>.inOrder {
Verify.minLength(5, otherwise: invalidEmail)
Verify.that({ $0.contains("@")}, otherwise: invalidEmail)
}
let password = Verify<String>.inOrder {
Verify<String>.that({ $0.count > 5}, otherwise: invalidPassword)
Verify.containsSomeOf(CharacterSet.symbols, otherwise: invalidPassword)
}
let registrationValidator = Verify<UserRegistration>.atOnce {
Verify<UserRegistration>.at(\.email, validator: emailValidator)
Verify<UserRegistration>.at(\.password, validator: password)
Verify<UserRegistration>.that({ $0.password == $0.passwordConfirmation }, otherwise: UserRegistrationError.passwordsDontMatch)
}
let errors = registrationValidator.errors(UserRegistration(email: "", password: "19d", passwordConfirmation: "12d"))
Running a validator is a simple as passing in a parameter since its just a function.
To be a bit more eloquent a verify
method is provided, this method is special because besides
forwarding the argument to the calling validator it can also be used to filter the error list and
have it cast to a specific error type. Just supply a specific type parameter.
Often times you will have modeled your error type similar to:
struct FormError<FieldType>: Error {
enum Reason {
case invalidFormat, required
}
let reason: Reason
let field: FieldType
}
enum LoginField {
case email, password
}
In these scenarios its convenient to be able to group errors by field.
typealias LoginFormError = FormError<LoginField>
let validator = Verify<Int>.atOnce {
Verify<Int>.error(LoginFormError(reason: .invalidFormat, field: .email))
Verify<Int>.error(LoginFormError(reason: .required, field: .password))
}
let groupedErrors: [LoginField: [LoginFormError]] = validator.groupedErrors(0, by: { (error: LoginFormError) in error.field })
// Or even
let fieldErrors: [LoginField: [LoginFormError.Reason]] = groupedErrors.mapValues({ $0.map({ $0.reason })})