HTTP in Swift, Part 6: Chaining Loaders
Part 6 in a series on building a Swift HTTP framework:
- HTTP in Swift, Part 1: An Intro to HTTP
- HTTP in Swift, Part 2: Basic Structures
- HTTP in Swift, Part 3: Request Bodies
- HTTP in Swift, Part 4: Loading Requests
- HTTP in Swift, Part 5: Testing and Mocking
- HTTP in Swift, Part 6: Chaining Loaders
- HTTP in Swift, Part 7: Dynamically Modifying Requests
- HTTP in Swift, Part 8: Request Options
- HTTP in Swift, Part 9: Resetting
- HTTP in Swift, Part 10: Cancellation
- HTTP in Swift, Part 11: Throttling
- HTTP in Swift, Part 12: Retrying
- HTTP in Swift, Part 13: Basic Authentication
- HTTP in Swift, Part 14: OAuth Setup
- HTTP in Swift, Part 15: OAuth
- HTTP in Swift, Part 16: Composite Loaders
- HTTP in Swift, Part 17: Brain Dump
- HTTP in Swift, Part 18: Wrapping Up
So far, the HTTPLoading
types we’ve created have all been loaders that directly respond to an HTTPRequest
. In order to create new kinds of loaders, we’ll need to revisit the HTTPLoading
protocol.
Here’s where it currently stands:
public protocol HTTPLoading {
func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void)
}
If we think back to part 4, we’ll remember that we made this observation about the protocol:
We send a request, and at some point in the future, our closure will be executed with whatever response we got back.
I want to point out here that this definition says nothing about how the response is retrieved. That distinction is why we could create the MockLoader
in the previous post. This definition also says nothing about this particular loader creating the response. All this protocol defines is that if we call this method with a request, at some point in the future the completion
block will be executed with a result.
So… what if we didn’t invoke the completion block in this loader, but instead allowed another loader to invoke it? We would still be meeting the terms of the API contract (request comes in, completion eventually gets executed).
To put it more concretely, let’s imagine an AnyLoader
type:
class AnyLoader: HTTPLoading {
private let loader: HTTPLoading
init(_ other: HTTPLoading) {
self.loader = other
}
func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {
loader.load(request: request, completion: completion)
}
}
Even though this loader doesn’t execute the completion block itself, it still satisfies the all of the requirements of being an “HTTPLoading” type. This ability to have loaders to delegate loading responsibility to other loaders is the core of our framework’s functionality. It will allow us to compose a custom sequence of loaders to execute requests. We’ll formalize this notion of a “next loader” as part of our protocol:
public protocol HTTPLoading {
var nextLoader: HTTPLoader? { get set }
func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void)
}
We’ll want to have some common logic on all HTTPLoading
instances. For example, it seems reasonable to say that if you’ve set the nextLoader
once, you shouldn’t be able to change it. Also if you attempt to load a request without having a nextLoader
, it seems logical to fail the request.
Therefore, we’ll change HTTPLoading
into a class where we can encapsulate the logic that is common to all loaders:
open class HTTPLoader {
public var nextLoader: HTTPLoader? {
willSet {
guard nextLoader == nil else { fatalError("The nextLoader may only be set once") }
}
}
public init() { }
open func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {
if let next = nextLoader {
next.load(request: request, completion: completion)
} else {
let error = HTTPError(code: .cannotConnect, request: request)
completion(.failure(error))
}
}
}
It may not be immediately obvious what this gets us, so let’s create another loader that prints
requests as they execute:
public class PrintLoader: HTTPLoader {
override func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {
print("Loading \(request)")
super.load(request: request, completion: { result in
print("Got result: \(result)")
completion(result)
})
}
}
With this and another similar loader to wrap a URLSession
, we can construct this chain:
let sessionLoader = URLSessionLoader(session: URLSession.shared)
let printLoader = PrintLoader()
printLoader.nextLoader = sessionLoader
let loader: HTTPLoader = printLoader
Now whenever we use our loader
value to execute network requests, every request will be logged before being executed on the URLSession
, and every response will be logged after it comes back.
A Custom Operator
This is one of the rare circumstances where I think defining a custom operator might be worthwhile. If we end up in a situation where we have several loaders we want to chain together, then defining that chain can be quite verbose. An “arrow” operator makes this definition simpler:
precedencegroup LoaderChainingPrecedence {
higherThan: NilCoalescingPrecedence
associativity: right
}
infix operator --> : LoaderChainingPrecedence
@discardableResult
public func --> (lhs: HTTPLoader?, rhs: HTTPLoader?) -> HTTPLoader? {
lhs?.nextLoader = rhs
return lhs ?? rhs
}
First, we’ll define a custom precedencegroup
, in order to define our relative order of operations. The precedencegroup
we define indicates that we have a precedence higher than “nil coalescing”, but lower than anything that comes next. It also defines that we have “right associativity”. The “associativity” of the operator tells the compiler the ordering to follow when multiple operators of the same precedence appear in the same statement.
For example, if we have the statement a <op> b <op> c
and <op>
is left associative, then a <op> b
will be executed before ... <op> c
. However, if it were right associative, then b <op> c
will be executed first, and then the result will be used as the right-hand side of a <op> ...
.
Next, we define the symbol for our operator (-->
) and a function to implement it. Since we’ve defined our operator to be right associative, we’ll prefer to return the left-hand value of the operator as the result.
The associativity is important because of how we’ll want to use this operator:
let chain = loaderA --> loaderB --> loaderC --> loaderD
If -->
were left associative, then this would evaluate left-to-right and be equivalent to:
loaderA.nextLoader = loaderB
loaderB.nextLoader = loaderC
loaderC.nextLoader = loaderD
chain = loaderD
This looks mostly correct, except that we end up with the wrong value in the chain
variable.
On the other hand, by making this a right-associative operator, this code gets evaluated from right-to-left:
loaderC.nextLoader = loaderD
loaderB.nextLoader = loaderC
loaderA.nextLoader = loaderB
chain = loaderA
All of the loaders still get hooked up as we expect, but we end up with the “first” loader as the value of the chain
variable.
Chaining Loaders
The ability to create a chain of loaders is the basis for all features we’ll eventually want to add to this framework. It allows us to create massively composable networking stacks, with individual “links” in the chain performing specialized tasks. We can define the chain dynamically; we’ll have the ability to skip portions of the chain entirely; and we’ll even be able to distribute the chain across separate devices.
In the next post, we’ll build a loader that will allow us to dynamically alter in-flight requests.