A rule-based validation framework.
- rule-based
- easily extensible
- type-safe
- well tested
- combine support
- input formatting
- optional validation
The basic idea is to separate the validation logic from the rest of your project. Validated self-encapsulates all validation logic into reusable rules. All rules are type-safe and tested. Additionally it features input formatting. Fields can also be marked as optional.
The project includes an Example project.
Using Validated
is very simple and straight-forward.
@Validated(rules: [.notEmpty, .isEmail], formatters: [.trimmed, .lowercased])
var email: String?
// validation
let validation = ValidationResult.validate("[email protected]", with: [.notEmpty, .isEmail])
XCTAssertTrue(validation.isValid) // true
// formatting
let newValue = ValidationResult.format(" abc ", with: [.trimmed, .uppercased])
XCTAssertEqual(newValue, "ABC") // true
The ValidationResult
is an Enum
that has two states:
case valid(_ value: Value?)
case notValid
.valid
has an associated value, that will return the validated value, if there is one. This is optional, because the strategy can be .optional
.
The ValidationResult
also has two computed properties: isValid
which returns a Bool
and value
which returns the optional value.
Additionally, ValidationResult
houses the .validate(..)
and .format(..)
functions to manually invoke a validation and formatting of an input value.
Note:
ValidationResult
also conforms to theEquatable
protocol.
A rule is defined with a closure, that receives a Value
and returns a Bool
. The Value
is not optional.
Right now there are 42 rules included in the standard package.
extension ValidationRule where Value: StringProtocol {
static var notEmpty: Self {
return ValidationRule {
return !$0.isEmpty
}
}
}
Note: return keywords can be ommitted, to make a rule even more lightweight.
A formatter is similar to a rule. It is defined as a closure, that receives a Value
and returns a Value
. Neither are optional.
Right now there are 7 formatters included in the standard package.
extension ValidationFormatter where Value == String {
static var uppercased: Self {
return ValidationFormatter {
return $0.uppercased()
}
}
}
Note: return keywords can be ommitted, to make a formatter even more lightweight.
In some situations we only want to validate fields that actually contain something. To deal with this kind of situation Validated
has two modes, that are reflected in ValidationStrategy
.
The two modes are:
case required
case optional
If you are using a @Validated
object, you can simply pass the strategy to the property wrapper like this:
@Validated(rule: .isURL, formatters: [.trimmed, .lowercased], strategy: .optional)
var website: String?
Note: By default,
@Validated
uses the.required
strategy.
The main usage of this framework is bundled within the @Validated
property wrapper. It takes care of all the validating, formatting and contains the strategy logic. Additionally it contains optional Combine support.
Defining it, is very straight-forward:
@Validated(rules: [.notEmpty, .isEmail], formatters: [.trimmed, .lowercased])
var email: String?
Note: Formatting will be done, before the rules are checked. Therefore, it will validate the formatted values.
@Validated
can be initialized in many ways:
- one
ValidationRule
or an Array ofValidationRule
s - zero, one or an Array of
ValidationFormatter
s - a
ValidationStrategy
. By default, if none provided, it uses the.required
strategy.
@Validated
also contains some Combine extensions. They will only take affect, if Combine
can be imported. Basically, there are two publishers:
validationPublisher
-> publishes aValidationResult
for every change to the value.valuePublisher
-> publishes a formatted value, whenever a.valid
ValidationResult
occurs.
Note: Even though Combine is supported, you can still use it without it. Validated is usable from iOS9+.
The projected value returns the validationPublisher()
. It publishes a ValidationResult
every time you make a change to the properties' value.
$email
.sink { [weak self] (result) in
switch result {
case .valid(let value):
self?.passwordValidLabel.text = "✅"
print("new password: valid -> \(value ?? "nil")")
case .notValid:
self?.passwordValidLabel.text = "❌"
print("new password: notValid")
}
}
.store(in: &cancellables)
Note: Make sure you connect your TextField to your
@Validated
object. This can be done by binding your TextField values to the property with Combine or using the NotificationCenter. Examples of how this can be done are included in the Example project.
Validated
was designed to make it easily extensible. Therefore making your own rules and formatters is encouraged. If you think that you created a rule or a formatter that would fit into the standard library, feel free to create a pull request.
Note: Every Rule and Formatter should be tested.
Here is an example of how you could make your own rule called isAwesome
.
extension ValidationRule where Value == String {
static var isAwesome: Self {
return ValidationRule {
return $0 == "Awesome"
}
}
}
This could also be written like this:
extension ValidationRule {
static var isAwesome<String>: Self {
ValidationRule { $0 == "Awesome" }
}
}
Note: You can also have static functions that take additional input to validate.
Here is an example of how you could make your own formatter called replacedAWithB
, that replaces every occurance of A
with a B
.
extension ValidationFormatter where Value == String {
static var replacedAWithB: Self {
return ValidationFormatter {
return $0.replacingOccurrences(of: "A", with: "B")
}
}
}
This could also be written like this:
extension ValidationFormatter {
static var replacedAWithB<String>: Self {
ValidationFormatter { $0.replacingOccurrences(of: "A", with: "B") }
}
}
There are certain naming conventions that should be followed to guarantee simple code and improve readability.
Every ValidationRule
should be named in the present tense and should be treated in the third person. They should also be as short as possible while also being as expressive as possible.
Examples:
notEmpty
contains
startsWith
isNumber
isEmail
Every ValidationFormatter
should be named in the simple past tense (e.g. trimmed
or uppercased
). The reason behind this is, that formatters are applied inside the @Validated
object, before it is returned when accessing the value. Therefore, the formatters have already been applied and the text is now trimmed
or uppercased
.
Add the following dependency to your Package.swift
file:
.package(url: "https://github.com/fgeistert/Validated.git", from: "1.0.2")
Add the following line to your Cartfile.
github "fgeistert/Validated" ~> 1.0.2
Then run carthage update
.
Just drag and drop the .swift
files from the Validated
folder into your project.
Validated is built with Swift 5.2. It supports iOS9+.
- Open an issue
- Fork it
- Create new branch
- Commit all your changes to your branch
- Create a pull request
You can check out my website or follow me on twitter.