Skip to content

Instantly share code, notes, and snippets.

@IanKeen
Last active August 2, 2023 16:39
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save IanKeen/88224046426a588a29ac40e753c1bb6c to your computer and use it in GitHub Desktop.
Save IanKeen/88224046426a588a29ac40e753c1bb6c to your computer and use it in GitHub Desktop.
Simple, highly composable Validator
extension Validator where Input == User, Output == User {
static var validUser: Validator<User, User> {
return .keyPath(\.name, .isNotNil && .isNotEmpty)
}
}
struct User {
let name: String?
}
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)
}
*/
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
}
}
}
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
}
}
}
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
}
}
}
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
}
}
}
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