Generating documentation with Rspec Rails Swagger

Reading Time: 8 minutes

Keeping the documentation updated while the API has been developed is one of the most demanding tasks regardless of the language that you've selected for your backend.

There are many tools for managing and keeping your documentation updated like Slate, that allows you to write it in a simple way using Markdown or apipie-rails. These two provide a DSL to generate the documentation automatically. Rspec-rails-swagger also creates documentation automatically using rspec tests. In this tutorial, I'll show you how rspec-rails-swagger can help you with this task.

Preliminary Concepts

Rspec

From Wikipedia: RSpec is a 'Domain Specific Language' (DSL) testing tool written in Ruby to test Ruby code. It is a behavior-driven development (BDD) framework which is extensively used in the production applications.

Swagger

From the official site: Swagger is the world’s largest framework of API developer tools for the OpenAPI Specification(OAS), enabling development across the entire API lifecycle, from design and documentation, to test and deployment.

What is the OpenAPI Specification? At the heart of the above tools is the OpenAPI Specification (formerly called the Swagger Specification). The specification creates the RESTful contract for your API, detailing all of its resources and operations in a human and machine readable format for easy development, discovery, and integration.

Creating a new application

In this tutorial we will create a basic address book API.
rails new --api addressbook

Adding dependencies

We need to add the dependencies that we will use to create the tests. In this exercise we will use rspec-rails to develop the specs, factory_bot_rails to create database records in the tests scenarios, database_cleaner to ensure keeping a clean state during the tests, and finally rspec-rails-swagger to create the tests that will generate the documentation.

Gemfile

source 'https://rubygems.org'

git_source(:github) do |repo_name|
  repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?('/')
  "https://github.com/#{repo_name}.git"
end

gem 'rails', '~> 5.1.5'
gem 'sqlite3'
gem 'puma', '~> 3.7'

group :development, :test do
  gem 'byebug', platforms: %i[mri mingw x64_mingw]

  gem 'factory_bot_rails'
  gem 'rspec-rails', '~> 3.7.0'
  gem 'rspec-rails-swagger', '~> 0.1.4'
end

group :development do
  gem 'listen', '>= 3.0.5', ' 2.0.0'
end

group :test do
  gem 'database_cleaner'
end

gem 'tzinfo-data', platforms: %I[mingw mswin x64_mingw jruby]

Don’t forget to install the new dependencies.

bundle install

bundle install

Rspec

To install rspec we need to run the generator that adds the basic configuration.

rails generate rspec:install

rspec generate

You can test your config with the following command:

bundle exec rspec

run rspec

Factory bot

To complete the Factory bot setup, add the following code at the beginning of the document:

spec/rails_helper.rb

require 'factory_bot_rails'
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

You should have something like this:
spec/rails_helper.rb

require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
require 'factory_bot_rails'


Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

And add the following file:

spec/support/factory_bot.rb

# spec/support/factory_bot.rb

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

Database Cleaner

To get the database cleaner working you need to add the following configuration inside the RSpec.configure block:

spec/rails_helper.rb

  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end

  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end

Rspec Rails Swagger

Add the following configuration inside the RSpec.configure block:

spec/rails_helper.rb
config.backtrace_inclusion_patterns = [%r{app|spec}]

Add the following file, we will use it to add the swagger definitions:

spec/swagger_helper.rb

require 'rspec/rails/swagger'
require 'rails_helper'

RSpec.configure do |config|
  # Specify a root directory where the generated Swagger files will be saved.
  config.swagger_root = Rails.root.to_s + '/swagger'

  # Define one or more Swagger documents and global metadata for each.
  config.swagger_docs = {
    'v1/swagger.json' => {
      swagger: '2.0',
      info: {
        title: 'API V1',
        version: 'v1'
      },
      definitions: {

      }
    }
  }
end

Creating the contact migration

We will add a migration for the contacts table, where we will store name, phone, email and address.

rails g migration create_contacts

rails generate migration

XXX_create_contacts.rb

class CreateContacts < ActiveRecord::Migration[5.1]
  def change
    create_table :contacts do |t|
      t.string :name, null: false
      t.string :phone, null: false
      t.string :email
      t.string :address

      t.timestamps
    end
  end
end

Run the migrations.
rails db:migrate

rails db:migrate

Adding Contact Model and factory

Before start developing the endpoints we need to add the Contact model and the factory that we will use on the rspec tests.

app/models/contact.rb

class Contact < ApplicationRecord
  validates :name, :phone, presence: true
end

spec/factories/contacts.rb

FactoryBot.define do
  factory :contact do
    sequence :name do |n|
      "John Doe #{n}"
    end
    phone { rand(10**9..10**10) }

    sequence :email do |n|
      "john-doe#{n}@test.magmalabs.io"
    end

    sequence :address do |n|
      "Fake Address #{n}"
    end
  end
