Skip to content

Commit 5fff2c6

Browse files
committed
Implement AsyncMapErrorSequence
1 parent 6ae9a05 commit 5fff2c6

File tree

2 files changed

+161
-0
lines changed

2 files changed

+161
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
public extension AsyncSequence {
14+
15+
/// Converts any failure into a new error.
16+
///
17+
/// - Parameter transform: A closure that takes the failure as a parameter and returns a new error.
18+
/// - Returns: An asynchronous sequence that maps the error thrown into the one produced by the transform closure.
19+
///
20+
/// Use the ``mapError(_:)`` operator when you need to replace one error type with another.
21+
func mapError<ErrorType>(transform: @Sendable @escaping (Error) -> ErrorType) -> AsyncMapErrorSequence<Self, ErrorType> {
22+
.init(base: self, transform: transform)
23+
}
24+
}
25+
26+
/// An asynchronous sequence that converts any failure into a new error.
27+
public struct AsyncMapErrorSequence<Base: AsyncSequence, ErrorType: Error>: AsyncSequence, Sendable where Base: Sendable {
28+
29+
public typealias AsyncIterator = Iterator
30+
public typealias Element = Base.Element
31+
32+
private let base: Base
33+
private let transform: @Sendable (Error) -> ErrorType
34+
35+
init(
36+
base: Base,
37+
transform: @Sendable @escaping (Error) -> ErrorType
38+
) {
39+
self.base = base
40+
self.transform = transform
41+
}
42+
43+
public func makeAsyncIterator() -> Iterator {
44+
Iterator(
45+
base: base.makeAsyncIterator(),
46+
transform: transform
47+
)
48+
}
49+
}
50+
51+
public extension AsyncMapErrorSequence {
52+
53+
/// The iterator that produces elements of the map sequence.
54+
struct Iterator: AsyncIteratorProtocol {
55+
56+
public typealias Element = Base.Element
57+
58+
private var base: Base.AsyncIterator
59+
60+
private let transform: @Sendable (Error) -> ErrorType
61+
62+
public init(
63+
base: Base.AsyncIterator,
64+
transform: @Sendable @escaping (Error) -> ErrorType
65+
) {
66+
self.base = base
67+
self.transform = transform
68+
}
69+
70+
public mutating func next() async throws -> Element? {
71+
do {
72+
return try await base.next()
73+
} catch {
74+
throw transform(error)
75+
}
76+
}
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import AsyncAlgorithms
2+
import XCTest
3+
4+
final class TestMapError: XCTestCase {
5+
6+
func test_mapError() async throws {
7+
let array = [URLError(.badURL)]
8+
let sequence = array.async
9+
.map { throw $0 }
10+
.mapError { _ in
11+
MyAwesomeError()
12+
}
13+
14+
do {
15+
for try await _ in sequence {
16+
XCTFail("sequence should throw")
17+
}
18+
} catch {
19+
_ = try XCTUnwrap(error as? MyAwesomeError)
20+
}
21+
}
22+
23+
func test_nonThrowing() async throws {
24+
let array = [1, 2, 3, 4, 5]
25+
let sequence = array.async
26+
.mapError { _ in
27+
MyAwesomeError()
28+
}
29+
30+
var actual: [Int] = []
31+
for try await value in sequence {
32+
actual.append(value)
33+
}
34+
XCTAssertEqual(array, actual)
35+
}
36+
37+
func test_cancellation() async throws {
38+
let source = Indefinite(value: "test").async
39+
let sequence = source.mapError { _ in MyAwesomeError() }
40+
41+
let finished = expectation(description: "finished")
42+
let iterated = expectation(description: "iterated")
43+
44+
let task = Task {
45+
var firstIteration = false
46+
for try await el in sequence {
47+
XCTAssertEqual(el, "test")
48+
49+
if !firstIteration {
50+
firstIteration = true
51+
iterated.fulfill()
52+
}
53+
}
54+
finished.fulfill()
55+
}
56+
57+
// ensure the other task actually starts
58+
await fulfillment(of: [iterated], timeout: 1.0)
59+
// cancellation should ensure the loop finishes
60+
// without regards to the remaining underlying sequence
61+
task.cancel()
62+
await fulfillment(of: [finished], timeout: 1.0)
63+
}
64+
65+
func test_empty() async throws {
66+
let array: [Int] = []
67+
let sequence = array.async
68+
.mapError { _ in
69+
MyAwesomeError()
70+
}
71+
72+
var actual: [Int] = []
73+
for try await value in sequence {
74+
actual.append(value)
75+
}
76+
XCTAssert(actual.isEmpty)
77+
}
78+
}
79+
80+
private extension TestMapError {
81+
82+
struct MyAwesomeError: Error {}
83+
}

0 commit comments

Comments
 (0)