parent
fef0a975ef
commit
bd218202a6
@ -0,0 +1,4 @@
|
||||
# SimpleSwiftPromises
|
||||
|
||||
A simple implementation of promises in Swift. Goes with [my blog post](https://shadowfacts.net/2020/simple-swift-promises/) on the subject.
|
||||
|
@ -0,0 +1,170 @@
|
||||
//
|
||||
// Promise.swift
|
||||
// SimpleSwiftPromises
|
||||
//
|
||||
// Created by Shadowfacts on 2/14/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class Promise<Result> {
|
||||
private var handlers: [(Result) -> Void] = []
|
||||
private var result: Result?
|
||||
private var catchers: [(Error) -> Void] = []
|
||||
private var error: Error?
|
||||
|
||||
func resolve(_ result: Result) {
|
||||
self.result = result
|
||||
self.handlers.forEach { $0(result) }
|
||||
}
|
||||
|
||||
func reject(_ error: Error) {
|
||||
self.error = error
|
||||
self.catchers.forEach { $0(error) }
|
||||
}
|
||||
|
||||
func addHandler(_ handler: @escaping (Result) -> Void) {
|
||||
if let result = result {
|
||||
handler(result)
|
||||
} else {
|
||||
handlers.append(handler)
|
||||
}
|
||||
}
|
||||
|
||||
func addCatcher(_ catcher: @escaping (Error) -> Void) {
|
||||
if let error = error {
|
||||
catcher(error)
|
||||
} else {
|
||||
catchers.append(catcher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Promise {
|
||||
static func resolve<Result>(_ value: Result) -> Promise<Result> {
|
||||
let promise = Promise<Result>()
|
||||
promise.resolve(value)
|
||||
return promise
|
||||
}
|
||||
|
||||
static func reject<Result>(_ error: Error) -> Promise<Result> {
|
||||
let promise = Promise<Result>()
|
||||
promise.reject(error)
|
||||
return promise
|
||||
}
|
||||
|
||||
static func all<Result>(_ promises: [Promise<Result>], queue: DispatchQueue = .main) -> Promise<[Result]> {
|
||||
let group = DispatchGroup()
|
||||
|
||||
var results = [Result?](repeating: nil, count: promises.count)
|
||||
var firstError: Error?
|
||||
|
||||
for (index, promise) in promises.enumerated() {
|
||||
group.enter()
|
||||
promise.then { (res) in
|
||||
queue.async {
|
||||
results[index] = res
|
||||
group.leave()
|
||||
}
|
||||
}.catch { (err) -> Void in
|
||||
if firstError == nil {
|
||||
firstError = err
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
return Promise<[Result]> { (resolve, reject) in
|
||||
group.notify(queue: queue) {
|
||||
if let firstError = firstError {
|
||||
reject(firstError)
|
||||
} else {
|
||||
resolve(results.compactMap { $0 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
convenience init(resultProvider: @escaping (_ resolve: @escaping (Result) -> Void, _ reject: @escaping (Error) -> Void) -> Void) {
|
||||
self.init()
|
||||
resultProvider(self.resolve, self.reject)
|
||||
}
|
||||
|
||||
convenience init<ErrorType>(_ resultProvider: @escaping ((Swift.Result<Result, ErrorType>) -> Void) -> Void) {
|
||||
self.init { (resolve, reject) in
|
||||
resultProvider { (result) in
|
||||
switch result {
|
||||
case let .success(res):
|
||||
resolve(res)
|
||||
case let .failure(error):
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func then(_ func: @escaping (Result) -> Void) -> Promise<Result> {
|
||||
addHandler(`func`)
|
||||
return self
|
||||
}
|
||||
|
||||
func then<Next>(_ mapper: @escaping (Result) -> Promise<Next>) -> Promise<Next> {
|
||||
let next = Promise<Next>()
|
||||
addHandler { (parentResult) in
|
||||
let newPromise = mapper(parentResult)
|
||||
newPromise.addHandler(next.resolve)
|
||||
newPromise.addCatcher(next.reject)
|
||||
}
|
||||
addCatcher(next.reject)
|
||||
return next
|
||||
}
|
||||
|
||||
func then<Next>(_ mapper: @escaping (Result) -> Next) -> Promise<Next> {
|
||||
let next = Promise<Next>()
|
||||
addHandler { (parentResult) in
|
||||
let newResult = mapper(parentResult)
|
||||
next.resolve(newResult)
|
||||
}
|
||||
addCatcher(next.reject)
|
||||
return next
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func `catch`(_ catcher: @escaping (Error) -> Void) -> Promise<Result> {
|
||||
addCatcher(catcher)
|
||||
return self
|
||||
}
|
||||
|
||||
func `catch`(_ catcher: @escaping (Error) -> Promise<Result>) -> Promise<Result> {
|
||||
let next = Promise<Result>()
|
||||
addHandler(next.resolve)
|
||||
addCatcher { (error) in
|
||||
let newPromise = catcher(error)
|
||||
newPromise.addHandler(next.resolve)
|
||||
newPromise.addCatcher(next.reject)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
func `catch`(_ catcher: @escaping (Error) -> Result) -> Promise<Result> {
|
||||
let next = Promise<Result>()
|
||||
addHandler(next.resolve)
|
||||
addCatcher { (error) in
|
||||
let newResult = catcher(error)
|
||||
next.resolve(newResult)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
func handle(on queue: DispatchQueue) -> Promise<Result> {
|
||||
return self.then { (result) in
|
||||
return Promise { (resolve, reject) in
|
||||
queue.async {
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
//
|
||||
// PromiseTests.swift
|
||||
// SimpleSwiftPromisesTests
|
||||
//
|
||||
// Created by Shadowfacts on 2/14/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SimpleSwiftPromises
|
||||
|
||||
class PromiseTests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func assertResultEqual<Result: Equatable>(_ promise: Promise<Result>, _ value: Result, message: String? = nil) {
|
||||
let expectation = self.expectation(description: message ?? "promise result assertion")
|
||||
promise.then {
|
||||
XCTAssertEqual($0, value)
|
||||
expectation.fulfill()
|
||||
}
|
||||
self.waitForExpectations(timeout: 2) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("didn't resolve promise: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testResolveImmediate() {
|
||||
assertResultEqual(Promise<String>.resolve("blah"), "blah")
|
||||
}
|
||||
|
||||
func testResolveImmediateMapped() {
|
||||
let promise = Promise<String>.resolve("foo").then {
|
||||
"test \($0)"
|
||||
}.then {
|
||||
Promise<String>.resolve("\($0) bar")
|
||||
}
|
||||
assertResultEqual(promise, "test foo bar")
|
||||
}
|
||||
|
||||
func testContinueAfterReject() {
|
||||
let promise = Promise<String>.reject(TestError()).then { (res) in
|
||||
XCTFail("then on rejected promise is unreachable")
|
||||
}.catch { (error) -> String in
|
||||
XCTAssertTrue(error is TestError)
|
||||
return "caught"
|
||||
}.then {
|
||||
"\($0) error"
|
||||
}
|
||||
assertResultEqual(promise, "caught error")
|
||||
}
|
||||
|
||||
func testResolveDelayed() {
|
||||
let promise = Promise<String> { (resolve, reject) in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
resolve("blah")
|
||||
}
|
||||
}
|
||||
assertResultEqual(promise, "blah")
|
||||
}
|
||||
|
||||
func testResolveMappedDelayed() {
|
||||
let promise = Promise<String> { (resolve, reject) in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
resolve("foo")
|
||||
}
|
||||
}.then {
|
||||
"\($0) bar"
|
||||
}.then { (result) in
|
||||
Promise<String> { (resolve, reject) in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
resolve("\(result) baz")
|
||||
}
|
||||
}
|
||||
}
|
||||
assertResultEqual(promise, "foo bar baz")
|
||||
}
|
||||
|
||||
func testResolveAll() {
|
||||
let promise = Promise<[String]>.all([
|
||||
Promise<String>.resolve("a"),
|
||||
Promise<String>.resolve("b"),
|
||||
Promise<String>.resolve("c"),
|
||||
])
|
||||
assertResultEqual(promise, ["a", "b", "c"])
|
||||
}
|
||||
|
||||
func testIntermediateReject() {
|
||||
let promise = Promise<String>.resolve("foo").then { (_) -> Promise<String> in
|
||||
Promise<String>.reject(TestError())
|
||||
}.catch { (error) -> String in
|
||||
XCTAssertTrue(error is TestError)
|
||||
return "caught"
|
||||
}.then { (result) -> String in
|
||||
"\(result) error"
|
||||
}
|
||||
assertResultEqual(promise, "caught error")
|
||||
}
|
||||
|
||||
func testResultHelper() {
|
||||
let success = Promise<String> { (handler) in
|
||||
handler(Result<String, Never>.success("asdf"))
|
||||
}
|
||||
assertResultEqual(success, "asdf")
|
||||
let failure = Promise<String> { (handler) in
|
||||
handler(Result<String, TestError>.failure(TestError()))
|
||||
}.catch { (error) -> String in
|
||||
"blah"
|
||||
}
|
||||
assertResultEqual(failure, "blah")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct TestError: Error {
|
||||
var localizedDescription: String {
|
||||
"test error"
|
||||
}
|
||||
}
|
Loading…
Reference in new issue