425 lines
14 KiB
Swift
425 lines
14 KiB
Swift
import Dispatch
|
||
|
||
/// Thenable represents an asynchronous operation that can be chained.
|
||
public protocol Thenable: class {
|
||
/// The type of the wrapped value
|
||
associatedtype T
|
||
|
||
/// `pipe` is immediately executed when this `Thenable` is resolved
|
||
func pipe(to: @escaping(Result<T>) -> Void)
|
||
|
||
/// The resolved result or nil if pending.
|
||
var result: Result<T>? { get }
|
||
}
|
||
|
||
public extension Thenable {
|
||
/**
|
||
The provided closure executes when this promise resolves.
|
||
|
||
This allows chaining promises. The promise returned by the provided closure is resolved before the promise returned by this closure resolves.
|
||
|
||
- Parameter on: The queue to which the provided closure dispatches.
|
||
- Parameter body: The closure that executes when this promise fulfills. It must return a promise.
|
||
- Returns: A new promise that resolves when the promise returned from the provided closure resolves. For example:
|
||
|
||
firstly {
|
||
URLSession.shared.dataTask(.promise, with: url1)
|
||
}.then { response in
|
||
transform(data: response.data)
|
||
}.done { transformation in
|
||
//…
|
||
}
|
||
*/
|
||
func then<U: Thenable>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) throws -> U) -> Promise<U.T> {
|
||
let rp = Promise<U.T>(.pending)
|
||
pipe {
|
||
switch $0 {
|
||
case .fulfilled(let value):
|
||
on.async(flags: flags) {
|
||
do {
|
||
let rv = try body(value)
|
||
guard rv !== rp else { throw PMKError.returnedSelf }
|
||
rv.pipe(to: rp.box.seal)
|
||
} catch {
|
||
rp.box.seal(.rejected(error))
|
||
}
|
||
}
|
||
case .rejected(let error):
|
||
rp.box.seal(.rejected(error))
|
||
}
|
||
}
|
||
return rp
|
||
}
|
||
|
||
/**
|
||
The provided closure is executed when this promise is resolved.
|
||
|
||
This is like `then` but it requires the closure to return a non-promise.
|
||
|
||
- Parameter on: The queue to which the provided closure dispatches.
|
||
- Parameter transform: The closure that is executed when this Promise is fulfilled. It must return a non-promise.
|
||
- Returns: A new promise that is resolved with the value returned from the provided closure. For example:
|
||
|
||
firstly {
|
||
URLSession.shared.dataTask(.promise, with: url1)
|
||
}.map { response in
|
||
response.data.length
|
||
}.done { length in
|
||
//…
|
||
}
|
||
*/
|
||
func map<U>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T) throws -> U) -> Promise<U> {
|
||
let rp = Promise<U>(.pending)
|
||
pipe {
|
||
switch $0 {
|
||
case .fulfilled(let value):
|
||
on.async(flags: flags) {
|
||
do {
|
||
rp.box.seal(.fulfilled(try transform(value)))
|
||
} catch {
|
||
rp.box.seal(.rejected(error))
|
||
}
|
||
}
|
||
case .rejected(let error):
|
||
rp.box.seal(.rejected(error))
|
||
}
|
||
}
|
||
return rp
|
||
}
|
||
|
||
/**
|
||
The provided closure is executed when this promise is resolved.
|
||
|
||
In your closure return an `Optional`, if you return `nil` the resulting promise is rejected with `PMKError.compactMap`, otherwise the promise is fulfilled with the unwrapped value.
|
||
|
||
firstly {
|
||
URLSession.shared.dataTask(.promise, with: url)
|
||
}.compactMap {
|
||
try JSONSerialization.jsonObject(with: $0.data) as? [String: String]
|
||
}.done { dictionary in
|
||
//…
|
||
}.catch {
|
||
// either `PMKError.compactMap` or a `JSONError`
|
||
}
|
||
*/
|
||
func compactMap<U>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T) throws -> U?) -> Promise<U> {
|
||
let rp = Promise<U>(.pending)
|
||
pipe {
|
||
switch $0 {
|
||
case .fulfilled(let value):
|
||
on.async(flags: flags) {
|
||
do {
|
||
if let rv = try transform(value) {
|
||
rp.box.seal(.fulfilled(rv))
|
||
} else {
|
||
throw PMKError.compactMap(value, U.self)
|
||
}
|
||
} catch {
|
||
rp.box.seal(.rejected(error))
|
||
}
|
||
}
|
||
case .rejected(let error):
|
||
rp.box.seal(.rejected(error))
|
||
}
|
||
}
|
||
return rp
|
||
}
|
||
|
||
/**
|
||
The provided closure is executed when this promise is resolved.
|
||
|
||
Equivalent to `map { x -> Void in`, but since we force the `Void` return Swift
|
||
is happier and gives you less hassle about your closure’s qualification.
|
||
|
||
- Parameter on: The queue to which the provided closure dispatches.
|
||
- Parameter body: The closure that is executed when this Promise is fulfilled.
|
||
- Returns: A new promise fulfilled as `Void`.
|
||
|
||
firstly {
|
||
URLSession.shared.dataTask(.promise, with: url)
|
||
}.done { response in
|
||
print(response.data)
|
||
}
|
||
*/
|
||
func done(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) throws -> Void) -> Promise<Void> {
|
||
let rp = Promise<Void>(.pending)
|
||
pipe {
|
||
switch $0 {
|
||
case .fulfilled(let value):
|
||
on.async(flags: flags) {
|
||
do {
|
||
try body(value)
|
||
rp.box.seal(.fulfilled(()))
|
||
} catch {
|
||
rp.box.seal(.rejected(error))
|
||
}
|
||
}
|
||
case .rejected(let error):
|
||
rp.box.seal(.rejected(error))
|
||
}
|
||
}
|
||
return rp
|
||
}
|
||
|
||
/**
|
||
The provided closure is executed when this promise is resolved.
|
||
|
||
This is like `done` but it returns the same value that the handler is fed.
|
||
`get` immutably accesses the fulfilled value; the returned Promise maintains that value.
|
||
|
||
- Parameter on: The queue to which the provided closure dispatches.
|
||
- Parameter body: The closure that is executed when this Promise is fulfilled.
|
||
- Returns: A new promise that is resolved with the value that the handler is fed. For example:
|
||
|
||
firstly {
|
||
.value(1)
|
||
}.get { foo in
|
||
print(foo, " is 1")
|
||
}.done { foo in
|
||
print(foo, " is 1")
|
||
}.done { foo in
|
||
print(foo, " is Void")
|
||
}
|
||
*/
|
||
func get(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping (T) throws -> Void) -> Promise<T> {
|
||
return map(on: on, flags: flags) {
|
||
try body($0)
|
||
return $0
|
||
}
|
||
}
|
||
|
||
/**
|
||
The provided closure is executed with promise result.
|
||
|
||
This is like `get` but provides the Result<T> of the Promise so you can inspect the value of the chain at this point without causing any side effects.
|
||
|
||
- Parameter on: The queue to which the provided closure dispatches.
|
||
- Parameter body: The closure that is executed with Result of Promise.
|
||
- Returns: A new promise that is resolved with the result that the handler is fed. For example:
|
||
|
||
promise.tap{ print($0) }.then{ /*…*/ }
|
||
*/
|
||
func tap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(Result<T>) -> Void) -> Promise<T> {
|
||
return Promise { seal in
|
||
pipe { result in
|
||
on.async(flags: flags) {
|
||
body(result)
|
||
seal.resolve(result)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// - Returns: a new promise chained off this promise but with its value discarded.
|
||
func asVoid() -> Promise<Void> {
|
||
return map(on: nil) { _ in }
|
||
}
|
||
}
|
||
|
||
public extension Thenable {
|
||
/**
|
||
- Returns: The error with which this promise was rejected; `nil` if this promise is not rejected.
|
||
*/
|
||
var error: Error? {
|
||
switch result {
|
||
case .none:
|
||
return nil
|
||
case .some(.fulfilled):
|
||
return nil
|
||
case .some(.rejected(let error)):
|
||
return error
|
||
}
|
||
}
|
||
|
||
/**
|
||
- Returns: `true` if the promise has not yet resolved.
|
||
*/
|
||
var isPending: Bool {
|
||
return result == nil
|
||
}
|
||
|
||
/**
|
||
- Returns: `true` if the promise has resolved.
|
||
*/
|
||
var isResolved: Bool {
|
||
return !isPending
|
||
}
|
||
|
||
/**
|
||
- Returns: `true` if the promise was fulfilled.
|
||
*/
|
||
var isFulfilled: Bool {
|
||
return value != nil
|
||
}
|
||
|
||
/**
|
||
- Returns: `true` if the promise was rejected.
|
||
*/
|
||
var isRejected: Bool {
|
||
return error != nil
|
||
}
|
||
|
||
/**
|
||
- Returns: The value with which this promise was fulfilled or `nil` if this promise is pending or rejected.
|
||
*/
|
||
var value: T? {
|
||
switch result {
|
||
case .none:
|
||
return nil
|
||
case .some(.fulfilled(let value)):
|
||
return value
|
||
case .some(.rejected):
|
||
return nil
|
||
}
|
||
}
|
||
}
|
||
|
||
public extension Thenable where T: Sequence {
|
||
/**
|
||
`Promise<[T]>` => `T` -> `U` => `Promise<[U]>`
|
||
|
||
firstly {
|
||
.value([1,2,3])
|
||
}.mapValues { integer in
|
||
integer * 2
|
||
}.done {
|
||
// $0 => [2,4,6]
|
||
}
|
||
*/
|
||
func mapValues<U>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U]> {
|
||
return map(on: on, flags: flags){ try $0.map(transform) }
|
||
}
|
||
|
||
/**
|
||
`Promise<[T]>` => `T` -> `[U]` => `Promise<[U]>`
|
||
|
||
firstly {
|
||
.value([1,2,3])
|
||
}.flatMapValues { integer in
|
||
[integer, integer]
|
||
}.done {
|
||
// $0 => [1,1,2,2,3,3]
|
||
}
|
||
*/
|
||
func flatMapValues<U: Sequence>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.Iterator.Element]> {
|
||
return map(on: on, flags: flags){ (foo: T) in
|
||
try foo.flatMap{ try transform($0) }
|
||
}
|
||
}
|
||
|
||
/**
|
||
`Promise<[T]>` => `T` -> `U?` => `Promise<[U]>`
|
||
|
||
firstly {
|
||
.value(["1","2","a","3"])
|
||
}.compactMapValues {
|
||
Int($0)
|
||
}.done {
|
||
// $0 => [1,2,3]
|
||
}
|
||
*/
|
||
func compactMapValues<U>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U?) -> Promise<[U]> {
|
||
return map(on: on, flags: flags) { foo -> [U] in
|
||
#if !swift(>=3.3) || (swift(>=4) && !swift(>=4.1))
|
||
return try foo.flatMap(transform)
|
||
#else
|
||
return try foo.compactMap(transform)
|
||
#endif
|
||
}
|
||
}
|
||
|
||
/**
|
||
`Promise<[T]>` => `T` -> `Promise<U>` => `Promise<[U]>`
|
||
|
||
firstly {
|
||
.value([1,2,3])
|
||
}.thenMap { integer in
|
||
.value(integer * 2)
|
||
}.done {
|
||
// $0 => [2,4,6]
|
||
}
|
||
*/
|
||
func thenMap<U: Thenable>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.T]> {
|
||
return then(on: on, flags: flags) {
|
||
when(fulfilled: try $0.map(transform))
|
||
}
|
||
}
|
||
|
||
/**
|
||
`Promise<[T]>` => `T` -> `Promise<[U]>` => `Promise<[U]>`
|
||
|
||
firstly {
|
||
.value([1,2,3])
|
||
}.thenFlatMap { integer in
|
||
.value([integer, integer])
|
||
}.done {
|
||
// $0 => [1,1,2,2,3,3]
|
||
}
|
||
*/
|
||
func thenFlatMap<U: Thenable>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.T.Iterator.Element]> where U.T: Sequence {
|
||
return then(on: on, flags: flags) {
|
||
when(fulfilled: try $0.map(transform))
|
||
}.map(on: nil) {
|
||
$0.flatMap{ $0 }
|
||
}
|
||
}
|
||
|
||
/**
|
||
`Promise<[T]>` => `T` -> Bool => `Promise<[U]>`
|
||
|
||
firstly {
|
||
.value([1,2,3])
|
||
}.filterValues {
|
||
$0 > 1
|
||
}.done {
|
||
// $0 => [2,3]
|
||
}
|
||
*/
|
||
func filterValues(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ isIncluded: @escaping (T.Iterator.Element) -> Bool) -> Promise<[T.Iterator.Element]> {
|
||
return map(on: on, flags: flags) {
|
||
$0.filter(isIncluded)
|
||
}
|
||
}
|
||
}
|
||
|
||
public extension Thenable where T: Collection {
|
||
/// - Returns: a promise fulfilled with the first value of this `Collection` or, if empty, a promise rejected with PMKError.emptySequence.
|
||
var firstValue: Promise<T.Iterator.Element> {
|
||
return map(on: nil) { aa in
|
||
if let a1 = aa.first {
|
||
return a1
|
||
} else {
|
||
throw PMKError.emptySequence
|
||
}
|
||
}
|
||
}
|
||
|
||
func firstValue(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, where test: @escaping (T.Iterator.Element) -> Bool) -> Promise<T.Iterator.Element> {
|
||
return map(on: on, flags: flags) {
|
||
for x in $0 where test(x) {
|
||
return x
|
||
}
|
||
throw PMKError.emptySequence
|
||
}
|
||
}
|
||
|
||
/// - Returns: a promise fulfilled with the last value of this `Collection` or, if empty, a promise rejected with PMKError.emptySequence.
|
||
var lastValue: Promise<T.Iterator.Element> {
|
||
return map(on: nil) { aa in
|
||
if aa.isEmpty {
|
||
throw PMKError.emptySequence
|
||
} else {
|
||
let i = aa.index(aa.endIndex, offsetBy: -1)
|
||
return aa[i]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
public extension Thenable where T: Sequence, T.Iterator.Element: Comparable {
|
||
/// - Returns: a promise fulfilled with the sorted values of this `Sequence`.
|
||
func sortedValues(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil) -> Promise<[T.Iterator.Element]> {
|
||
return map(on: on, flags: flags){ $0.sorted() }
|
||
}
|
||
}
|