HTTP in Swift, Part 6: Chaining Loaders

Part 6 in a series on building a Swift HTTP framework:

  1. HTTP in Swift, Part 1: An Intro to HTTP
  2. HTTP in Swift, Part 2: Basic Structures
  3. HTTP in Swift, Part 3: Request Bodies
  4. HTTP in Swift, Part 4: Loading Requests
  5. HTTP in Swift, Part 5: Testing and Mocking
  6. HTTP in Swift, Part 6: Chaining Loaders
  7. HTTP in Swift, Part 7: Dynamically Modifying Requests
  8. HTTP in Swift, Part 8: Request Options
  9. HTTP in Swift, Part 9: Resetting
  10. HTTP in Swift, Part 10: Cancellation
  11. HTTP in Swift, Part 11: Throttling
  12. HTTP in Swift, Part 12: Retrying
  13. HTTP in Swift, Part 13: Basic Authentication
  14. HTTP in Swift, Part 14: OAuth Setup
  15. HTTP in Swift, Part 15: OAuth
  16. HTTP in Swift, Part 16: Composite Loaders
  17. HTTP in Swift, Part 17: Brain Dump
  18. 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.


Related️️ Posts️

HTTP in Swift, Part 18: Wrapping Up
HTTP in Swift, Part 17: Brain Dump
HTTP in Swift, Part 16: Composite Loaders
HTTP in Swift, Part 15: OAuth
HTTP in Swift, Part 14: OAuth Setup
HTTP in Swift, Part 13: Basic Authentication
HTTP in Swift, Part 12: Retrying
HTTP in Swift, Part 11: Throttling
HTTP in Swift, Part 10: Cancellation
HTTP in Swift, Part 9: Resetting
HTTP in Swift, Part 8: Request Options
HTTP in Swift, Part 7: Dynamically Modifying Requests
HTTP in Swift, Part 5: Testing and Mocking
HTTP in Swift, Part 4: Loading Requests
HTTP in Swift, Part 3: Request Bodies
HTTP in Swift, Part 2: Basic Structures
HTTP in Swift, Part 1: An Intro to HTTP