Skip to content

Commit

Permalink
Make NetworkResource request creation async 📬 (#179)
Browse files Browse the repository at this point in the history
Until now, `Resource` fetch authentication was made via a single
`NetworkAuthenticator` injected on a `URLSessionNetworkStack`, which
was used to enrich the requests of the Resources being scheduled on the
stack. This had some limitations:

1. Either all `Resource` fetches were authenticated, or none (depending
on whether `authenticator` was set or not on the stack).

2. Since the `authenticator` was shared when set, supporting different
authentication schemes (e.g. different API's) or unauthenticated
fetches on the same stack required creating a complex
`NetworkAuthenticator` (containing / proxying all logic), which was
obviously not ideal.

To solve the above problems and give more flexibility to our stack, the
`NetworkResource` keeps being responsible for creating its requests,
with the difference that now it does so *asynchronously*. This allows
each `NetworkResource` implementation to create (and enrich) its
requests, only passing them to the stack when they are ready to be
scheduled. As such, more complex request building logic like
authentication can now be done by the Resource itself (which can have
its own `RequestAuthenticator` instance, for example).

## Changes

- Renamed `NetworkAuthenticator` to `RequestAuthenticator` and
`RetryableNetworkAuthenticator` to `RetryableRequestAuthenticator`,
and refined to contain associated types.

- Created specialized `URLRequestAuthenticator` and
`RetryableURLRequestAuthenticator` that work with `URLRequest`, `Data`
and `URLResponse`.

- Updated `NetworkResource` to have a `Request` associated type, and a
new asynchronous `makeRequest()` API.

- Removed `RelativeNetworkResource` and `StaticNetworkResource`.

- Created `HTTPResourceEndpoint` to represent an HTTP endpoint, and
contain default `URLRequest` building logic.

- Created `HTTPNetworkResource` to represent a Resource with a
`HTTPResourceEndpoint`, and contain a default `makeRequest()`
implementation.

- Created `AuthenticatedHTTPNetworkResource` that extends
`HTTPNetworkResource` and has a `NetworkAuthenticator` instance, and
contain a default `makeRequest()` implementation.

- Added UT's for the new types.

- Adjusted existing UT's.

- Cleaned up and unified some `MockResource`s in UT's.

- Added some documentation on the new stuff.
  • Loading branch information
p4checo committed Feb 11, 2019
1 parent 42bee00 commit fe77ade
Show file tree
Hide file tree
Showing 23 changed files with 960 additions and 819 deletions.
52 changes: 40 additions & 12 deletions Alicerce.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion Sources/Network/HTTP.swift
Expand Up @@ -4,7 +4,6 @@ import Foundation
public enum HTTP {

public typealias Headers = [String : String]
public typealias Query = [String : String]

/// An enum describing the HTTP methods.
public enum Method: String, Hashable {
Expand Down
11 changes: 4 additions & 7 deletions Sources/Network/Network.swift
Expand Up @@ -25,11 +25,11 @@ public enum Network {

public enum Error: Swift.Error {

case noRequest(Swift.Error)
case http(code: HTTP.StatusCode, apiError: Swift.Error?, response: URLResponse)
case noData(response: URLResponse)
case url(Swift.Error, response: URLResponse?)
case badResponse(response: URLResponse?)
case authenticator(Swift.Error)
case retry(errors: [Swift.Error],
totalDelay: ResourceRetry.Delay,
retryError: ResourceRetry.Error,
Expand All @@ -42,18 +42,15 @@ public enum Network {

let authenticationChallengeHandler: AuthenticationChallengeHandler?

let authenticator: NetworkAuthenticator?

let requestInterceptors: [RequestInterceptor]

let retryQueue: DispatchQueue

public init(authenticationChallengeHandler: AuthenticationChallengeHandler? = nil,
authenticator: NetworkAuthenticator? = nil,
requestInterceptors: [RequestInterceptor] = [],
retryQueue: DispatchQueue) {

self.authenticationChallengeHandler = authenticationChallengeHandler
self.authenticator = authenticator
self.requestInterceptors = requestInterceptors
self.retryQueue = retryQueue
}
Expand All @@ -63,6 +60,8 @@ public enum Network {
extension Network.Error {
var response: URLResponse? {
switch self {
case .noRequest:
return nil
case let .http(_, _, response):
return response
case let .noData(response):
Expand All @@ -71,8 +70,6 @@ extension Network.Error {
return response
case let .badResponse(response):
return response
case .authenticator:
return nil
case let .retry(_, _, _, response):
return response
}
Expand Down
16 changes: 0 additions & 16 deletions Sources/Network/NetworkAuthenticator.swift

This file was deleted.

53 changes: 53 additions & 0 deletions Sources/Network/RequestAuthenticator.swift
@@ -0,0 +1,53 @@
import Foundation
import Result

// A type representing a request authenticator.
public protocol RequestAuthenticator: class {

/// A type that represents a network request.
associatedtype Request

/// A type that represents a request authentication error.
associatedtype Error: Swift.Error

/// The authenticator's authentication handler closure, invoked when a request's authentication finishes.
typealias AuthenticationHandler = (Result<Request, Error>) -> Cancelable

/// Authenticates a request.
///
/// - Important: The cancelable returned by the `handler` closure *when called asynchronously* should be added
/// as a child of the cancelable returned by this method, so that the async work gets chained and can be cancelled.
///
/// - Parameters:
/// - request: The request to authenticate.
/// - handler: The closure to handle the request authentication's result (i.e. either the authenticated request
/// or an error).
/// - Returns: A cancelable to cancel the operation.
@discardableResult
func authenticate(_ request: Request, handler: @escaping AuthenticationHandler) -> Cancelable
}

/// A type representing a request authenticator that provides a retry policy rule (to handle authentication errors).
public protocol RetryableRequestAuthenticator: RequestAuthenticator {

/// A type that represents a resource's remote type.
associatedtype Remote

/// A type that represent a network response.
associatedtype Response

/// The authenticator's specialized retry policy.
typealias RetryPolicy = ResourceRetry.Policy<Remote, Request, Response>

/// The retry policy used to evaluate which action to take when an error occurs.
var retryPolicyRule: RetryPolicy.Rule { get }
}

// A type representing a request authenticator specialized to authenticate `URLRequest`'s.
public protocol URLRequestAuthenticator: RequestAuthenticator
where Request == URLRequest {}

/// A type representing a request authenticator specialized to authenticate `URLRequest`'s that provides a retry policy
/// rule (to handle authentication errors) specialized for `Data` remote type, `URLRequest`'s and `URLResponse`'s.
public protocol RetryableURLRequestAuthenticator: RetryableRequestAuthenticator
where Remote == Data, Request == URLRequest, Response == URLResponse {}
48 changes: 15 additions & 33 deletions Sources/Network/URLSessionNetworkStack.swift
Expand Up @@ -12,7 +12,6 @@ public extension Network {
public typealias URLSessionDataTaskClosure = (Data?, URLResponse?, Swift.Error?) -> Void

private let authenticationChallengeHandler: AuthenticationChallengeHandler?
private let authenticator: NetworkAuthenticator?
private let requestInterceptors: [RequestInterceptor]
private let retryQueue: DispatchQueue

Expand All @@ -34,34 +33,38 @@ public extension Network {
}

public init(authenticationChallengeHandler: AuthenticationChallengeHandler? = nil,
authenticator: NetworkAuthenticator? = nil,
requestInterceptors: [RequestInterceptor] = [],
retryQueue: DispatchQueue) {

self.authenticationChallengeHandler = authenticationChallengeHandler
self.authenticator = authenticator
self.requestInterceptors = requestInterceptors
self.retryQueue = retryQueue
}

public convenience init(configuration: Network.Configuration) {

self.init(authenticationChallengeHandler: configuration.authenticationChallengeHandler,
authenticator: configuration.authenticator,
requestInterceptors: configuration.requestInterceptors,
retryQueue: configuration.retryQueue)
}

@discardableResult
public func fetch<R>(resource: R, completion: @escaping Network.CompletionClosure<R.Remote>)
-> Cancelable
public func fetch<R>(resource: R, completion: @escaping Network.CompletionClosure<R.Remote>) -> Cancelable
where R: NetworkResource & RetryableResource, R.Remote == Remote, R.Request == Request, R.Response == Response {

guard let authenticator = authenticator else {
let request = resource.request
return resource.makeRequest { [ weak self] result -> Cancelable in

return perform(request: request, resource: resource, completion: completion)
}
guard let strongSelf = self else { return DummyCancelable() }

switch result {
case let .success(request):
return strongSelf.perform(request: request, resource: resource, completion: completion)

return authenticatedFetch(using: authenticator, resource: resource, completion: completion)
case let .failure(error):
completion(.failure(.noRequest(error.error)))
return DummyCancelable()
}
}
}

// MARK: - URLSessionDelegate Methods
Expand Down Expand Up @@ -109,6 +112,7 @@ public extension Network {
return cancelableBag
}

// swiftlint:disable:next function_body_length
private func handleHTTPResponse<R>(with completion: @escaping Network.CompletionClosure<R.Remote>,
request: Request,
resource: R,
Expand Down Expand Up @@ -177,28 +181,6 @@ public extension Network {
}
}

private func authenticatedFetch<R>(using authenticator: NetworkAuthenticator,
resource: R,
completion: @escaping Network.CompletionClosure<R.Remote>) -> Cancelable
where R: NetworkResource & RetryableResource, R.Remote == Remote, R.Request == Request, R.Response == Response {

let request = resource.request

return authenticator.authenticate(request: request) { [weak self] result -> Cancelable in

guard let strongSelf = self else { return DummyCancelable() }

switch result {
case let .success(authenticatedRequest):
return strongSelf.perform(request: authenticatedRequest, resource: resource, completion: completion)

case let .failure(error):
completion(.failure(.authenticator(error.error)))
return DummyCancelable()
}
}
}

// swiftlint:disable:next function_body_length function_parameter_count
private func handleError<R>(with completion: @escaping Network.CompletionClosure<R.Remote>,
request: Request,
Expand Down
21 changes: 21 additions & 0 deletions Sources/Resource/AuthenticatedHTTPNetworkResource.swift
@@ -0,0 +1,21 @@
import Foundation
import struct Result.AnyError

/// A type representing a resource that is fetched via HTTP using a specific type of endpoint requiring authentication.
public protocol AuthenticatedHTTPNetworkResource: HTTPNetworkResource {

/// A type that represents a request authenticator.
associatedtype Authenticator: RequestAuthenticator where Authenticator.Request == Request

/// The resource's request authenticator.
var authenticator: Authenticator { get }
}

extension AuthenticatedHTTPNetworkResource {

@discardableResult
public func makeRequest(_ handler: @escaping MakeRequestHandler) -> Cancelable {

return authenticator.authenticate(endpoint.request) { handler($0.mapError(AnyError.init)) }
}
}
20 changes: 20 additions & 0 deletions Sources/Resource/HTTPNetworkResource.swift
@@ -0,0 +1,20 @@
import Foundation

/// A type representing a resource that is fetched via HTTP using a specific type of endpoint.
public protocol HTTPNetworkResource: NetworkResource where Request == URLRequest {

/// A type that represents an HTTP endpoint.
associatedtype Endpoint: HTTPResourceEndpoint

/// The resource's endpoint.
var endpoint: Endpoint { get }
}

extension HTTPNetworkResource {

@discardableResult
public func makeRequest(_ handler: @escaping MakeRequestHandler) -> Cancelable {

return handler(.success(endpoint.request))
}
}
66 changes: 66 additions & 0 deletions Sources/Resource/HTTPResourceEndpoint.swift
@@ -0,0 +1,66 @@
import Foundation

/// A type representing an HTTP resource's endpoint, to generate its request.
///
/// Especially useful when conformed to by an enum, allowing a type safe modelling of an API's endpoints.
public protocol HTTPResourceEndpoint {

/// The HTTP method.
var method: HTTP.Method { get }

/// The base URL.
var baseURL: URL { get }

/// The URL's path subcomponent.
var path: String? { get }

/// The URL's query string items.
var queryItems: [URLQueryItem]? { get }

/// The HTTP header fields.
var headers: HTTP.Headers? { get }

// The HTTP message body data.
var body: Data? { get }
}

public extension HTTPResourceEndpoint {

var path: String? { return nil }
var queryItems: [URLQueryItem]? { return nil }
var headers: HTTP.Headers? { return nil }
var body: Data? { return nil }
}

public extension HTTPResourceEndpoint {

/// The endpoint's generated request.
var request: URLRequest {

guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else {
assertionFailure("😱: failed to create components from URL: \(baseURL) on \(type(of: self))!")
return URLRequest(url: baseURL)
}

if let queryItems = queryItems {
components.queryItems = (components.queryItems ?? []) + queryItems
}

if let path = path {
components.path = components.path.appending(path).replacingOccurrences(of: "//", with: "/")
}

guard let url = components.url else {
assertionFailure("😱: failed to extract URL from components: \(components) on \(type(of: self))!")
return URLRequest(url: baseURL)
}

var urlRequest = URLRequest(url: url)

urlRequest.httpMethod = method.rawValue
urlRequest.allHTTPHeaderFields = headers
urlRequest.httpBody = body

return urlRequest
}
}

0 comments on commit fe77ade

Please sign in to comment.