Last active
August 2, 2023 16:39
-
-
Save IanKeen/88224046426a588a29ac40e753c1bb6c to your computer and use it in GitHub Desktop.
Simple, highly composable Validator
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
extension Validator where Input == User, Output == User { | |
static var validUser: Validator<User, User> { | |
return .keyPath(\.name, .isNotNil && .isNotEmpty) | |
} | |
} | |
struct User { | |
let name: String? | |
} |
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
enum PasswordError: Error { | |
case empty | |
case containsInvalidCharacters | |
case tooShort | |
case notEnoughLetters | |
case notEnoughNumbers | |
case notEnoughSpecialCharacters | |
case notEqual | |
} | |
extension Validator where Input == String { | |
static var password: Validator<String, String> { | |
return .isNotEmpty !! PasswordError.empty | |
&& .containing(no: .whitespacesAndNewlines) !! PasswordError.containsInvalidCharacters | |
&& .count(atLeast: 9) !! PasswordError.tooShort | |
&& .containing(atLeast: 1, in: .letters) !! PasswordError.notEnoughLetters | |
&& .containing(atLeast: 1, in: .decimalDigits) !! PasswordError.notEnoughNumbers | |
&& .containing(atLeast: 1, in: CharacterSet.alphanumerics.inverted) !! PasswordError.notEnoughSpecialCharacters | |
} | |
} | |
/* Usage is something like: | |
func signIn(username: String, password: String) async throws { | |
let request = try SignInRequest( | |
username: username, | |
password: Validator.password.validate(password) // throws if validation fails, otherwise uses input | |
) | |
await api.execute(request) | |
} | |
*/ |
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
extension Validator where Input: Collection { | |
struct NotEmpty: Error { } | |
static var isEmpty: Validator<Input, Input> { | |
return .init { value in | |
guard value.isEmpty else { throw NotEmpty() } | |
return value | |
} | |
} | |
struct Empty: Error { } | |
static var isNotEmpty: Validator<Input, Input> { | |
return .init { value in | |
guard !value.isEmpty else { throw Empty() } | |
return value | |
} | |
} | |
struct TooShort: Error { } | |
static func count(atLeast count: Int) -> Validator<Input, Input> { | |
return .init { value in | |
guard value.count >= count else { throw TooShort() } | |
return value | |
} | |
} | |
} |
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
extension Validator { | |
static func keyPath<T, U>(_ keyPath: KeyPath<Input, T>, _ validator: Validator<T, U>) -> Validator<Input, Input> { | |
return .init { value in | |
_ = try validator.validate(value[keyPath: keyPath]) | |
return value | |
} | |
} | |
} |
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
extension Validator where Input: OptionalType { | |
struct Nil: Error { } | |
static var isNotNil: Validator<Input, Input.WrappedType> { | |
return .init { value in | |
guard let value = value.value() else { throw Nil() } | |
return value | |
} | |
} | |
} | |
extension Validator where Input: OptionalType, Input == Output { | |
static var isNotNil: Validator<Input, Input> { | |
return .init { value in | |
guard let _ = value.value() else { throw Nil() } | |
return value | |
} | |
} | |
struct NotNil: Error { } | |
static var isNil: Validator<Input, Input> { | |
return .init { value in | |
guard value.value() == nil else { throw NotNil() } | |
return value | |
} | |
} | |
} | |
public protocol OptionalType: ExpressibleByNilLiteral { | |
associatedtype WrappedType | |
func value() -> WrappedType? | |
} | |
extension Optional: OptionalType { | |
public func value() -> Wrapped? { | |
switch self { | |
case .some(let value): return value | |
case .none: return nil | |
} | |
} | |
} |
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
extension Validator where Input == String { | |
struct InvalidString: Error { } | |
static func containing(no set: CharacterSet) -> Validator<String, String> { | |
return .init { value in | |
guard value.contains(where: { set.contains($0) }) else { throw InvalidString() } | |
return value | |
} | |
} | |
static func containing(atLeast count: Int, in set: CharacterSet) -> Validator<String, String> { | |
return .init { value in | |
let total = value.reduce(0) { count, char in | |
guard set.contains(char) else { return count } | |
return count + 1 | |
} | |
guard total >= count else { throw InvalidString() } | |
return value | |
} | |
} | |
struct Regex: Error { } | |
static func matches(regex: String, options: NSRegularExpression.Options = []) -> Validator<String, String> { | |
let expr = try! NSRegularExpression(pattern: regex, options: options) | |
return .init { value in | |
let range = NSRange(location: 0, length: value.count) | |
guard | |
expr.rangeOfFirstMatch(in: value, range: range).location != NSNotFound | |
else { throw Regex() } | |
return value | |
} | |
} | |
} |
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
struct Validator<Input, Output> { | |
let validate: (Input) throws -> Output | |
} | |
extension Validator { | |
func and<Value>(_ other: Validator<Output, Value>) -> Validator<Input, Value> { | |
return .init { value in | |
return try other.validate(self.validate(value)) | |
} | |
} | |
} | |
func &&<A, B, C>(lhs: Validator<A, B>, rhs: Validator<B, C>) -> Validator<A, C> { | |
return lhs.and(rhs) | |
} | |
extension Validator { | |
func or(_ other: Validator) -> Validator { | |
return .init { value in | |
do { return try self.validate(value) } | |
catch { return try other.validate(value) } | |
} | |
} | |
} | |
func ||<A, B>(lhs: Validator<A, B>, rhs: Validator<A, B>) -> Validator<A, B> { | |
return lhs.or(rhs) | |
} | |
extension Validator { | |
func fail(with newError: Error) -> Validator { | |
return .init { value in | |
do { return try self.validate(value) } | |
catch { throw newError } | |
} | |
} | |
} | |
infix operator !!: NilCoalescingPrecedence | |
func !!<A, B>(lhs: Validator<A, B>, rhs: Error) -> Validator<A, B> { | |
return lhs.fail(with: rhs) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment