From 96512e9029754bc8e3334a372db0ed1fc341d7a7 Mon Sep 17 00:00:00 2001 From: SooHwanCho <51935215+JCSooHwanCho@users.noreply.github.com> Date: Thu, 24 Mar 2022 14:32:28 +0900 Subject: [PATCH] opeators: add a MapError opeator --- README.md | 19 ++++ .../Operators/AsyncSequence+MapError.swift | 97 +++++++++++++++++++ .../AsyncSequence+MapErrorTests.swift | 35 +++++++ 3 files changed, 151 insertions(+) create mode 100644 Sources/Operators/AsyncSequence+MapError.swift create mode 100644 Tests/Operators/AsyncSequence+MapErrorTests.swift diff --git a/README.md b/README.md index 2405e96..7d43aca 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ AsyncSequences * [Share](#Share) * [WithLatestFrom](#WithLatestFrom) * [EraseToAnyAsyncSequence](#EraseToAnyAsyncSequence) +* [MapError](#MapError) More operators and extensions are to come. Pull requests are of course welcome. @@ -545,3 +546,21 @@ seq1.send(3) ### EraseToAnyAsyncSequence `eraseToAnyAsyncSequence()` type-erases the async sequence into an AnyAsyncSequence. + +### MapError + +`mapError(_:)` transforms error from upstream async sequence. + +``` +struct CustomError: Error { } + +let failSequence = AsyncSequences.Fail(error: NSError(domain: "", code: 1)) + +do { + for try await element in failSequence.mapError({ _ in CustomError() }) { + print(element) // will never execute + } +} catch { + print(error) // CustomError() +} +``` diff --git a/Sources/Operators/AsyncSequence+MapError.swift b/Sources/Operators/AsyncSequence+MapError.swift new file mode 100644 index 0000000..58ac89c --- /dev/null +++ b/Sources/Operators/AsyncSequence+MapError.swift @@ -0,0 +1,97 @@ +// +// AsyncSequence+MapError.swift +// +// Created by JCSooHwanCho on 2022/03/24. +// + +public extension AsyncSequence { + /// transform error from upstream async sequence + /// + /// ``` + /// struct CustomError: Error { } + /// + /// let failSequence = AsyncSequences.Fail(error: NSError(domain: "", code: 1)) + /// + /// do { + /// for try await element in failSequence.mapError({ _ in CustomError() }) { + /// print(element) + /// } + /// } catch { + /// print(error) // CustomError() + /// } + /// + /// ``` + /// + /// - Parameter transformError: A transform to apply to error that upstream emits. + /// - Returns: The async sequence error transformed + @inlinable + __consuming func mapError( + _ transformError: @Sendable @escaping (Error) async -> Error + ) -> AsyncMapErrorSequence { + return AsyncMapErrorSequence(self, transformError: transformError) + } +} + +public struct AsyncMapErrorSequence { + @usableFromInline + let base: Base + + @usableFromInline + let transformError: (Error) async -> Error + + @usableFromInline + init( + _ base: Base, + transformError: @escaping (Error) async -> Error + ) { + self.base = base + self.transformError = transformError + } +} + +extension AsyncMapErrorSequence: AsyncSequence { + + public typealias Element = Base.Element + + public typealias AsyncIterator = Iterator + + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + var baseIterator: Base.AsyncIterator + + @usableFromInline + let transformError: (Error) async -> Error + + @usableFromInline + init( + _ baseIterator: Base.AsyncIterator, + transformError: @escaping (Error) async -> Error + ) { + self.baseIterator = baseIterator + self.transformError = transformError + } + + @inlinable + public mutating func next() async rethrows -> Element? { + do { + return try await baseIterator.next() + } catch { + throw await transformError(error) + } + } + } + + @inlinable + public __consuming func makeAsyncIterator() -> Iterator { + return Iterator(base.makeAsyncIterator(), transformError: transformError) + } +} + +extension AsyncMapErrorSequence: @unchecked Sendable + where Base: Sendable, + Base.Element: Sendable { } + +extension AsyncMapErrorSequence.Iterator: @unchecked Sendable + where Base.AsyncIterator: Sendable, + Base.Element: Sendable { } + diff --git a/Tests/Operators/AsyncSequence+MapErrorTests.swift b/Tests/Operators/AsyncSequence+MapErrorTests.swift new file mode 100644 index 0000000..4dddb9a --- /dev/null +++ b/Tests/Operators/AsyncSequence+MapErrorTests.swift @@ -0,0 +1,35 @@ +// +// AsyncSequence+MapErrorTests.swift +// +// +// Created by JCSooHwanCho on 2022/03/24. +// + +import AsyncExtensions +import XCTest + +private struct CustomError: Error, Equatable { } + +final class AsyncSequence_MapErrorTests: XCTestCase { + func testMapError_with_CustomError() async throws { + let failSequence = AsyncSequences.Fail(error: NSError(domain: "", code: 1)) + + do { + for try await _ in failSequence.mapError({ _ in CustomError() }) { + XCTFail() + } + } catch { + XCTAssertEqual(error as! CustomError, CustomError()) + } + } + + func testMappError_doesnot_affect_on_normal_finis() async throws { + let intSequence = (1...5).asyncElements + + let reduced = try await intSequence + .mapError { _ in CustomError() } + .reduce(into: 0, +=) + + XCTAssertEqual(reduced, 15) + } +}