TestsTested | ✓ |
LangLanguage | SwiftSwift |
License | MIT |
ReleasedLast Release | Sep 2015 |
SPMSupports SPM | ✗ |
Maintained by Gwendal Roué.
GRValidation is a validation toolkit for Swift 2.
It lets you validate both simple values and complex models, and won’t let you down when your validations leave the trivial zone.
August 25, 2015: GRValidation 0.2.0 is out - Release notes. Follow @groue on Twitter for release announcements and usage tips.
Missing features so far:
GRValidation distinguishes Value Validation from Model Validation.
Precisely speaking, Value Validation throws errors like “12 should be greater than 18”, and is responsible for:
Model validation, on the other side, throws errors like “age should be greater than 18”. It is built on top of value validation and provides:
ValidationType is a protocol that checks a value of type TestedType, and eventually returns a value of type ValidType, or throws a ValidationError:
public protocol ValidationType {
typealias TestedType
typealias ValidType
func validate(value: TestedType) throws -> ValidType
}
For example:
// Positive integer
let v = ValidationRange(minimum: 0)
try v.validate(1) // OK: 1
try v.validate(nil) // ValidationError: nil should be greater than or equal to 0.
try v.validate(-1) // ValidationError: -1 should be greater than or equal to 0.
The returned value may be different from the input:
enum Color : Int {
case Red
case White
case Rose
}
let v = ValidationRawValue<Color>()
try v.validate(0) // OK: Color.Red
try v.validate(3) // ValidationError: 3 is an invalid Color.
The validate() method may throw a ValidationError:
let positiveInt = ValidationRange(minimum: 0)
try positiveInt.validate(-1) // Throws a ValidationError
You may also perform a simple boolean check with the ~=
operator, or via pattern matching:
positiveInt ~= 10 // true
positiveInt ~= -1 // false
switch int {
case positiveInt:
// int passes validation
...
}
Validation type | TestedType | ValidType | |
---|---|---|---|
Validation | T | T | All values pass. |
ValidationFailure | T | T | All values fail. |
ValidationNil | T? | T? | Checks that input is nil. |
ValidationNotNil | T? | T | Checks that input is not nil. |
ValidationTrim | String? | String? | All strings pass. Non nil strings are trimmed. |
ValidationStringLength | String? | String | Checks that a string is not nil and has length in a specific range. |
ValidationRegularExpression | String? | String | Checks that a string is not nil and matches a regular expression. |
ValidationCollectionNotEmpty | CollectionType? | CollectionType | Checks that a collection is not nil and not empty. |
ValidationCollectionEmpty | CollectionType? | CollectionType? | Checks that a collection is nil or empty. |
ValidationEqual | T? where T:Equatable | T | Checks that a value is not nil and equal to a reference value. |
ValidationNotEqual | T? where T:Equatable | T? | Checks that a value is nil or not equal to a reference value. |
ValidationElementOf | T? where T:Equatable | T | Checks that a value is not nil and member of a reference collection. |
ValidationNotElementOf | T? where T:Equatable | T? | Checks that a value is nil or not member of a reference collection. |
ValidationRawValue | T.RawValue? where T: RawRepresentable | T | Checks that a value is not nil and a valid raw value for type T. |
ValidationRange | T? where T: ForwardIndexType, T: Comparable | T | Checks that a value is not nil and in a specific range. |
Basic Value Validations can be chained, or composed using boolean operators:
v1 >>> v2
Chains two validations. Returns the value returned by v2.
// Checks that a string matches a regular expression, after trimming:
let v = ValidationTrim() >>> ValidationRegularExpression(pattern: "^[0-9]+$")
try v.validate(" 123 ") // "123"
try v.validate("foo") // ValidationError
v1 || v2
Checks that a value passes at least one validation. Returns the value returned by the first validation that passes, or the input value when output types don’t match.
// Checks that an Int is not nil and equal to 1 or 2:
let v = ValidationEqual(1) || ValidationEqual(2)
try v.validate(1) // 1
try v.validate(2) // 2
try v.validate(3) // ValidationError
v1 && v2
Checks that a value passes both validations. Returns the value returned by v2.
// Checks that an Int is nil, or not 1, and not 2:
let v = ValidationNotEqual(1) && ValidationNotEqual(2)
try v.validate(1) // ValidationError
try v.validate(2) // ValidationError
try v.validate(3) // 3
!v1
Inverts a validation. Returns the input value, or throws a generic “is invalid.” error.
// Checks that an Int is not 1.
let v = !ValidationEqual(1)
try v.validate(1) // ValidationError
try v.validate(2) // 2
The Validable protocol provides methods that help validating models.
Let’s start with a simple model:
struct Person: Validable {
var name: String?
func validate() throws {
// Name should not be nil or empty.
try validate(property: "name", with: name >>> ValidationStringLength(minimum: 1))
}
}
let person = Person(name: "Arthur")
try person.validate() // OK
let person = Person(name: nil)
try person.validate()
// Invalid Person(name: nil): name should not be empty.
Feel at your ease, and don’t hesitate building more complex validations:
struct Person : Validable {
var name: String?
var age: Int?
var email: String?
var phoneNumber: String?
mutating func validate() throws {
// ValidationPlan doesn't fail on the first validation error. Instead,
// it gathers all of them, and eventually throws a single ValidationError.
try ValidationPlan()
.append {
// Name should not be empty after whitespace trimming:
let nameValidation = ValidationTrim() >>> ValidationStringLength(minimum: 1)
name = try validate(
property: "name",
with: name >>> nameValidation)
}
.append {
// Age should be nil, or positive:
let ageValidation = ValidationNil() || ValidationRange(minimum: 0)
try validate(
property: "age",
with: age >>> ageValidation)
}
.append {
// Email should be nil, or contain @ after whitespace trimming:
let emailValidation = ValidationNil() || (ValidationTrim() >>> ValidationRegularExpression(pattern:"@"))
email = try validate(
property: "email",
with: email >>> emailValidation)
}
.append {
// Phone number should be nil, or be a valid phone number.
// ValidationPhoneNumber applies international formatting.
let phoneNumberValidation = ValidationNil() || (ValidationTrim() >>> ValidationPhoneNumber(format: .International))
phoneNumber = try validate(
property: "phoneNumber",
with: phoneNumber >>> phoneNumberValidation)
}
.append {
// An email or a phone number is required.
try validate(
properties: ["email", "phoneNumber"],
message: "Please provide an email or a phone number.",
with: email >>> ValidationNotNil() || phoneNumber >>> ValidationNotNil())
}
.validate()
}
}
var person = Person(name: " Arthur ", age: 35, email: nil, phoneNumber: "0123456789 ")
try person.validate() // OK
person.name // "Arthur" (trimmed)
person.phoneNumber // "+33 1 23 45 67 89" (trimmed & formatted)
var person = Person(name: nil, age: nil, email: "[email protected]", phoneNumber: nil)
try person.validate()
// Invalid Person: name should not be empty.
var person = Person(name: "Arthur", age: -1, email: "[email protected]", phoneNumber: nil)
try person.validate()
// Invalid Person: age should be greater than or equal to 0.
var person = Person(name: "Arthur", age: 35, email: nil, phoneNumber: nil)
try person.validate()
// Invalid Person: Please provide an email or a phone number.
var person = Person(name: "Arthur", age: 35, email: "foo", phoneNumber: nil)
try person.validate()
// Invalid Person: email is invalid.