From 61ca706dd31330c0782013a2cdbc1af550de56a9 Mon Sep 17 00:00:00 2001 From: crane-hiromu Date: Sat, 29 Oct 2022 19:44:33 +0900 Subject: [PATCH] add functions --- .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++ Package.swift | 28 ++++ Sources/CombineAsyncable/Future+Task.swift | 65 +++++++++ Sources/CombineAsyncable/Publisher+Task.swift | 129 ++++++++++++++++++ .../CombineAsyncableTests.swift | 11 ++ 6 files changed, 248 insertions(+) create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 .swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Package.swift create mode 100644 Sources/CombineAsyncable/Future+Task.swift create mode 100644 Sources/CombineAsyncable/Publisher+Task.swift create mode 100644 Tests/CombineAsyncableTests/CombineAsyncableTests.swift diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..8397ed6 --- /dev/null +++ b/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.6 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "CombineAsyncable", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "CombineAsyncable", + targets: ["CombineAsyncable"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "CombineAsyncable", + dependencies: []), + .testTarget( + name: "CombineAsyncableTests", + dependencies: ["CombineAsyncable"]), + ] +) diff --git a/Sources/CombineAsyncable/Future+Task.swift b/Sources/CombineAsyncable/Future+Task.swift new file mode 100644 index 0000000..3dd90fd --- /dev/null +++ b/Sources/CombineAsyncable/Future+Task.swift @@ -0,0 +1,65 @@ +// +// Future+Task.swift +// +// +// Created by h.tsuruta on 2022/10/06. +// + +#if compiler(>=5.5) && canImport(_Concurrency) +import Combine + +// MARK: - Future Extension (Never) +@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) +extension Future where Failure == Never { + + /// Example + /// + /// func exec() -> Future<(), Never> { + /// Future { + /// await callAsyncFunction() + /// } + /// } + /// + convenience init( + priority: TaskPriority? = nil, + operation: @escaping () async -> Output + ) { + self.init { promise in + Task(priority: priority) { + let result = await operation() + promise(.success(result)) + } + } + } +} + +// MARK: - Future Extension (Error) +@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) +extension Future where Failure == Error { + + /// Example + /// + /// func exec() -> Future<(), Error> { + /// Future { + /// try await callAsyncThrowsFunction() + /// } + /// } + /// + convenience init( + priority: TaskPriority? = nil, + operation: @escaping () async throws -> Output + ) { + self.init { promise in + Task(priority: priority) { + do { + let result = try await operation() + promise(.success(result)) + } catch { + promise(.failure(error)) + } + } + } + } +} + +#endif diff --git a/Sources/CombineAsyncable/Publisher+Task.swift b/Sources/CombineAsyncable/Publisher+Task.swift new file mode 100644 index 0000000..59bae3a --- /dev/null +++ b/Sources/CombineAsyncable/Publisher+Task.swift @@ -0,0 +1,129 @@ +// +// Publisher+Task.swift +// +// +// Created by h.tsuruta on 2022/10/06. +// + +#if compiler(>=5.5) && canImport(_Concurrency) +import Combine + +// MARK: - Publisher Extension (Never) +@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) +public extension Publisher where Self.Failure == Never { + + /// Example + /// + /// Just(99) + /// .sink { number in + /// // do some task + /// } + /// .store(in: &cancellable) + /// + func sink( + receiveValue: @escaping ((Self.Output) async -> Void) + ) -> AnyCancellable { + self.sink { value in + Task { + await receiveValue(value) + } + } + } +} + +// MARK: - Publisher Extension (Error) +@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) +public extension Publisher where Self.Failure == Error { + + /// Example + /// + /// Just(99) + /// .setFailureType(to: Error.self) + /// .sinkWithThrows(receiveCompletion: { result in + /// // do some resut handling task + /// }, receiveValue: { value in + /// // do some value handling task + /// }) + /// .store(in: &cancellable) + /// + func sinkWithThrows( + receiveCompletion: @escaping ((Subscribers.Completion) async throws -> Void), + receiveValue: @escaping ((Self.Output) async throws -> Void) + ) -> AnyCancellable { + self.sink( + receiveCompletion: { result in + Task { + try await receiveCompletion(result) + } + }, + receiveValue: { value in + Task { + try await receiveValue(value) + } + } + ) + } +} + +@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) +public extension Publisher { + + /// Example + /// + /// Just(99) + /// .asyncMap { number in + /// // do some task + /// } + /// .sink { number in + /// // do some handling + /// } + /// .store(in: &cancellable) + /// + func asyncMap( + _ asyncFunction: @escaping (Output) async -> V + ) -> Publishers.FlatMap, Self> { + + flatMap { value in + Future { promise in + Task { + promise(.success(await asyncFunction(value))) + } + } + } + } + + /// Example + /// + /// URL(string: "https....") + /// .publisher + /// .compactMap { $0 } + /// .asyncMapWithThrows { + /// try await URLSession.shared.data(from: $0) + /// } + /// .sink(receiveCompletion: { result in + /// // do some result handling task + /// }, receiveValue: { value in + /// // do some value handling task + /// }) + /// .store(in: &cancellable) + /// + func asyncMapWithThrows( + _ transform: @escaping (Output) async throws -> V + ) -> Publishers.FlatMap, Publishers.SetFailureType> { + + flatMap { value in + Future { promise in + Task { + do { + let output = try await transform(value) + promise(.success(output)) + } catch { + promise(.failure(error)) + } + } + } + } + } +} + +#endif diff --git a/Tests/CombineAsyncableTests/CombineAsyncableTests.swift b/Tests/CombineAsyncableTests/CombineAsyncableTests.swift new file mode 100644 index 0000000..efe6418 --- /dev/null +++ b/Tests/CombineAsyncableTests/CombineAsyncableTests.swift @@ -0,0 +1,11 @@ +import XCTest +@testable import CombineAsyncable + +final class CombineAsyncableTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(CombineAsyncable().text, "Hello, World!") + } +}