Skip to content

Instantly share code, notes, and snippets.

@josefdolezal
Last active May 29, 2020 05:26
Show Gist options
  • Select an option

  • Save josefdolezal/9786b9ccac4df7b9c1504c5a36bcdbb7 to your computer and use it in GitHub Desktop.

Select an option

Save josefdolezal/9786b9ccac4df7b9c1504c5a36bcdbb7 to your computer and use it in GitHub Desktop.
[Swift] Functional Form Fields Validation
import Foundation
// MARK: Validation result
enum ValidationResult<Value>: CustomStringConvertible {
case success(Value)
case failure(Error)
var description: String {
switch self {
case let .success(value): return "\(value)"
case let .failure(error): return error.localizedDescription
}
}
}
// MARK: Result binding
precedencegroup LeftAssociative {
associativity: left
}
infix operator >>=: LeftAssociative
func validate<InputValue, OutputValue>(_ a: ValidationResult<InputValue>, _ f: (InputValue) -> ValidationResult<OutputValue>) -> ValidationResult<OutputValue> {
switch a {
case let .success(x): return f(x)
case let .failure(error): return .failure(error)
}
}
func >>=<InputValue, OutputValue>(_ a: ValidationResult<InputValue>, _ f: (InputValue) -> ValidationResult<OutputValue>) -> ValidationResult<OutputValue> {
return validate(a, f)
}
// MARK: Required field validator
struct RequiredFieldError: Error, LocalizedError {
let errorDescription: String? = "This field is required."
}
func required(_ value: String?) -> ValidationResult<String> {
if let value = value {
return .success(value)
}
return .failure(RequiredFieldError())
}
// MARK: Minimal field length validator
struct MinimalFieldLengthError: Error, LocalizedError {
let minimalLength: Int
var errorDescription: String? { return "This field must be at least \(minimalLength) characters long." }
}
func minLength(_ length: Int) -> (String) -> ValidationResult<String> {
return { x in
if x.characters.count >= length {
return .success(x)
}
return .failure(MinimalFieldLengthError(minimalLength: length))
}
}
// MARK: Format match field validator
struct PatternMatchError: Error, LocalizedError {
let pattern: String
var errorDescription: String? { return "This field must match following pattern: `\(pattern)`." }
}
func match(_ expression: NSRegularExpression) -> (String) -> ValidationResult<String> {
return { x in
if expression.matches(in: x, options: [], range: NSRange(location: 0, length: x.characters.count)).count > 0 {
return .success(x)
}
return .failure(PatternMatchError(pattern: expression.pattern))
}
}
// MARK: Enumeration field validator
struct TypeMatchError<T>: Error, LocalizedError {
let type: T.Type
var errorDescription: String? { return "This field must have value of type `\(type)`." }
}
func type<T: RawRepresentable>(_ enumType: T.Type) -> (T.RawValue) -> ValidationResult<T> {
return { x in
guard let value = enumType.init(rawValue: x) else {
return .failure(TypeMatchError(type: enumType))
}
return .success(value)
}
}
// MARK: String fixed length validator
struct StringExactLengthError: Error, LocalizedError {
let length: Int
var errorDescription: String? { return "The value must have exactly \(length) characters." }
}
func length(_ length: Int) -> (String) -> ValidationResult<String> {
return { x in
guard x.characters.count == length else {
return .failure(StringExactLengthError(length: length))
}
return .success(x)
}
}
// MARK: Field exact value validator
struct FieldExactValueError: Error, LocalizedError {
let errorDescription: String? = "The given value does not match the expected value."
}
func match<T: Equatable>(_ expected: T) -> (T) -> ValidationResult<T> {
return { x in
guard expected == x else {
return .failure(FieldExactValueError())
}
return .success(x)
}
}
// MARK: Confirmation field validator
struct FieldConfirmationValueError: Error, LocalizedError {
}
func match<T: Equatable>(_ other: ValidationResult<T>) -> (T) -> ValidationResult<T> {
fatalError("Not implemented yet.")
}
// Possible validators:
// - [Numbers]
// - Natural number: naturalNumber
// - Floating point number: floatingPointNumber / double / float
// - Whole number: wholeNumber / int
// - Upper bound: lessThan(x)
// - Lower bound: greateThan(x)
// - [Strings]
// - [x] Fixed length string: length(x)
// - [Generic]
// - [x] Exact value: match<T: Equatable>(x)
// - Match value of other field (e.q. password confirmation): match<T>(x), where x is ValidationResult / ??
// - Default value, fallback for failure of validation: default<T>(x), where x is ValidationResult
// - Force failure: fail(x), where x is Error, LocalizedError (possibli with ==x operator)
// --------
// MARK: Usage
// --------
enum Gender: String {
case man
case woman
case unknown
}
let allUppercased = try! match(NSRegularExpression(pattern: "^[A-Z]+$", options: []))
let validEmail = try! match(NSRegularExpression(pattern: "^\\S+@\\S+$", options: []))
let validGender = type(Gender.self)
print(required(nil)) // Fail
print(required("Value")) // Passes
print(minLength(1)("")) // Fails
print(minLength(1)("Value")) // Passes
print(allUppercased("Value")) // Fails
print(allUppercased("VALUE")) // Passes
print(validEmail("")) // Fails
print(validEmail("[email protected]")) // Passes
print(validGender("Man")) // Fails
print(validGender("man")) // Passes
// Inline validator composition example
print("")
print(required(nil) >>= minLength(5) >>= allUppercased) // Fails (required)
print(required("Value") >>= minLength(6) >>= allUppercased) // Fails (minLength)
print(required("Value") >>= minLength(5) >>= allUppercased) // Fails (allUppercased)
print(required("VALUE") >>= minLength(5) >>= allUppercased) // Passes
// Shared validator example
print("")
let passwordValidator: (String?) -> ValidationResult<String> = {
required($0) >>= minLength(5) >>= allUppercased
}
print(passwordValidator(nil)) // Fails
print(passwordValidator("VALUE")) // Passes
// MARK: Usage with UIKit
//import UIKit
//let passwordField = UITextField()
//func submitForm() {
// let result = passwordValidator(passwordField.text)
//
// /*
// ... handle result
// */
//}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment