Update, August 2013: The ideas here have been turned in to a gem!

For the last few days I’ve been use Rails’ built in JSON facilities to build API capabilities in to an existing app. Rails makes this easy so kudos, etc, blah blah. Seeing as how I need to test these features I was following a Test First approach and since a JSON API is basically a web-request in drag I decided to use Cucumber and Rack::Test. I wrote a lot of tests like:

Scenario: Get a list of all widgets
Given the following widget exists:
| name | job |
| Billy the Widget | Be a widget |
| Sally the Widget | Be a widget |
When I GET "/widgets"
Then I should get a successful response
ANd the response should include 2 widgets
And the response should include the Widget data for "Billy the Widget"
And the response should include the Widget data for "Sally the Widget"

Scenario: Create a new widget
Given I have a JSON request for “widget”
| name | Bob the Widget |
| job | Be a widget |
And I POST that JSON to “/widgets.json”
Then I should get a successful response
And the response should include the Widget data for “Bob the Widget”

.. and everything was great.

Until I needed to document that API for an outside developer. I sent him the raw Cucumber scenarios but he wanted something more basic. “Tell me”, he said, “what I need to post to the API and what response I’ll get when I do it, for ever action on every URL.”

Difficult but fair, so I started writing out long form docs spelling out what the incoming JSON for a resource should look like and what the results would be, what errors would look like, etc. After about 35 seconds of this I got bored and started daydreaming. 45 seconds after that I had the idea: All the docs I need are already in the code, I just need to format the data and output it to a file!

In my Cucumber steps I am using the standard Rack::Test helpers for REST commands:put(path, @hash.to_json), post(path, @hash.to_json), get(path) and delete(path).

To read the response I get back from API resource I use a method like this that in called inside “Then I should get a successful response”:

def parse_response
JSON.parse(last_response.body)
end

The hooks to add in order to get ‘self documentation’ are pretty easy.

Get the straight poop

We want to get both the data we send out and the data we accept as raw JSON, so we can call some methods to do that right before we use the Rack::Test REST helpers:

document(:post, @hash, path)
post(path, @hash.to_json)

..and in the code that parses the response:

def parse_response
response = JSON.parse(last_response.body)
document(:response, response)
response
end

..and we add a basic ‘document’ method (@documentation_file will be dealt with in the next section):

def document(method, data, path=nil)
@documentation_file.puts("#{method.to_s.upcase} #{path}:n#{data.to_json}") if @documentation_file
end

Getting it on paper

We’re outputing the json info, but to where? @documentation_file is nil. We need to get that file open for writing. Let’s get in to the guts of our setup.rb.

We want to open the file one at the start of ever run, but keep it open for the whole run. So somewhere at the root level in setup.rb (not in a ‘do’ block, class or anything) we add:

$documentation_file = File.new(Rails.root.to_s+"/apidocs.txt", "w")

We’re using a global there. Globals are evil. Except this once.

And because we want to keep things clean, add one of these also to close the file after the Cucumber run is complete:

at_exit do
$documentation_file.close if $documentation_file
end

Change the @documentation_file in out #document method to $documentation_file.

How do we make it useful?

So we have the requests and responses written to a file now, but what good are they? We don’t know what they are really supposed to be doing. For that we need to get information about the Cucumber Scenarios so we can preface each request/response in our docs with the scenario info. We have to hijack some internals in Cucumber for this

We want to add something that will output both the feature name and the scenario name whenever they change so the parse/response blocks appear to have a rhyme and reason. Get inside the setup.rb file and add:

Before do |scenario|
if scenario.feature != $current_feature
$current_feature = scenario.feature
$documentation_file.puts("nnFeature: #{$current_feature.name}n")
end
$documentation_file.puts("Scenario: #{scenario.name}")
end

..that change will print the current feature name whenever it changes (once per .feature file) and the scenario name. More evil globals there.

Pretty-ify

Not all REST commands send a post body (like, say, a get or delete), so let’s not print those:

def document(method, data, path=nil)
str = "#{method.to_s.upcase}"
str << " #{path}" if path.present?
str << ":n#{data.to_json}" if data.present?
$documentation_file.puts(str) if $documentation_file
end

And let’s make the boundaries between features very prominent:

Before do |scenario|
if scenario.feature != $current_feature
$current_feature = scenario.feature
$documentation_file.puts("n#{'-'*80}nFeature: #{$current_feature.name}n")
...

Friendly-ify

You probably don’t want to have this regenerate your API docs every time you test, so let’s add a switch so it only regenerates if we say so. We’ll do this in the typical UNIX way, with an environment variable. Remember our even global file handle? Let’s alter it a bit:

$documentation_file = File.new(ENV["API_DOC_FILE"], "w") if ENV["API_DOC_FILE"].present?

Also touch our Before do |scenario| block:

Before do |scenario|
if $documentation_file
if scenario.feature != $current_feature
$current_feature = scenario.feature
$documentation_file.puts("n#{'-'*80}nFeature: #{$current_feature.name}n")
end
$documentation_file.puts("nScenario: #{scenario.name}")
end
end

Now, by default, the docs won’t be regenerated unless you add the “API_DOC_FILE” env variable like this:

API_DOC_FILE=./apidocs.txt rake features

Wrap up: what does that give us?

So what do we get from our trouble? let’s take our Cucumber example from the top of this document:

Feature: /widgets
Scenario: Get a list of all widgets
Given the following widget exists:
| name | job |
| Billy the Widget | Be a widget |
| Sally the Widget | Be a widget |
When I GET "/widgets"
Then I should get a successful response
ANd the response should include 2 widgets
And the response should include the Widget data for "Billy the Widget"
And the response should include the Widget data for "Sally the Widget"

Scenario: Create a new widget
Given I have a JSON request for “widget”
| name | Bob the Widget |
| job | Be a widget |
And I POST that JSON to “/widgets.json”
Then I should get a successful response
And the response should include the Widget data for “Bob the Widget”

With our changes we would have generated docs that look like this:
——————————————————————————–
Feature: /widgets

Scenario: Get a list of all widgets
GET /widgets.json
RESPONSE:
{
“widgets”:[
{
"name":"Billy the Widget",
"job":"Be a widget"
},
{
"name":"Sally the Widget",
"job":"Be a widget"
}
]
}

Scenario: Create a new widget
POST /widgets.json:
{
“widget”:{
“name”:”Bob the Widget”,
“job”:”Be a widget”
}
}
RESPONSE:
{
“widget”:{
“name”:”Billy the Widget”,
“job”:”Be a widget”
}
}

  • http://twitter.com/smsohan S M Sohan

    Cool! I am wondering if it is possible to make it a gem, please let me know if you will be OK with it. Or if you want, you can start and I can join at github.

    • Matt Hale

      Did anyone ever make a gem on this concept? Seems very useful.

      • xunker

        I never did, since then I’ve using Seahorse to describe APIs. However, if you find it useful I would have no problem with someone else taking up the idea and making it a gem :)

      • xunker

        I ended up using the concept once again at work, so I have made it in to a gem after all:

        http://rubygems.org/gems/goodall

  • guest

    Interesting stuff.  Probably makes good github-style docs.  Not sure this would fly in some enterprise companies that are driven by docs. 

  • artemk

    +1 for gem.

  • Gustavo Barron

    Somehow it seems a lot like iodocs from mashery, even better, you could make iodocs config generator from cucumber, that way you could have a interactive API documentation.