diff --git a/Sources/FoundationExtensions/Combine/AnyPublisher+Create.swift b/Sources/FoundationExtensions/Combine/AnyPublisher+Create.swift new file mode 100644 index 0000000..2752f97 --- /dev/null +++ b/Sources/FoundationExtensions/Combine/AnyPublisher+Create.swift @@ -0,0 +1,318 @@ +// CombineExt +// +// Created by Shai Mishali on 21/02/2020. +// Copyright © 2020 Combine Community. All rights reserved. + +#if canImport(Combine) +import Combine +import Foundation + +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension AnyPublisher { + /// Create a publisher which accepts a closure with a subscriber argument, + /// to which you can dynamically send value or completion events. + /// + /// You should return a `Cancelable`-conforming object from the closure in + /// which you can define any cleanup actions to execute when the publisher + /// completes or the subscription to the publisher is canceled. + /// + /// - parameter factory: A factory with a closure to which you can + /// dynamically send value or completion events. + /// You should return a `Cancelable`-conforming object + /// from it to encapsulate any cleanup-logic for your work. + /// + /// An example usage could look as follows: + /// + /// ``` + /// AnyPublisher.create { subscriber in + /// // Values + /// subscriber.send("Hello") + /// subscriber.send("World!") + /// + /// // Complete with error + /// subscriber.send(completion: .failure(MyError.someError)) + /// + /// // Or, complete successfully + /// subscriber.send(completion: .finished) + /// + /// return AnyCancellable { + /// // Perform clean-up + /// } + /// } + /// + init(_ factory: @escaping Publishers.Create.SubscriberHandler) { + self = Publishers.Create(factory: factory).eraseToAnyPublisher() + } + + /// Create a publisher which accepts a closure with a subscriber argument, + /// to which you can dynamically send value or completion events. + /// + /// You should return a `Cancelable`-conforming object from the closure in + /// which you can define any cleanup actions to execute when the publisher + /// completes or the subscription to the publisher is canceled. + /// + /// - parameter factory: A factory with a closure to which you can + /// dynamically send value or completion events. + /// You should return a `Cancelable`-conforming object + /// from it to encapsulate any cleanup-logic for your work. + /// + /// An example usage could look as follows: + /// + /// ``` + /// AnyPublisher.create { subscriber in + /// // Values + /// subscriber.send("Hello") + /// subscriber.send("World!") + /// + /// // Complete with error + /// subscriber.send(completion: .failure(MyError.someError)) + /// + /// // Or, complete successfully + /// subscriber.send(completion: .finished) + /// + /// return AnyCancellable { + /// // Perform clean-up + /// } + /// } + /// + static func create(_ factory: @escaping Publishers.Create.SubscriberHandler) + -> AnyPublisher { + AnyPublisher(factory) + } +} + +// MARK: - Publisher +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Publishers { + /// A publisher which accepts a closure with a subscriber argument, + /// to which you can dynamically send value or completion events. + /// + /// You should return a `Cancelable`-conforming object from the closure in + /// which you can define any cleanup actions to execute when the publisher + /// completes or the subscription to the publisher is canceled. + struct Create: Publisher { + typealias SubscriberHandler = (Subscriber) -> Cancellable + private let factory: SubscriberHandler + + /// Initialize the publisher with a provided factory + /// + /// - parameter factory: A factory with a closure to which you can + /// dynamically push value or completion events + init(factory: @escaping SubscriberHandler) { + self.factory = factory + } + + func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { + subscriber.receive(subscription: Subscription(factory: factory, downstream: subscriber)) + } + } +} + +// MARK: - Subscription +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +private extension Publishers.Create { + class Subscription: Combine.Subscription where Output == Downstream.Input, Failure == Downstream.Failure { + private let buffer: DemandBuffer + private var cancelable: Cancellable? + + init(factory: @escaping SubscriberHandler, + downstream: Downstream) { + self.buffer = DemandBuffer(subscriber: downstream) + + let subscriber = Subscriber(onValue: { [weak self] in _ = self?.buffer.buffer(value: $0) }, + onCompletion: { [weak self] in self?.buffer.complete(completion: $0) }) + + self.cancelable = factory(subscriber) + } + + func request(_ demand: Subscribers.Demand) { + _ = self.buffer.demand(demand) + } + + func cancel() { + self.cancelable?.cancel() + } + } +} + +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Publishers.Create.Subscription: CustomStringConvertible { + var description: String { + return "Create.Subscription<\(Output.self), \(Failure.self)>" + } +} + +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Publishers.Create { + struct Subscriber { + private let onValue: (Output) -> Void + private let onCompletion: (Subscribers.Completion) -> Void + + fileprivate init(onValue: @escaping (Output) -> Void, + onCompletion: @escaping (Subscribers.Completion) -> Void) { + self.onValue = onValue + self.onCompletion = onCompletion + } + + /// Sends a value to the subscriber. + /// + /// - Parameter value: The value to send. + func send(_ input: Output) { + onValue(input) + } + + /// Sends a completion event to the subscriber. + /// + /// - Parameter completion: A `Completion` instance which indicates whether publishing has finished normally or failed with an error. + func send(completion: Subscribers.Completion) { + onCompletion(completion) + } + } +} +#endif + +#if canImport(Combine) +import Combine +import class Foundation.NSRecursiveLock + +/// A buffer responsible for managing the demand of a downstream +/// subscriber for an upstream publisher +/// +/// It buffers values and completion events and forwards them dynamically +/// according to the demand requested by the downstream +/// +/// In a sense, the subscription only relays the requests for demand, as well +/// the events emitted by the upstream — to this buffer, which manages +/// the entire behavior and backpressure contract +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +class DemandBuffer { + private let lock = NSRecursiveLock() + private var buffer = [S.Input]() + private let subscriber: S + private var completion: Subscribers.Completion? + private var demandState = Demand() + + /// Initialize a new demand buffer for a provided downstream subscriber + /// + /// - parameter subscriber: The downstream subscriber demanding events + init(subscriber: S) { + self.subscriber = subscriber + } + + /// Buffer an upstream value to later be forwarded to + /// the downstream subscriber, once it demands it + /// + /// - parameter value: Upstream value to buffer + /// + /// - returns: The demand fulfilled by the bufferr + func buffer(value: S.Input) -> Subscribers.Demand { + precondition(self.completion == nil, + "How could a completed publisher sent values?! Beats me 🤷‍♂️") + lock.lock() + defer { lock.unlock() } + + switch demandState.requested { + case .unlimited: + return subscriber.receive(value) + default: + buffer.append(value) + return flush() + } + } + + /// Complete the demand buffer with an upstream completion event + /// + /// This method will deplete the buffer immediately, + /// based on the currently accumulated demand, and relay the + /// completion event down as soon as demand is fulfilled + /// + /// - parameter completion: Completion event + func complete(completion: Subscribers.Completion) { + precondition(self.completion == nil, + "Completion have already occured, which is quite awkward 🥺") + + self.completion = completion + _ = flush() + } + + /// Signal to the buffer that the downstream requested new demand + /// + /// - note: The buffer will attempt to flush as many events rqeuested + /// by the downstream at this point + func demand(_ demand: Subscribers.Demand) -> Subscribers.Demand { + flush(adding: demand) + } + + /// Flush buffered events to the downstream based on the current + /// state of the downstream's demand + /// + /// - parameter newDemand: The new demand to add. If `nil`, the flush isn't the + /// result of an explicit demand change + /// + /// - note: After fulfilling the downstream's request, if completion + /// has already occured, the buffer will be cleared and the + /// completion event will be sent to the downstream subscriber + private func flush(adding newDemand: Subscribers.Demand? = nil) -> Subscribers.Demand { + lock.lock() + defer { lock.unlock() } + + if let newDemand = newDemand { + demandState.requested += newDemand + } + + // If buffer isn't ready for flushing, return immediately + guard demandState.requested > 0 || newDemand == Subscribers.Demand.none else { return .none } + + while !buffer.isEmpty && demandState.processed < demandState.requested { + demandState.requested += subscriber.receive(buffer.remove(at: 0)) + demandState.processed += 1 + } + + if let completion = completion { + // Completion event was already sent + buffer = [] + demandState = .init() + self.completion = nil + subscriber.receive(completion: completion) + return .none + } + + let sentDemand = demandState.requested - demandState.sent + demandState.sent += sentDemand + return sentDemand + } +} + +// MARK: - Private Helpers +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +private extension DemandBuffer { + /// A model that tracks the downstream's + /// accumulated demand state + struct Demand { + var processed: Subscribers.Demand = .none + var requested: Subscribers.Demand = .none + var sent: Subscribers.Demand = .none + } +} + +// MARK: - Internally-scoped helpers +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Subscription { + /// Reqeust demand if it's not empty + /// + /// - parameter demand: Requested demand + func requestIfNeeded(_ demand: Subscribers.Demand) { + guard demand > .none else { return } + request(demand) + } +} + +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Optional where Wrapped == Subscription { + /// Cancel the Optional subscription and nullify it + mutating func kill() { + self?.cancel() + self = nil + } +} +#endif diff --git a/Sources/FoundationExtensions/Combine/Promise+Operators.swift b/Sources/FoundationExtensions/Combine/Promise+Operators.swift new file mode 100644 index 0000000..9d52cf4 --- /dev/null +++ b/Sources/FoundationExtensions/Combine/Promise+Operators.swift @@ -0,0 +1,671 @@ +// Copyright © 2025 Lautsprecher Teufel GmbH. All rights reserved. + +#if canImport(Combine) +import Combine +import OSLog + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Publishers.Promise: Publisher { + @inlinable + public func receive(subscriber: S) + where Output == S.Input, Failure == S.Failure { + publisher.receive(subscriber: subscriber) + } + + @inlinable + public func subscribe(_ subscriber: some Subscriber) { + publisher.subscribe(subscriber) + } + + @inlinable + public func subscribe(_ subscriber: some Subject) -> AnyCancellable { + publisher.subscribe(subscriber) + } + + @inlinable + public func map(_ transform: @escaping (Output) -> T) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.map(transform).assertPromise("Publishers.Promise.map(_:)") + } + return open(publisher) + } + + @inlinable + public func tryMap(_ transform: @escaping (Output) throws -> T) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.tryMap(transform).assertPromise("Publishers.Promise.tryMap(_:)") + } + return open(publisher) + } + + @inlinable + public func mapError(_ transform: @escaping (Failure) -> E) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.mapError(transform).assertPromise("Publishers.Promise.mapError(_:)") + } + return open(publisher) + } + + @inlinable + public func replaceNil(with output: T) -> Publishers.Promise where Self.Output == T? { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.replaceNil(with: output).assertPromise("Publishers.Promise.replaceNil(with:)") + } + return open(publisher) + } + + @inlinable + public func replaceError(with output: Output) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.replaceError(with: output).assertPromise("Publishers.Promise.replaceError(with:)") + } + return open(publisher) + } + + @inlinable + public func contains(where predicate: @escaping (Output) -> Bool) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.contains(where: predicate).assertPromise("Publishers.Promise.contains(where:)") + } + return open(publisher) + } + + @inlinable + public func tryContains(where predicate: @escaping (Output) throws -> Bool) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.tryContains(where: predicate).assertPromise("Publishers.Promise.tryContains(where:)") + } + return open(publisher) + } + + @inlinable + public static func zip( + _ p1: Publishers.Promise, + _ p2: Publishers.Promise + ) -> Publishers.Promise where Output == (T, S) { + Publishers.Zip(p1, p2).assertPromise("Publishers.Promise.zip(_:_:)") + } + + @inlinable + public static func zip( + _ p1: Publishers.Promise, + _ p2: Publishers.Promise, + _ p3: Publishers.Promise + ) -> Publishers.Promise where Output == (T, S, G) { + Publishers.Zip3(p1, p2, p3).assertPromise("Publishers.Promise.zip(_:_:_:)") + } + + @inlinable + public static func zip( + _ p1: Publishers.Promise, + _ p2: Publishers.Promise, + _ p3: Publishers.Promise, + _ p4: Publishers.Promise + ) -> Publishers.Promise where Output == (T, S, G, H) { + Publishers.Zip4(p1, p2, p3, p4).assertPromise("Publishers.Promise.zip(_:_:_:_:)") + } + + @inlinable + public static func zip( + _ p1: Publishers.Promise, + _ p2: Publishers.Promise, + _ p3: Publishers.Promise, + _ p4: Publishers.Promise, + _ p5: Publishers.Promise + ) -> Publishers.Promise where Output == (T, S, G, H, J) { + Publishers.Promise<(T, (S, G, H, J)), Failure>.zip( + p1, + Publishers.Promise<(S, G, H, J), Failure>.zip(p2, p3, p4, p5) + ) + .map { ($0, $1.0, $1.1, $1.2, $1.3) } + } + + @inlinable + public static func zip( + _ p1: Publishers.Promise, + _ p2: Publishers.Promise, + _ p3: Publishers.Promise, + _ p4: Publishers.Promise, + _ p5: Publishers.Promise, + _ p6: Publishers.Promise + ) -> Publishers.Promise where Output == (T, S, G, H, J, K) { + Publishers.Promise<(T, (S, G, H, J, K)), Failure>.zip( + p1, + Publishers.Promise<(S, G, H, J, K), Failure>.zip(p2, p3, p4, p5, p6) + ) + .map { ($0, $1.0, $1.1, $1.2, $1.3, $1.4) } + } + + @inlinable + public static func zip( + _ p1: Publishers.Promise, + _ p2: Publishers.Promise, + _ p3: Publishers.Promise, + _ p4: Publishers.Promise, + _ p5: Publishers.Promise, + _ p6: Publishers.Promise, + _ p7: Publishers.Promise + ) -> Publishers.Promise where Output == (T, S, G, H, J, K, L) { + Publishers.Promise<(T, (S, G, H, J, K, L)), Failure>.zip( + p1, + Publishers.Promise<(S, G, H, J, K, L), Failure>.zip(p2, p3, p4, p5, p6, p7) + ) + .map { ($0, $1.0, $1.1, $1.2, $1.3, $1.4, $1.5) } + } + + @inlinable + public static func zip( + _ p1: Publishers.Promise, + _ p2: Publishers.Promise, + _ p3: Publishers.Promise, + _ p4: Publishers.Promise, + _ p5: Publishers.Promise, + _ p6: Publishers.Promise, + _ p7: Publishers.Promise, + _ p8: Publishers.Promise + ) -> Publishers.Promise where Output == (T, S, G, H, J, K, L, M) { + Publishers.Promise<(T, (S, G, H, J, K, L, M)), Failure>.zip( + p1, + Publishers.Promise<(S, G, H, J, K, L, M), Failure>.zip(p2, p3, p4, p5, p6, p7, p8) + ) + .map { ($0, $1.0, $1.1, $1.2, $1.3, $1.4, $1.5, $1.6) } + } + + @inlinable + public static func zip( + _ p1: Publishers.Promise, + _ p2: Publishers.Promise, + _ p3: Publishers.Promise, + _ p4: Publishers.Promise, + _ p5: Publishers.Promise, + _ p6: Publishers.Promise, + _ p7: Publishers.Promise, + _ p8: Publishers.Promise, + _ p9: Publishers.Promise + ) -> Publishers.Promise where Output == (T, S, G, H, J, K, L, M, N) { + Publishers.Promise<(T, (S, G, H, J, K, L, M, N)), Failure>.zip( + p1, + Publishers.Promise<(S, G, H, J, K, L, M, N), Failure>.zip(p2, p3, p4, p5, p6, p7, p8, p9) + ) + .map { ($0, $1.0, $1.1, $1.2, $1.3, $1.4, $1.5, $1.6, $1.7) } + } + + @inlinable + public static func zip( + _ p1: Publishers.Promise, + _ p2: Publishers.Promise, + _ p3: Publishers.Promise, + _ p4: Publishers.Promise, + _ p5: Publishers.Promise, + _ p6: Publishers.Promise, + _ p7: Publishers.Promise, + _ p8: Publishers.Promise, + _ p9: Publishers.Promise, + _ p10: Publishers.Promise + ) -> Publishers.Promise where Output == (T, S, G, H, J, K, L, M, N, O) { + Publishers.Promise<(T, (S, G, H, J, K, L, M, N, O)), Failure>.zip( + p1, + Publishers.Promise<(S, G, H, J, K, L, M, N, O), Failure>.zip(p2, p3, p4, p5, p6, p7, p8, p9, p10) + ) + .map { ($0, $1.0, $1.1, $1.2, $1.3, $1.4, $1.5, $1.6, $1.7, $1.8) } + } + + @inlinable + public static func zip( + _ p1: Publishers.Promise, + _ p2: Publishers.Promise, + _ p3: Publishers.Promise, + _ p4: Publishers.Promise, + _ p5: Publishers.Promise, + _ p6: Publishers.Promise, + _ p7: Publishers.Promise, + _ p8: Publishers.Promise, + _ p9: Publishers.Promise, + _ p10: Publishers.Promise, + _ p11: Publishers.Promise + ) -> Publishers.Promise where Output == (T, S, G, H, J, K, L, M, N, O, P) { + Publishers.Promise<(T, (S, G, H, J, K, L, M, N, O, P)), Failure>.zip( + p1, + Publishers.Promise<(S, G, H, J, K, L, M, N, O, P), Failure>.zip(p2, p3, p4, p5, p6, p7, p8, p9, p10, p11) + ) + .map { ($0, $1.0, $1.1, $1.2, $1.3, $1.4, $1.5, $1.6, $1.7, $1.8, $1.9) } + } + + @inlinable + public static func zip( + _ p1: Publishers.Promise, + _ p2: Publishers.Promise, + _ p3: Publishers.Promise, + _ p4: Publishers.Promise, + _ p5: Publishers.Promise, + _ p6: Publishers.Promise, + _ p7: Publishers.Promise, + _ p8: Publishers.Promise, + _ p9: Publishers.Promise, + _ p10: Publishers.Promise, + _ p11: Publishers.Promise, + _ p12: Publishers.Promise + ) -> Publishers.Promise where Output == (T, S, G, H, J, K, L, M, N, O, P, Q) { + Publishers.Promise<(T, (S, G, H, J, K, L, M, N, O, P, Q)), Failure>.zip( + p1, + Publishers.Promise<(S, G, H, J, K, L, M, N, O, P, Q), Failure>.zip(p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12) + ) + .map { ($0, $1.0, $1.1, $1.2, $1.3, $1.4, $1.5, $1.6, $1.7, $1.8, $1.9, $1.10) } + } + + public static func zip(_ promises: [Publishers.Promise]) -> Publishers.Promise<[Output], Failure> { + switch promises.count { + case ...0: + return .init(value: []) + case 1: + return promises[0].map { [$0] } + default: + let result: Publishers.Promise<[Output], Failure> = promises[0].map { [$0] } + + return promises.dropFirst().reduce(result) { partial, current -> Publishers.Promise<[Output], Failure> in + Publishers.Promise.zip( + partial, + current + ) + .map { accumulation, next in accumulation + [next] } + } + } + } + + @inlinable + public func flatMap(_ transform: @escaping (Output) -> Publishers.Promise) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.map(transform).switchToLatest().assertPromise("Publishers.Promise.flatMap(_:)") + } + return open(publisher) + } + + @_disfavoredOverload + @inlinable + public func flatMap(_ transform: @escaping (Output) -> Publishers.Promise) -> Publishers.Promise { + flatMap { transform($0).setFailureType(to: Failure.self) } + } + + @_disfavoredOverload + @inlinable + public func flatMap(_ transform: @escaping (Output) -> Publishers.Promise) -> Publishers.Promise where Failure == Never { + setFailureType(to: E.self).flatMap(transform) + } + + @inlinable + public func assertNoFailure( + _ prefix: String = "", + file: StaticString = #file, + line: UInt = #line + ) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher + .assertNoFailure(prefix, file: file, line: line) + .assertPromise("Publishers.Promise.assertNoFailure(_:file:line:)") + } + return open(publisher) + } + + @inlinable + public func `catch`(_ handler: @escaping (Failure) -> Publishers.Promise) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.catch(handler).assertPromise("Publishers.Promise.catch(_:)") + } + return open(publisher) + } + + @inlinable + public func tryCatch(_ handler: @escaping (Failure) throws -> Publishers.Promise) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.tryCatch(handler).assertPromise("Publishers.Promise.tryCatch(_:)") + } + return open(publisher) + } + + @inlinable + public func retry(_ retries: Int) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.retry(retries).assertPromise("Publishers.Promise.retry(_:)") + } + return open(publisher) + } + + @inlinable + public func delay( + for interval: S.SchedulerTimeType.Stride, + tolerance: S.SchedulerTimeType.Stride? = nil, + scheduler: S, + options: S.SchedulerOptions? = nil + ) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.delay( + for: interval, + tolerance: tolerance, + scheduler: scheduler, + options: options + ) + .assertPromise("Publishers.Promise.delay(for:tolerance:scheduler:options:)") + } + return open(publisher) + } + + @inlinable + public func throttle( + for interval: S.SchedulerTimeType.Stride, + scheduler: S + ) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher + .throttle(for: interval, scheduler: scheduler, latest: true) + .assertPromise("Publishers.Promise.throttle(for:scheduler:)") + } + return open(publisher) + } + + public func timeout( + _ interval: S.SchedulerTimeType.Stride, + scheduler: S, + options: S.SchedulerOptions? = nil, + fallback: @escaping () -> Result + ) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher + .eraseFailureToError() + .timeout(interval, scheduler: scheduler, options: options, customError: TimeOutFailure.init) + .catch { error in + if error is TimeOutFailure { + return Publishers.Promise(fallback()) + } else if let error = error as? Failure { + return Publishers.Promise(error: error) + } else { + os_log( + .error, + log: .publishersPromise, + """ + Timeout after %{public}@ on Promise of type '%{public}@' while awaiting a value. + Ensure the publisher completes within the specified interval or provide a fallback to prevent hanging states. + """, + String(describing: interval), + String(describing: Self.self) + ) + return Publishers.Promise(outputType: Output.self, failureType: Failure.self) + } + } + .assertPromise("Publishers.Promise.timeout(_:scheduler:options:fallback:)") + } + return open(publisher) + } + + public func timeout( + _ interval: S.SchedulerTimeType.Stride, + scheduler: S, + options: S.SchedulerOptions? = nil, + fallback: @autoclosure @escaping () -> Result + ) -> Publishers.Promise { + timeout(interval, scheduler: scheduler, options: options, fallback: fallback) + } + + @inlinable + public func encode(encoder: Coder) -> Publishers.Promise where Output: Encodable { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.encode(encoder: encoder).assertPromise("Publishers.Promise.encode(encoder:)") + } + return open(publisher) + } + + @inlinable + public func decode( + type: Item.Type, + decoder: Coder + ) -> Publishers.Promise where Output == Coder.Input { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.decode(type: type, decoder: decoder).assertPromise("Publishers.Promise.decode(type:decoder:)") + } + return open(publisher) + } + + @inlinable + public func map(_ keyPath: KeyPath) -> Publishers.Promise { + map { $0[keyPath: keyPath] } + } + + @inlinable + public func map( + _ keyPath0: KeyPath, + _ keyPath1: KeyPath + ) -> Publishers.Promise<(T0, T1), Failure> { + map { ($0[keyPath: keyPath0], $0[keyPath: keyPath1]) } + } + + @inlinable + public func map( + _ keyPath0: KeyPath, + _ keyPath1: KeyPath, + _ keyPath2: KeyPath + ) -> Publishers.Promise<(T0, T1, T2), Failure> { + map { ($0[keyPath: keyPath0], $0[keyPath: keyPath1], $0[keyPath: keyPath2]) } + } + + @inlinable + public func share() -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + nonisolated(unsafe) var value: Result? + let lock = NSLock() + return publisher + .share() + .map { + lock.lock() + value = .success($0) + lock.unlock() + return $0 + } + .mapError { + lock.lock() + value = .failure($0) + lock.unlock() + return $0 + } + .eraseToPromise { + lock.lock() + let value = value + lock.unlock() + return value + } + } + return open(publisher) + } + + @inlinable + public func subscribe( + on scheduler: S, + options: S.SchedulerOptions? = nil + ) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.subscribe(on: scheduler, options: options).assertPromise("Publishers.Promise.subscribe(on:options:)") + } + return open(publisher) + } + + @inlinable + public func receive( + on scheduler: S, + options: S.SchedulerOptions? = nil + ) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.receive(on: scheduler, options: options).assertPromise("Publishers.Promise.receive(on:options:)") + } + return open(publisher) + } + + @inlinable + public func breakpoint( + receiveSubscription: ((any Subscription) -> Bool)? = nil, + receiveOutput: ((Output) -> Bool)? = nil, + receiveCompletion: ((Subscribers.Completion) -> Bool)? = nil + ) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher + .breakpoint( + receiveSubscription: receiveSubscription, + receiveOutput: receiveOutput, + receiveCompletion: receiveCompletion + ) + .assertPromise("Publishers.Promise.breakpoint(receiveSubscription:receiveOutput:receiveCompletion:)") + } + return open(publisher) + } + + @inlinable + public func breakpointOnError() -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.breakpointOnError().assertPromise("Publishers.Promise.breakpointOnError()") + } + return open(publisher) + } + + @inlinable + public func print(_ prefix: String = "", to stream: TextOutputStream? = nil) -> Publishers.Promise { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.print(prefix, to: stream).assertPromise("Publishers.Promise.print(_:to:)") + } + return open(publisher) + } + + @inlinable + public func flatMapResult(_ transform: @escaping (Output) -> Result) -> Publishers.Promise { + flatMap { .init(transform($0)) } + } + + @inlinable + public func setFailureType(to failureType: E.Type) -> Publishers.Promise where Failure == Never { + func open(_ publisher: some Publisher) -> Publishers.Promise { + publisher.setFailureType(to: failureType).assertPromise("Publishers.Promise.setFailureType(to:)") + } + return open(publisher) + } + + @inlinable + public func eraseFailureToError() -> Publishers.Promise { + mapError(identity) + } + + /// Similar to .sink, but with correct semantic for a single-value success or a failure. Creates demand for 1 + /// value and completes after it, or on error. + /// - Parameters: + /// - onSuccess: called when the promise is fulfilled successful with a value. Called only once and then + /// you can consider this stream finished + /// - onFailure: called when the promise finds error. Called only once completing the stream. It's never + /// called if a success was already called + /// - Returns: the subscription that can be cancelled at any point. + @inlinable + public func run(onSuccess: @escaping (Output) -> Void, onFailure: @escaping (Failure) -> Void) -> AnyCancellable { + sink( + receiveCompletion: { completion in + if case let .failure(error) = completion { + onFailure(error) + } + }, + receiveValue: onSuccess + ) + } + + /// Similar to .sink, but with correct semantic for a single-value success or a failure. Creates demand for 1 + /// value and completes after it, or on error. + /// - Parameters: + /// - onSuccess: called when the promise is fulfilled successful with a value. Called only once and then + /// you can consider this stream finished + /// - Returns: the subscription that can be cancelled at any point. + @inlinable + public func run(onSuccess: @escaping (Output) -> Void) -> AnyCancellable where Failure == Never { + run(onSuccess: onSuccess, onFailure: { _ in }) + } + + /// Similar to .sink, but with correct semantic for a single-value success or a failure. Creates demand for 1 + /// value and completes after it, or on error. + /// - Parameters: + /// - onFailure: called when the promise finds error. Called only once completing the stream. It's never + /// called if a success was already called + /// - Returns: the subscription that can be cancelled at any point. + @inlinable + public func run(onFailure: @escaping (Failure) -> Void) -> AnyCancellable where Output == Void { + run(onSuccess: { _ in }, onFailure: onFailure) + } + + /// Similar to .sink, but with correct semantic for a single-value success or a failure. Creates demand for 1 + /// value and completes after it, or on error. + /// - Returns: the subscription that can be cancelled at any point. + @inlinable + public func run() -> AnyCancellable where Output == Void, Failure == Never { + run(onSuccess: { _ in }, onFailure: { _ in }) + } + + /// Convert a `Publishers.Promise` to `async throws -> Output`. + /// + /// Usage + /// + /// Publishers.Promise(value: 1).value() + @available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 8.0, *) + @inlinable + public func value() async throws -> Output { + var iterator = values.makeAsyncIterator() + guard let result = try await iterator.next() else { + throw CancellationError() + } + return result + } + + @inlinable + public func validStatusCode() -> Publishers.Promise where Output == (data: Data, response: URLResponse), Failure == URLError { + flatMapResult { _, response -> Result in + guard let httpResponse = response as? HTTPURLResponse else { + return .failure(URLError(.badServerResponse)) + } + + return (200..<400) ~= httpResponse.statusCode + ? .success(()) + : .failure(URLError(.badServerResponse)) + } + } + + /// Case folding for Promise. Given functions that convert either errors or wrapped values into the same type FoldedValue + /// evaluates the promise and returns Deferred. + /// + /// - Parameters: + /// - onSuccess: a function that, given a wrapped value `Success`, executes an operation that returns `TargetType`, + /// which will be the result of this function + /// - onFailure: a function that, given an error `Failure`, executes an operation that returns `TargetType`, + /// which will be the result of this function + /// - Returns: the value produced by applying `ifFailure` to `failure` Promise, or `ifSuccess` to `success` Promise, any of them + /// translated into the same `Deferred`. + @inlinable + public func fold(onSuccess: @escaping (Output) -> TargetType, onFailure: @escaping (Failure) -> TargetType) + -> Publishers.Promise { + map(onSuccess) + .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 + /// complete with future Void when the analysis is done. + /// + /// - Parameters: + /// - ifSuccess: a function that, given a wrapped `Success`, executes an operation with no return value + /// - ifFailure: a function that, given an error `Failure`, executes an operation with no return value + /// - Returns: returns a future Void, indicating that Promise ran and evaluated the two possible scenarios, success or failure. + @inlinable + public func analysis(ifSuccess: @escaping (Output) -> Void, ifFailure: @escaping (Failure) -> Void) + -> Publishers.Promise { + fold(onSuccess: ifSuccess, onFailure: ifFailure) + } + + /// If this promise results in an error, executes the provided closure passing the inner error + /// + /// - Parameter run: the block to execute in case this is an error + /// - Returns: returns always `self` with no changes, for chaining purposes. + @inlinable + public func onError(_ run: @escaping (Failure) -> Void) -> Publishers.Promise { + `catch` { run($0); return self } + } + + private struct TimeOutFailure: Error {} +} +#endif diff --git a/Sources/FoundationExtensions/Combine/Promise.swift b/Sources/FoundationExtensions/Combine/Promise.swift new file mode 100644 index 0000000..25c0150 --- /dev/null +++ b/Sources/FoundationExtensions/Combine/Promise.swift @@ -0,0 +1,189 @@ +// Copyright © 2025 Lautsprecher Teufel GmbH. All rights reserved. + +#if canImport(Combine) +import Combine +import OSLog + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Publishers { + /// Promise Publisher - Unsupported Modifiers + /// --------------------------------------------- + /// The following modifiers are unsupported for Promise because they lead to + /// undefined behavior, hangs, or crashes. + /// + /// ⚠️ Developers can technically call these modifiers since Promise is a Publisher. + /// + /// 🛑 Unsupported Modifiers: + /// - scan, tryScan, filter, tryFilter, compactMap, tryCompactMap + /// - removeDuplicates, replaceEmpty + /// - collect, reduce, tryReduce, count, max, tryMax, min, tryMin, ignoreOutput + /// - allSatisfy, tryAllSatisfy + /// - drop, dropFirst, tryDrop, prefix, tryPrefix, first, tryFirst, last, tryLast, output + /// - merge, append, prepend, switchToLatest, combineLatest + /// - debounce, timeout(_:scheduler:options:customError:), measureInterval + /// - multicast, buffer, makeConnectable, handleEvents + /// + /// ❗ Example: Calling `.filter` on an Promise can hang indefinitely if the promise resolves with a non-matching value. + /// + /// ➡️ Best Practice: Avoid using modifiers that does not return Promise. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + public struct Promise: CustomStringConvertible, CustomPlaygroundDisplayConvertible { + @usableFromInline + internal let publisher: any Publisher + + public init( + _ operation: @Sendable @escaping (@Sendable @escaping (Result) -> ()) -> Cancellable + ) { + self.init( + AnyPublisher.create { promise in + // FIXME: https://forums.swift.org/t/promise-is-non-sendable-type/68383 + // Apple does not really care about Combine framework anymore... + #if swift(>=6) + nonisolated(unsafe) let promise = promise + #endif + return operation { result in + switch result { + case let .success(value): + promise.send(value) + promise.send(completion: .finished) + case let .failure(error): + promise.send(completion: .failure(error)) + } + } + } + ) + } + + @inlinable + public init(_ asyncFunc: @escaping @Sendable () async throws -> Output) where Failure == Error { + self.init { promise in + let task = Task { + do { + let result = try await asyncFunc() + promise(.success(result)) + } catch { + promise(.failure(error)) + } + } + + return AnyCancellable { task.cancel() } + } + } + + @inlinable + public init(_ asyncFunc: @escaping @Sendable () async -> Output) where Failure == Never { + self.init { promise in + let task = Task { @Sendable in + let result = await asyncFunc() + promise(.success(result)) + } + + return AnyCancellable { task.cancel() } + } + } + + @inlinable + public init(_ publisher: P, onEmpty fallback: @Sendable @escaping () -> Result? = { nil }) + where Output == P.Output, Failure == P.Failure { + if let erased = publisher as? Self { + self.publisher = erased.publisher + } else { + self.publisher = publisher + .first() + .map { value in return { Result.success(value) } } + .replaceEmpty(with: { fallback() }) + .flatMap { output in + switch output() { + case let .some(result): + return Future { promise in + promise(result) + } + .eraseToAnyPublisher() + case .none: + os_log( + .error, + log: .publishersPromise, + "%@", + """ + Publisher of type '\(String(describing: P.self))' completed unexpectedly without publishing any value or error. \ + No fallback value was provided, leaving the Promise in a hanging state. \ + Ensure the Publisher emits a value or provide a fallback to avoid this issue. + """ + ) + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + } + } + } + + @inlinable + public init(value: Output, failureType: Failure.Type = Failure.self) { + self.publisher = Just(value).setFailureType(to: Failure.self) + } + + @_disfavoredOverload + @inlinable + public init(value: Output) where Failure == Never { + self.init(value: value, failureType: Failure.self) + } + + @inlinable + public init(value: Output) { + self.init(value: value, failureType: Failure.self) + } + + @inlinable + public init(outputType: Output.Type = Output.self, error: Failure) { + self.publisher = Fail(error: error) + } + + @inlinable + public init(error: Failure) where Output == Void { + self.publisher = Fail(error: error) + } + + @inlinable + public init(_ result: Result) { + switch result { + case let .success(output): + self.init(value: output, failureType: Failure.self) + case let .failure(error): + self.init(outputType: Output.self, error: error) + } + } + + @inlinable + public init( + outputType: Output.Type = Output.self, + failureType: Failure.Type = Failure.self + ) { + self.publisher = Empty(completeImmediately: false, outputType: outputType, failureType: failureType) + } + + @inlinable + public var description: String { + "Publishers.Promise" + } + + @inlinable + public var playgroundDescription: Any { + description + } + + static func unsafe(_ publisher: P, prefix: String = "", file: StaticString = #file, line: UInt = #line) -> Self + where P.Output == Output, P.Failure == Failure { + self.init( + publisher, + onEmpty: { + fatalError( + "\(prefix)Publisher of type '\(String(describing: P.self))' completed unexpectedly without publishing any value or error.", + file: file, + line: line + ) + } + ) + } + } +} +#endif diff --git a/Sources/FoundationExtensions/Combine/Publisher+Promise.swift b/Sources/FoundationExtensions/Combine/Publisher+Promise.swift new file mode 100644 index 0000000..cffa28d --- /dev/null +++ b/Sources/FoundationExtensions/Combine/Publisher+Promise.swift @@ -0,0 +1,33 @@ +// Copyright © 2025 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 { + @inlinable + public func eraseToPromise( + onEmpty fallback: @Sendable @escaping () -> Result? = { nil } + ) -> Publishers.Promise { + Publishers.Promise(self, onEmpty: fallback) + } + + @inlinable + public func eraseToPromise( + onEmpty fallback: @Sendable @autoclosure @escaping () -> Result? = nil + ) -> Publishers.Promise { + eraseToPromise(onEmpty: fallback) + } + + @inline(never) + public func assertPromise(_ prefix: String = "", file: StaticString = #file, line: UInt = #line) -> Publishers.Promise { + Publishers.Promise.unsafe(self, prefix: prefix, file: file, line: line) + } + + @inline(never) + public func assertPromise(_ prefix: String = "", file: StaticString = #file, line: UInt = #line) -> Publishers.Promise where Failure == Never { + Publishers.Promise.unsafe(self, prefix: prefix, file: file, line: line) + } +} +#endif diff --git a/Sources/FoundationExtensions/Combine/Publishers+Retry.swift b/Sources/FoundationExtensions/Combine/Publishers+Retry.swift deleted file mode 100644 index a260dec..0000000 --- a/Sources/FoundationExtensions/Combine/Publishers+Retry.swift +++ /dev/null @@ -1,29 +0,0 @@ -#if canImport(Combine) -import Combine -import Foundation - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension Publisher { - public func retry() -> AnyPublisher { - Publishers - .Retry(upstream: self, retries: nil) - .assertNoFailure() // It theoretically throws fatalError on Publisher Failure. - // But Retry forever will never allow errors to pass anyway. - .eraseToAnyPublisher() - } -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension Publishers.Promise { - public func retry() -> Publishers.Promise { - Publishers - .Retry(upstream: self, retries: nil) - .assertNoFailure() // It theoretically throws fatalError on Publisher Failure. - // But Retry forever will never allow errors to pass anyway. - .assertNonEmptyPromise() // It theoretically throws fatalError on Promise empty completion - // But upstream is a promise and it will never complete without values - // and Retry will ensure to only complete once the upstream (Promise) completes - // with success, therefore, with one value. - } -} -#endif diff --git a/Sources/FoundationExtensions/FileSystem/FileExists.swift b/Sources/FoundationExtensions/FileSystem/FileExists.swift index 7179b0b..74ed8f0 100644 --- a/Sources/FoundationExtensions/FileSystem/FileExists.swift +++ b/Sources/FoundationExtensions/FileSystem/FileExists.swift @@ -16,9 +16,11 @@ public struct FileExists: Sendable { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public func async(in path: URL, on queue: DispatchQueue) -> Publishers.Promise { - Publishers.Promise.perform(in: queue) { - _run(path) + Publishers.Promise { promise in + promise(_run(path)) + return AnyCancellable {} } + .subscribe(on: queue) } } diff --git a/Sources/FoundationExtensions/FileSystem/MoveFile.swift b/Sources/FoundationExtensions/FileSystem/MoveFile.swift index 270948a..aecd557 100644 --- a/Sources/FoundationExtensions/FileSystem/MoveFile.swift +++ b/Sources/FoundationExtensions/FileSystem/MoveFile.swift @@ -16,9 +16,11 @@ public struct MoveFile: Sendable { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public func async(from origin: URL, into destination: URL, replace: Bool = false, on queue: DispatchQueue) -> Publishers.Promise { - .perform(in: queue) { - _run(origin, destination, replace) + Publishers.Promise { promise in + promise(_run(origin, destination, replace)) + return AnyCancellable {} } + .subscribe(on: queue) } } diff --git a/Sources/FoundationExtensions/FileSystem/ReadFileContents.swift b/Sources/FoundationExtensions/FileSystem/ReadFileContents.swift index 3b6df20..2eaf5a1 100644 --- a/Sources/FoundationExtensions/FileSystem/ReadFileContents.swift +++ b/Sources/FoundationExtensions/FileSystem/ReadFileContents.swift @@ -16,9 +16,11 @@ public struct ReadFileContents: Sendable { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public func async(from origin: URL, on queue: DispatchQueue) -> Publishers.Promise { - .perform(in: queue) { - _run(origin) + Publishers.Promise { promise in + promise(_run(origin)) + return AnyCancellable {} } + .subscribe(on: queue) } } diff --git a/Sources/FoundationExtensions/Promise/NonEmptyPublisher.swift b/Sources/FoundationExtensions/Promise/NonEmptyPublisher.swift deleted file mode 100644 index 2930b47..0000000 --- a/Sources/FoundationExtensions/Promise/NonEmptyPublisher.swift +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright © 2023 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) - .flatMapLatest { 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 { - internal static func unsafe(nonEmptyUpstream: Upstream) -> Self { - NonEmptyPublisher( - upstream: nonEmptyUpstream, - onEmpty: { - fatalError(""" - Upstream Publisher completed empty, which is not an expected behaviour. - Type: \(Upstream.self) - Instance: \(nonEmptyUpstream) - """) - } - ) - } -} -#endif diff --git a/Sources/FoundationExtensions/Promise/Promise+HTTPStatusCode.swift b/Sources/FoundationExtensions/Promise/Promise+HTTPStatusCode.swift deleted file mode 100644 index 8126c59..0000000 --- a/Sources/FoundationExtensions/Promise/Promise+HTTPStatusCode.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Combine -import Foundation - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension Publishers.Promise where Success == (data: Data, response: URLResponse), Failure == URLError { - public func validStatusCode() -> Publishers.Promise { - flatMapResult { _, response -> Result in - guard let httpResponse = response as? HTTPURLResponse else { - return .failure(URLError(.badServerResponse)) - } - - return (200..<400) ~= httpResponse.statusCode - ? .success(()) - : .failure(URLError(.badServerResponse)) - } - } -} diff --git a/Sources/FoundationExtensions/Promise/Promise+PerformInQueue.swift b/Sources/FoundationExtensions/Promise/Promise+PerformInQueue.swift deleted file mode 100644 index 9005ce1..0000000 --- a/Sources/FoundationExtensions/Promise/Promise+PerformInQueue.swift +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright © 2023 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 () -> Result) -> Self { - .init { completion in - let workItem = DispatchWorkItem { - operation() - .analysis( - ifSuccess: { completion(.success($0)) }, - ifFailure: { completion(.failure($0)) } - ) - } - - 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+Value.swift b/Sources/FoundationExtensions/Promise/Promise+Value.swift deleted file mode 100644 index ac55e38..0000000 --- a/Sources/FoundationExtensions/Promise/Promise+Value.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright © 2023 Lautsprecher Teufel GmbH. All rights reserved. - -import Combine -import Foundation - -@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 8.0, *) -extension Publishers.Promise where Output: Sendable { - /// Convert a `Promise` to `async throws -> Output`. - /// - /// Usage - /// - /// Publishers.Promise(value: 1).value() - public func value() async throws -> Output { - var iterator = self.values.makeAsyncIterator() - guard let result = try await iterator.next() else { - throw CancellationError() - } - return result - } -} diff --git a/Sources/FoundationExtensions/Promise/Promise.swift b/Sources/FoundationExtensions/Promise/Promise.swift deleted file mode 100644 index 5ea80de..0000000 --- a/Sources/FoundationExtensions/Promise/Promise.swift +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright © 2023 Lautsprecher Teufel GmbH. All rights reserved. - -#if canImport(Combine) -import Combine -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 - /// 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. - /// Promises can be created from any Publisher, but only the first element will be relevant. It can also be created from - /// hardcoded success or failure values, or from a Result. In any of these cases, the evaluation of the value will be deferred - /// so be sure to use values with copy semantic (value type). - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - public struct Promise: PromiseType { - public typealias Output = Success - 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 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 () -> NonEmptyPublisher

) where P.Output == Success, P.Failure == Failure { - operation = { sinkNotification in - upstream() - .first() - .sink( - receiveCompletion: sinkNotification.receiveCompletion, - receiveValue: sinkNotification.receiveValue - ) - } - } - - internal static func unsafe(upstreamUncheckedForEmptiness upstream: @escaping () -> P) -> Self - where P.Output == Success, P.Failure == Failure { - .init { NonEmptyPublisher.unsafe(nonEmptyUpstream: upstream()) } - } - - 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() - } - } - } - - public init(_ asyncFunc: @escaping @Sendable () async throws -> Success) where Failure == Error { - self.init { promise in - // FIXME: https://forums.swift.org/t/promise-is-non-sendable-type/68383 - // Apple does not really care about Combine framework anymore... - #if swift(>=6) - nonisolated(unsafe) let promise = promise - #endif - let task = Task { @Sendable in - do { - let result = try await asyncFunc() - promise(.success(result)) - } catch { - promise(.failure(error)) - } - } - - return AnyCancellable { task.cancel() } - } - } - - public init(_ asyncFunc: @escaping @Sendable () async -> Success) where Failure == Never { - self.init { promise in - // FIXME: https://forums.swift.org/t/promise-is-non-sendable-type/68383 - // Apple does not really care about Combine framework anymore... - #if swift(>=6) - nonisolated(unsafe) let promise = promise - #endif - - let task = Task { @Sendable in - let result = await asyncFunc() - promise(.success(result)) - } - - return AnyCancellable { task.cancel() } - } - } - - @available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 8.0, *) - public init(_ asyncPromise: @escaping @Sendable () async -> Publishers.Promise) where Failure == Error, Success: Sendable { - self.init { promise in - // FIXME: https://forums.swift.org/t/promise-is-non-sendable-type/68383 - // Apple does not really care about Combine framework anymore... - #if swift(>=6) - nonisolated(unsafe) let promise = promise - #endif - let task = Task { @Sendable in - let innerPromise = await asyncPromise() - do { - let result = try await innerPromise.value() - promise(.success(result)) - } catch { - promise(.failure(error)) - } - } - - return AnyCancellable { task.cancel() } - } - } - - /// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)` - /// - /// - SeeAlso: `subscribe(_:)` - /// - Parameters: - /// - 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(operation: operation, downstream: subscriber)) - } - - public var promise: Publishers.Promise { - self - } - } -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension Publishers.Promise { - class Subscription: Combine.Subscription where Output == Downstream.Input, Failure == Downstream.Failure { - private let lock = NSRecursiveLock() - private var hasStarted = false - private let operation: SinkClosure - private let downstream: Downstream - private var cancellable: AnyCancellable? - - public init(operation: @escaping SinkClosure, downstream: Downstream) { - self.operation = operation - self.downstream = downstream - } - - func request(_ demand: Subscribers.Demand) { - guard demand > .none else { return } - - lock.lock() - let shouldRun = !hasStarted - hasStarted = true - lock.unlock() - - guard shouldRun else { return } - - 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() { - cancellable?.cancel() - } - } -} -#endif diff --git a/Sources/FoundationExtensions/Promise/PromiseConvertibleType.swift b/Sources/FoundationExtensions/Promise/PromiseConvertibleType.swift deleted file mode 100644 index 6dc5a50..0000000 --- a/Sources/FoundationExtensions/Promise/PromiseConvertibleType.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright © 2023 Lautsprecher Teufel GmbH. All rights reserved. - -#if canImport(Combine) -import Combine -import Foundation - -/// Any type or protocol that can be converted into `Promise` -/// Used for type erasure. -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -public protocol PromiseConvertibleType { - /// A value wrapped in this container when the promise eventually returns success - associatedtype Success - - /// An error wrapped in this container when the promise eventually returns a failure - associatedtype Failure: Error - - /// Identity, return itself. Necessary for protocol programming. - var promise: Publishers.Promise { get } -} -#endif diff --git a/Sources/FoundationExtensions/Promise/PromiseType+DefaultOperators.swift b/Sources/FoundationExtensions/Promise/PromiseType+DefaultOperators.swift deleted file mode 100644 index d251b6d..0000000 --- a/Sources/FoundationExtensions/Promise/PromiseType+DefaultOperators.swift +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright © 2023 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.unsafe { self.eraseToAnyPublisher().map(transform) } - } - - public func tryMap(_ transform: @escaping (Output) throws -> T) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().tryMap(transform) } - } - - public func mapError(_ transform: @escaping (Failure) -> E) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().mapError(transform) } - } - - public func `catch`(_ handler: @escaping (Failure) -> Publishers.Promise) -> Publishers.Promise { - Publishers.Promise.unsafe { 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.unsafe { - 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.unsafe { self.eraseToAnyPublisher().print(prefix, to: stream) } - } - - public func flatMapResult(_ transform: @escaping (Output) -> Result) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().flatMapResult(transform) } - } - - public func flatMap(_ transform: @escaping (Output) -> Publishers.Promise) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().flatMapLatest { transform($0).setFailureType(to: Failure.self) } } - } - - public func flatMap(_ transform: @escaping (Output) -> Publishers.Promise) -> Publishers.Promise where Failure == Never { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().setFailureType(to: E.self).flatMapLatest(transform) } - } - - public func flatMap(_ transform: @escaping (Output) -> Publishers.Promise) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().flatMapLatest(transform) } - } - - @available(*, deprecated, message: "Please use the function without providing maxPublishers, as the original Combine FlatMap may have some issues when returning error.") - public func flatMap(maxPublishers: Subscribers.Demand, _ transform: @escaping (Output) -> Publishers.Promise) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().flatMap(maxPublishers: maxPublishers) { transform($0).setFailureType(to: Failure.self) } } - } - - @available(*, deprecated, message: "Please use the function without providing maxPublishers, as the original Combine FlatMap may have some issues when returning error.") - public func flatMap(maxPublishers: Subscribers.Demand, _ transform: @escaping (Output) -> Publishers.Promise) -> Publishers.Promise where Failure == Never { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().setFailureType(to: E.self).flatMap(maxPublishers: maxPublishers, transform) } - } - - @available(*, deprecated, message: "Please use the function without providing maxPublishers, as the original Combine FlatMap may have some issues when returning error.") - public func flatMap(maxPublishers: Subscribers.Demand, _ transform: @escaping (Output) -> Publishers.Promise) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().flatMap(maxPublishers: maxPublishers, transform) } - } - - public func contains(_ output: Output) -> Publishers.Promise where Output: Equatable { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().contains(output) } - } - - public func allSatisfy(_ predicate: @escaping (Output) -> Bool) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().allSatisfy(predicate) } - } - - public func tryAllSatisfy(_ predicate: @escaping (Output) throws -> Bool) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().tryAllSatisfy(predicate) } - } - - public func contains(where predicate: @escaping (Output) -> Bool) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().contains(where: predicate) } - } - - public func tryContains(where predicate: @escaping (Output) throws -> Bool) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().tryContains(where: predicate) } - } - - public func collect() -> Publishers.Promise<[Output], Failure> { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().collect() } - } - - public func count() -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().count() } - } - - public func first() -> Self { self } - - public func last() -> Self { self } - - public func replaceError(with output: Output) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().replaceError(with: output) } - } - - public func replaceEmpty(with output: Output) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().replaceEmpty(with: output) } - } - - public func retry(_ times: Int) -> Publishers.Promise { - Publishers.Promise.unsafe { self.eraseToAnyPublisher().retry(times) } - } - - public func setFailureType(to failureType: E.Type) -> Publishers.Promise where Failure == Never { - Publishers.Promise.unsafe { 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.unsafe { self.eraseToAnyPublisher().delay(for: interval, tolerance: tolerance, scheduler: scheduler, options: options) } - } - - @available(*, deprecated, message: "Don't call .promise in a Promise") - public var promise: Publishers.Promise { - self as? Publishers.Promise ?? Publishers.Promise.unsafe { self } - } -} -#endif diff --git a/Sources/FoundationExtensions/Promise/PromiseType+EraseFailureToError.swift b/Sources/FoundationExtensions/Promise/PromiseType+EraseFailureToError.swift deleted file mode 100644 index f28acb6..0000000 --- a/Sources/FoundationExtensions/Promise/PromiseType+EraseFailureToError.swift +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright © 2023 Lautsprecher Teufel GmbH. All rights reserved. - -#if canImport(Combine) -import Foundation -import Combine - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension PromiseType { - /// Erases `Failure` type to `Error`. - /// - /// Publishers.Promise(value: 1) // Publishers.Promise - /// .eraseFailureToError() // Publishers.Promise.Output, Error> - /// - /// - Returns: An ``Publishers.Promise`` wrapping this promise. - public func eraseFailureToError() -> Publishers.Promise { - mapError(identity) - } -} -#endif diff --git a/Sources/FoundationExtensions/Promise/PromiseType+Fold.swift b/Sources/FoundationExtensions/Promise/PromiseType+Fold.swift deleted file mode 100644 index 4f37098..0000000 --- a/Sources/FoundationExtensions/Promise/PromiseType+Fold.swift +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright © 2023 Lautsprecher Teufel GmbH. All rights reserved. - -#if canImport(Combine) -import Combine -import Foundation - -// MARK: - Coproduct -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension PromiseType { - /// Case folding for Promise. Given functions that convert either errors or wrapped values into the same type FoldedValue - /// evaluates the promise and returns Deferred. - /// - /// - Parameters: - /// - onSuccess: a function that, given a wrapped value `Success`, executes an operation that returns `TargetType`, - /// which will be the result of this function - /// - onFailure: a function that, given an error `Failure`, executes an operation that returns `TargetType`, - /// which will be the result of this function - /// - Returns: the value produced by applying `ifFailure` to `failure` Promise, or `ifSuccess` to `success` Promise, any of them - /// translated into the same `Deferred`. - public func fold(onSuccess: @escaping (Success) -> TargetType, onFailure: @escaping (Failure) -> TargetType) - -> Publishers.Promise { - map(onSuccess) - .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 - /// complete with future Void when the analysis is done. - /// - /// - Parameters: - /// - ifSuccess: a function that, given a wrapped `Success`, executes an operation with no return value - /// - ifFailure: a function that, given an error `Failure`, executes an operation with no return value - /// - Returns: returns a future Void, indicating that Promise ran and evaluated the two possible scenarios, success or failure. - public func analysis(ifSuccess: @escaping (Success) -> Void, ifFailure: @escaping (Failure) -> Void) - -> Publishers.Promise { - fold(onSuccess: ifSuccess, onFailure: ifFailure) - } - - /// If this promise results in an error, executes the provided closure passing the inner error - /// - /// - Parameter run: the block to execute in case this is an error - /// - Returns: returns always `self` with no changes, for chaining purposes. - public func onError(_ run: @escaping (Failure) -> Void) -> Publishers.Promise { - handleEvents( - receiveCompletion: { completion in - if case let .failure(error) = completion { - run(error) - } - } - ).promise - } -} -#endif diff --git a/Sources/FoundationExtensions/Promise/PromiseType+Init.swift b/Sources/FoundationExtensions/Promise/PromiseType+Init.swift deleted file mode 100644 index ec76509..0000000 --- a/Sources/FoundationExtensions/Promise/PromiseType+Init.swift +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright © 2023 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 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 { 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 { 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 { Empty().nonEmpty(fallback: { result }) } - } - - /// Creates a new promise by evaluating a synchronous throwing closure, capturing the - /// returned value as a success, or any thrown error as a failure. - /// - /// - 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)) - } -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension PromiseType where Failure == Error { - /// Creates a new promise by evaluating a synchronous throwing closure, capturing the - /// 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() }) - } -} -#endif diff --git a/Sources/FoundationExtensions/Promise/PromiseType+Run.swift b/Sources/FoundationExtensions/Promise/PromiseType+Run.swift deleted file mode 100644 index f772eae..0000000 --- a/Sources/FoundationExtensions/Promise/PromiseType+Run.swift +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright © 2023 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 PromiseType { - /// Similar to .sink, but with correct semantic for a single-value success or a failure. Creates demand for 1 - /// value and completes after it, or on error. - /// - Parameters: - /// - onSuccess: called when the promise is fulfilled successful with a value. Called only once and then - /// you can consider this stream finished - /// - onFailure: called when the promise finds error. Called only once completing the stream. It's never - /// called if a success was already called - /// - Returns: the subscription that can be cancelled at any point. - public func run(onSuccess: @escaping (Output) -> Void, onFailure: @escaping (Failure) -> Void) -> AnyCancellable { - sink( - receiveCompletion: { completion in - if case let .failure(error) = completion { - onFailure(error) - } - }, - receiveValue: onSuccess - ) - } -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension PromiseType where Failure == Never { - /// Similar to .sink, but with correct semantic for a single-value success or a failure. Creates demand for 1 - /// value and completes after it, or on error. - /// - Parameters: - /// - onSuccess: called when the promise is fulfilled successful with a value. Called only once and then - /// you can consider this stream finished - /// - Returns: the subscription that can be cancelled at any point. - public func run(onSuccess: @escaping (Output) -> Void) -> AnyCancellable { - run(onSuccess: onSuccess, onFailure: { _ in }) - } -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension PromiseType where Success == Void { - /// Similar to .sink, but with correct semantic for a single-value success or a failure. Creates demand for 1 - /// value and completes after it, or on error. - /// - Parameters: - /// - onFailure: called when the promise finds error. Called only once completing the stream. It's never - /// called if a success was already called - /// - Returns: the subscription that can be cancelled at any point. - public func run(onFailure: @escaping (Failure) -> Void) -> AnyCancellable { - run(onSuccess: { _ in }, onFailure: onFailure) - } -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension PromiseType where Success == Void, Failure == Never { - /// Similar to .sink, but with correct semantic for a single-value success or a failure. Creates demand for 1 - /// value and completes after it, or on error. - /// - Returns: the subscription that can be cancelled at any point. - public func run() -> AnyCancellable { - run(onSuccess: { _ in }, onFailure: { _ in }) - } -} -#endif diff --git a/Sources/FoundationExtensions/Promise/PromiseType+Zip.swift b/Sources/FoundationExtensions/Promise/PromiseType+Zip.swift deleted file mode 100644 index acd9b4a..0000000 --- a/Sources/FoundationExtensions/Promise/PromiseType+Zip.swift +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright © 2023 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 PromiseType { - /// 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 combined in a tuple and then forwarded to the downstream. - /// - Parameters: - /// - p1: first promise - /// - p2: second promise - /// - 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 - ) -> Publishers.Promise<(P1.Output, P2.Output), Failure> - where P1.Failure == Failure, P2.Failure == Failure, - Output == (P1.Output, P2.Output) { - Publishers.Promise.unsafe { - Publishers.Zip(p1, p2) - } - } - - /// 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 combined in a tuple and then forwarded to the downstream. - /// - Parameters: - /// - p1: first promise - /// - p2: second promise - /// - p3: third promise - /// - 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 - ) -> 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.unsafe { - Publishers.Zip3(p1, p2, p3) - } - } - - /// 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 combined in a tuple and then forwarded to the downstream. - /// - Parameters: - /// - p1: first promise - /// - p2: second promise - /// - p3: third promise - /// - p4: fourth promise - /// - 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 - ) -> 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.unsafe { - Publishers.Zip4(p1, p2, p3, p4) - } - } - - /// 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 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 - /// - 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 - ) -> 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.unsafe { - Publishers.Zip( - Publishers.Zip4(p1, p2, p3, p4), - p5 - ) - .map { left, right in (left.0, left.1, left.2, left.3, right) } - } - } - - /// 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 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 - /// - p6: sixth promise - /// - 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 - ) -> 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.unsafe { - Publishers.Zip( - Publishers.Zip4(p1, p2, p3, p4), - Publishers.Zip(p5, p6) - ) - .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 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 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 - /// - p6: sixth promise - /// - p7: seventh promise - /// - 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, - _ 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.unsafe { - Publishers.Zip( - Publishers.Zip4(p1, p2, p3, p4), - Publishers.Zip3(p5, p6, p7) - ) - .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 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 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 - /// - p6: sixth promise - /// - p7: seventh promise - /// - p8: eighth promise - /// - 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, - _ p7: P7, - _ 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.unsafe { - Publishers.Zip( - Publishers.Zip4(p1, p2, p3, p4), - Publishers.Zip4(p5, p6, p7, p8) - ) - .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 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 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 - /// - p6: sixth promise - /// - p7: seventh promise - /// - p8: eighth promise - /// - p9: ninth promise - /// - 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, - _ p7: P7, - _ p8: P8, - _ 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.unsafe { - Publishers.Zip3( - Publishers.Zip4(p1, p2, p3, p4), - Publishers.Zip4(p5, p6, p7, p8), - p9 - ) - .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 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 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 - /// - p6: sixth promise - /// - p7: seventh promise - /// - p8: eighth promise - /// - p9: ninth promise - /// - p10: tenth promise - /// - 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, - _ p7: P7, - _ p8: P8, - _ p9: P9, - _ 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.unsafe { - Publishers.Zip3( - Publishers.Zip4(p1, p2, p3, p4), - Publishers.Zip4(p5, p6, p7, p8), - Publishers.Zip(p9, p10) - ) - .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 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 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 - /// - p6: sixth promise - /// - p7: seventh promise - /// - p8: eighth promise - /// - p9: ninth promise - /// - p10: tenth promise - /// - p11: eleventh promise - /// - 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, - _ p2: P2, - _ p3: P3, - _ p4: P4, - _ p5: P5, - _ p6: P6, - _ p7: P7, - _ p8: P8, - _ p9: P9, - _ p10: P10, - _ 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.unsafe { - Publishers.Zip3( - Publishers.Zip4(p1, p2, p3, p4), - Publishers.Zip4(p5, p6, p7, p8), - Publishers.Zip3(p9, p10, p11) - ) - .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 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 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 - /// - p6: sixth promise - /// - p7: seventh promise - /// - p8: eighth promise - /// - p9: ninth promise - /// - p10: tenth promise - /// - p11: eleventh promise - /// - p12: twelfth promise - /// - 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, - _ p7: P7, - _ p8: P8, - _ p9: P9, - _ p10: P10, - _ p11: P11, - _ 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.unsafe { - Publishers.Zip3( - Publishers.Zip4(p1, p2, p3, p4), - Publishers.Zip4(p5, p6, p7, p8), - Publishers.Zip4(p9, p10, p11, p12) - ) - .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) - } - } - } -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension Publisher { - public static func zip(_ promises: [P]) -> Publishers.Promise<[P.Output], P.Failure> - where P.Output == Output, P.Failure == Failure { - switch promises.count { - case ...0: - return .init(value: []) - case 1: - return promises[0].map { [$0] } - default: - let result: Publishers.Promise<[P.Output], P.Failure> = promises[0].map { [$0] } - - return promises.dropFirst().reduce(result) { partial, current -> Publishers.Promise<[P.Output], P.Failure> in - Publishers.Promise.zip( - partial, - current - ) - .map { accumulation, next in accumulation + [next] } - } - } - } -} -#endif diff --git a/Sources/FoundationExtensions/Promise/PromiseType.swift b/Sources/FoundationExtensions/Promise/PromiseType.swift deleted file mode 100644 index aa56a81..0000000 --- a/Sources/FoundationExtensions/Promise/PromiseType.swift +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright © 2023 Lautsprecher Teufel GmbH. All rights reserved. - -#if canImport(Combine) -import Combine -import Foundation - -/// Any type or protocol that can be converted into `Promise` -/// Used for type erasure. -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -public protocol PromiseType: PromiseConvertibleType, Publisher where Success == Output { - /// 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 () -> 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 deleted file mode 100644 index 25501c5..0000000 --- a/Sources/FoundationExtensions/Promise/Publisher+NonEmpty.swift +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright © 2023 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 deleted file mode 100644 index 97d7a3c..0000000 --- a/Sources/FoundationExtensions/Promise/Publisher+PromiseConvertibleType.swift +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright © 2023 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 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. - /// 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 func promise(onEmpty fallback: @escaping () -> Result) -> Publishers.Promise { - self.nonEmpty(fallback: fallback).promise - } - - public func assertNonEmptyPromise() -> Publishers.Promise { - NonEmptyPublisher.unsafe(nonEmptyUpstream: self).promise - } -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension Just: PromiseConvertibleType { - public var promise: Publishers.Promise { - Publishers.Promise.unsafe { self } - } -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension Fail: PromiseConvertibleType { - public var promise: Publishers.Promise { - Publishers.Promise.unsafe { 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.unsafe { 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.unsafe { self } - } -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension Future: PromiseConvertibleType { - public var promise: Publishers.Promise { - Publishers.Promise.unsafe { self } - } -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension URLSession.DataTaskPublisher { - public var promise: Publishers.Promise { - Publishers.Promise.unsafe { self } - } -} -#endif diff --git a/Sources/FoundationExtensions/Result/Result+Promise.swift b/Sources/FoundationExtensions/Result/Result+Promise.swift new file mode 100644 index 0000000..f665532 --- /dev/null +++ b/Sources/FoundationExtensions/Result/Result+Promise.swift @@ -0,0 +1,10 @@ +// Copyright © 2024 Lautsprecher Teufel GmbH. All rights reserved. + +import Combine +import Foundation + +extension Result { + public var promise: Publishers.Promise { + Publishers.Promise(self) + } +} diff --git a/Sources/FoundationExtensions/Utils/OSLog.swift b/Sources/FoundationExtensions/Utils/OSLog.swift new file mode 100644 index 0000000..fbfda94 --- /dev/null +++ b/Sources/FoundationExtensions/Utils/OSLog.swift @@ -0,0 +1,11 @@ +// Copyright © 2025 Lautsprecher Teufel GmbH. All rights reserved. + +import OSLog + +extension OSLog { + private static let subsystem = Bundle.main.bundleIdentifier ?? "FoundationExtensions" + + /// All logs related to tracking and analytics. + @usableFromInline + static let publishersPromise = OSLog(subsystem: subsystem, category: "Publishers.Promise") +} diff --git a/Tests/FoundationExtensionsTests/Combine/PromiseTests.swift b/Tests/FoundationExtensionsTests/Combine/PromiseTests.swift new file mode 100644 index 0000000..421e1ee --- /dev/null +++ b/Tests/FoundationExtensionsTests/Combine/PromiseTests.swift @@ -0,0 +1,1734 @@ +// Copyright © 2025 Lautsprecher Teufel GmbH. All rights reserved. + +import XCTest +@preconcurrency import Combine +@testable @preconcurrency import FoundationExtensions + +final class PromiseTests: XCTestCase { + func testPromise_OperationClosure() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + let expectationPromise1Operation = XCTestExpectation(description: "promise1 operation executed") + + let promise1 = Publishers.Promise { promise in + expectationPromise1Operation.fulfill() + promise(.success(42)) + return AnyCancellable { } + } + + var value1: Int? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + let resultExpectations1 = XCTWaiter.wait(for: [ + expectationPromise1ReceiveCompletion, + expectationPromise1Operation + ], timeout: 0.1) + XCTAssertEqual(resultExpectations1, .completed) + XCTAssertEqual(value1, 42) + + // Test error case + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 failure") + let expectationPromise2Operation = XCTestExpectation(description: "promise2 operation executed") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + + let promise2 = Publishers.Promise { promise in + expectationPromise2Operation.fulfill() + promise(.failure(.foo)) + return AnyCancellable { } + } + + let cancellable2 = promise2.sink( + receiveCompletion: { if case .failure(MockError.foo) = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromise2ReceiveValue.fulfill() } + ) + + let resultExpectations2 = XCTWaiter.wait(for: [ + expectationPromise2ReceiveCompletion, + expectationPromise2Operation + ], timeout: 0.1) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectations2, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + // Test cancellation + let expectationPromise3Operation = XCTestExpectation(description: "promise3 operation executed") + let expectationPromise3Cancellation = XCTestExpectation(description: "promise3 cancelled") + + let promise3 = Publishers.Promise { _ in + expectationPromise3Operation.fulfill() + return AnyCancellable { + expectationPromise3Cancellation.fulfill() + } + } + + let cancellable3 = promise3.sink( + receiveCompletion: { _ in }, + receiveValue: { _ in } + ) + + cancellable3.cancel() + + let resultExpectations3 = XCTWaiter.wait(for: [ + expectationPromise3Operation, + expectationPromise3Cancellation + ], timeout: 0.1) + XCTAssertEqual(resultExpectations3, .completed) + + _ = cancellable1 + _ = cancellable2 + } + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func testPromise_asyncInit() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let promise1 = Publishers.Promise { + try await Task.sleep(nanoseconds: 1) + return 42 + } + + var value1: Int? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 1.0) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, 42) + + // Error case + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 failure") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + + let promise2 = Publishers.Promise { + try await Task.sleep(nanoseconds: 1) + throw MockError.foo + } + + let cancellable2 = promise2.sink( + receiveCompletion: { if case .failure = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromise2ReceiveValue.fulfill() } + ) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 1.0) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + _ = cancellable2 + } + + func testPromise_Value() { + let expectation = XCTestExpectation(description: "value") + let promise = Publishers.Promise(value: 1) + var value: Int? + let cancellable = promise + .sink( + receiveCompletion: { if case .finished = $0 { expectation.fulfill() } }, + receiveValue: { value = $0 } + ) + + + let result = XCTWaiter.wait(for: [expectation], timeout: 0.1) + XCTAssertEqual(result, .completed) + XCTAssertEqual(value, 1) + _ = cancellable + } + + func testPromise_Error() { + let expectation = XCTestExpectation(description: "failure") + let promise = Publishers.Promise(outputType: Void.self, error: MockError.foo) + + let cancellable = promise + .sink( + receiveCompletion: { if case .failure(.foo) = $0 { expectation.fulfill() } }, + receiveValue: { } + ) + + let result = XCTWaiter.wait(for: [expectation], timeout: 0.1) + XCTAssertEqual(result, .completed) + _ = cancellable + } + + func testPromise_Result() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 fails") + let expectationPromise1ReceiveValue = XCTestExpectation(description: "promise1 does not receive value") + let promise1 = Publishers.Promise(.failure(.foo)) + + let cancellable1 = promise1 + .sink( + receiveCompletion: { if case .failure(.foo) = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { expectationPromise1ReceiveValue.fulfill() } + ) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise1ReceiveValue = XCTWaiter.wait(for: [expectationPromise1ReceiveValue], timeout: 0.3) + + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise1ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 finished") + let promise2 = Publishers.Promise(.success(1)) + var value2: Int? + + let cancellable2 = promise2 + .sink( + receiveCompletion: { if case .finished = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { value2 = $0 } + ) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(value2, 1) + + _ = cancellable1 + _ = cancellable2 + } + + func testPromise_Empty() { + let expectation = XCTestExpectation(description: "hangs") + let promise = Publishers.Promise(outputType: Void.self, failureType: MockError.self) + + let cancellable = promise + .sink( + receiveCompletion: { _ in expectation.fulfill() }, + receiveValue: { expectation.fulfill() } + ) + + let result = XCTWaiter.wait(for: [expectation], timeout: 0.3) + XCTAssertEqual(result, .timedOut, "Expectation was unexpectedly fulfilled") + _ = cancellable + } + + func testPromise_map() { + let expectationPromiseReceiveCompletion = XCTestExpectation(description: "promise is finished") + + let subject = PassthroughSubject() + let promise = subject + .assertPromise("testPromise_map: ") + .map(String.init) + + var value: String? + + let cancellable = promise + .sink( + receiveCompletion: { if case .finished = $0 { expectationPromiseReceiveCompletion.fulfill() } }, + receiveValue: { value = $0 } + ) + + subject.send(1) + subject.send(2) + + let resultExpectationPromiseReceiveCompletion = XCTWaiter.wait(for: [expectationPromiseReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromiseReceiveCompletion, .completed) + XCTAssertEqual(value, "1") + + _ = cancellable + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 success") + + struct TestModel { + let value: Int + var stringValue: String { String(value) } + } + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .assertPromise("testPromise_map_keyPath: ") + .map(\.value) + + var value2: Int? + let cancellable2 = promise2.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { value2 = $0 } + ) + + subject2.send(TestModel(value: 42)) + subject2.send(completion: .finished) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(value2, 42) + + let expectationPromise3ReceiveCompletion = XCTestExpectation(description: "promise3 success") + + let subject3 = PassthroughSubject() + let promise3 = subject3 + .assertPromise("testPromise_map_keyPath: ") + .map(\.stringValue, \.value) + + var value3: (String, Int)? + let cancellable3 = promise3.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise3ReceiveCompletion.fulfill() } }, + receiveValue: { value3 = $0 } + ) + + subject3.send(TestModel(value: 42)) + subject3.send(completion: .finished) + + let resultExpectationPromise3ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise3ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise3ReceiveCompletion, .completed) + XCTAssertEqual(value3?.0, "42") + XCTAssertEqual(value3?.1, 42) + + _ = cancellable2 + _ = cancellable3 + } + + func testPromise_tryMap() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 is finished") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_tryMap: ") + .tryMap { + guard let str = Int($0) + else { throw MockError.foo } + return str + } + + var value1: Int? + + let cancellable1 = promise1 + .sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send("1") + subject1.send("a") + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, 1) + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 is failed") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .assertPromise("testPromise_tryMap: ") + .tryMap { + guard let str = Int($0) + else { throw MockError.foo } + return str + } + + let cancellable2 = promise2 + .sink( + receiveCompletion: { if case .failure(MockError.foo) = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromise2ReceiveValue.fulfill() } + ) + + + subject2.send("a") + subject2.send("1") + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + _ = cancellable2 + } + + func testPromise_mapError() { + let expectationPromiseReceiveCompletion = XCTestExpectation(description: "promise is failed") + let expectationPromiseReceiveValue = XCTestExpectation(description: "promise does not receive value") + + let subject = PassthroughSubject() + let promise = subject + .assertPromise("testPromise_mapError: ") + .mapError { _ in MockError.foo } + + let cancellable = promise + .sink( + receiveCompletion: { if case .failure(.foo) = $0 { expectationPromiseReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromiseReceiveValue.fulfill() } + ) + + subject.send(completion: .failure(.init(.badURL))) + subject.send(()) + + let resultExpectationPromiseReceiveCompletion = XCTWaiter.wait(for: [expectationPromiseReceiveCompletion], timeout: 0.1) + let resultExpectationPromiseReceiveValue = XCTWaiter.wait(for: [expectationPromiseReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromiseReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromiseReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable + } + + func testPromise_zip() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 is finished") + + let subject1 = PassthroughSubject() + let subject2 = PassthroughSubject() + let promise1 = Publishers.Promise.zip( + subject1.assertPromise("testPromise_zip: "), + subject2.assertPromise("testPromise_zip: ") + ) + var value1: Int? = nil + var value2: String? = nil + let cancellable1 = promise1 + .sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0; value2 = $1 } + ) + + subject1.send(1) + subject2.send("1") + subject1.send(2) + subject2.send("2") + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, 1) + XCTAssertEqual(value2, "1") + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 is failed") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + + let subject3 = PassthroughSubject() + let subject4 = PassthroughSubject() + let promise2 = Publishers.Promise.zip( + subject3.assertPromise("testPromise_zip: "), + subject4.assertPromise("testPromise_zip: ") + ) + + let cancellable2 = promise2 + .sink( + receiveCompletion: { if case .failure(.foo) = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromise2ReceiveValue.fulfill() } + ) + + subject3.send(completion: .failure(.foo)) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + let expectationPromise3ReceiveCompletion = XCTestExpectation(description: "promise3 is failed") + let expectationPromise3ReceiveValue = XCTestExpectation(description: "promise3 does not receive value") + + let subject5 = PassthroughSubject() + let subject6 = PassthroughSubject() + let promise3 = Publishers.Promise.zip( + subject5.assertPromise("testPromise_zip: "), + subject6.assertPromise("testPromise_zip: ") + ) + + let cancellable3 = promise3 + .sink( + receiveCompletion: { if case .failure(.foo) = $0 { expectationPromise3ReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromise3ReceiveValue.fulfill() } + ) + + subject5.send(1) + subject6.send(completion: .failure(.foo)) + + let resultExpectationPromise3ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise3ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise3ReceiveValue = XCTWaiter.wait(for: [expectationPromise3ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise3ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise3ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + _ = cancellable2 + _ = cancellable3 + } + + func testPromise_assertNoFailure() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_assertNoFailure: ") + .assertNoFailure() + + var value1: Int? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(42) + subject1.send(completion: .finished) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, 42) + + _ = cancellable1 + } + + @available(iOS 14.0, *) + func testPromise_catch() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 is finished") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_catch: ") + .catch { _ in .init(error: MockError.foo) } + + var value1: Int? + + let cancellable1 = promise1 + .sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(1) + subject1.send(completion: .failure(URLError(.badURL))) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, 1) + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 is failed") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .assertPromise("testPromise_catch: ") + .catch { _ in .init(error: MockError.foo) } + + let cancellable2 = promise2 + .sink( + receiveCompletion: { if case .failure(.foo) = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { expectationPromise2ReceiveValue.fulfill() } + ) + + subject2.send(completion: .failure(URLError(.badURL))) + subject2.send(()) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + _ = cancellable2 + } + + func testPromise_tryCatch() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_tryCatch: ") + .tryCatch { error -> Publishers.Promise in + XCTAssertEqual(error, .foo) + return .init(value: 42) + } + + var value1: Int? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(completion: .failure(.foo)) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, 42) + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 failure") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .assertPromise("testPromise_tryCatch: ") + .tryCatch { error -> Publishers.Promise in + throw error + } + + let cancellable2 = promise2.sink( + receiveCompletion: { if case .failure(MockError.foo) = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromise2ReceiveValue.fulfill() } + ) + + subject2.send(completion: .failure(.foo)) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + _ = cancellable2 + } + + func testPromise_retry() { + let expectationFiniteSuccess = XCTestExpectation(description: "Finite retry: success after 2 failures") + let attemptCounterFiniteSuccess = Atomic(0) + let maxRetriesFiniteSuccess = 2 + + let promiseFiniteSuccess = Publishers.Promise { resolver in + let attempt = attemptCounterFiniteSuccess.withLock { $0 += 1; return $0 } + if attempt <= maxRetriesFiniteSuccess { + resolver(.failure(.foo)) + } else { + resolver(.success(42)) + expectationFiniteSuccess.fulfill() + } + return AnyCancellable { } + } + .retry(maxRetriesFiniteSuccess) + + var valueFiniteSuccess: Int? + let cancellableFiniteSuccess = promiseFiniteSuccess.sink( + receiveCompletion: { _ in }, + receiveValue: { valueFiniteSuccess = $0 } + ) + + wait(for: [expectationFiniteSuccess], timeout: 1.0) + XCTAssertEqual(attemptCounterFiniteSuccess.value, maxRetriesFiniteSuccess + 1) + XCTAssertEqual(valueFiniteSuccess, 42) + _ = cancellableFiniteSuccess + + let expectationFiniteExhaust = XCTestExpectation(description: "Finite retry: exhaust attempts") + let attemptCounterFiniteExhaust = Atomic(0) + let maxRetriesFiniteExhaust = 3 + + let promiseFiniteExhaust = Publishers.Promise { resolver in + let attempt = attemptCounterFiniteExhaust.withLock { $0 += 1; return $0 } + resolver(.failure(.foo)) + if attempt == maxRetriesFiniteExhaust + 1 { + expectationFiniteExhaust.fulfill() + } + return AnyCancellable { } + } + .retry(maxRetriesFiniteExhaust) + + var errorFiniteExhaust: MockError? + let cancellableFiniteExhaust = promiseFiniteExhaust.sink( + receiveCompletion: { + if case .failure(let error) = $0 { errorFiniteExhaust = error } + }, + receiveValue: { _ in } + ) + + wait(for: [expectationFiniteExhaust], timeout: 1.0) + XCTAssertEqual(attemptCounterFiniteExhaust.value, maxRetriesFiniteExhaust + 1) + XCTAssertEqual(errorFiniteExhaust, .foo) + _ = cancellableFiniteExhaust + + let expectationImmediateSuccess = XCTestExpectation(description: "Immediate success") + let attemptCounterImmediateSuccess = Atomic(0) + + let promiseImmediateSuccess = Publishers.Promise { resolver in + attemptCounterImmediateSuccess.withLock { $0 += 1 } + resolver(.success(42)) + expectationImmediateSuccess.fulfill() + return AnyCancellable { } + } + .retry(3) + + var valueImmediateSuccess: Int? + let cancellableImmediateSuccess = promiseImmediateSuccess.sink( + receiveCompletion: { _ in }, + receiveValue: { valueImmediateSuccess = $0 } + ) + + wait(for: [expectationImmediateSuccess], timeout: 1.0) + XCTAssertEqual(attemptCounterImmediateSuccess.value, 1) + XCTAssertEqual(valueImmediateSuccess, 42) + _ = cancellableImmediateSuccess + } + + func testPromise_share() { + // Success case with multiple subscribers + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 success") + let expectationUpstream1 = XCTestExpectation(description: "Upstream1 executes once") + let sideEffectCounter1 = Atomic(0) + + let promise1 = Publishers.Promise { resolver in + sideEffectCounter1.withLock { $0 += 1 } + expectationUpstream1.fulfill() + resolver(.success(42)) + return AnyCancellable { } + } + .share() + + var value1: Int? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + var value2: Int? + let cancellable2 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { value2 = $0 } + ) + + let resultExpectations1 = XCTWaiter.wait(for: [ + expectationPromise1ReceiveCompletion, + expectationPromise2ReceiveCompletion, + expectationUpstream1 + ], timeout: 0.1) + XCTAssertEqual(resultExpectations1, .completed) + XCTAssertEqual(sideEffectCounter1.value, 1, "Upstream must execute exactly once") + XCTAssertEqual(value1, 42) + XCTAssertEqual(value2, 42) + + // Late subscriber after completion + var value3: Int? + let cancellable3 = promise1.sink( + receiveCompletion: { _ in }, + receiveValue: { value3 = $0 } + ) + XCTAssertEqual(value3, 42, "Late subscriber should receive cached value") + + // Error case with multiple subscribers + let expectationPromise4ReceiveCompletion = XCTestExpectation(description: "promise4 failure") + let expectationPromise5ReceiveCompletion = XCTestExpectation(description: "promise5 failure") + let expectationUpstream2 = XCTestExpectation(description: "Upstream2 executes once") + let sideEffectCounter2 = Atomic(0) + + let promise2 = Publishers.Promise { resolver in + sideEffectCounter2.withLock { $0 += 1 } + expectationUpstream2.fulfill() + resolver(.failure(.foo)) + return AnyCancellable { } + } + .share() + + var error1: MockError? + let cancellable4 = promise2.sink( + receiveCompletion: { if case .failure(let error) = $0 { + error1 = error + expectationPromise4ReceiveCompletion.fulfill() + }}, + receiveValue: { _ in } + ) + + var error2: MockError? + let cancellable5 = promise2.sink( + receiveCompletion: { if case .failure(let error) = $0 { + error2 = error + expectationPromise5ReceiveCompletion.fulfill() + }}, + receiveValue: { _ in } + ) + + let resultExpectations2 = XCTWaiter.wait(for: [ + expectationPromise4ReceiveCompletion, + expectationPromise5ReceiveCompletion, + expectationUpstream2 + ], timeout: 0.1) + XCTAssertEqual(resultExpectations2, .completed) + XCTAssertEqual(sideEffectCounter2.value, 1, "Upstream must execute exactly once") + XCTAssertEqual(error1, .foo) + XCTAssertEqual(error2, .foo) + + // Late subscriber after completion + var error3: MockError? + let cancellable6 = promise2.sink( + receiveCompletion: { if case .failure(let error) = $0 { error3 = error }}, + receiveValue: { _ in } + ) + XCTAssertEqual(error3, .foo, "Late subscriber should receive cached error") + + _ = [cancellable1, cancellable2, cancellable3, cancellable4, cancellable5, cancellable6] + } + + func testPromise_setFailureType() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_setFailureType: ") + .setFailureType(to: MockError.self) + + var value1: Int? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(42) + subject1.send(completion: .finished) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, 42) + + _ = cancellable1 + } + + func testPromise_eraseFailureToError() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_eraseFailureToError: ") + .eraseFailureToError() + + var value1: Int? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(42) + subject1.send(completion: .finished) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, 42) + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 failure") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .assertPromise("testPromise_eraseFailureToError: ") + .eraseFailureToError() + + let cancellable2 = promise2.sink( + receiveCompletion: { + if case .failure(MockError.foo) = $0 { expectationPromise2ReceiveCompletion.fulfill() } + }, + receiveValue: { _ in expectationPromise2ReceiveValue.fulfill() } + ) + + subject2.send(completion: .failure(.foo)) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + _ = cancellable2 + } + + func testPromise_run() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let promise1 = Publishers.Promise(value: 42) + + var value1: Int? + let cancellable1 = promise1.run( + onSuccess: { + value1 = $0 + expectationPromise1ReceiveCompletion.fulfill() + }, + onFailure: { _ in } + ) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, 42) + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 failure") + + let promise2 = Publishers.Promise(error: .foo) + + var error2: MockError? + let cancellable2 = promise2.run( + onSuccess: { _ in }, + onFailure: { + error2 = $0 + expectationPromise2ReceiveCompletion.fulfill() + } + ) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(error2, .foo) + + let expectationPromise3ReceiveCompletion = XCTestExpectation(description: "promise3 void success") + + let promise3 = Publishers.Promise(value: ()) + let cancellable3 = promise3.run { + expectationPromise3ReceiveCompletion.fulfill() + } + + let resultExpectationPromise3ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise3ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise3ReceiveCompletion, .completed) + + _ = cancellable1 + _ = cancellable2 + _ = cancellable3 + } + + @available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 8.0, *) + func testPromise_asyncValue() async throws { + let expectationPromise1ReceiveValue = XCTestExpectation(description: "promise1 success") + + // Success case + let promise1 = Publishers.Promise(value: 42) + Task { + do { + let value = try await promise1.value() + XCTAssertEqual(value, 42) + expectationPromise1ReceiveValue.fulfill() + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + let resultExpectationPromise1ReceiveValue = await XCTWaiter().fulfillment(of: [expectationPromise1ReceiveValue], timeout: 1.0) + XCTAssertEqual(resultExpectationPromise1ReceiveValue, .completed) + + // Error case + let expectationPromise2ReceiveError = XCTestExpectation(description: "promise2 error") + + let promise2 = Publishers.Promise(error: MockError.foo) + Task { + do { + _ = try await promise2.value() + XCTFail("Expected error to be thrown") + } catch { + XCTAssertTrue(error is MockError) + expectationPromise2ReceiveError.fulfill() + } + } + + let resultExpectationPromise2ReceiveError = await XCTWaiter().fulfillment(of: [expectationPromise2ReceiveError], timeout: 1.0) + XCTAssertEqual(resultExpectationPromise2ReceiveError, .completed) + + // Cancellation case + let expectationPromise3ReceiveCancellation = XCTestExpectation(description: "promise3 cancellation") + + let promise3 = Publishers.Promise { _ in + AnyCancellable { expectationPromise3ReceiveCancellation.fulfill() } + } + + let task = Task { + _ = try await promise3.value() + } + + task.cancel() + + let resultExpectationPromise3ReceiveCancellation = await XCTWaiter().fulfillment(of: [expectationPromise3ReceiveCancellation], timeout: 1.0) + XCTAssertEqual(resultExpectationPromise3ReceiveCancellation, .completed) + } + + func testPromise_flatMapResult() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_flatMapResult: ") + .flatMapResult { value -> Result in + .success(String(value)) + } + + var value1: String? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(42) + subject1.send(completion: .finished) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, "42") + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 failure") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .assertPromise("testPromise_flatMapResult: ") + .flatMapResult { _ -> Result in + .failure(.foo) + } + + let cancellable2 = promise2.sink( + receiveCompletion: { if case .failure(MockError.foo) = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromise2ReceiveValue.fulfill() } + ) + + subject2.send(42) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + _ = cancellable2 + } + + func testPromise_fold() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_fold: ") + .fold( + onSuccess: { "Success: \($0)" }, + onFailure: { "Error: \($0)" } + ) + + var value1: String? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(42) + subject1.send(completion: .finished) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, "Success: 42") + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 success with error fold") + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .assertPromise("testPromise_fold: ") + .fold( + onSuccess: { "Success: \($0)" }, + onFailure: { "Error: \($0)" } + ) + + var value2: String? + let cancellable2 = promise2.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { value2 = $0 } + ) + + subject2.send(completion: .failure(.foo)) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(value2, "Error: foo") + + _ = cancellable1 + _ = cancellable2 + } + + func testPromise_analysis() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + let expectationPromise1Analysis = XCTestExpectation(description: "promise1 analysis success") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_analysis: ") + .analysis( + ifSuccess: { _ in expectationPromise1Analysis.fulfill() }, + ifFailure: { _ in XCTFail("Unexpected error") } + ) + + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { _ in } + ) + + subject1.send(42) + subject1.send(completion: .finished) + + let resultExpectations1 = XCTWaiter.wait(for: [ + expectationPromise1ReceiveCompletion, + expectationPromise1Analysis + ], timeout: 0.1) + XCTAssertEqual(resultExpectations1, .completed) + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 success") + let expectationPromise2Analysis = XCTestExpectation(description: "promise2 analysis error") + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .assertPromise("testPromise_analysis: ") + .analysis( + ifSuccess: { _ in XCTFail("Unexpected success") }, + ifFailure: { _ in expectationPromise2Analysis.fulfill() } + ) + + let cancellable2 = promise2.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { _ in } + ) + + subject2.send(completion: .failure(.foo)) + + let resultExpectations2 = XCTWaiter.wait(for: [ + expectationPromise2ReceiveCompletion, + expectationPromise2Analysis + ], timeout: 0.1) + XCTAssertEqual(resultExpectations2, .completed) + + _ = cancellable1 + _ = cancellable2 + } + + func testPromise_onError() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + let expectationPromise1OnError = XCTestExpectation(description: "promise1 onError not called") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_onError: ") + .onError { _ in expectationPromise1OnError.fulfill() } + + var value1: Int? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(42) + subject1.send(completion: .finished) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise1OnError = XCTWaiter.wait(for: [expectationPromise1OnError], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise1OnError, .timedOut, "OnError was unexpectedly called") + XCTAssertEqual(value1, 42) + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 failure") + let expectationPromise2OnError = XCTestExpectation(description: "promise2 onError called") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .assertPromise("testPromise_onError: ") + .onError { error in + XCTAssertEqual(error, .foo) + expectationPromise2OnError.fulfill() + } + + let cancellable2 = promise2.sink( + receiveCompletion: { if case .failure(MockError.foo) = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromise2ReceiveValue.fulfill() } + ) + + subject2.send(completion: .failure(.foo)) + + let resultExpectations2 = XCTWaiter.wait(for: [ + expectationPromise2ReceiveCompletion, + expectationPromise2OnError + ], timeout: 0.1) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectations2, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + _ = cancellable2 + } + + func testPublisher_EraseToPromise_Empty() { + let expectationPromise1Fallback = XCTestExpectation(description: "promise1 fallback executed") + let expectationPromise1Hangs = XCTestExpectation(description: "promise1 hangs") + let promise1 = Empty(completeImmediately: true, outputType: Void.self, failureType: MockError.self) + .eraseToPromise(onEmpty: { expectationPromise1Fallback.fulfill(); return nil }) + + let cancellable1 = promise1 + .sink( + receiveCompletion: { _ in expectationPromise1Hangs.fulfill() }, + receiveValue: { expectationPromise1Hangs.fulfill() } + ) + + let resultExpectationPromise1Fallback = XCTWaiter.wait(for: [expectationPromise1Fallback], timeout: 0.1) + let resultExpectationPromise1Hangs = XCTWaiter.wait(for: [expectationPromise1Hangs], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise1Fallback, .completed) + XCTAssertEqual(resultExpectationPromise1Hangs, .timedOut, "Expectation was unexpectedly fulfilled") + + let expectationPromise2Fallback = XCTestExpectation(description: "promise2 fallback should not be executed") + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 hangs") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + let promise2 = Empty(completeImmediately: false, outputType: Void.self, failureType: MockError.self) + .eraseToPromise(onEmpty: { expectationPromise2Fallback.fulfill(); return nil }) + + let cancellable2 = promise2 + .sink( + receiveCompletion: { _ in expectationPromise2ReceiveCompletion.fulfill() }, + receiveValue: { expectationPromise2ReceiveValue.fulfill() } + ) + + let resultExpectationPromise2Fallback = XCTWaiter.wait(for: [expectationPromise2Fallback], timeout: 0.1) + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.3) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise2Fallback, .timedOut, "Expectation was unexpectedly fulfilled") + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .timedOut, "Expectation was unexpectedly fulfilled") + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + let expectationPromise3ReceiveCompletion = XCTestExpectation(description: "promise3 is completed") + var value3: Int? + let promise3 = Empty(completeImmediately: true, outputType: Int.self, failureType: MockError.self) + .eraseToPromise(onEmpty: { return .success(1) }) + + let cancellable3 = promise3 + .sink( + receiveCompletion: { if case .finished = $0 { expectationPromise3ReceiveCompletion.fulfill() } }, + receiveValue: { value3 = $0 } + ) + + let resultExpectationPromise3ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise3ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise3ReceiveCompletion, .completed) + XCTAssertEqual(value3, 1) + + let expectationPromise4ReceiveCompletion = XCTestExpectation(description: "promise4 is falied") + let expectationPromise4ReceiveValue = XCTestExpectation(description: "promise4 does not receive value") + let promise4 = Empty(completeImmediately: true, outputType: Void.self, failureType: MockError.self) + .eraseToPromise(onEmpty: { return .failure(.foo) }) + + let cancellable4 = promise4 + .sink( + receiveCompletion: { if case .failure(.foo) = $0 { expectationPromise4ReceiveCompletion.fulfill() } }, + receiveValue: { expectationPromise4ReceiveValue.fulfill() } + ) + + let resultExpectationPromise4ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise4ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise4ReceiveValue = XCTWaiter.wait(for: [expectationPromise4ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise4ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise4ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + _ = cancellable2 + _ = cancellable3 + _ = cancellable4 + } + + func testPublisher_AssertPromise_Empty() throws { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 hangs") + let expectationPromise1ReceiveValue = XCTestExpectation(description: "promise1 does not receive value") + let promise1 = Empty(completeImmediately: false, outputType: Void.self, failureType: MockError.self) + .assertPromise("testPublisher_AssertPromise_Empty: ") + + let cancellable1 = promise1 + .sink( + receiveCompletion: { _ in expectationPromise1ReceiveCompletion.fulfill() }, + receiveValue: { expectationPromise1ReceiveValue.fulfill() } + ) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.3) + let resultExpectationPromise1ReceiveValue = XCTWaiter.wait(for: [expectationPromise1ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .timedOut, "Expectation was unexpectedly fulfilled") + XCTAssertEqual(resultExpectationPromise1ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + + throw XCTSkip("Expectation of below test case is fatalError, XCTest cannot assert against fatalError.") + // let promise2 = Empty(completeImmediately: true, outputType: Void.self, failureType: MockError.self) + // .assertPromise("testPublisher_AssertPromise_Empty: ") + // + // let cancellable2 = promise2 + // .sink( + // receiveCompletion: { _ in }, + // receiveValue: { } + // ) + // + // _ = cancellable2 + } + + func testPromise_EraseToPromise_CurrentValueSubject() { + let expectationPromiseReceiveCompletion = XCTestExpectation(description: "promise is finished") + let expectationPromiseFallback = XCTestExpectation(description: "promise fallback is not called") + + let subject = CurrentValueSubject(0) + let promise = subject + .eraseToPromise(onEmpty: { expectationPromiseFallback.fulfill(); return nil }) + var value: Int? + + let cancellable = promise + .sink( + receiveCompletion: { if case .finished = $0 { expectationPromiseReceiveCompletion.fulfill() } }, + receiveValue: { value = $0 } + ) + + let resultExpectationPromiseReceiveCompletion = XCTWaiter.wait(for: [expectationPromiseReceiveCompletion], timeout: 0.1) + let resultExpectationPromiseFallback = XCTWaiter.wait(for: [expectationPromiseFallback], timeout: 0.3) + XCTAssertEqual(resultExpectationPromiseReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromiseFallback, .timedOut, "Expectation was unexpectedly fulfilled") + XCTAssertEqual(value, 0) + + _ = cancellable + } + + func testPromise_EraseToPromise_PassthroughSubject() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 finished") + let expectationPromise1Fallback = XCTestExpectation(description: "promise1 fallback is not called") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .eraseToPromise(onEmpty: { expectationPromise1Fallback.fulfill(); return nil }) + var value1: Int? + let cancellable1 = promise1 + .sink( + receiveCompletion: { _ in expectationPromise1ReceiveCompletion.fulfill() }, + receiveValue: { value1 = $0 } + ) + + subject1.send(1) + subject1.send(2) + + let resultExpectationPromise1Fallback = XCTWaiter.wait(for: [expectationPromise1Fallback], timeout: 0.3) + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise1Fallback, .timedOut, "Expectation was unexpectedly fulfilled") + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, 1) + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 is finished") + let expectationPromise2Fallback = XCTestExpectation(description: "promise2 fallback is not called") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 didnt receive any value") + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .eraseToPromise(onEmpty: { expectationPromise2Fallback.fulfill(); return .success(()) }) + + let cancellable2 = promise2 + .sink( + receiveCompletion: { if case .failure(.foo) = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { expectationPromise2ReceiveValue.fulfill() } + ) + + subject2.send(completion: .failure(.foo)) + subject2.send(()) + + let resultExpectationPromise2Fallback = XCTWaiter.wait(for: [expectationPromise2Fallback], timeout: 0.3) + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise2Fallback, .timedOut, "Expectation was unexpectedly fulfilled") + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + _ = cancellable2 + } + + func testPromise_validStatusCode() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let subject1 = PassthroughSubject<(data: Data, response: URLResponse), URLError>() + let promise1 = subject1 + .assertPromise("testPromise_validStatusCode: ") + .validStatusCode() + + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { } + ) + + let response1 = HTTPURLResponse( + url: URL(string: "https://example.com")!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + subject1.send((data: Data(), response: response1)) + subject1.send(completion: .finished) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + + // Test invalid HTTP status code + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 failure") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + + let subject2 = PassthroughSubject<(data: Data, response: URLResponse), URLError>() + let promise2 = subject2 + .assertPromise("testPromise_validStatusCode: ") + .validStatusCode() + + let cancellable2 = promise2.sink( + receiveCompletion: { if case .failure(URLError.badServerResponse) = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromise2ReceiveValue.fulfill() } + ) + + let response2 = HTTPURLResponse( + url: URL(string: "https://example.com")!, + statusCode: 404, + httpVersion: nil, + headerFields: nil + )! + subject2.send((data: Data(), response: response2)) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + // Test non-HTTP response + let expectationPromise3ReceiveCompletion = XCTestExpectation(description: "promise3 failure") + let expectationPromise3ReceiveValue = XCTestExpectation(description: "promise3 does not receive value") + + let subject3 = PassthroughSubject<(data: Data, response: URLResponse), URLError>() + let promise3 = subject3 + .assertPromise("testPromise_validStatusCode: ") + .validStatusCode() + + let cancellable3 = promise3.sink( + receiveCompletion: { if case .failure(URLError.badServerResponse) = $0 { expectationPromise3ReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromise3ReceiveValue.fulfill() } + ) + + let response3 = URLResponse( + url: URL(string: "https://example.com")!, + mimeType: nil, + expectedContentLength: 0, + textEncodingName: nil + ) + subject3.send((data: Data(), response: response3)) + + let resultExpectationPromise3ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise3ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise3ReceiveValue = XCTWaiter.wait(for: [expectationPromise3ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise3ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise3ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + _ = cancellable2 + _ = cancellable3 + } + + func testPromise_replaceNil() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_replaceNil: ") + .replaceNil(with: 42) + + var value1: Int? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(nil) + subject1.send(completion: .finished) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, 42) + + _ = cancellable1 + } + + func testPromise_replaceError() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_replaceError: ") + .replaceError(with: 42) + + var value1: Int? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(completion: .failure(.foo)) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, 42) + + _ = cancellable1 + } + + func testPromise_contains() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_contains: ") + .contains { $0 == 42 } + + var value1: Bool? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(42) + subject1.send(completion: .finished) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, true) + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 success") + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .assertPromise("testPromise_contains: ") + .contains { $0 == 42 } + + var value2: Bool? + let cancellable2 = promise2.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { value2 = $0 } + ) + + subject2.send(1) + subject2.send(completion: .finished) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(value2, false) + + _ = cancellable1 + _ = cancellable2 + } + + func testPromise_tryContains() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_tryContains: ") + .tryContains { value -> Bool in + if value < 0 { throw MockError.foo } + return value == 42 + } + + var value1: Bool? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(42) + subject1.send(completion: .finished) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, true) + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 failure") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .assertPromise("testPromise_tryContains: ") + .tryContains { value -> Bool in + if value < 0 { throw MockError.foo } + return value == 42 + } + + let cancellable2 = promise2.sink( + receiveCompletion: { if case .failure = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromise2ReceiveValue.fulfill() } + ) + + subject2.send(-1) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + _ = cancellable2 + } + + func testPromise_encode() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + struct TestModel: Codable, Equatable { + let value: Int + } + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_encode: ") + .encode(encoder: JSONEncoder()) + + var value1: Data? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(TestModel(value: 42)) + subject1.send(completion: .finished) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + + let decoder = JSONDecoder() + let decodedModel = try? decoder.decode(TestModel.self, from: value1 ?? Data()) + XCTAssertEqual(decodedModel, TestModel(value: 42)) + + _ = cancellable1 + } + + func testPromise_decode() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + struct TestModel: Codable, Equatable { + let value: Int + } + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_decode: ") + .decode(type: TestModel.self, decoder: JSONDecoder()) + + var value1: TestModel? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + let encoder = JSONEncoder() + let data = try! encoder.encode(TestModel(value: 42)) + subject1.send(data) + subject1.send(completion: .finished) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, TestModel(value: 42)) + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 failure") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .assertPromise("testPromise_decode: ") + .decode(type: TestModel.self, decoder: JSONDecoder()) + + let cancellable2 = promise2.sink( + receiveCompletion: { if case .failure = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromise2ReceiveValue.fulfill() } + ) + + subject2.send(Data("invalid json".utf8)) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + _ = cancellable1 + _ = cancellable2 + } + + func testPromise_flatMap() { + let expectationPromise1ReceiveCompletion = XCTestExpectation(description: "promise1 success") + + let subject1 = PassthroughSubject() + let promise1 = subject1 + .assertPromise("testPromise_flatMap: ") + .flatMap { value -> Publishers.Promise in + .init(value: String(value)) + } + + var value1: String? + let cancellable1 = promise1.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise1ReceiveCompletion.fulfill() } }, + receiveValue: { value1 = $0 } + ) + + subject1.send(42) + subject1.send(completion: .finished) + + let resultExpectationPromise1ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise1ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise1ReceiveCompletion, .completed) + XCTAssertEqual(value1, "42") + + let expectationPromise2ReceiveCompletion = XCTestExpectation(description: "promise2 upstream failure") + let expectationPromise2ReceiveValue = XCTestExpectation(description: "promise2 does not receive value") + + let subject2 = PassthroughSubject() + let promise2 = subject2 + .assertPromise("testPromise_flatMap: ") + .flatMap { value -> Publishers.Promise in + .init(value: String(value)) + } + + let cancellable2 = promise2.sink( + receiveCompletion: { if case .failure(MockError.foo) = $0 { expectationPromise2ReceiveCompletion.fulfill() } }, + receiveValue: { _ in expectationPromise2ReceiveValue.fulfill() } + ) + + subject2.send(completion: .failure(.foo)) + + let resultExpectationPromise2ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise2ReceiveCompletion], timeout: 0.1) + let resultExpectationPromise2ReceiveValue = XCTWaiter.wait(for: [expectationPromise2ReceiveValue], timeout: 0.3) + XCTAssertEqual(resultExpectationPromise2ReceiveCompletion, .completed) + XCTAssertEqual(resultExpectationPromise2ReceiveValue, .timedOut, "Expectation was unexpectedly fulfilled") + + let expectationPromise3ReceiveCompletion = XCTestExpectation(description: "promise3 transform failure") + + let subject3 = PassthroughSubject() + let promise3 = subject3 + .assertPromise("testPromise_flatMap: ") + .flatMap { int -> Publishers.Promise in + .init(value: String(int)) + } + + var value3: String? + let cancellable3 = promise3.sink( + receiveCompletion: { if case .finished = $0 { expectationPromise3ReceiveCompletion.fulfill() } }, + receiveValue: { value3 = $0 } + ) + + subject3.send(42) + + let resultExpectationPromise3ReceiveCompletion = XCTWaiter.wait(for: [expectationPromise3ReceiveCompletion], timeout: 0.1) + XCTAssertEqual(resultExpectationPromise3ReceiveCompletion, .completed) + XCTAssertEqual(value3, "42") + + let expectationPromise4Operation = XCTestExpectation(description: "promise4 operation executed") + let expectationPromise4Cancellation = XCTestExpectation(description: "promise4 cancelled") + + let subject4 = PassthroughSubject() + let promise4 = subject4 + .assertPromise("testPromise_flatMap: ") + .flatMap { _ -> Publishers.Promise in + Publishers.Promise { _ in + expectationPromise4Operation.fulfill() + return AnyCancellable { + expectationPromise4Cancellation.fulfill() + } + } + } + + let cancellable4 = promise4.sink( + receiveCompletion: { _ in }, + receiveValue: { _ in } + ) + + subject4.send(42) + cancellable4.cancel() + + let resultExpectations4 = XCTWaiter.wait(for: [ + expectationPromise4Operation, + expectationPromise4Cancellation + ], timeout: 0.1) + XCTAssertEqual(resultExpectations4, .completed) + + // Update the cancellables array at the end + _ = [cancellable1, cancellable2, cancellable3, cancellable4] + } +} + +fileprivate enum MockError: Error, Sendable { + case foo +} + +fileprivate final class Atomic: @unchecked Sendable { + private var _value: Value + private let lock = NSLock() + var value: Value { withLock { $0 } } + + init(_ value: Value) { + self._value = value + } + + func withLock(_ operation: (inout Value) throws -> T) rethrows -> T { + lock.lock() + defer { lock.unlock() } + return try operation(&_value) + } +} diff --git a/Tests/FoundationExtensionsTests/Promise/PromiseTests.swift b/Tests/FoundationExtensionsTests/Promise/PromiseTests.swift deleted file mode 100644 index 06a2afb..0000000 --- a/Tests/FoundationExtensionsTests/Promise/PromiseTests.swift +++ /dev/null @@ -1,483 +0,0 @@ -// Copyright © 2023 Lautsprecher Teufel GmbH. All rights reserved. - -#if !os(watchOS) -import Combine -import FoundationExtensions -import XCTest - -@available(macOS 14, iOS 17.0, tvOS 17.0, watchOS 8.0, *) -@MainActor -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 14, iOS 17.0, tvOS 17.0, watchOS 8.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() - } - - func testPromiseZipManySuccess() { - let subject1 = PassthroughSubject() - let subject2 = PassthroughSubject() - let subject3 = PassthroughSubject() - let subject4 = PassthroughSubject() - - let promises: [Publishers.Promise] = [subject1, subject2, subject3, subject4].map { subject in - 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 zipped: Publishers.Promise<[String], String> = Publishers.Promise.zip(promises) - - let waiter = assert( - publisher: zipped, - eventuallyReceives: ["p1", "p2", "p3", "p4"], - andCompletes: true, - timeout: 0.00001 - ) - subject1.send("p1") - subject4.send("p4") - subject2.send("p2") - subject3.send("p3") - waiter() - } - - func testValidStatusCodeWithIncorrectURLResponse() { - let promise = Publishers.Promise<(data: Data, response: URLResponse), URLError> - .init(value: (data: Data(), response: URLResponse())) - .validStatusCode() - - let waiter = assert(publisher: promise, completesWithoutValues: .failedWithError { urlError in - urlError.code == .badServerResponse - }, timeout: 0.1) - waiter() - } - - func testValidStatusCodeWithStatus400() { - let response = HTTPURLResponse( - url: URL(string: "https://teufel.de")!, - statusCode: 400, - httpVersion: nil, - headerFields: nil - )! - - let promise = Publishers.Promise<(data: Data, response: URLResponse), URLError> - .init(value: (data: Data(), response: response)) - .validStatusCode() - - let waiter = assert(publisher: promise, completesWithoutValues: .failedWithError { urlError in - urlError.code == .badServerResponse - }, timeout: 0.1) - waiter() - } - - func testValidStatusCodeWithStatus200() { - let response = HTTPURLResponse( - url: URL(string: "https://teufel.de")!, - statusCode: 200, - httpVersion: nil, - headerFields: nil - )! - - let promise = Publishers.Promise<(data: Data, response: URLResponse), URLError> - .init(value: (data: Data(), response: response)) - .validStatusCode() - - let waiter = assert(publisher: promise, eventuallyReceives: [()], validatingOutput: { _, _ in true }, andCompletes: true, timeout: 0.1) - waiter() - } - - func testRetryForeverEventuallySucceeds() { - var count = 0 - let promiseThatSucceedsAfter10Attempts = Publishers.Promise.init { completion in - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(5)) { - if count < 10 { - completion(.failure(AnyError())) - count += 1 - } else { - completion(.success("hello")) - } - } - return AnyCancellable { } - }.retry() - - let waiter = assert(publisher: promiseThatSucceedsAfter10Attempts, - eventuallyReceives: "hello", - andCompletes: true, - timeout: 0.1) - waiter() - } - - func testRetryForeverWithTimeout() { - let promiseThatSucceedsAfter10Attempts = Publishers.Promise { completion in - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(5)) { - completion(.failure(AnyError())) - } - return AnyCancellable { } - } - .retry() - .setFailureType(to: AnyError.self) - .timeout(.milliseconds(50), scheduler: DispatchQueue.main, customError: { AnyError() }) - - let waiter = assert(publisher: promiseThatSucceedsAfter10Attempts, - completesWithoutValues: .isFailure, - timeout: 0.1) - waiter() - } - - func test_PromiseValueComputedProperty_WhenPromisePublishesInt1After1SecondDelay_TryAwaitValueReturns1() async throws { - // given - let result = 1 - let sut = Publishers.Promise { - try await Task.sleep(nanoseconds: NSEC_PER_SEC) - return result - } - - // when - let promisedValue = try await sut.value() - - // then - XCTAssertEqual(promisedValue, result) - } - - func test_PromiseValueComputedProperty_WhenPromisePublishesErrorAfter1SecondDelay_TryAwaitValueThrows() async throws { - // given - let result = TestFailure.foo - let sut = Publishers.Promise { - try await Task.sleep(nanoseconds: NSEC_PER_SEC) - throw result - } - - // when - let error = await returnError { @MainActor in _ = try await sut.value() } as? TestFailure - - XCTAssertEqual(error, result) - } -} - -// MARK: - Helpers -private enum TestFailure: Error { - case foo -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -private func returnError(_ context: @escaping @Sendable () async throws -> Void) async -> Error? { - do { - try await context() - return nil - } catch { - return error - } -} - -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 { - assert( - publisher: publisher, - eventuallyReceives: values, - validatingOutput: ==, - andCompletes: andCompletes, - timeout: timeout - ) - } - - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - public func assert( - publisher: P, - eventuallyReceives values: [P.Output], - validatingOutput: @escaping (P.Output, P.Output) -> Bool, - andCompletes: Bool = false, - timeout: TimeInterval - ) -> () -> Void { - 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.count, values.count, "Values don't match:\nreceived:\n\(collectedValues)\n\nexpected:\n\(values)") - zip(collectedValues, values).forEach { collected, expected in - XCTAssertTrue(validatingOutput(collected, expected), "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