mirror of
https://github.com/pointfreeco/swift-composable-architecture.git
synced 2025-12-20 09:11:33 +01:00
* Apply existential any to protocol for Swift 6 * Update Package@swift-6.0.swift * Update Package.swift * Apply any to Macro.Type * Apply any for the rest * Applying the any keyword internally for typealias in a _KeyPath * Undoing accidental syntax --------- Co-authored-by: Stephen Celis <stephen@stephencelis.com> Co-authored-by: Stephen Celis <stephen.celis@gmail.com>
333 lines
11 KiB
Swift
333 lines
11 KiB
Swift
/// A value that represents either a success or a failure. This type differs from Swift's `Result`
|
|
/// type in that it uses only one generic for the success case, leaving the failure case as an
|
|
/// untyped `Error`.
|
|
///
|
|
/// This type is needed because Swift's concurrency tools can only express untyped errors, such as
|
|
/// `async` functions and `AsyncSequence`, and so their output can realistically only be bridged to
|
|
/// `Result<_, any Error>`. However, `Result<_, any Error>` is never `Equatable` since `Error` is not
|
|
/// `Equatable`, and equatability is very important for testing in the Composable Architecture. By
|
|
/// defining our own type we get the ability to recover equatability in most situations.
|
|
///
|
|
/// If someday Swift gets typed `throws`, then we can eliminate this type and rely solely on
|
|
/// `Result`.
|
|
///
|
|
/// You typically use this type as the payload of an action which receives a response from an
|
|
/// effect:
|
|
///
|
|
/// ```swift
|
|
/// enum Action: Equatable {
|
|
/// case factButtonTapped
|
|
/// case factResponse(TaskResult<String>)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Then you can model your dependency as using simple `async` and `throws` functionality:
|
|
///
|
|
/// ```swift
|
|
/// struct NumberFactClient {
|
|
/// var fetch: (Int) async throws -> String
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// And finally you can use ``Effect/run(priority:operation:catch:fileID:filePath:line:column:)`` to construct an
|
|
/// effect in the reducer that invokes the `numberFact` endpoint and wraps its response in a
|
|
/// ``TaskResult`` by using its catching initializer, ``TaskResult/init(catching:)``:
|
|
///
|
|
/// ```swift
|
|
/// case .factButtonTapped:
|
|
/// return .run { send in
|
|
/// await send(
|
|
/// .factResponse(
|
|
/// TaskResult { try await self.numberFact.fetch(state.number) }
|
|
/// )
|
|
/// )
|
|
/// }
|
|
///
|
|
/// case let .factResponse(.success(fact)):
|
|
/// // do something with fact
|
|
///
|
|
/// case .factResponse(.failure):
|
|
/// // handle error
|
|
///
|
|
/// // ...
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// ## Equality
|
|
///
|
|
/// The biggest downside to using an untyped `Error` in a result type is that the result will not
|
|
/// be equatable even if the success type is. This negatively affects your ability to test features
|
|
/// that use ``TaskResult`` in their actions with the ``TestStore``.
|
|
///
|
|
/// ``TaskResult`` does extra work to try to maintain equatability when possible. If the underlying
|
|
/// type masked by the `Error` is `Equatable`, then it will use that `Equatable` conformance
|
|
/// on two failures. Luckily, most errors thrown by Apple's frameworks are already equatable, and
|
|
/// because errors are typically simple value types, it is usually possible to have the compiler
|
|
/// synthesize a conformance for you.
|
|
///
|
|
/// If you are testing the unhappy path of a feature that feeds a ``TaskResult`` back into the
|
|
/// system, be sure to conform the error to equatable, or the test will fail:
|
|
///
|
|
/// ```swift
|
|
/// // Set up a failing dependency
|
|
/// struct RefreshFailure: Error {}
|
|
/// store.dependencies.apiClient.fetchFeed = { throw RefreshFailure() }
|
|
///
|
|
/// // Simulate pull-to-refresh
|
|
/// store.send(.refresh) { $0.isLoading = true }
|
|
///
|
|
/// // Assert against failure
|
|
/// await store.receive(.refreshResponse(.failure(RefreshFailure())) { // 🛑
|
|
/// $0.errorLabelText = "An error occurred."
|
|
/// $0.isLoading = false
|
|
/// }
|
|
/// // 🛑 'RefreshFailure' is not equatable
|
|
/// ```
|
|
///
|
|
/// To get a passing test, explicitly conform your custom error to the `Equatable` protocol:
|
|
///
|
|
/// ```swift
|
|
/// // Set up a failing dependency
|
|
/// struct RefreshFailure: Error, Equatable {} // 👈
|
|
/// store.dependencies.apiClient.fetchFeed = { throw RefreshFailure() }
|
|
///
|
|
/// // Simulate pull-to-refresh
|
|
/// store.send(.refresh) { $0.isLoading = true }
|
|
///
|
|
/// // Assert against failure
|
|
/// await store.receive(.refreshResponse(.failure(RefreshFailure())) { // ✅
|
|
/// $0.errorLabelText = "An error occurred."
|
|
/// $0.isLoading = false
|
|
/// }
|
|
/// ```
|
|
@available(
|
|
iOS,
|
|
deprecated: 9999,
|
|
message:
|
|
"Use 'Result', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Moving-off-of-TaskResult"
|
|
)
|
|
@available(
|
|
macOS,
|
|
deprecated: 9999,
|
|
message:
|
|
"Use 'Result', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Moving-off-of-TaskResult"
|
|
)
|
|
@available(
|
|
tvOS,
|
|
deprecated: 9999,
|
|
message:
|
|
"Use 'Result', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Moving-off-of-TaskResult"
|
|
)
|
|
@available(
|
|
watchOS,
|
|
deprecated: 9999,
|
|
message:
|
|
"Use 'Result', instead. See the following migration guide for more information: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.4#Moving-off-of-TaskResult"
|
|
)
|
|
public enum TaskResult<Success: Sendable>: Sendable {
|
|
/// A success, storing a `Success` value.
|
|
case success(Success)
|
|
|
|
/// A failure, storing an error.
|
|
case failure(any Error)
|
|
|
|
/// Creates a new task result by evaluating an async throwing closure, capturing the returned
|
|
/// value as a success, or any thrown error as a failure.
|
|
///
|
|
/// This initializer is most often used in an async effect being returned from a reducer. See the
|
|
/// documentation for ``TaskResult`` for a concrete example.
|
|
///
|
|
/// - Parameter body: An async, throwing closure.
|
|
@_transparent
|
|
public init(catching body: @Sendable () async throws -> Success) async {
|
|
do {
|
|
self = .success(try await body())
|
|
} catch {
|
|
self = .failure(error)
|
|
}
|
|
}
|
|
|
|
/// Transforms a `Result` into a `TaskResult`, erasing its `Failure` to `Error`.
|
|
///
|
|
/// - Parameter result: A result.
|
|
@inlinable
|
|
public init<Failure>(_ result: Result<Success, Failure>) {
|
|
switch result {
|
|
case let .success(value):
|
|
self = .success(value)
|
|
case let .failure(error):
|
|
self = .failure(error)
|
|
}
|
|
}
|
|
|
|
/// Returns the success value as a throwing property.
|
|
@inlinable
|
|
public var value: Success {
|
|
get throws {
|
|
switch self {
|
|
case let .success(value):
|
|
return value
|
|
case let .failure(error):
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns a new task result, mapping any success value using the given transformation.
|
|
///
|
|
/// Like `map` on `Result`, `Optional`, and many other types.
|
|
///
|
|
/// - Parameter transform: A closure that takes the success value of this instance.
|
|
/// - Returns: A `TaskResult` instance with the result of evaluating `transform` as the new
|
|
/// success value if this instance represents a success.
|
|
@inlinable
|
|
public func map<NewSuccess>(_ transform: (Success) -> NewSuccess) -> TaskResult<NewSuccess> {
|
|
switch self {
|
|
case let .success(value):
|
|
return .success(transform(value))
|
|
case let .failure(error):
|
|
return .failure(error)
|
|
}
|
|
}
|
|
|
|
/// Returns a new task result, mapping any success value using the given transformation and
|
|
/// unwrapping the produced result.
|
|
///
|
|
/// Like `flatMap` on `Result`, `Optional`, and many other types.
|
|
///
|
|
/// - Parameter transform: A closure that takes the success value of the instance.
|
|
/// - Returns: A `TaskResult` instance, either from the closure or the previous `.failure`.
|
|
@inlinable
|
|
public func flatMap<NewSuccess>(
|
|
_ transform: (Success) -> TaskResult<NewSuccess>
|
|
) -> TaskResult<NewSuccess> {
|
|
switch self {
|
|
case let .success(value):
|
|
return transform(value)
|
|
case let .failure(error):
|
|
return .failure(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension TaskResult: CasePathable {
|
|
public static var allCasePaths: AllCasePaths {
|
|
AllCasePaths()
|
|
}
|
|
|
|
public struct AllCasePaths {
|
|
public var success: AnyCasePath<TaskResult, Success> {
|
|
AnyCasePath(
|
|
embed: { .success($0) },
|
|
extract: {
|
|
guard case let .success(value) = $0 else { return nil }
|
|
return value
|
|
}
|
|
)
|
|
}
|
|
|
|
public var failure: AnyCasePath<TaskResult, any Error> {
|
|
AnyCasePath(
|
|
embed: { .failure($0) },
|
|
extract: {
|
|
guard case let .failure(value) = $0 else { return nil }
|
|
return value
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Result where Success: Sendable, Failure == any Error {
|
|
/// Transforms a `TaskResult` into a `Result`.
|
|
///
|
|
/// - Parameter result: A task result.
|
|
@inlinable
|
|
public init(_ result: TaskResult<Success>) {
|
|
switch result {
|
|
case let .success(value):
|
|
self = .success(value)
|
|
case let .failure(error):
|
|
self = .failure(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum TaskResultDebugging {
|
|
@TaskLocal static var emitRuntimeWarnings = true
|
|
}
|
|
|
|
extension TaskResult: Equatable where Success: Equatable {
|
|
public static func == (lhs: Self, rhs: Self) -> Bool {
|
|
switch (lhs, rhs) {
|
|
case let (.success(lhs), .success(rhs)):
|
|
return lhs == rhs
|
|
case let (.failure(lhs), .failure(rhs)):
|
|
return _isEqual(lhs, rhs)
|
|
?? {
|
|
#if DEBUG
|
|
let lhsType = type(of: lhs)
|
|
if TaskResultDebugging.emitRuntimeWarnings, lhsType == type(of: rhs) {
|
|
let lhsTypeName = typeName(lhsType)
|
|
reportIssue(
|
|
"""
|
|
"\(lhsTypeName)" is not equatable. …
|
|
|
|
To test two values of this type, it must conform to the "Equatable" protocol. For \
|
|
example:
|
|
|
|
extension \(lhsTypeName): Equatable {}
|
|
|
|
See the documentation of "TaskResult" for more information.
|
|
"""
|
|
)
|
|
}
|
|
#endif
|
|
return false
|
|
}()
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
extension TaskResult: Hashable where Success: Hashable {
|
|
public func hash(into hasher: inout Hasher) {
|
|
switch self {
|
|
case let .success(value):
|
|
hasher.combine(value)
|
|
hasher.combine(0)
|
|
case let .failure(error):
|
|
if let error = (error as Any) as? AnyHashable {
|
|
hasher.combine(error)
|
|
hasher.combine(1)
|
|
} else {
|
|
#if DEBUG
|
|
if TaskResultDebugging.emitRuntimeWarnings {
|
|
let errorType = typeName(type(of: error))
|
|
reportIssue(
|
|
"""
|
|
"\(errorType)" is not hashable. …
|
|
|
|
To hash a value of this type, it must conform to the "Hashable" protocol. For example:
|
|
|
|
extension \(errorType): Hashable {}
|
|
|
|
See the documentation of "TaskResult" for more information.
|
|
"""
|
|
)
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension TaskResult {
|
|
// NB: For those that try to interface with `TaskResult` using `Result`'s old API.
|
|
@available(*, unavailable, renamed: "value")
|
|
public func get() throws -> Success {
|
|
try self.value
|
|
}
|
|
}
|