Skip to content

Instantly share code, notes, and snippets.

@jamiehannaford
Last active August 29, 2015 14:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamiehannaford/33113b649545c7a47eb9 to your computer and use it in GitHub Desktop.
Save jamiehannaford/33113b649545c7a47eb9 to your computer and use it in GitHub Desktop.

Overview of problem

Here is the top-level overview of what is expected for a normal SDK interaction:

  1. An end-user calls an operation on a service client. For example, $objectStorage->listContainers();
  2. This service client knows how to communicate with the API, and calls its transport client to make the necessary HTTP transaction. For example, $this->transport->get('containers');
  3. The transport client communicates with the API, sending the request and receiving back a response which it hands over to the service.

This is a fairly simple workflow, but there are a few issues here:

  • How are base URLs handled for a particular service? The baseURL value is extracted from the service catalog, which is constructed from the parsed body of the authentication HTTP call. A service should not be responsible for doing this; and nor should it really hold its own baseURL value because that's a transport detail.

  • How are default values going to be applied to all requests for a service? For example, if a service requires that all requests that are sent to the API contain a particular request header, how will this be achieved? This is identical to the previous issue (which is concerned with 1 detail, a baseURL). Both issues require a way to modify requests before they're sent. A service cannot do this because peering into and adjusting objects in the transport layer is not its responsibility. It tells the transport client to do a job; it doesn't tell the transport client how to do its job.

  • Lastly, how does authentication happen? In order for a service to communicate with an API, authentication must happen somewhere in the middle. Something must keep track of the latest token and service catalog in order to know when to re-authenticate and how to modify the request's X-Auth-Token header. Again, this is not the responsibility of the service.

In essence, we have two problems that need to be overcome:

  1. Modifying requests with default values
  2. Authentication

Proposal solution

So, how are we going to address these two areas? We've already talked about how a transport client should be as generic as possible because it can be injected into multiple services. This means that we cannot have any service-specific state associated with a transport object.

But if authentication and baseURLs cannot be kept on the service or the transport object - where the heck does it sit?

The key to solving this problem is separating the problems and assigning objects to each one. An object is really a container of behaviour with a single, well-established responsibility. So if we have two problems, we need two objects: one for handling authentication (an Authenticator); and one for decorating requests before they're sent (a RequestModifier). These objects contain state and behaviour relevant to their responsibilities; but what's important is that they act independently of each other.

I've just mentioned that each object needs isolation - but when we think about it, a RequestModifier might need to rely on details provided by the Authenticator. For example, it will need to know what the baseURL is in order to apply it to a request. So we need a structure that allows both isolation and communication. The way we can do this is by having an subscriber system. Each object attaches itself to a Mediator who decides how and when to invoke them. In our case, the Mediator would be the transport client. When the transport client is about to send a request, it notifies all its subscribers. It does this by passing each one an Even object that contains relevant information it might be interested in.

This event is like a parcel: it contains the raw request before its sent. The parcel is first sent to the Authenticator who remembers any previous authentication attempts. It looks to see whether it has a token, and if it doesn't, it authenticates. If on the other hand it does have a token, it inspects to see whether that token is still valid. If it has expired, it re-authenticates. During this authentication procedure, it communicates with the API and gets back a JSON body containing a serialized service catalog - which it parses and builds into a full catalog object.

The Authenticator then inserts this catalog object into the Event parcel and passes it on to whoever is next in the chain. In this particular case, the next subscriber is the RequestModifier. The responsibility of the RequestModifier is simple: it modifies a raw Request object based on service-specific data. So, for Marconi, it might add a unique Client-ID header. But what will usually happen (for the majority of services) is that it will inspect the catalog object (provided by the Authenticator) and retrieve the relevant baseURL for its service. Once it has that, it will modify the request URL to incorporate this hostname/IP. It modifies the Request and re-inserts it back into the event.

Once this event chain has fully propagated and reached the end of its cycle, the transport client will get back the parcel it sent - containing a fully decorated Request object. It then sends this Request to the API without every worrying about where its headers, payload or URI came from. That is not its responsibility after all - all it does is prepare a request, notify interested parties (with their own responsibilities), and send the finished product. It's the service's responsibility to add the relevant subscriber(s) it needs to - the transport client is fully decoupled from any kind of implementation detail.


Final thoughts

We might not even need to separate Authenticator from RequestModifier. We might be able to combine them into a unified subscriber that takes care of authentication and modifies requests before they're sent. This subscriber would be tailored for a specific service and be attached to the transport client when it's injected into a client. This would then override any previous subscriber added to the transport client.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment