-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make
NetworkResource
request creation async 📬 (#179)
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
Showing
23 changed files
with
960 additions
and
819 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |
Oops, something went wrong.