Last active
May 29, 2020 05:26
-
-
Save josefdolezal/9786b9ccac4df7b9c1504c5a36bcdbb7 to your computer and use it in GitHub Desktop.
[Swift] Functional Form Fields Validation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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