Add promises

This commit is contained in:
Shadowfacts 2020-02-18 22:05:32 -05:00
parent fef0a975ef
commit bd218202a6
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
4 changed files with 308 additions and 0 deletions

4
README.md Normal file
View File

@ -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.

View File

@ -12,6 +12,8 @@
D683419C23FCDCCE00D06703 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D683419B23FCDCCE00D06703 /* Assets.xcassets */; };
D683419F23FCDCCE00D06703 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D683419D23FCDCCE00D06703 /* Main.storyboard */; };
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 */
/* Begin PBXContainerItemProxy section */
@ -35,6 +37,8 @@
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>"; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@ -77,6 +81,7 @@
isa = PBXGroup;
children = (
D683419723FCDCCB00D06703 /* AppDelegate.swift */,
D68341B523FCDCF000D06703 /* Promise.swift */,
D683419923FCDCCB00D06703 /* ViewController.swift */,
D683419B23FCDCCE00D06703 /* Assets.xcassets */,
D683419D23FCDCCE00D06703 /* Main.storyboard */,
@ -90,6 +95,7 @@
isa = PBXGroup;
children = (
D68341AA23FCDCCE00D06703 /* SimpleSwiftPromisesTests.swift */,
D68341B723FCDCF800D06703 /* PromiseTests.swift */,
D68341AC23FCDCCE00D06703 /* Info.plist */,
);
path = SimpleSwiftPromisesTests;
@ -197,6 +203,7 @@
files = (
D683419A23FCDCCB00D06703 /* ViewController.swift in Sources */,
D683419823FCDCCB00D06703 /* AppDelegate.swift in Sources */,
D68341B623FCDCF000D06703 /* Promise.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -205,6 +212,7 @@
buildActionMask = 2147483647;
files = (
D68341AB23FCDCCE00D06703 /* SimpleSwiftPromisesTests.swift in Sources */,
D68341B823FCDCF800D06703 /* PromiseTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -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)
}
}
}
}
}

View File

@ -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"
}
}