end

FactoryBot.create

Creating the contact index endpoint

For this first endpoint, we need to add the contact resource on the routes file and specify the index endpoint.

config/routes.rb
Rails.application.routes.draw do
  resources :contacts, as: :users, only: [:index]
end

We can start writing the index test. We need to define the contacts path and inside it, we will define the GET operation that calls the index method. In this case, we test the response code is 200 and the body returns 3 contact records.

spec/requests/contacts_request_spec.rb

require 'swagger_helper'

RSpec.describe 'Contacts', type: :request, capture_examples: true do
  path '/contacts' do
    get(summary: 'Get contacts') do
      consumes 'application/json'
      produces 'application/json'
      tags :contacts

      let!(:contacts) do
        3.times do
          create(:contact)
        end
      end

      response(200, description: 'Return all the available contacts') do
        it 'Return 3 contacts' do
          body = JSON(response.body)
          expect(body.count).to eq(3)
        end
      end
    end
  end
end

On the contacts controller we need to define the index method that is called when the request is performed.

app/controllers/contacts_controller.rb

class ContactsController < ApplicationController
  def index
    response = Contact.all

    render json: response, status: :ok
  end
end

Creating the contact create endpoint

To create the endpoint we need to specify the method on the contact resource.

config/routes.rb

Rails.application.routes.draw do
  resources :contacts, as: :users, only: %I[index create]
end

To create and to update endpoints, we will need to specify in the swagger helper file the data models based on the swagger specification. You can learn more about it on the project page.

spec/swagger_helper.rb

require 'rspec/rails/swagger'
require 'rails_helper'

RSpec.configure do |config|
  # Specify a root directory where the generated Swagger files will be saved.
  config.swagger_root = Rails.root.to_s + '/swagger'

  # Define one or more Swagger documents and global metadata for each.
  config.swagger_docs = {
    'v1/swagger.json' => {
      swagger: '2.0',
      info: {
        title: 'API V1',
        version: 'v1'
      },
      definitions: {
        createContact: {
          type: :object,
          properties: {
            data: {
              type: :object,
              required: %i[name phone email address],
              properties: {
                name: { type: :string, example: 'magmaLabs.io' },
                phone: { type: :string, example: '+52 1 667 317 9035' },
                email: { type: :string, example: 'hello@magmalabs.io' },
                address: { type: :string, example: 'Av. Constitución #2035. Colima, Colima, MX, 28017' }
              }
            }
          }
        }
      }
    }
  }
end

Once we have defined the model, we can proceed to set the POST method in the contacts test path. Unlike the index test, we need to add the parameter method to document the body of our request. In this case, we are only testing if the response returns 201. You can improve the test as much as you want.

spec/requests/contacts_request_spec.rb

require 'swagger_helper'

RSpec.describe 'Contacts', type: :request, capture_examples: true do
  path '/contacts' do
    get(summary: 'Get contacts') do
      ...
    end

    post(summary: 'Create a new contact') do
      consumes 'application/json'
      produces 'application/json'
      tags :contacts

      parameter :data,
                in: :body,
                required: true,
                schema: {
                  '$ref' => '#/definitions/createContact'
                }

      response(201, description: 'Contact created') do
        let(:data) do
          {
            data: {
              name: 'Magmalabs.io',
              phone: '+52 1 667 317 9035',
              email: 'hello@magmalabs.io',
              address: 'Av. Constitución #2035. Colima, Colima, MX, 28017'
            }
          }
        end
      end
    end
  end
end

Again we will have to define the logic for the method we are testing.

app/controllers/contacts_controller.rb

class ContactsController < ApplicationController
  ...

  def create
    response = Contact.create(create_contact_params)

    render json: response, status: :created
  end

  private

  def create_contact_params
    params.require(:data).permit(:name, :phone, :email, :address)
  end
end

Creating the contact show endpoint

As in previous cases we need to add the show method in the contact resource.

config/routes.rb

Rails.application.routes.draw do
  resources :contacts, as: :users, only: %I[index create show]
end

The test for the show method is very similar to the index with the only difference that you have to input the id of the contact you want to get.

spec/requests/contacts_request_spec.rb

require 'swagger_helper'

RSpec.describe 'Contacts', type: :request, capture_examples: true do
  path '/contacts' do
    ...
  end

  path '/contacts/{id}' do
    get(summary: 'Get Contact') do
      consumes 'application/json'
      produces 'application/json'
      tags :contacts

      parameter :id, in: :path, type: :integer, required: true, description: 'Contact ID'

      let(:contact_1) do
        create(:contact)
      end

      let(:contact_2) do
        create(:contact)
      end

      response(200, description: 'Return the selected contact') do
        let(:id) { contact_1.id }
      end

      response(404, description: 'Contact not found') do
        let(:id) { 999 }
      end
    end
  end
