From e82a96f756f12b07c15fa99b693c377f2defa462 Mon Sep 17 00:00:00 2001 From: Eduardo Sanches Bocato Date: Sat, 11 Jan 2020 09:04:07 -0300 Subject: [PATCH] Moving the adapter to the dispatcher. --- Networking.xcodeproj/project.pbxproj | 8 ++-- ...ching.swift => URLRequestDispatcher.swift} | 9 ++-- .../Dispatcher/URLSessionDispatcher.swift | 33 +++++++------ .../Abstractions/NetworkingService.swift | 2 +- .../Abstractions/URLRequestProtocol.swift | 4 +- .../URLRequests/Tools/URLRequestBuilder.swift | 11 ++--- .../URLSessionDispatcherTests.swift | 33 +++++++++++++ .../CodableRequestingTests.swift | 12 +++-- .../Tools/DefaultURLRequestBuilderTests.swift | 2 +- README.md | 46 +++++++++++++++++-- 10 files changed, 118 insertions(+), 42 deletions(-) rename Networking/Sources/Dispatcher/{URLRequestDispatching.swift => URLRequestDispatcher.swift} (87%) diff --git a/Networking.xcodeproj/project.pbxproj b/Networking.xcodeproj/project.pbxproj index 615e276..8cd0165 100644 --- a/Networking.xcodeproj/project.pbxproj +++ b/Networking.xcodeproj/project.pbxproj @@ -20,7 +20,7 @@ E5CE42B923344DCA00295277 /* Networking.h in Headers */ = {isa = PBXBuildFile; fileRef = E5CE42AB23344DC900295277 /* Networking.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5CE42EE23344F5800295277 /* NetworkingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE42ED23344F5800295277 /* NetworkingService.swift */; }; E5CE42F123344F6200295277 /* URLSessionDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE42EF23344F6200295277 /* URLSessionDispatcher.swift */; }; - E5CE42F223344F6200295277 /* URLRequestDispatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE42F023344F6200295277 /* URLRequestDispatching.swift */; }; + E5CE42F223344F6200295277 /* URLRequestDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE42F023344F6200295277 /* URLRequestDispatcher.swift */; }; E5CE42F523344F8500295277 /* URLRequestProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE42F323344F8500295277 /* URLRequestProtocol.swift */; }; E5CE42F623344F8500295277 /* URLRequestAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE42F423344F8500295277 /* URLRequestAdapter.swift */; }; E5CE42F823344FBE00295277 /* SimpleURLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CE42F723344FBE00295277 /* SimpleURLRequest.swift */; }; @@ -63,7 +63,7 @@ E5CE42B823344DCA00295277 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E5CE42ED23344F5800295277 /* NetworkingService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkingService.swift; sourceTree = ""; }; E5CE42EF23344F6200295277 /* URLSessionDispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionDispatcher.swift; sourceTree = ""; }; - E5CE42F023344F6200295277 /* URLRequestDispatching.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequestDispatching.swift; sourceTree = ""; }; + E5CE42F023344F6200295277 /* URLRequestDispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequestDispatcher.swift; sourceTree = ""; }; E5CE42F323344F8500295277 /* URLRequestProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequestProtocol.swift; sourceTree = ""; }; E5CE42F423344F8500295277 /* URLRequestAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequestAdapter.swift; sourceTree = ""; }; E5CE42F723344FBE00295277 /* SimpleURLRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleURLRequest.swift; sourceTree = ""; }; @@ -227,7 +227,7 @@ E5CE42C923344E0D00295277 /* Dispatcher */ = { isa = PBXGroup; children = ( - E5CE42F023344F6200295277 /* URLRequestDispatching.swift */, + E5CE42F023344F6200295277 /* URLRequestDispatcher.swift */, E5CE42EF23344F6200295277 /* URLSessionDispatcher.swift */, ); path = Dispatcher; @@ -456,7 +456,7 @@ E5CE4309233450A400295277 /* NSErrorExtension.swift in Sources */, E5CE43062334503500295277 /* NetworkingError.swift in Sources */, E5CE430223344FCA00295277 /* URLRequestBuilder.swift in Sources */, - E5CE42F223344F6200295277 /* URLRequestDispatching.swift in Sources */, + E5CE42F223344F6200295277 /* URLRequestDispatcher.swift in Sources */, E5CE430F2334517300295277 /* CodableSerializing.swift in Sources */, E5CE42EE23344F5800295277 /* NetworkingService.swift in Sources */, E5CE4311233456CF00295277 /* CodableRequesting.swift in Sources */, diff --git a/Networking/Sources/Dispatcher/URLRequestDispatching.swift b/Networking/Sources/Dispatcher/URLRequestDispatcher.swift similarity index 87% rename from Networking/Sources/Dispatcher/URLRequestDispatching.swift rename to Networking/Sources/Dispatcher/URLRequestDispatcher.swift index 6a9f670..cace066 100644 --- a/Networking/Sources/Dispatcher/URLRequestDispatching.swift +++ b/Networking/Sources/Dispatcher/URLRequestDispatcher.swift @@ -1,5 +1,5 @@ // -// URLRequestDispatching.swift +// URLRequestDispatcher.swift // Networking // // Created by Eduardo Sanches Bocato on 27/09/19. @@ -7,7 +7,10 @@ // /// This guy is responsible for executing the requests by calling whoever we want to use as a client to deal with networking. -public protocol URLRequestDispatching { +public protocol URLRequestDispatcher { + + /// Defines an adapter to modify requests when needed + var adapter: URLRequestAdapter? { get set } /// Executes the request and provides a completion with the response. /// @@ -28,7 +31,7 @@ public protocol URLRequestDispatching { @discardableResult func execute(request: URLRequestProtocol, completion: @escaping (_ response: Result) -> Void) -> URLRequestToken? } -extension URLRequestDispatching { +extension URLRequestDispatcher { @discardableResult public func execute(request: URLRequestProtocol, completion: @escaping (_ response: Result) -> Void) -> URLRequestToken? { diff --git a/Networking/Sources/Dispatcher/URLSessionDispatcher.swift b/Networking/Sources/Dispatcher/URLSessionDispatcher.swift index 8399f3b..a41fdf6 100644 --- a/Networking/Sources/Dispatcher/URLSessionDispatcher.swift +++ b/Networking/Sources/Dispatcher/URLSessionDispatcher.swift @@ -6,14 +6,6 @@ // Copyright © 2019 Bocato. All rights reserved. // -// -// URLSessionDispatcher.swift -// Networking -// -// Created by Eduardo Sanches Bocato on 27/09/19. -// Copyright © 2019 Bocato. All rights reserved. -// - import Foundation private struct DataTaskResponse { @@ -22,12 +14,16 @@ private struct DataTaskResponse { let httpResponse: HTTPURLResponse } -public final class URLSessionDispatcher: URLRequestDispatching { +public final class URLSessionDispatcher: URLRequestDispatcher { + + // MARK: - Private Properties - // MARK: - Properties + private let session: URLSessionProtocol + private let requestBuilderType: URLRequestBuilder.Type - private var session: URLSessionProtocol - private var requestBuilderType: URLRequestBuilder.Type + // MARK: - Public Properties + + public var adapter: URLRequestAdapter? // MARK: - Initialization @@ -36,10 +32,10 @@ public final class URLSessionDispatcher: URLRequestDispatching { /// - Parameter session: An URLSession. public required init( session: URLSessionProtocol = URLSession.shared, - requestBuilderType: URLRequestBuilder.Type = DefaultURLRequestBuilder.self + requestBuilderType: URLRequestBuilder.Type? = nil ) { self.session = session - self.requestBuilderType = requestBuilderType + self.requestBuilderType = requestBuilderType ?? DefaultURLRequestBuilder.self } // MARK: - Public @@ -53,11 +49,14 @@ public final class URLSessionDispatcher: URLRequestDispatching { var urlRequestToken: URLRequestToken? + let requestBuilder = requestBuilderType.init(request: request) + if let adapter = adapter { + requestBuilder.add(adapter: adapter) + } + do { - let urlRequest = try requestBuilderType - .init(request: request) - .build() as NSURLRequest + let urlRequest = try requestBuilder.build() as NSURLRequest dispatch(request: urlRequest, urlRequestToken: &urlRequestToken) { result in queue.async { completion(result) } diff --git a/Networking/Sources/Service/Abstractions/NetworkingService.swift b/Networking/Sources/Service/Abstractions/NetworkingService.swift index 8da25f5..fac64d5 100644 --- a/Networking/Sources/Service/Abstractions/NetworkingService.swift +++ b/Networking/Sources/Service/Abstractions/NetworkingService.swift @@ -11,5 +11,5 @@ import Foundation /// Defines an API service. public protocol NetworkingService { /// The dispatcher to take care of the network requests. - var dispatcher: URLRequestDispatching { get } + var dispatcher: URLRequestDispatcher { get } } diff --git a/Networking/Sources/URLRequests/Abstractions/URLRequestProtocol.swift b/Networking/Sources/URLRequests/Abstractions/URLRequestProtocol.swift index 4193e99..4d16bad 100644 --- a/Networking/Sources/URLRequests/Abstractions/URLRequestProtocol.swift +++ b/Networking/Sources/URLRequests/Abstractions/URLRequestProtocol.swift @@ -26,7 +26,7 @@ public protocol URLRequestProtocol { /// Defines the list of headers we want to pass along with each request. var headers: [String: String]? { get } - /// Defines the adapters for the request - var adapters: [URLRequestAdapter]? { get } +// /// Defines the adapters for the request +// var adapters: [URLRequestAdapter]? { get set } } diff --git a/Networking/Sources/URLRequests/Tools/URLRequestBuilder.swift b/Networking/Sources/URLRequests/Tools/URLRequestBuilder.swift index aca8729..0fb9ff7 100644 --- a/Networking/Sources/URLRequests/Tools/URLRequestBuilder.swift +++ b/Networking/Sources/URLRequests/Tools/URLRequestBuilder.swift @@ -66,7 +66,7 @@ public protocol URLRequestBuilder { func build() throws -> URLRequest } -public final class DefaultURLRequestBuilder: URLRequestBuilder { +final class DefaultURLRequestBuilder: URLRequestBuilder { // MARK: - Properties @@ -75,7 +75,7 @@ public final class DefaultURLRequestBuilder: URLRequestBuilder { private var method: HTTPMethod = .get private var headers: [String: String]? private var parameters: URLRequestParameters? - private var adapters: [URLRequestAdapter] = [] + private var adapter: URLRequestAdapter? // MARK: - Initialization @@ -87,7 +87,6 @@ public final class DefaultURLRequestBuilder: URLRequestBuilder { self.method = request.method self.headers = request.headers self.parameters = request.parameters - self.adapters = request.adapters ?? [] } /// Initializes the request. @@ -149,7 +148,7 @@ public final class DefaultURLRequestBuilder: URLRequestBuilder { /// - Returns: Itself, for sugar syntax purposes. @discardableResult public func add(adapter: URLRequestAdapter) -> Self { - adapters.append(adapter) + self.adapter = adapter return self } @@ -174,8 +173,8 @@ public final class DefaultURLRequestBuilder: URLRequestBuilder { urlRequest.addValue($0.value, forHTTPHeaderField: $0.key) } - try adapters.forEach { - urlRequest = try $0.adapt(urlRequest) + if let adapter = adapter { + urlRequest = try adapter.adapt(urlRequest) } return urlRequest diff --git a/NetworkingTests/Tests/Dispatcher/URLSessionDispatcherTests.swift b/NetworkingTests/Tests/Dispatcher/URLSessionDispatcherTests.swift index ccf0649..ac058f5 100644 --- a/NetworkingTests/Tests/Dispatcher/URLSessionDispatcherTests.swift +++ b/NetworkingTests/Tests/Dispatcher/URLSessionDispatcherTests.swift @@ -277,6 +277,39 @@ final class URLSessionDispatcherTests: XCTestCase { } } + func test_whenAdapterAndSucceed_adaptShouldBeCalled() { + // Given + guard let url = URL(string: "http://www.someurl.com/") else { + XCTFail("Could not create URL.") + return + } + let urlResponse = HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: nil, + headerFields: nil + ) + let urlSessionStub = URLSessionStub( + data: nil, + urlResponse: urlResponse + ) + let sut = URLSessionDispatcher(session: urlSessionStub) + let request: SimpleURLRequest = .init(url: url) + + let adapterSpy = URLRequestAdapterSpy() + sut.adapter = adapterSpy + + // When + let executeRequestExpectation = expectation(description: "executeRequestExpectation") + sut.execute(request: request) { _ in + executeRequestExpectation.fulfill() + } + wait(for: [executeRequestExpectation], timeout: 1.0) + + // Then + XCTAssertTrue(adapterSpy.adaptCalled, "adapt method should have been called.") + } + } // MARK: - Testing Helpers diff --git a/NetworkingTests/Tests/Service/Serialization/CodableRequestingTests.swift b/NetworkingTests/Tests/Service/Serialization/CodableRequestingTests.swift index 705e6ea..e8e4851 100644 --- a/NetworkingTests/Tests/Service/Serialization/CodableRequestingTests.swift +++ b/NetworkingTests/Tests/Service/Serialization/CodableRequestingTests.swift @@ -20,7 +20,7 @@ final class CodableRequestingTests: XCTestCase { return } let resultToReturn: Result = .success(validDataToReturn) - let urlRequestDispatchingStub = URLRequestDispatchingStub( + let urlRequestDispatcherStub = URLRequestDispatcherStub( resultToReturn: resultToReturn ) guard let url = URL(string: "http://www.someurl.com/") else { @@ -29,7 +29,7 @@ final class CodableRequestingTests: XCTestCase { } let request: SimpleURLRequest = .init(url: url) let expectedCodableResult = SomeResponse(value: "something") - let sut = CodableRequestingService(dispatcher: urlRequestDispatchingStub) + let sut = CodableRequestingService(dispatcher: urlRequestDispatcherStub) // When let requestCodableExpectation = expectation(description: "serializeCodableExpectation") @@ -53,18 +53,20 @@ final class CodableRequestingTests: XCTestCase { // MARK: - Testing Helpers private class CodableRequestingService: CodableRequesting { - var dispatcher: URLRequestDispatching + var dispatcher: URLRequestDispatcher - init(dispatcher: URLRequestDispatching) { + init(dispatcher: URLRequestDispatcher) { self.dispatcher = dispatcher } } -final class URLRequestDispatchingStub: URLRequestDispatching { +final class URLRequestDispatcherStub: URLRequestDispatcher { private let resultToReturn: Result + var adapter: URLRequestAdapter? + init(resultToReturn: Result) { self.resultToReturn = resultToReturn } diff --git a/NetworkingTests/Tests/URLRequests/Tools/DefaultURLRequestBuilderTests.swift b/NetworkingTests/Tests/URLRequests/Tools/DefaultURLRequestBuilderTests.swift index 9c5a7ac..4574345 100644 --- a/NetworkingTests/Tests/URLRequests/Tools/DefaultURLRequestBuilderTests.swift +++ b/NetworkingTests/Tests/URLRequests/Tools/DefaultURLRequestBuilderTests.swift @@ -127,7 +127,7 @@ final class DefaultURLRequestBuilderTests: XCTestCase { // MARK: - Testing Helpers -private final class URLRequestAdapterSpy: URLRequestAdapter { +final class URLRequestAdapterSpy: URLRequestAdapter { private(set) var adaptCalled = false func adapt(_ urlRequest: URLRequest) throws -> URLRequest { adaptCalled = true diff --git a/README.md b/README.md index e89d78e..89aed59 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Networking layer abstraction Add this to Cartfile: -`git "https://github.com/bocato/Networking.git" ~> 1.0` +`git "https://github.com/bocato/Networking.git" ~> 1.1` Then: @@ -64,11 +64,11 @@ final class PokemonsDataService: NetworkingService { // MARK: - Properties - var dispatcher: URLRequestDispatching + var dispatcher: URLRequestDispatcher // MARK: - Initialization - init(dispatcher: URLRequestDispatching) { + init(dispatcher: URLRequestDispatcher) { self.dispatcher = dispatcher } @@ -134,3 +134,43 @@ final class PokemonsDataService: CodableRequesting { } ``` + +#### URLRequestAdapter + +The `URLRequestAdapter` protocol allows each request that conforms with `URLRequestProtocol` request made on a `URLRequestDispatcher` to be inspected and adapted before being created. +One very specific way to use an adapter is to append an `Authorization` header to requests behind a certain type of authentication. + +```swift +final class AccessTokenAdapter: URLRequestAdapter { + private let accessToken: String + + init(accessToken: String) { + self.accessToken = accessToken + } + + func adapt(_ urlRequest: URLRequest) throws -> URLRequest { + var urlRequest = urlRequest + + if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix("https://httpbin.org") { + urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization") + } + + return urlRequest + } +} +``` + +```swift +let dispatcher = URLSessionDispatcher() +dispatcher.adapter = AccessTokenAdapter(accessToken: "1234") + +let request: PokemonsRequest = .list +dispatcher.execute(request: request) { (result) in + switch result { + case let .failure(error): + // Do something + case .success(data): + // Do something + } +} +```