On one of our rails projects I am creating an api to allow mobile clients to access a web service. I need to provide documentation of this API to the developers of several different clients during its development. Normally I would prefer to let my API tests act as documentation of the expected API behavior but in this case I cannot give all of the client developers access to the rails project’s git repository and even the cleanest rspec specs might not be legible to developers without ruby experience.
I also don’t want to spend time manually updating documentation every time I change the API and don’t want to risk wasting other developers’ time by providing out of date or inaccurate documentation.
Instead I hacked together a rake task to covert my API controller specs into documentation using a custom rspec formatter. Now I can automatically update the documentation as part of our CI build process.
This example is available as https://gist.github.com/934902
First I wrote a rake task to generate my API documentation. This runs my API controller specs after adding some additional files to the ruby load path and requiring them to patch ActionController::TestCase::Behavior and load a custom rspec formatter.
require 'rubygems' require 'rspec/core/rake_task' namespace :project_name do namespace :api do desc 'Generate API request documentation from API specs' RSpec::Core::RakeTask.new('generate_docs') do |t| t.pattern = 'spec/controllers/api/**/*_spec.rb' t.rspec_opts = ["-Ilib/tasks/api_docs", "-rinstrument_specs", "-rapi_formatter", "--format APIFormatter"] end end end
I patched ActionController::TestCase::Behavior to capture the requests and responses from process (called by get, put, post, and delete) and store them in rspec’s example metadata.
require 'action_dispatch' require 'action_controller' module InstrumentSpecs module ::ActionController::TestCase::Behavior alias_method :process_without_logging, :process def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET') version, route = described_class.to_s.match(/::(Vd)::(S+)Controller/)[1..2].collect(&:downcase) curl = "curl " parameters.each {|key, value| curl << "-d "#{key}=#{CGI.escape(value)}" " } if parameters.present? curl << "-X #{http_method} " curl << "https://api.host.example/#{version}/#{route} " example.metadata[:curl] = curl response = process_without_logging(action, parameters, session, flash, http_method) example.metadata[:response] = "#{response.status} #{response.body}" response end end end
Finally I added a custom rspec formatter to output the requests and responses as markdown.
require 'rspec/core/formatters/base_formatter' class APIFormatter < RSpec::Core::Formatters::BaseFormatter def initialize(output) super(output) end def example_group_started(example_group) indented_message("#{example_group.description}n", example_group) end def example_group_finished(example_group) end def example_started(example) indented_message("#{example.description}n", example) end def example_passed(example) indented_message("#{example.metadata[:curl]}n") indented_message("#{example.metadata[:response]}nn") end def example_failed(example) end def example_pending(example) end private def indented_message(message, context = nil) if context.present? if context.respond_to?(:example_group) (context.example_group.ancestors.count + 1).times do output << '#' end else context.ancestors.count.times do output << '#' end end else output << ' ' end output << message end end
#Api::V1::AccountsController
###index
###without an API key
####behaves like an unauthenticated user
#####responds with success status
curl -X GET https://api.host.example/v1/accounts
200 {"error":{"type":"Unauthorized","message":"Authentication is required to access this resource"}}
...
Now I can generate a description of my API based on my specs complete with examples of hitting the API using curl and the expected response status and body. As long as I name my rspec assertions and contexts well this should be legible and always up to date since I can regenerate the docs and push them to a github wiki page every time my specs pass on a continuous integration server.