end

As in the last two examples, we need to add the required logic for the show endpoint.

app/controllers/contacts_controller.rb

class ContactsController < ApplicationController
  before_action :find_contact, only: [:show]

  ...

  def show
    render json: @contact, status: :ok
  end

  private

  …

  def find_contact
    @contact = Contact.where(id: params[:id]).first
    render :nothing, status: :not_found unless @contact
  end
end

Creating the contact update endpoint

config/routes.rb

Rails.application.routes.draw do
  resources :contacts, as: :users, only: %I[index create show update]
end

For this example, we will allow to update all the contact data, so we will use the same definition that we had for the create method, so we should change the definition name from createContact to contact.

spec/swagger_helper.rb

require 'rspec/rails/swagger'
require 'rails_helper'

RSpec.configure do |config|
  # Specify a root directory where the generated Swagger files will be saved.
  config.swagger_root = Rails.root.to_s + '/swagger'

  # Define one or more Swagger documents and global metadata for each.
  config.swagger_docs = {
    'v1/swagger.json' => {
      swagger: '2.0',
      info: {
        title: 'API V1',
        version: 'v1'
      },
      definitions: {
        contact: {
          type: :object,
          properties: {
            data: {
              type: :object,
              required: %i[name phone email address],
              properties: {
                name: { type: :string, example: 'magmaLabs.io' },
                phone: { type: :string, example: '+52 1 667 317 9035' },
                email: { type: :string, example: 'hello@magmalabs.io' },
                address: { type: :string, example: 'Av. Constitución #2035. Colima, Colima, MX, 28017' }
              }
            }
          }
        }
      }
    }
  }
end

The update test is inside the contact/{id} path because we need to specify which contact is being updated. This test is similar to the create test.

spec/requests/contacts_request_spec.rb

 spec/requests/contacts_request_spec.rb
require 'swagger_helper'

RSpec.describe 'Contacts', type: :request, capture_examples: true do
  path '/contacts' do
   ...
  end

  path '/contacts/{id}' do
    ...

    put(summary: 'Update Contact') do
      consumes 'application/json'
      produces 'application/json'
      tags :contacts

      parameter :id, in: :path, type: :integer, required: true, description: 'Contact ID'

      parameter :data,
                in: :body,
                required: true,
                schema: {
                  '$ref' => '#/definitions/contact'
                }

      let(:contact_1) do
        create(:contact)
      end

      response(200, description: 'Contact updated') do
        let(:id) { contact_1.id }

        let(:data) do
          {
            data: {
              name: 'Magmalabs.io',
              phone: '+52 1 667 317 9035',
              email: 'hello@magmalabs.io',
              address: 'Av. Constitución #2035. Colima, Colima, MX, 28017'
            }
          }
        end
      end

      response(404, description: 'Contact not found') do
        let(:id) { 999 }

        let(:data) do
          {
            data: {
              name: 'Magmalabs.io',
              phone: '+52 1 667 317 9035',
              email: 'hello@magmalabs.io',
              address: 'Av. Constitución #2035. Colima, Colima, MX, 28017'
            }
          }
        end
      end
    end
  end
end

As in previous cases, we should implement the required logic to cover the tests.

app/controllers/contacts_controller.rb

class ContactsController < ApplicationController
  before_action :find_contact, only: %I[show update]
  def index
   ...
  end

  def create
    response = Contact.create(contact_params)

    render json: response, status: :created
  end

  ...

  def update
    response = @contact.update(contact_params)

    render :nothing, status: :ok
  end

  private

  def contact_params
    params.require(:data).permit(:name, :phone, :email, :address)
  end

  ...
end

Generating the swagger docs

Finally, you can run your specs with the rspec command, but if you want to generate the swagger doc file, you ought to use rails swagger to generate a swagger.json file inside the swagger folder.

rails swagger

You can visualize this file in any client that allows swagger files. I used to use the Official Swagger Editor that allows you to load the file and check the structure in a more comfortable way.

swagger editor

The best way to keep your repetitive process up to date is to automate them. Using rspec rails swagger to document your APIs can save you a lot of time. Your documentation will always be updated and supported by tests that ensure your endpoints will work. If you want to check the project source code, you can visit the following repository.

0 Shares:
You May Also Like

Sprockets + Sinatra

Reading Time: 2 minutes Integrating Sprockets and Sinatra is easy because Sprockets is Rack-based. Let's do a simple example focusing on: Creating…