The Spy Pattern

Writing software documentation. Ugh. I like reading it, sure, but hate writing it. Consumers, from users to developers to QA, want it. But no one wants to write it, so it’s delegated to the intern. Said intern spends weeks manually putting together things that quickly become stale.

Recently, I was tasked with leading a project to develop an API for others to consume. APIs are useful, but oh such a pain to build and maintain. Before starting, we needed documentation regarding the format of the requests and responses, to show to potential users.

To build the documentation, we settled on Swagger UI, and went about to create the famed swagger.json file. Swagger is a framework of API developer tools. Swagger UI accepts a .json file using the OpenAPI specification and outputs beautiful documentation. Sample JSON file along with docs.

The main parts of the .json file are:

  • Basic Info
  • Paths (your routes)
  • Definitions

After writing the first definition manually, we realized this process will fall prey to the mass of other documentations out there. Rust. As the project lead, I am not willing to let that happen on my watch. The obvious solution? Code. Code the documentation. When people preach self-documenting code, they usually refer to clever noun-based class names, verb-based method names, and clear variables. For developers. Self-documenting code was always FDBD, For Developers By Developers. Our challenge here is to code the documentation for users.

A quick command-line script to put the basic static header information into the jso file was easy. Next up were the URIs. How to get the list of URIs? Before we go further, I will mention that each API call is implemented in its own class. My first thought was to glob() the directories, and loop through the files. This will guarantee to cover all calls. However, the classes themselves are not aware of its URIs. Sure, I could copy the URI into the class as a property or in the docblock. But that is also manual, and won’t cut it for me.

Routes

Our API routes are handled using the wonderful Route package from The PHP League. My next thought was to somehow ask the Route package for the list of routes. No such thing. The routes aren’t stored in any static location. Ain’t gonna work.

Let’s pause for a minute to explore the structure of the routes file. While we were building the routes file, it occurred to us that we’ll wind up with a very long file, and we don’t like long files. The easy solution was to segment each API’s routes to an individual file that returns a closure:

// file: /config/routes/api/v1/orders.php
return function ($routes) {
$route->get('/', [new \Api\V1\Orders\All, 'paginate']);
...
}

We had split these routes into individual files for purely aesthetic reasons. Turns out, this allows us to pull a neat trick. Welcome RouteScraper. This class functions as a drop-in replacement for the Route package's RouteCollection class, using IoC. Instead of binding URIs to callbacks, we scrape the routes. Each method, get(), post(), etc., has access to the bread and butter of the API: the URIs and classes responding thereto. Now, we have all URIs in a neat array, straight from the source. Beautiful.

Really, I could stop here. The above example shows that we were able to get a significant portion of the documentation directly from the code. But it gets even better. Let’s move on to the next part of swagger.json.

Definitions

Each model (or API) has a definition that Swagger uses to describe the response formats. How do we get that from the code?

Fractal, another package from The PHP League, exposes an expressive syntax for transforming your data for a consumer’s eyes. The basic syntax can be something like this:

public function transform(array $order)
{
return [
'id' => (int)$order['id'],
...
]
}

Here too, I went though a similar thought process. Can we grab the result and re-use it somehow? Unfortunately, the answer is no, because this only returns data (which it rightly should do) and that won’t help for documentation.

Enter The Spy Pattern, an extension of IoC. Similar to how we injected a specialized class for purposes of collecting route information, the Spy Pattern is injected and collects meta-data. This design pattern calls for two classes that implement a common interface. In our scenario, we create Cast classes, whose job is to cast the data. A BonafideCast class along with a SpyCast class. Both classes have a series of methods that, at face value, do something simple: cast values to a certain format. asInteger(), asBoolean(), etc. One method for every data type.

But that is where the similarity ends. While BonafideCast does actually cast the values and return the data, the SpyCast class uses the context (read: method name) to describe the field type and return meta-data for Swagger:

BonafideCast::asInteger('4') // 4
SpyCast::asInteger('4') // ['type' => 'integer']

Here’s what my new transform() method now looks like:

public function transform(array $order)
{
$cast = $this->cast; // injected in the constructor

return [
'id' => $cast::asInteger($order['id']),
...
]
}

During the real request/response cycle, when $cast is BonafideCast, we get something like this:

[
'id' => 4,
...
]

However, when the CLI script runs and uses SpyCast, we get something completely different:

[
'id' => ['type' => 'integer'],
...
]

Coincidentally, this is exactly what Swagger needs.

Links:
The PHP League http://thephpleague.com/
Route Package http://route.thephpleague.com/
Fractal http://fractal.thephpleague.com/
Swagger UI https://swagger.io/swagger-ui/
OpenAPI specification https://swagger.io/specification/

Photo by Pawel Janiak on Unsplash