diff --git a/Sources/StackNetwork/Combine/NetworkProvider+Combine.swift b/Sources/StackNetwork/Combine/NetworkProvider+Combine.swift new file mode 100644 index 0000000..ceb38cd --- /dev/null +++ b/Sources/StackNetwork/Combine/NetworkProvider+Combine.swift @@ -0,0 +1,87 @@ +import Foundation +import UIKit + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, *) +extension NetworkProvider { + + /// Retuns a publisher to execute a network-request with given target. + public func requestPublisher(_ target: Target, callbackQueue: DispatchQueue? = nil) -> AnyPublisher { + var requestToken: StackNetwork.Cancellable? + return Deferred { + Future { promise in + requestToken = self.request(target, callbackQueue: callbackQueue, completion: promise) + } + .handleEvents( + receiveCancel: { + requestToken?.cancel() + } + ) + } + .eraseToAnyPublisher() + } +} + +@available(iOS 13.0, *) +extension Publisher where Output == StackNetwork.Response { + + /// Maps response' data into a Decodable object. + public func map( + _ type: D.Type, + atKeyPath path: String? = nil, + using decoder: JSONDecoder = JSONDecoder(), + failsOnEmptyData: Bool = true) + -> AnyPublisher { + + tryMap { + try $0.map(D.self, atKeyPath: path, using: decoder, failsOnEmptyData: failsOnEmptyData) + } + .eraseToAnyPublisher() + } + + /// Maps response's data into a JSON object. + public func mapJSON(failsOnEmptyData: Bool = true) -> AnyPublisher { + tryMap { + try $0.mapJSON(failsOnEmptyData: failsOnEmptyData) + } + .eraseToAnyPublisher() + } + + /// Maps response's data into an image. + public func mapImage() -> AnyPublisher { + tryMap { + try $0.mapImage() + } + .eraseToAnyPublisher() + } +} + +@available(iOS 13.0, *) +extension Publisher where Output == StackNetwork.Response { + + /// Filters out response with status code that falls within the given range. + public func filter(statusCodes: R) -> AnyPublisher where R.Bound == Int { + tryMap { + try $0.filter(statusCodes: statusCodes) + } + .eraseToAnyPublisher() + } + + /// Filters out response with status code that matches the given code. + public func filter(statusCode: Int) -> AnyPublisher { + filter(statusCodes: statusCode...statusCode) + } + + /// Filters out response with successful status codes, range 200 - 299. + public func filterSuccessfulStatusCodes() -> AnyPublisher { + filter(statusCodes: 200...299) + } + + /// Filters out response with successful and redirect status codes, range 200 - 399. + public func filterSuccessfulStatusAndRedirectCodes() -> AnyPublisher { + filter(statusCodes: 200...399) + } +} +#endif diff --git a/Sources/StackNetwork/Source/JSONDecoder+KeyPath.swift b/Sources/StackNetwork/Source/JSONDecoder+KeyPath.swift index fb1ccfb..fbd5d91 100644 --- a/Sources/StackNetwork/Source/JSONDecoder+KeyPath.swift +++ b/Sources/StackNetwork/Source/JSONDecoder+KeyPath.swift @@ -89,7 +89,6 @@ extension JSONDecoder { do { return try self.decode(type, from: data) } catch { - NSLog("MMM Shit - \(error)") return nil } }.first diff --git a/StackNetwork.xcodeproj/project.pbxproj b/StackNetwork.xcodeproj/project.pbxproj index 133a8d5..d2c3253 100644 --- a/StackNetwork.xcodeproj/project.pbxproj +++ b/StackNetwork.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ 3A24D7C423C674C800EDC931 /* Aliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A24D7C323C674C800EDC931 /* Aliases.swift */; }; 3A24D7C723C67C3200EDC931 /* MultiTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A24D7C623C67C3200EDC931 /* MultiTarget.swift */; }; 3A2EBCDD23EDDB440051E223 /* PluginType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2EBCDC23EDDB440051E223 /* PluginType.swift */; }; + 3A5E8EF32777B3F300E03A05 /* NetworkProvider+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5E8EF22777B3F300E03A05 /* NetworkProvider+Combine.swift */; }; + 3A5E8EF52777B40400E03A05 /* NetworkProviderPublisherSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5E8EF42777B40400E03A05 /* NetworkProviderPublisherSpec.swift */; }; 3A61F49223AE1F4200A8E47C /* StackNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A61F48823AE1F4200A8E47C /* StackNetwork.framework */; }; 3A61F49923AE1F4200A8E47C /* StackNetwork.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A61F48B23AE1F4200A8E47C /* StackNetwork.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3A61F4A423AE277400A8E47C /* NetworkProviderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A61F4A323AE277400A8E47C /* NetworkProviderType.swift */; }; @@ -67,6 +69,8 @@ 3A24D7C323C674C800EDC931 /* Aliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Aliases.swift; sourceTree = ""; }; 3A24D7C623C67C3200EDC931 /* MultiTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiTarget.swift; sourceTree = ""; }; 3A2EBCDC23EDDB440051E223 /* PluginType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginType.swift; sourceTree = ""; }; + 3A5E8EF22777B3F300E03A05 /* NetworkProvider+Combine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NetworkProvider+Combine.swift"; sourceTree = ""; }; + 3A5E8EF42777B40400E03A05 /* NetworkProviderPublisherSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProviderPublisherSpec.swift; sourceTree = ""; }; 3A61F48823AE1F4200A8E47C /* StackNetwork.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StackNetwork.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3A61F48B23AE1F4200A8E47C /* StackNetwork.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StackNetwork.h; sourceTree = ""; }; 3A61F48C23AE1F4200A8E47C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -160,6 +164,14 @@ path = Source; sourceTree = ""; }; + 3A5E8EF12777B3F300E03A05 /* Combine */ = { + isa = PBXGroup; + children = ( + 3A5E8EF22777B3F300E03A05 /* NetworkProvider+Combine.swift */, + ); + path = Combine; + sourceTree = ""; + }; 3A61F47E23AE1F4200A8E47C = { isa = PBXGroup; children = ( @@ -182,6 +194,7 @@ 3A61F48A23AE1F4200A8E47C /* StackNetwork */ = { isa = PBXGroup; children = ( + 3A5E8EF12777B3F300E03A05 /* Combine */, 3A24D7C523C67C1400EDC931 /* Target */, 3A1C4BF623B61EB90012FC6F /* Response */, 3A1C4BEA23B0113E0012FC6F /* Decoding */, @@ -205,6 +218,7 @@ 3A1C4BF723B62C160012FC6F /* Resources */, 3A61F49823AE1F4200A8E47C /* Info.plist */, 3A61F4CB23AE596100A8E47C /* ParameterEncoderSpec.swift */, + 3A5E8EF42777B40400E03A05 /* NetworkProviderPublisherSpec.swift */, 3A1C4BD023AE8D1E0012FC6F /* JSONEncoderSpec.swift */, 3A1C4BD623AEA9BF0012FC6F /* NetworkProviderSpec.swift */, 3A1C4BD823AEAA210012FC6F /* TestHelpers.swift */, @@ -401,6 +415,7 @@ 3A61F4A823AE27D400A8E47C /* TargetType.swift in Sources */, 3A61F4B723AE2E7B00A8E47C /* Cancellable.swift in Sources */, 3A24D7C223C66F1D00EDC931 /* RetryBehavior.swift in Sources */, + 3A5E8EF32777B3F300E03A05 /* NetworkProvider+Combine.swift in Sources */, 3A61F4B023AE281600A8E47C /* HTTPMethod.swift in Sources */, 3A61F4A423AE277400A8E47C /* NetworkProviderType.swift in Sources */, 3A1C4BEE23B011EB0012FC6F /* Response+Map.swift in Sources */, @@ -417,6 +432,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3A5E8EF52777B40400E03A05 /* NetworkProviderPublisherSpec.swift in Sources */, 3A1C4BF223B0C2380012FC6F /* JSONDecoderSpec.swift in Sources */, 3A1C4BD723AEA9BF0012FC6F /* NetworkProviderSpec.swift in Sources */, 3A61F4CC23AE596100A8E47C /* ParameterEncoderSpec.swift in Sources */, diff --git a/Tests/StackNetworkTests/NetworkProviderPublisherSpec.swift b/Tests/StackNetworkTests/NetworkProviderPublisherSpec.swift new file mode 100644 index 0000000..c78c5e0 --- /dev/null +++ b/Tests/StackNetworkTests/NetworkProviderPublisherSpec.swift @@ -0,0 +1,67 @@ +import Quick +import Nimble +import OHHTTPStubs + +@testable import StackNetwork + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, *) +class NetworkProviderPublisherSpec: QuickSpec { + + override func spec() { + var sut: NetworkProvider! + var subscription: Combine.AnyCancellable? + + afterEach { + subscription?.cancel() + subscription = nil + } + + describe("A network provider") { + + beforeEach { + let stubBehavior: StubBehaviorClosure = { target in .immediate(TestHelper.stubSampleResponse(target: target)) } + sut = NetworkProvider(stubBehavior: stubBehavior) + } + + it("emits one and only one Response object") { + var numberOfEvents = 0 + + waitUntil { done in + subscription = sut.requestPublisher(.zen) + .sink( + receiveCompletion: { completion in + switch completion { + case .failure(let error): fail("Unexpected error: \(error)") + case .finished: done() + } + }, + receiveValue: { _ in numberOfEvents += 1 } + ) + } + expect(numberOfEvents).to(equal(1)) + } + + it("emits stubbed data for zen request") { + waitUntil { done in + let target = GitHub.zen + subscription = sut.requestPublisher(target) + .sink( + receiveCompletion: { completion in + switch completion { + case .failure(let error): fail("Unexpected error: \(error)") + case .finished: done() + } + }, + receiveValue: { response in + expect(response.data).to(equal(target.sampleData)) + } + ) + } + } + } + } +} +#endif