Add promises
This commit is contained in:
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.
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
D683419C23FCDCCE00D06703 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D683419B23FCDCCE00D06703 /* Assets.xcassets */; };
|
D683419C23FCDCCE00D06703 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D683419B23FCDCCE00D06703 /* Assets.xcassets */; };
|
||||||
D683419F23FCDCCE00D06703 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D683419D23FCDCCE00D06703 /* Main.storyboard */; };
|
D683419F23FCDCCE00D06703 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D683419D23FCDCCE00D06703 /* Main.storyboard */; };
|
||||||
D68341AB23FCDCCE00D06703 /* SimpleSwiftPromisesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68341AA23FCDCCE00D06703 /* SimpleSwiftPromisesTests.swift */; };
|
D68341AB23FCDCCE00D06703 /* SimpleSwiftPromisesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68341AA23FCDCCE00D06703 /* SimpleSwiftPromisesTests.swift */; };
|
||||||
|
D68341B623FCDCF000D06703 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68341B523FCDCF000D06703 /* Promise.swift */; };
|
||||||
|
D68341B823FCDCF800D06703 /* PromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68341B723FCDCF800D06703 /* PromiseTests.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -35,6 +37,8 @@
|
||||||
D68341A623FCDCCE00D06703 /* SimpleSwiftPromisesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimpleSwiftPromisesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
D68341A623FCDCCE00D06703 /* SimpleSwiftPromisesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimpleSwiftPromisesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D68341AA23FCDCCE00D06703 /* SimpleSwiftPromisesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleSwiftPromisesTests.swift; sourceTree = "<group>"; };
|
D68341AA23FCDCCE00D06703 /* SimpleSwiftPromisesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleSwiftPromisesTests.swift; sourceTree = "<group>"; };
|
||||||
D68341AC23FCDCCE00D06703 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D68341AC23FCDCCE00D06703 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
D68341B523FCDCF000D06703 /* Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Promise.swift; sourceTree = "<group>"; };
|
||||||
|
D68341B723FCDCF800D06703 /* PromiseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromiseTests.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -77,6 +81,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D683419723FCDCCB00D06703 /* AppDelegate.swift */,
|
D683419723FCDCCB00D06703 /* AppDelegate.swift */,
|
||||||
|
D68341B523FCDCF000D06703 /* Promise.swift */,
|
||||||
D683419923FCDCCB00D06703 /* ViewController.swift */,
|
D683419923FCDCCB00D06703 /* ViewController.swift */,
|
||||||
D683419B23FCDCCE00D06703 /* Assets.xcassets */,
|
D683419B23FCDCCE00D06703 /* Assets.xcassets */,
|
||||||
D683419D23FCDCCE00D06703 /* Main.storyboard */,
|
D683419D23FCDCCE00D06703 /* Main.storyboard */,
|
||||||
|
@ -90,6 +95,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D68341AA23FCDCCE00D06703 /* SimpleSwiftPromisesTests.swift */,
|
D68341AA23FCDCCE00D06703 /* SimpleSwiftPromisesTests.swift */,
|
||||||
|
D68341B723FCDCF800D06703 /* PromiseTests.swift */,
|
||||||
D68341AC23FCDCCE00D06703 /* Info.plist */,
|
D68341AC23FCDCCE00D06703 /* Info.plist */,
|
||||||
);
|
);
|
||||||
path = SimpleSwiftPromisesTests;
|
path = SimpleSwiftPromisesTests;
|
||||||
|
@ -197,6 +203,7 @@
|
||||||
files = (
|
files = (
|
||||||
D683419A23FCDCCB00D06703 /* ViewController.swift in Sources */,
|
D683419A23FCDCCB00D06703 /* ViewController.swift in Sources */,
|
||||||
D683419823FCDCCB00D06703 /* AppDelegate.swift in Sources */,
|
D683419823FCDCCB00D06703 /* AppDelegate.swift in Sources */,
|
||||||
|
D68341B623FCDCF000D06703 /* Promise.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -205,6 +212,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D68341AB23FCDCCE00D06703 /* SimpleSwiftPromisesTests.swift in Sources */,
|
D68341AB23FCDCCE00D06703 /* SimpleSwiftPromisesTests.swift in Sources */,
|
||||||
|
D68341B823FCDCF800D06703 /* PromiseTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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