Last active
June 12, 2019 06:11
-
-
Save p69/0bc4c3a7e1cd64b77139057a42e48973 to your computer and use it in GitHub Desktop.
Railway Oriented Programming example in Swift https://medium.com/@pavelshilyagov/railway-oriented-programming-in-swift-d4131c05d627
This file contains 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 Cocoa | |
precedencegroup PipeOperatorPrecedence { | |
associativity: left | |
higherThan: LogicalConjunctionPrecedence | |
} | |
infix operator |>: PipeOperatorPrecedence | |
public func |> <A, B>(arg: A, | |
f: @escaping (A) -> B) -> B { | |
return f(arg) | |
} | |
precedencegroup KleisliOperatorPrecedence { | |
associativity: left | |
higherThan: PipeOperatorPrecedence | |
} | |
infix operator >=>: KleisliOperatorPrecedence | |
public enum Result<T, E> { | |
case ok(T) | |
case error(E) | |
} | |
extension Result { | |
public func bind<B>(_ f: @escaping (T) -> Result<B, E>) -> Result<B, E> { | |
switch self { | |
case .ok(let x): | |
return f(x) | |
case .error(let e): | |
return Result<B, E>.error(e) | |
} | |
} | |
} | |
public func >=> <A, B, C, E>(f: @escaping (A) -> Result<B, E>, | |
g: @escaping (B) -> Result<C, E>) -> (A)->Result<C, E> { | |
return { a in | |
f(a).bind(g) | |
} | |
} | |
struct UserInput { | |
let email: String | |
let password: String | |
} | |
struct User { | |
let id = UUID.init() | |
let email: String | |
} | |
extension User: CustomDebugStringConvertible { | |
var debugDescription: String { | |
return "{ id: \(id), email: \(email) }" | |
} | |
} | |
enum UserError: Error { | |
case invalidEmail, invalidPassword, alreadyExist | |
case unknown(cause: Error) | |
} | |
//key-value as DB store | |
var usersDb = [String: User]() | |
//simulate DB runtime error | |
enum DbError: Error { | |
case duplicateKeyError | |
} | |
func saveToDb(_ input: UserInput) throws -> User { | |
if usersDb[input.email] != nil { | |
print("user \(input.email) already exists") | |
throw DbError.duplicateKeyError | |
} | |
let newUser = User(email: input.email) | |
usersDb[input.email] = newUser | |
print("user saved: \(newUser)") | |
return newUser | |
} | |
func validateEmail(input: UserInput) -> Result<UserInput, UserError> { | |
//Simple regular expression for email validation | |
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}") | |
//Validate email | |
return emailPredicate.evaluate(with: input.email) ? .ok(input) : .error(UserError.invalidEmail) | |
} | |
func validatePassword(input: UserInput) -> Result<UserInput, UserError> { | |
return input.password.count > 6 ? .ok(input) : .error(UserError.invalidPassword) | |
} | |
func saveToDbWithResult(input: UserInput) -> Result<User, UserError> { | |
do { | |
let user = try saveToDb(input) | |
return .ok(user) | |
} catch DbError.duplicateKeyError { | |
return .error(UserError.alreadyExist) | |
} catch { | |
return .error(UserError.unknown(cause: error)) | |
} | |
} | |
func register(input: UserInput) -> Result<User, UserError> { | |
return validateEmail(input: input) | |
.bind(validatePassword) | |
.bind(saveToDbWithResult) | |
} | |
func registerOperators(input: UserInput) -> Result<User, UserError> { | |
return input | |
|> validateEmail | |
>=> validatePassword | |
>=> saveToDbWithResult | |
} | |
let test1 = UserInput(email: "123@mail.com", password: "password") | |
let registrationResult = register(input: test1) | |
switch registrationResult { | |
case .ok(let user): | |
print("user \(user) successfully registered") | |
case .error(let error): | |
print("failed to register new user. Error: \(error)") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment