From c030f2323ca29b9c41ecc9d5f467dcc1bb1db17a Mon Sep 17 00:00:00 2001 From: Luiz Rodrigo Martins Barbosa Date: Thu, 22 Apr 2021 10:49:25 +0200 Subject: [PATCH] Rewrite promises to allow callback option (#19) * Rewrite promises to allow callback option * setFailureType for Never * Add more error transformation * Add init that flattens PromiseError * Promise from Publisher as func, not var, so it can be made generic * Oops, fix wrong generic usage * Oops, fix wrong generics usage * More Promise extensions * Promises to use NonEmptyPublisher as upstream * Improve tests * Promise Zip to not automatically map * Promise timeout * Add generic constraint --- .../Combine/Publisher+FlatMapResult.swift | 5 +- .../Promise/NonEmptyPublisher.swift | 61 ++++ .../Promise/Promise+PerformInQueue.swift | 46 +++ .../Promise/Promise.swift | 82 +++-- .../PromiseType+DefaultOperators.swift | 164 ++++++++++ .../Promise/PromiseType+Fold.swift | 3 +- .../Promise/PromiseType+Init.swift | 10 +- .../Promise/PromiseType+Zip.swift | 241 ++++++-------- .../Promise/PromiseType.swift | 2 +- .../Promise/Publisher+NonEmpty.swift | 19 ++ .../Publisher+PromiseConvertibleType.swift | 73 ++++- .../Result+PromiseConvertibleType.swift | 32 -- .../Timer/TimerProtocol.swift | 2 +- .../Promise/PromiseTests.swift | 305 ++++++++++++++++++ 14 files changed, 847 insertions(+), 198 deletions(-) create mode 100644 Sources/FoundationExtensions/Promise/NonEmptyPublisher.swift create mode 100644 Sources/FoundationExtensions/Promise/Promise+PerformInQueue.swift create mode 100644 Sources/FoundationExtensions/Promise/PromiseType+DefaultOperators.swift create mode 100644 Sources/FoundationExtensions/Promise/Publisher+NonEmpty.swift delete mode 100644 Sources/FoundationExtensions/Promise/Result+PromiseConvertibleType.swift create mode 100644 Tests/FoundationExtensionsTests/Promise/PromiseTests.swift diff --git a/Sources/FoundationExtensions/Combine/Publisher+FlatMapResult.swift b/Sources/FoundationExtensions/Combine/Publisher+FlatMapResult.swift index 61e5b1a..4895318 100644 --- a/Sources/FoundationExtensions/Combine/Publisher+FlatMapResult.swift +++ b/Sources/FoundationExtensions/Combine/Publisher+FlatMapResult.swift @@ -12,8 +12,9 @@ import Foundation @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension Publisher { - public func flatMapResult(maxPublishers: Subscribers.Demand = .unlimited, - _ transform: @escaping (Self.Output) -> Result + public func flatMapResult( + maxPublishers: Subscribers.Demand = .unlimited, + _ transform: @escaping (Self.Output) -> Result ) -> Publishers.FlatMap.Publisher, Self> { flatMap(maxPublishers: maxPublishers) { value in transform(value).publisher diff --git a/Sources/FoundationExtensions/Promise/NonEmptyPublisher.swift b/Sources/FoundationExtensions/Promise/NonEmptyPublisher.swift new file mode 100644 index 0000000..405d093 --- /dev/null +++ b/Sources/FoundationExtensions/Promise/NonEmptyPublisher.swift @@ -0,0 +1,61 @@ +// +// NonEmptyPublisher.swift +// FoundationExtensions +// +// Created by Luiz Rodrigo Martins Barbosa on 19.04.21. +// Copyright © 2021 Lautsprecher Teufel GmbH. All rights reserved. +// + +#if canImport(Combine) +import Combine +import Foundation + +/// A Publisher that ensures that at least 1 value will be emitted before successful completion, but not necessarily in case of failure completion. +/// This requires a fallback success or error in case the upstream decides to terminate empty. +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public struct NonEmptyPublisher: Publisher { + public typealias Output = Upstream.Output + public typealias Failure = Upstream.Failure + + private let upstream: Upstream + let fallback: () -> Result + + public init(upstream: Upstream, onEmpty fallback: @escaping () -> Result) { + self.upstream = upstream + self.fallback = fallback + } + + private enum EmptyStream { + case empty + case someValue(Output) + } + + public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input { + upstream + .map(EmptyStream.someValue) + .replaceEmpty(with: EmptyStream.empty) + .flatMap { valueType -> AnyPublisher in + switch valueType { + case .empty: + switch fallback() { + case let .success(fallbackValue): + return Just(fallbackValue).setFailureType(to: Failure.self).eraseToAnyPublisher() + case let .failure(fallbackError): + return Fail(error: fallbackError).eraseToAnyPublisher() + } + case let .someValue(value): + return Just(value).setFailureType(to: Failure.self).eraseToAnyPublisher() + } + } + .subscribe(subscriber) + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension NonEmptyPublisher where Upstream: PromiseType { + public init(upstream: Upstream) { + self.upstream = upstream + self.fallback = { fatalError() } + } +} +#endif diff --git a/Sources/FoundationExtensions/Promise/Promise+PerformInQueue.swift b/Sources/FoundationExtensions/Promise/Promise+PerformInQueue.swift new file mode 100644 index 0000000..d0f0054 --- /dev/null +++ b/Sources/FoundationExtensions/Promise/Promise+PerformInQueue.swift @@ -0,0 +1,46 @@ +// +// Promise+PerformInQueue.swift +// FoundationExtensions +// +// Created by Luiz Rodrigo Martins Barbosa on 17.04.21. +// Copyright © 2021 Lautsprecher Teufel GmbH. All rights reserved. +// + +#if canImport(Combine) +import Combine +import Dispatch +import Foundation + +extension DispatchWorkItem: Cancellable { } + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Publishers.Promise { + public static func perform(in queue: DispatchQueue, operation: @escaping () throws -> Output) -> Self where Failure == Error { + .init { completion in + let workItem = DispatchWorkItem { + do { + let value = try operation() + completion(.success(value)) + } catch { + completion(.failure(error)) + } + } + + queue.async(execute: workItem) + return workItem + } + } + + public static func perform(in queue: DispatchQueue, operation: @escaping () -> Output) -> Self where Failure == Never { + .init { completion in + let workItem = DispatchWorkItem { + let value = operation() + completion(.success(value)) + } + + queue.async(execute: workItem) + return workItem + } + } +} +#endif diff --git a/Sources/FoundationExtensions/Promise/Promise.swift b/Sources/FoundationExtensions/Promise/Promise.swift index 970a473..87d18f3 100644 --- a/Sources/FoundationExtensions/Promise/Promise.swift +++ b/Sources/FoundationExtensions/Promise/Promise.swift @@ -12,7 +12,6 @@ import class Foundation.NSRecursiveLock @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension Publishers { - /// A Promise is a Publisher that lives in between First and Deferred. /// It will listen to one and only one event, and finish successfully, or finish with error without any successful output. /// It will hang until an event arrives from upstream. If the upstream is eager, it will be deferred, that means it won't @@ -28,15 +27,57 @@ extension Publishers { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public struct Promise: PromiseType { public typealias Output = Success - public typealias Failure = Failure + public typealias CompletionHandler = (Result) -> Void + public typealias OperationClosure = (@escaping CompletionHandler) -> Cancellable + + struct SinkNotification { + let receiveCompletion: (Subscribers.Completion) -> Void + let receiveValue: (Output) -> Void + } + typealias SinkClosure = (SinkNotification) -> AnyCancellable - private let upstream: () -> AnyPublisher + private let operation: SinkClosure /// A promise from an upstream publisher. Because this is an closure parameter, the upstream will become a factory /// and its creation will be deferred until there's some positive demand from the downstream. /// - Parameter upstream: a closure that creates an upstream publisher. This is an closure, so creation will be deferred. - public init(_ upstream: @escaping () -> P) where P.Output == Success, P.Failure == Failure { - self.upstream = { upstream().eraseToAnyPublisher() } + public init(_ upstream: @escaping () -> NonEmptyPublisher

) where P.Output == Success, P.Failure == Failure { + self.init(upstreamUncheckedForEmptiness: upstream) + } + + // https://www.fivestars.blog/articles/disfavoredOverload/ + @_disfavoredOverload + internal init(upstreamUncheckedForEmptiness upstream: @escaping () -> P) where P.Output == Success, P.Failure == Failure { + self.operation = { sinkNotification in + upstream() + .first() + .sink( + receiveCompletion: sinkNotification.receiveCompletion, + receiveValue: sinkNotification.receiveValue + ) + } + } + + public init(_ promise: @escaping () -> Promise) { + self = promise() + } + + public init(operation: @escaping OperationClosure) { + self.operation = { sinkNotification in + let cancellable = operation { result in + switch result { + case let .failure(error): + sinkNotification.receiveCompletion(.failure(error)) + case let .success(value): + sinkNotification.receiveValue(value) + sinkNotification.receiveCompletion(.finished) + } + } + + return AnyCancellable { + cancellable.cancel() + } + } } /// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)` @@ -46,10 +87,10 @@ extension Publishers { /// - subscriber: The subscriber to attach to this `Publisher`. /// once attached it can begin to receive values. public func receive(subscriber: S) where S: Subscriber, Failure == S.Failure, Success == S.Input { - subscriber.receive(subscription: Subscription.init(upstream: upstream, downstream: subscriber)) + subscriber.receive(subscription: Subscription(operation: operation, downstream: subscriber)) } - public func asPromise() -> Publishers.Promise { + public var promise: Publishers.Promise { self } } @@ -60,12 +101,12 @@ extension Publishers.Promise { class Subscription: Combine.Subscription where Output == Downstream.Input, Failure == Downstream.Failure { private let lock = NSRecursiveLock() private var hasStarted = false - private let upstream: () -> AnyPublisher + private let operation: SinkClosure private let downstream: Downstream private var cancellable: AnyCancellable? - public init(upstream: @escaping () -> AnyPublisher, downstream: Downstream) { - self.upstream = upstream + public init(operation: @escaping SinkClosure, downstream: Downstream) { + self.operation = operation self.downstream = downstream } @@ -79,17 +120,20 @@ extension Publishers.Promise { guard shouldRun else { return } - cancellable = upstream() - .first() - .sink( - receiveCompletion: { [weak self] completion in - self?.downstream.receive(completion: completion) - }, - receiveValue: { [weak self] value in - _ = self?.downstream.receive(value) + cancellable = operation(SinkNotification( + receiveCompletion: { [weak self] result in + switch result { + case .finished: self?.downstream.receive(completion: .finished) + case let .failure(error): + self?.downstream.receive(completion: .failure(error)) } - ) + }, + receiveValue: { [weak self] value in + _ = self?.downstream.receive(value) + self?.downstream.receive(completion: .finished) + } + )) } func cancel() { diff --git a/Sources/FoundationExtensions/Promise/PromiseType+DefaultOperators.swift b/Sources/FoundationExtensions/Promise/PromiseType+DefaultOperators.swift new file mode 100644 index 0000000..2ccdbb3 --- /dev/null +++ b/Sources/FoundationExtensions/Promise/PromiseType+DefaultOperators.swift @@ -0,0 +1,164 @@ +// +// PromiseType+DefaultOperators.swift +// FoundationExtensions +// +// Created by Luiz Rodrigo Martins Barbosa on 19.04.21. +// Copyright © 2021 Lautsprecher Teufel GmbH. All rights reserved. +// +#if canImport(Combine) +import Combine +import Foundation + + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + +// The stock publisher `Just` overrides most Publisher operators from the base protocol, to implement alternatives that return `Just` and not +// `Publisher.Map` or so. This allows us to execute a chain of operations to a Just publisher and remain in the Just world, following its +// constraints, traits and rules. +// https://developer.apple.com/documentation/combine/just +// For Promise this is also important. Promise is not only a very important Publisher, as most cases will complete with a single value only, but +// also it's the one that demands more chained operations. Leaving the world of promises means giving up on its main constraint: the upstream must +// emit one value before completing with success (or completes with failure regardless of value emission). For that reason, let's override the default +// operators to ensure they will return Promise, and not a different publisher that loses this trait. +// This can be done for any operator that makes sense and that doesn't filter the output. For example, `first` in a Promise is useless, as Promise +// will return only one element anyway, but it can be implemented. However, `first(where predicate:)` is dangerous, because it could filter out the +// only emission from this Promise and no emission would be sent downstream. In that case, we should keep this operator emitting Publisher, as we +// can't ensure anymore that it will be NonEmpty. So this must be evaluated case by case. +extension PromiseType { + public func map(_ transform: @escaping (Output) -> T) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().map(transform) } + } + + public func tryMap(_ transform: @escaping (Output) throws -> T) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().tryMap(transform) } + } + + public func mapError(_ transform: @escaping (Failure) -> E) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().mapError(transform) } + } + + public func `catch`(_ handler: @escaping (Failure) -> Publishers.Promise) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().catch(handler) } + } + + public func handleEvents( + receiveSubscription: ((Subscription) -> Void)? = nil, + receiveOutput: ((Self.Output) -> Void)? = nil, + receiveCompletion: ((Subscribers.Completion) -> Void)? = nil, + receiveCancel: (() -> Void)? = nil, + receiveRequest: ((Subscribers.Demand) -> Void)? = nil) + -> Publishers.Promise { + Publishers.Promise { + self.eraseToAnyPublisher().handleEvents( + receiveSubscription: receiveSubscription, + receiveOutput: receiveOutput, + receiveCompletion: receiveCompletion, + receiveCancel: receiveCancel, + receiveRequest: receiveRequest + ) + } + } + + public func print(_ prefix: String = "", to stream: TextOutputStream? = nil) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().print(prefix, to: stream) } + } + + public func flatMapResult(maxPublishers: Subscribers.Demand = .unlimited, _ transform: @escaping (Output) -> Result) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().flatMapResult(maxPublishers: maxPublishers, transform) } + } + + public func flatMap(maxPublishers: Subscribers.Demand = .unlimited, _ transform: @escaping (Output) -> Publishers.Promise) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().flatMap(maxPublishers: maxPublishers) { transform($0).setFailureType(to: Failure.self) } } + } + + public func flatMap(maxPublishers: Subscribers.Demand = .unlimited, _ transform: @escaping (Output) -> Publishers.Promise) -> Publishers.Promise where Failure == Never { + Publishers.Promise { self.eraseToAnyPublisher().setFailureType(to: E.self).flatMap(maxPublishers: maxPublishers, transform) } + } + + public func flatMap(maxPublishers: Subscribers.Demand = .unlimited, _ transform: @escaping (Output) -> Publishers.Promise) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().flatMap(maxPublishers: maxPublishers, transform) } + } + + public func contains(_ output: Output) -> Publishers.Promise where Output: Equatable { + Publishers.Promise { self.eraseToAnyPublisher().contains(output) } + } + + public func allSatisfy(_ predicate: @escaping (Output) -> Bool) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().allSatisfy(predicate) } + } + + public func tryAllSatisfy(_ predicate: @escaping (Output) throws -> Bool) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().tryAllSatisfy(predicate) } + } + + public func contains(where predicate: @escaping (Output) -> Bool) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().contains(where: predicate) } + } + + public func tryContains(where predicate: @escaping (Output) throws -> Bool) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().tryContains(where: predicate) } + } + + public func collect() -> Publishers.Promise<[Output], Failure> { + Publishers.Promise { self.eraseToAnyPublisher().collect() } + } + + public func count() -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().count() } + } + + public func first() -> Self { self } + + public func last() -> Self { self } + + public func replaceError(with output: Output) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().replaceError(with: output) } + } + + public func replaceEmpty(with output: Output) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().replaceEmpty(with: output) } + } + + + public func retry(_ times: Int) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().retry(times) } + } + + public func scan(_ initialResult: T, _ nextPartialResult: @escaping (T, Output) -> T) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().scan(initialResult, nextPartialResult) } + } + + public func tryScan(_ initialResult: T, _ nextPartialResult: @escaping (T, Output) throws -> T) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().tryScan(initialResult, nextPartialResult) } + } + + public func setFailureType(to failureType: E.Type) -> Publishers.Promise where Failure == Never { + Publishers.Promise { self.eraseToAnyPublisher().setFailureType(to: failureType) } + } + + public func delay( + for interval: S.SchedulerTimeType.Stride, + tolerance: S.SchedulerTimeType.Stride? = nil, + scheduler: S, + options: S.SchedulerOptions? = nil + ) -> Publishers.Promise { + Publishers.Promise { self.eraseToAnyPublisher().delay(for: interval, tolerance: tolerance, scheduler: scheduler, options: options) } + } + + public func timeout( + _ interval: S.SchedulerTimeType.Stride, + scheduler: S, + options: S.SchedulerOptions? = nil, + customError: @escaping () -> Failure + ) -> Publishers.Promise where S : Scheduler { + Publishers.Promise { + self.eraseToAnyPublisher().timeout(interval, scheduler: scheduler, options: options, customError: customError) + } + } + + @available(*, deprecated, message: "Don't call .promise in a Promise") + public var promise: Publishers.Promise { + self as? Publishers.Promise ?? Publishers.Promise { self } + } +} +#endif diff --git a/Sources/FoundationExtensions/Promise/PromiseType+Fold.swift b/Sources/FoundationExtensions/Promise/PromiseType+Fold.swift index fce2649..61a8ba7 100644 --- a/Sources/FoundationExtensions/Promise/PromiseType+Fold.swift +++ b/Sources/FoundationExtensions/Promise/PromiseType+Fold.swift @@ -26,8 +26,7 @@ extension PromiseType { public func fold(onSuccess: @escaping (Success) -> TargetType, onFailure: @escaping (Failure) -> TargetType) -> Publishers.Promise { map(onSuccess) - .catch { error in Just(onFailure(error)) } - .promise + .catch { error in .init(value: onFailure(error)) } } /// Case analysis for Promise. When Promises runs, this step will evaluate possible results, run actions for them and diff --git a/Sources/FoundationExtensions/Promise/PromiseType+Init.swift b/Sources/FoundationExtensions/Promise/PromiseType+Init.swift index 6aae250..d76a907 100644 --- a/Sources/FoundationExtensions/Promise/PromiseType+Init.swift +++ b/Sources/FoundationExtensions/Promise/PromiseType+Init.swift @@ -15,19 +15,19 @@ extension PromiseType { /// A promise from a hardcoded successful value /// - Parameter value: a hardcoded successful value. It's gonna be evaluated on demand from downstream public init(value: Success) { - self.init { Just(value).mapError(absurd).eraseToAnyPublisher() } + self.init { Empty().nonEmpty(fallback: { .success(value) }) } } /// A promise from a hardcoded error /// - Parameter error: a hardcoded error. It's gonna be evaluated on demand from downstream public init(error: Failure) { - self.init { Fail(error: error).map(absurd).eraseToAnyPublisher() } + self.init { Empty().nonEmpty(fallback: { .failure(error) }) } } /// A promise from a hardcoded result value /// - Parameter value: a hardcoded result value. It's gonna be evaluated on demand from downstream public init(result: Result) { - self.init { result.publisher.eraseToAnyPublisher() } + self.init { Empty().nonEmpty(fallback: { result }) } } /// Creates a new promise by evaluating a synchronous throwing closure, capturing the @@ -36,6 +36,8 @@ extension PromiseType { /// - Parameters: /// - body: A throwing closure to evaluate. /// - errorTransform: a way to transform the throwing error from type `Error` to type `Failure` of this `PromiseType` + // https://www.fivestars.blog/articles/disfavoredOverload/ + @_disfavoredOverload public init(catching body: @escaping () throws -> Success, errorTransform: (Error) -> Failure) { self.init(result: Result { try body() }.mapError(errorTransform)) } @@ -47,6 +49,8 @@ extension PromiseType where Failure == Error { /// returned value as a success, or any thrown error as a failure. /// /// - Parameter body: A throwing closure to evaluate. + // https://www.fivestars.blog/articles/disfavoredOverload/ + @_disfavoredOverload public init(catching body: @escaping () throws -> Success) { self.init(result: Result { try body() }) } diff --git a/Sources/FoundationExtensions/Promise/PromiseType+Zip.swift b/Sources/FoundationExtensions/Promise/PromiseType+Zip.swift index 6f0a246..34ddf69 100644 --- a/Sources/FoundationExtensions/Promise/PromiseType+Zip.swift +++ b/Sources/FoundationExtensions/Promise/PromiseType+Zip.swift @@ -11,107 +11,97 @@ import Combine @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension PromiseType { - /// Zips two promises in a promise. The upstream results will be merged with the provided map function. + /// Zips two promises in a promise. The upstream results will be paired into a tuple once all of them emit their values. /// This is useful for calling async operations in parallel, creating a race that waits for the slowest - /// one, so all of them will complete, be mapped and then forwarded to the downstream. + /// one, so all of them will complete, be combined in a tuple and then forwarded to the downstream. /// - Parameters: /// - p1: first promise /// - p2: second promise - /// - map: merging all the upstream outputs in the downstream output - /// - Returns: a new promise that will complete when all upstreams gave their results and the were - /// mapped into the downstream output + /// - Returns: a new promise that will complete when all upstreams gave their results and they were combined into a tuple public static func zip( _ p1: P1, - _ p2: P2, - map: @escaping (P1.Output, P2.Output) -> Output - ) -> Publishers.Promise where P1.Failure == Failure, P2.Failure == Failure { + _ p2: P2 + ) -> Publishers.Promise<(P1.Output, P2.Output), Failure> + where P1.Failure == Failure, P2.Failure == Failure, + Output == (P1.Output, P2.Output) { Publishers.Promise { Publishers.Zip(p1, p2) - .map(map) } } - /// Zips three promises in a promise. The upstream results will be merged with the provided map function. + /// Zips three promises in a promise. The upstream results will be paired into a tuple once all of them emit their values. /// This is useful for calling async operations in parallel, creating a race that waits for the slowest - /// one, so all of them will complete, be mapped and then forwarded to the downstream. + /// one, so all of them will complete, be combined in a tuple and then forwarded to the downstream. /// - Parameters: /// - p1: first promise /// - p2: second promise /// - p3: third promise - /// - map: merging all the upstream outputs in the downstream output - /// - Returns: a new promise that will complete when all upstreams gave their results and the were - /// mapped into the downstream output + /// - Returns: a new promise that will complete when all upstreams gave their results and they were combined into a tuple public static func zip( _ p1: P1, _ p2: P2, - _ p3: P3, - map: @escaping (P1.Output, P2.Output, P3.Output) -> Output - ) -> Publishers.Promise where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure { + _ p3: P3 + ) -> Publishers.Promise<(P1.Output, P2.Output, P3.Output), Failure> + where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, + Output == (P1.Output, P2.Output, P3.Output) { Publishers.Promise { Publishers.Zip3(p1, p2, p3) - .map(map) } } - /// Zips four promises in a promise. The upstream results will be merged with the provided map function. + /// Zips four promises in a promise. The upstream results will be paired into a tuple once all of them emit their values. /// This is useful for calling async operations in parallel, creating a race that waits for the slowest - /// one, so all of them will complete, be mapped and then forwarded to the downstream. + /// one, so all of them will complete, be combined in a tuple and then forwarded to the downstream. /// - Parameters: /// - p1: first promise /// - p2: second promise /// - p3: third promise /// - p4: fourth promise - /// - map: merging all the upstream outputs in the downstream output - /// - Returns: a new promise that will complete when all upstreams gave their results and the were - /// mapped into the downstream output + /// - Returns: a new promise that will complete when all upstreams gave their results and they were combined into a tuple public static func zip( _ p1: P1, _ p2: P2, _ p3: P3, - _ p4: P4, - map: @escaping (P1.Output, P2.Output, P3.Output, P4.Output) -> Output - ) -> Publishers.Promise where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure { + _ p4: P4 + ) -> Publishers.Promise<(P1.Output, P2.Output, P3.Output, P4.Output), Failure> + where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, + Output == (P1.Output, P2.Output, P3.Output, P4.Output) { Publishers.Promise { Publishers.Zip4(p1, p2, p3, p4) - .map(map) } } - /// Zips five promises in a promise. The upstream results will be merged with the provided map function. + /// Zips five promises in a promise. The upstream results will be paired into a tuple once all of them emit their values. /// This is useful for calling async operations in parallel, creating a race that waits for the slowest - /// one, so all of them will complete, be mapped and then forwarded to the downstream. + /// one, so all of them will complete, be combined in a tuple and then forwarded to the downstream. /// - Parameters: /// - p1: first promise /// - p2: second promise /// - p3: third promise /// - p4: fourth promise /// - p5: fifth promise - /// - map: merging all the upstream outputs in the downstream output - /// - Returns: a new promise that will complete when all upstreams gave their results and the were - /// mapped into the downstream output + /// - Returns: a new promise that will complete when all upstreams gave their results and they were combined into a tuple public static func zip( _ p1: P1, _ p2: P2, _ p3: P3, _ p4: P4, - _ p5: P5, - map: @escaping (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output) -> Output - ) -> Publishers.Promise where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, - P5.Failure == Failure { + _ p5: P5 + ) -> Publishers.Promise<(P1.Output, P2.Output, P3.Output, P4.Output, P5.Output), Failure> + where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, P5.Failure == Failure, + Output == (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output) { Publishers.Promise { Publishers.Zip( Publishers.Zip4(p1, p2, p3, p4), p5 ) - .map { tuple -> Output in - map(tuple.0.0, tuple.0.1, tuple.0.2, tuple.0.3, tuple.1) - } + .map { left, right in (left.0, left.1, left.2, left.3, right) } } } - /// Zips six promises in a promise. The upstream results will be merged with the provided map function. + /// Zips six promises in a promise. The upstream results will be paired into a tuple once all of them emit their values. /// This is useful for calling async operations in parallel, creating a race that waits for the slowest - /// one, so all of them will complete, be mapped and then forwarded to the downstream. + /// one, so all of them will complete, be combined in a tuple and then forwarded to the downstream. /// - Parameters: /// - p1: first promise /// - p2: second promise @@ -119,33 +109,29 @@ extension PromiseType { /// - p4: fourth promise /// - p5: fifth promise /// - p6: sixth promise - /// - map: merging all the upstream outputs in the downstream output - /// - Returns: a new promise that will complete when all upstreams gave their results and the were - /// mapped into the downstream output + /// - Returns: a new promise that will complete when all upstreams gave their results and they were combined into a tuple public static func zip( _ p1: P1, _ p2: P2, _ p3: P3, _ p4: P4, _ p5: P5, - _ p6: P6, - map: @escaping (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output) -> Output - ) -> Publishers.Promise where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, - P5.Failure == Failure, P6.Failure == Failure { + _ p6: P6 + ) -> Publishers.Promise<(P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output), Failure> + where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, P5.Failure == Failure, P6.Failure == Failure, + Output == (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output) { Publishers.Promise { Publishers.Zip( Publishers.Zip4(p1, p2, p3, p4), Publishers.Zip(p5, p6) ) - .map { tuple -> Output in - map(tuple.0.0, tuple.0.1, tuple.0.2, tuple.0.3, tuple.1.0, tuple.1.1) - } + .map { left, right in (left.0, left.1, left.2, left.3, right.0, right.1) } } } - /// Zips seven promises in a promise. The upstream results will be merged with the provided map function. + /// Zips seven promises in a promise. The upstream results will be paired into a tuple once all of them emit their values. /// This is useful for calling async operations in parallel, creating a race that waits for the slowest - /// one, so all of them will complete, be mapped and then forwarded to the downstream. + /// one, so all of them will complete, be combined in a tuple and then forwarded to the downstream. /// - Parameters: /// - p1: first promise /// - p2: second promise @@ -154,9 +140,7 @@ extension PromiseType { /// - p5: fifth promise /// - p6: sixth promise /// - p7: seventh promise - /// - map: merging all the upstream outputs in the downstream output - /// - Returns: a new promise that will complete when all upstreams gave their results and the were - /// mapped into the downstream output + /// - Returns: a new promise that will complete when all upstreams gave their results and they were combined into a tuple public static func zip( _ p1: P1, _ p2: P2, @@ -164,24 +148,23 @@ extension PromiseType { _ p4: P4, _ p5: P5, _ p6: P6, - _ p7: P7, - map: @escaping (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output) -> Output - ) -> Publishers.Promise where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, - P5.Failure == Failure, P6.Failure == Failure, P7.Failure == Failure { + _ p7: P7 + ) -> Publishers.Promise<(P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output), Failure> + where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, P5.Failure == Failure, P6.Failure == Failure, + P7.Failure == Failure, + Output == (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output) { Publishers.Promise { Publishers.Zip( Publishers.Zip4(p1, p2, p3, p4), Publishers.Zip3(p5, p6, p7) ) - .map { tuple -> Output in - map(tuple.0.0, tuple.0.1, tuple.0.2, tuple.0.3, tuple.1.0, tuple.1.1, tuple.1.2) - } + .map { left, right in (left.0, left.1, left.2, left.3, right.0, right.1, right.2) } } } - /// Zips eight promises in a promise. The upstream results will be merged with the provided map function. + /// Zips eight promises in a promise. The upstream results will be paired into a tuple once all of them emit their values. /// This is useful for calling async operations in parallel, creating a race that waits for the slowest - /// one, so all of them will complete, be mapped and then forwarded to the downstream. + /// one, so all of them will complete, be combined in a tuple and then forwarded to the downstream. /// - Parameters: /// - p1: first promise /// - p2: second promise @@ -191,11 +174,9 @@ extension PromiseType { /// - p6: sixth promise /// - p7: seventh promise /// - p8: eighth promise - /// - map: merging all the upstream outputs in the downstream output - /// - Returns: a new promise that will complete when all upstreams gave their results and the were - /// mapped into the downstream output - public static func zip < P1: PromiseType, P2: PromiseType, P3: PromiseType, P4: PromiseType, P5: PromiseType, P6: PromiseType, P7: PromiseType, - P8: PromiseType > ( + /// - Returns: a new promise that will complete when all upstreams gave their results and they were combined into a tuple + public static func zip ( _ p1: P1, _ p2: P2, _ p3: P3, @@ -203,24 +184,23 @@ extension PromiseType { _ p5: P5, _ p6: P6, _ p7: P7, - _ p8: P8, - map: @escaping (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output) -> Output - ) -> Publishers.Promise where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, - P5.Failure == Failure, P6.Failure == Failure, P7.Failure == Failure, P8.Failure == Failure { + _ p8: P8 + ) -> Publishers.Promise<(P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output), Failure> + where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, P5.Failure == Failure, P6.Failure == Failure, + P7.Failure == Failure, P8.Failure == Failure, + Output == (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output) { Publishers.Promise { Publishers.Zip( Publishers.Zip4(p1, p2, p3, p4), Publishers.Zip4(p5, p6, p7, p8) ) - .map { tuple -> Output in - map(tuple.0.0, tuple.0.1, tuple.0.2, tuple.0.3, tuple.1.0, tuple.1.1, tuple.1.2, tuple.1.3) - } + .map { left, right in (left.0, left.1, left.2, left.3, right.0, right.1, right.2, right.3) } } } - /// Zips nine promises in a promise. The upstream results will be merged with the provided map function. + /// Zips nine promises in a promise. The upstream results will be paired into a tuple once all of them emit their values. /// This is useful for calling async operations in parallel, creating a race that waits for the slowest - /// one, so all of them will complete, be mapped and then forwarded to the downstream. + /// one, so all of them will complete, be combined in a tuple and then forwarded to the downstream. /// - Parameters: /// - p1: first promise /// - p2: second promise @@ -231,11 +211,9 @@ extension PromiseType { /// - p7: seventh promise /// - p8: eighth promise /// - p9: ninth promise - /// - map: merging all the upstream outputs in the downstream output - /// - Returns: a new promise that will complete when all upstreams gave their results and the were - /// mapped into the downstream output - public static func zip < P1: PromiseType, P2: PromiseType, P3: PromiseType, P4: PromiseType, P5: PromiseType, P6: PromiseType, P7: PromiseType, - P8: PromiseType, P9: PromiseType > ( + /// - Returns: a new promise that will complete when all upstreams gave their results and they were combined into a tuple + public static func zip ( _ p1: P1, _ p2: P2, _ p3: P3, @@ -244,26 +222,24 @@ extension PromiseType { _ p6: P6, _ p7: P7, _ p8: P8, - _ p9: P9, - map: @escaping (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output, P9.Output) -> Output - ) -> Publishers.Promise where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, - P5.Failure == Failure, P6.Failure == Failure, P7.Failure == Failure, P8.Failure == Failure, - P9.Failure == Failure { + _ p9: P9 + ) -> Publishers.Promise<(P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output, P9.Output), Failure> + where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, P5.Failure == Failure, P6.Failure == Failure, + P7.Failure == Failure, P8.Failure == Failure, P9.Failure == Failure, + Output == (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output, P9.Output) { Publishers.Promise { Publishers.Zip3( Publishers.Zip4(p1, p2, p3, p4), Publishers.Zip4(p5, p6, p7, p8), p9 ) - .map { tuple -> Output in - map(tuple.0.0, tuple.0.1, tuple.0.2, tuple.0.3, tuple.1.0, tuple.1.1, tuple.1.2, tuple.1.3, tuple.2) - } + .map { left, center, right in (left.0, left.1, left.2, left.3, center.0, center.1, center.2, center.3, right) } } } - /// Zips ten promises in a promise. The upstream results will be merged with the provided map function. + /// Zips ten promises in a promise. The upstream results will be paired into a tuple once all of them emit their values. /// This is useful for calling async operations in parallel, creating a race that waits for the slowest - /// one, so all of them will complete, be mapped and then forwarded to the downstream. + /// one, so all of them will complete, be combined in a tuple and then forwarded to the downstream. /// - Parameters: /// - p1: first promise /// - p2: second promise @@ -275,11 +251,9 @@ extension PromiseType { /// - p8: eighth promise /// - p9: ninth promise /// - p10: tenth promise - /// - map: merging all the upstream outputs in the downstream output - /// - Returns: a new promise that will complete when all upstreams gave their results and the were - /// mapped into the downstream output - public static func zip < P1: PromiseType, P2: PromiseType, P3: PromiseType, P4: PromiseType, P5: PromiseType, P6: PromiseType, P7: PromiseType, - P8: PromiseType, P9: PromiseType, P10: PromiseType > ( + /// - Returns: a new promise that will complete when all upstreams gave their results and they were combined into a tuple + public static func zip ( _ p1: P1, _ p2: P2, _ p3: P3, @@ -289,26 +263,24 @@ extension PromiseType { _ p7: P7, _ p8: P8, _ p9: P9, - _ p10: P10, - map: @escaping (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output, P9.Output, P10.Output) -> Output - ) -> Publishers.Promise where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, - P5.Failure == Failure, P6.Failure == Failure, P7.Failure == Failure, P8.Failure == Failure, - P9.Failure == Failure, P10.Failure == Failure { + _ p10: P10 + ) -> Publishers.Promise<(P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output, P9.Output, P10.Output), Failure> + where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, P5.Failure == Failure, P6.Failure == Failure, + P7.Failure == Failure, P8.Failure == Failure, P9.Failure == Failure, P10.Failure == Failure, + Output == (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output, P9.Output, P10.Output) { Publishers.Promise { Publishers.Zip3( Publishers.Zip4(p1, p2, p3, p4), Publishers.Zip4(p5, p6, p7, p8), Publishers.Zip(p9, p10) ) - .map { tuple -> Output in - map(tuple.0.0, tuple.0.1, tuple.0.2, tuple.0.3, tuple.1.0, tuple.1.1, tuple.1.2, tuple.1.3, tuple.2.0, tuple.2.1) - } + .map { left, center, right in (left.0, left.1, left.2, left.3, center.0, center.1, center.2, center.3, right.0, right.1) } } } - /// Zips eleven promises in a promise. The upstream results will be merged with the provided map function. + /// Zips eleven promises in a promise. The upstream results will be paired into a tuple once all of them emit their values. /// This is useful for calling async operations in parallel, creating a race that waits for the slowest - /// one, so all of them will complete, be mapped and then forwarded to the downstream. + /// one, so all of them will complete, be combined in a tuple and then forwarded to the downstream. /// - Parameters: /// - p1: first promise /// - p2: second promise @@ -321,9 +293,7 @@ extension PromiseType { /// - p9: ninth promise /// - p10: tenth promise /// - p11: eleventh promise - /// - map: merging all the upstream outputs in the downstream output - /// - Returns: a new promise that will complete when all upstreams gave their results and the were - /// mapped into the downstream output + /// - Returns: a new promise that will complete when all upstreams gave their results and they were combined into a tuple public static func zip < P1: PromiseType, P2: PromiseType, P3: PromiseType, P4: PromiseType, P5: PromiseType, P6: PromiseType, P7: PromiseType, P8: PromiseType, P9: PromiseType, P10: PromiseType, P11: PromiseType > ( _ p1: P1, @@ -336,27 +306,25 @@ extension PromiseType { _ p8: P8, _ p9: P9, _ p10: P10, - _ p11: P11, - map: @escaping (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output, P9.Output, P10.Output, - P11.Output) -> Output - ) -> Publishers.Promise where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, - P5.Failure == Failure, P6.Failure == Failure, P7.Failure == Failure, P8.Failure == Failure, - P9.Failure == Failure, P10.Failure == Failure, P11.Failure == Failure { + _ p11: P11 + ) -> Publishers.Promise<(P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output, P9.Output, P10.Output, + P11.Output), Failure> + where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, P5.Failure == Failure, P6.Failure == Failure, + P7.Failure == Failure, P8.Failure == Failure, P9.Failure == Failure, P10.Failure == Failure, P11.Failure == Failure, + Output == (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output, P9.Output, P10.Output, P11.Output) { Publishers.Promise { Publishers.Zip3( Publishers.Zip4(p1, p2, p3, p4), Publishers.Zip4(p5, p6, p7, p8), Publishers.Zip3(p9, p10, p11) ) - .map { tuple -> Output in - map(tuple.0.0, tuple.0.1, tuple.0.2, tuple.0.3, tuple.1.0, tuple.1.1, tuple.1.2, tuple.1.3, tuple.2.0, tuple.2.1, tuple.2.2) - } + .map { left, center, right in (left.0, left.1, left.2, left.3, center.0, center.1, center.2, center.3, right.0, right.1, right.2) } } } - /// Zips twelve promises in a promise. The upstream results will be merged with the provided map function. + /// Zips twelve promises in a promise. The upstream results will be paired into a tuple once all of them emit their values. /// This is useful for calling async operations in parallel, creating a race that waits for the slowest - /// one, so all of them will complete, be mapped and then forwarded to the downstream. + /// one, so all of them will complete, be combined in a tuple and then forwarded to the downstream. /// - Parameters: /// - p1: first promise /// - p2: second promise @@ -370,11 +338,9 @@ extension PromiseType { /// - p10: tenth promise /// - p11: eleventh promise /// - p12: twelfth promise - /// - map: merging all the upstream outputs in the downstream output - /// - Returns: a new promise that will complete when all upstreams gave their results and the were - /// mapped into the downstream output - public static func zip < P1: PromiseType, P2: PromiseType, P3: PromiseType, P4: PromiseType, P5: PromiseType, P6: PromiseType, P7: PromiseType, - P8: PromiseType, P9: PromiseType, P10: PromiseType, P11: PromiseType, P12: PromiseType > ( + /// - Returns: a new promise that will complete when all upstreams gave their results and they were combined into a tuple + public static func zip ( _ p1: P1, _ p2: P2, _ p3: P3, @@ -386,21 +352,22 @@ extension PromiseType { _ p9: P9, _ p10: P10, _ p11: P11, - _ p12: P12, - map: @escaping (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output, P9.Output, P10.Output, P11.Output, - P12.Output) -> Output - ) -> Publishers.Promise where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, - P5.Failure == Failure, P6.Failure == Failure, P7.Failure == Failure, P8.Failure == Failure, - P9.Failure == Failure, P10.Failure == Failure, P11.Failure == Failure, P12.Failure == Failure { + _ p12: P12 + ) -> Publishers.Promise<(P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output, P9.Output, P10.Output, P11.Output, + P12.Output), Failure> + where P1.Failure == Failure, P2.Failure == Failure, P3.Failure == Failure, P4.Failure == Failure, P5.Failure == Failure, P6.Failure == Failure, + P7.Failure == Failure, P8.Failure == Failure, P9.Failure == Failure, P10.Failure == Failure, P11.Failure == Failure, + P12.Failure == Failure, + Output == (P1.Output, P2.Output, P3.Output, P4.Output, P5.Output, P6.Output, P7.Output, P8.Output, P9.Output, P10.Output, P11.Output, + P12.Output) { Publishers.Promise { Publishers.Zip3( Publishers.Zip4(p1, p2, p3, p4), Publishers.Zip4(p5, p6, p7, p8), Publishers.Zip4(p9, p10, p11, p12) ) - .map { tuple -> Output in - map(tuple.0.0, tuple.0.1, tuple.0.2, tuple.0.3, tuple.1.0, tuple.1.1, tuple.1.2, tuple.1.3, tuple.2.0, tuple.2.1, tuple.2.2, - tuple.2.3) + .map { left, center, right in + (left.0, left.1, left.2, left.3, center.0, center.1, center.2, center.3, right.0, right.1, right.2, right.3) } } } diff --git a/Sources/FoundationExtensions/Promise/PromiseType.swift b/Sources/FoundationExtensions/Promise/PromiseType.swift index 5c84abb..d90a86b 100644 --- a/Sources/FoundationExtensions/Promise/PromiseType.swift +++ b/Sources/FoundationExtensions/Promise/PromiseType.swift @@ -17,6 +17,6 @@ public protocol PromiseType: PromiseConvertibleType, Publisher where Success == /// A promise from an upstream publisher. Because this is an closure parameter, the upstream will become a factory /// and its creation will be deferred until there's some positive demand from the downstream. /// - Parameter upstream: a closure that creates an upstream publisher. This is an closure, so creation will be deferred. - init(_ upstream: @escaping () -> P) where P.Output == Success, P.Failure == Failure + init(_ upstream: @escaping () -> NonEmptyPublisher

) where P.Output == Success, P.Failure == Failure } #endif diff --git a/Sources/FoundationExtensions/Promise/Publisher+NonEmpty.swift b/Sources/FoundationExtensions/Promise/Publisher+NonEmpty.swift new file mode 100644 index 0000000..5140f6f --- /dev/null +++ b/Sources/FoundationExtensions/Promise/Publisher+NonEmpty.swift @@ -0,0 +1,19 @@ +// +// Publisher+PromiseConvertibleType.swift +// FoundationExtensions +// +// Created by Luiz Rodrigo Martins Barbosa on 19.04.21. +// Copyright © 2021 Lautsprecher Teufel GmbH. All rights reserved. +// + +#if canImport(Combine) +import Combine +import Foundation + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Publisher { + public func nonEmpty(fallback: @escaping () -> Result) -> NonEmptyPublisher { + NonEmptyPublisher(upstream: self, onEmpty: fallback) + } +} +#endif diff --git a/Sources/FoundationExtensions/Promise/Publisher+PromiseConvertibleType.swift b/Sources/FoundationExtensions/Promise/Publisher+PromiseConvertibleType.swift index ae47e88..9033cb8 100644 --- a/Sources/FoundationExtensions/Promise/Publisher+PromiseConvertibleType.swift +++ b/Sources/FoundationExtensions/Promise/Publisher+PromiseConvertibleType.swift @@ -10,6 +10,23 @@ import Combine import Foundation +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension NonEmptyPublisher: PromiseConvertibleType { + /// A Promise is a Publisher that lives in between First and Deferred. + /// It will listen to one and only one event, and finish successfully, or finish with error without any successful output. + /// It will hang until an event arrives from this upstream. + /// Calling it from `promise` property on the instance, means that this publisher was already created and can't be deferred + /// anymore. If you want to profit from lazy evaluation of Promise it's recommended to use the `Promise.init(Publisher)`, + /// that uses a closures to create the publisher and makes it lazy until the downstream sends demand (`.demand > .none`). + /// That way, you can safely add `Future` as upstream, for example, and be sure that its side-effect won't be started. The + /// behaviour, then, will be similar to `Deferred>`, however with some extra features such as better + /// zips, and a run function to easily start the effect. The cancellation is possible and will be forwarded to the upstream + /// if the effect had already started. + public var promise: Publishers.Promise { + Publishers.Promise { self } + } +} + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension Publisher { /// A Promise is a Publisher that lives in between First and Deferred. @@ -22,7 +39,61 @@ extension Publisher { /// behaviour, then, will be similar to `Deferred>`, however with some extra features such as better /// zips, and a run function to easily start the effect. The cancellation is possible and will be forwarded to the upstream /// if the effect had already started. + public func promise(onEmpty fallback: @escaping () -> Result) -> Publishers.Promise { + self.nonEmpty(fallback: fallback).promise + } + + public func assertNonEmptyPromise() -> Publishers.Promise { + self.nonEmpty(fallback: { fatalError("Empty promise, asserting it non-empty") }).promise + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Just: PromiseConvertibleType { + public var promise: Publishers.Promise { + Publishers.Promise { self } + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Fail: PromiseConvertibleType { + public var promise: Publishers.Promise { + Publishers.Promise { self } + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Result: PromiseConvertibleType { + public var promise: Publishers.Promise { + Publishers.Promise(result: self) + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Publishers.ReplaceEmpty: PromiseConvertibleType { + public var promise: Publishers.Promise { + Publishers.Promise { self } + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Result.Publisher: PromiseConvertibleType { + public var promise: Publishers.Promise { + Publishers.Promise { self } + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Future: PromiseConvertibleType { + public var promise: Publishers.Promise { + Publishers.Promise { self } + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension URLSession.DataTaskPublisher { public var promise: Publishers.Promise { Publishers.Promise { self } } -}#endif +} +#endif diff --git a/Sources/FoundationExtensions/Promise/Result+PromiseConvertibleType.swift b/Sources/FoundationExtensions/Promise/Result+PromiseConvertibleType.swift deleted file mode 100644 index b095a17..0000000 --- a/Sources/FoundationExtensions/Promise/Result+PromiseConvertibleType.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Result+PromiseConvertibleType.swift -// FoundationExtensions -// -// Created by Luiz Barbosa on 29.05.20. -// Copyright © 2020 Lautsprecher Teufel GmbH. All rights reserved. -// - -#if canImport(Combine) -import Combine - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension Result: PromiseConvertibleType { - public typealias Output = Success - public typealias Failure = Failure - - /// A Promise is a Publisher that lives in between First and Deferred. - /// It will listen to one and only one event, and finish successfully, or finish with error without any successful output. - /// It will hang until an event arrives from upstream. If the upstream is eager, it will be deferred, that means it won't - /// be created (therefore, no side-effect possible) until the downstream sends demand (`.demand > .none`). This, of course, - /// if it was not created yet before passed to this initializer. - /// That way, you can safely add `Future` as upstream, for example, and be sure that its side-effect won't be started. The - /// behaviour, then, will be similar to `Deferred>`, however with some extra features such as better - /// zips, and a run function to easily start the effect. The cancellation is possible and will be forwarded to the upstream - /// if the effect had already started. - /// This property will create a Promise that, upon subscription and `demand > .none` will emit success or failure, depending - /// on this `Result` instance. - public var promise: Publishers.Promise { - Publishers.Promise(result: self) - } -} -#endif diff --git a/Sources/FoundationExtensions/Timer/TimerProtocol.swift b/Sources/FoundationExtensions/Timer/TimerProtocol.swift index 2186519..ffe28b6 100644 --- a/Sources/FoundationExtensions/Timer/TimerProtocol.swift +++ b/Sources/FoundationExtensions/Timer/TimerProtocol.swift @@ -9,7 +9,7 @@ import Foundation // sourcery: AutoMockable -public protocol TimerProtocol: class { +public protocol TimerProtocol: AnyObject { func fire() var fireDate: Date { get set } var timeInterval: TimeInterval { get } diff --git a/Tests/FoundationExtensionsTests/Promise/PromiseTests.swift b/Tests/FoundationExtensionsTests/Promise/PromiseTests.swift new file mode 100644 index 0000000..7fe76d6 --- /dev/null +++ b/Tests/FoundationExtensionsTests/Promise/PromiseTests.swift @@ -0,0 +1,305 @@ +// +// PromiseTests.swift +// FoundationExtensionsTests +// +// Created by Luiz Rodrigo Martins Barbosa on 19.03.21. +// Copyright © 2021 Lautsprecher Teufel GmbH. All rights reserved. +// + +#if !os(watchOS) +import Combine +import FoundationExtensions +import XCTest + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +class PromiseTests: XCTestCase { + func testInitWithUpstreamThatSendsOneValueAndCompletes() { + let subject = PassthroughSubject() + let promise = Publishers.Promise { subject.nonEmpty(fallback: { .failure("wrooong") }) } + let waiter = assert(publisher: promise, eventuallyReceives: "!@$%@*#$", andCompletes: true, timeout: 0.00001) + subject.send("!@$%@*#$") + subject.send(completion: .finished) + waiter() + } + + func testInitWithUpstreamThatSendsOneValueAndNeverCompletes() { + let subject = PassthroughSubject() + let promise = Publishers.Promise { subject.nonEmpty(fallback: { .failure("wrooong") }) } + let waiter = assert(publisher: promise, eventuallyReceives: "!@$%@*#$", andCompletes: true, timeout: 0.00001) + subject.send("!@$%@*#$") + waiter() + } + + func testInitWithUpstreamThatSendsMultipleValuesAndNeverCompletes() { + let subject = PassthroughSubject() + let promise = Publishers.Promise { subject.nonEmpty(fallback: { .failure("wrooong") }) } + let waiter = assert(publisher: promise, eventuallyReceives: "!@$%@*#$", andCompletes: true, timeout: 0.00001) + subject.send("!@$%@*#$") + subject.send("will") + subject.send("never") + subject.send("get") + subject.send("this") + waiter() + } + + func testInitWithUpstreamThatCompletesWithErrorBeforeValues() { + let subject = PassthroughSubject() + let promise = Publishers.Promise { subject.nonEmpty(fallback: { .success("wrooong") }) } + let waiter = assert( + publisher: promise, + completesWithoutValues: .failedWithError("cataploft!"), + timeout: 0.00001 + ) + subject.send(completion: .failure("cataploft!")) + waiter() + } + + func testInitWithUpstreamThatCompletesWithErrorAfterSendingValueSoPromiseIgnoresTheError() { + let subject = PassthroughSubject() + let promise = Publishers.Promise { subject.nonEmpty(fallback: { .failure("cataploft!") }) } + let waiter = assert(publisher: promise, eventuallyReceives: "!@$%@*#$", andCompletes: true, timeout: 0.00001) + subject.send("!@$%@*#$") + subject.send(completion: .failure("cataploft!")) + waiter() + } + + func testInitWithUpstreamThatCompletesWithoutValuesButFallsbackToSuccess() { + let subject = PassthroughSubject() + let promise = Publishers.Promise { subject.nonEmpty(fallback: { .success("hey, nice fallback, dude") }) } + let waiter = assert( + publisher: promise, + eventuallyReceives: "hey, nice fallback, dude", + andCompletes: true, + timeout: 0.00001 + ) + subject.send(completion: .finished) + waiter() + } + + func testInitWithUpstreamThatCompletesWithoutValuesButFallsbackToFailure() { + let subject = PassthroughSubject() + let promise = Publishers.Promise { subject.nonEmpty(fallback: { .failure("failure fallback") }) } + let waiter = assert( + publisher: promise, + completesWithoutValues: .failedWithError("failure fallback"), + timeout: 0.00001 + ) + subject.send(completion: .finished) + waiter() + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension PromiseTests { + func testInitWithClosureThatSendsOneValueAndCompletes() { + let subject = PassthroughSubject() + let promise = Publishers.Promise { completion in + subject.sink( + receiveCompletion: { result in + if case let .failure(error) = result { + completion(.failure(error)) + } + }, + receiveValue: { value in + completion(.success(value)) + } + ) + } + let waiter = assert(publisher: promise, eventuallyReceives: "!@$%@*#$", andCompletes: true, timeout: 0.00001) + subject.send("!@$%@*#$") + subject.send(completion: .finished) + waiter() + } + + func testInitWithClosureThatSendsOneValueAndNeverCompletes() { + let subject = PassthroughSubject() + let promise = Publishers.Promise { completion in + subject.sink( + receiveCompletion: { result in + if case let .failure(error) = result { + completion(.failure(error)) + } + }, + receiveValue: { value in + completion(.success(value)) + } + ) + } + let waiter = assert(publisher: promise, eventuallyReceives: "!@$%@*#$", andCompletes: true, timeout: 0.00001) + subject.send("!@$%@*#$") + waiter() + } + + func testInitWithClosureThatSendsMultipleValuesAndNeverCompletes() { + let subject = PassthroughSubject() + let promise = Publishers.Promise { completion in + subject.sink( + receiveCompletion: { result in + if case let .failure(error) = result { + completion(.failure(error)) + } + }, + receiveValue: { value in + completion(.success(value)) + } + ) + } + let waiter = assert(publisher: promise, eventuallyReceives: "!@$%@*#$", andCompletes: true, timeout: 0.00001) + subject.send("!@$%@*#$") + subject.send("will") + subject.send("never") + subject.send("get") + subject.send("this") + waiter() + } + + func testInitWithClosureThatCompletesWithErrorBeforeValues() { + let subject = PassthroughSubject() + let promise = Publishers.Promise { completion in + subject.sink( + receiveCompletion: { result in + if case let .failure(error) = result { + completion(.failure(error)) + } + }, + receiveValue: { value in + completion(.success(value)) + } + ) + } + let waiter = assert( + publisher: promise, + completesWithoutValues: .failedWithError("cataploft!"), + timeout: 0.00001 + ) + subject.send(completion: .failure("cataploft!")) + waiter() + } + + func testInitWithClosureThatCompletesWithErrorAfterSendingValueSoPromiseIgnoresTheError() { + let subject = PassthroughSubject() + let promise = Publishers.Promise { completion in + subject.sink( + receiveCompletion: { result in + if case let .failure(error) = result { + completion(.failure(error)) + } + }, + receiveValue: { value in + completion(.success(value)) + } + ) + } + let waiter = assert(publisher: promise, eventuallyReceives: "!@$%@*#$", andCompletes: true, timeout: 0.00001) + subject.send("!@$%@*#$") + subject.send(completion: .failure("cataploft!")) + waiter() + } +} + +extension XCTestCase { + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + public func assert( + publisher: P, + eventuallyReceives values: P.Output..., + andCompletes: Bool = false, + timeout: TimeInterval + ) -> () -> Void where P.Output: Equatable { + var collectedValues: [P.Output] = [] + let valuesExpectation = expectation(description: "Expected values") + let completionExpectation = expectation(description: "Expected completion") + valuesExpectation.expectedFulfillmentCount = values.count + if !andCompletes { completionExpectation.fulfill() } + let cancellable = publisher.sink( + receiveCompletion: { result in + switch result { + case .finished: + if andCompletes { completionExpectation.fulfill() } + case let .failure(error): + XCTFail("Received failure: \(error)") + } + }, + receiveValue: { value in + collectedValues.append(value) + valuesExpectation.fulfill() + } + ) + + return { [weak self] in + guard let self = self else { + XCTFail("Test ended before waiting for expectations") + return + } + self.wait(for: [valuesExpectation, completionExpectation], timeout: timeout) + XCTAssertEqual(collectedValues, values, "Values don't match:\nreceived:\n\(collectedValues)\n\nexpected:\n\(values)") + _ = cancellable + } + } + + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + public func assert( + publisher: P, + completesWithoutValues: ValidateCompletion, + timeout: TimeInterval + ) -> () -> Void { + let completionExpectation = expectation(description: "Expected completion") + let cancellable = publisher.sink( + receiveCompletion: { result in + XCTAssertTrue(completesWithoutValues.isExpected(result), "Unexpected completion: \(result)") + completionExpectation.fulfill() + }, + receiveValue: { value in + XCTFail("Unexpected value received: \(value)") + } + ) + + return { [weak self] in + guard let self = self else { + XCTFail("Test ended before waiting for expectations") + return + } + self.wait(for: [completionExpectation], timeout: timeout) + _ = cancellable + } + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public struct ValidateCompletion { + let isExpected: (Subscribers.Completion) -> Bool + public init(validate: @escaping (Subscribers.Completion) -> Bool) { + self.isExpected = validate + } + + public static var isSuccess: ValidateCompletion { + ValidateCompletion { result in + guard case .finished = result else { return false } + return true + } + } + + public static var isFailure: ValidateCompletion { + ValidateCompletion { result in + guard case .failure = result else { return false } + return true + } + } + + public static func failedWithError(_ errorPredicate: @escaping (Failure) -> Bool) -> ValidateCompletion { + ValidateCompletion { result in + guard case let .failure(error) = result else { return false } + return errorPredicate(error) + } + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension ValidateCompletion where Failure: Equatable { + public static func failedWithError(_ expectedError: Failure) -> ValidateCompletion { + ValidateCompletion { result in + guard case let .failure(error) = result else { return false } + return error == expectedError + } + } +} +#endif