Duetcode

Integrate Swagger UI

09 August 2020

In the previous section, we build the endpoint /apidocs that produces the swagger file, and this week we will integrate it into the Swagger UI. If you didn’t read the previous parts of the course, you can check the content list from the overview page.

Create GET swagger endpoint

In addition to /apidocs endpoint, we will create /swagger endpoint that gets the JSON file from /apidocs and shows with Swagger UI to visualize and interact with the API’s resources. (We don’t use api/v1 namespace with those endpoints since documentation pages might cover different versions of the API in the future.)

Let’s create the swagger_controller.rb file inside of the controllers folder and place the index action that renders swagger layout without any view file.

app/controllers/swagger_controller.rb
# frozen_string_literal: true

class SwaggerController < ActionController::Base
  def index
    render html: nil, layout: 'swagger'
  end
end

Then we can create the layout file for the index action we defined.

app/views/layouts/swagger.html.erb
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Swagger UI</title>
    <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3.31.1/swagger-ui.css">
    <style>
      html
      {
        box-sizing: border-box;
        overflow: -moz-scrollbars-vertical;
        overflow-y: scroll;
      }

      *,
      *:before,
      *:after
      {
        box-sizing: inherit;
      }

      body
      {
        margin:0;
        background: #fafafa;
      }
    </style>
  </head>

  <body>
    <div id="swagger-ui"></div>

    <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3.31.1/swagger-ui-bundle.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3.31.1/swagger-ui-standalone-preset.js"></script>
    <script>
    window.onload = function() {

      // Begin Swagger UI call region
      const ui = SwaggerUIBundle({
        "dom_id": "#swagger-ui",
        deepLinking: true,
        presets: [
          SwaggerUIBundle.presets.apis,
          SwaggerUIStandalonePreset
        ],
        plugins: [
          SwaggerUIBundle.plugins.DownloadUrl
        ],
        layout: "StandaloneLayout",
        validatorUrl: "https://validator.swagger.io/validator",
        url: window.location.origin + "/apidocs",
      })

      // End Swagger UI call region
      window.ui = ui
    }
  </script>
  </body>
</html>

I got the HTML template from petstore.swagger.io address but changed the asset URLs to use jsdelivr.com as a free CDN. You can use something else as a CDN, but the main point here is we don’t want to deal with the assets just because of the swagger UI.

Lastly, we need to add the swagger resource to the routes.

config/routes.rb
# frozen_string_literal: true

Rails.application.routes.draw do
  # ...

  resources :swagger, only: [:index]
end

If you enter the http://localhost:3000/swagger address, you must be able to see the Swagger UI that takes the JSON file from /apidocs endpoint and provides the visual environment to test API endpoints and resources.

You will see the user creation endpoint in the Swagger UI, but we didn’t document the possible responses and response payloads for the endpoint. Let’s try to complete the missing parts with adding 201 (created) and 422 (unprocessable entity) responses to the UsersController class in the swagger folder.

app/controllers/swagger/controllers/users_controller.rb
class Swagger::Controllers::UsersController
  include Swagger::Blocks

  swagger_path '/users' do
    operation :post do
      # ...

      parameter do
        # ...
      end

      response 201 do
        key :description, 'User created'
        schema do
          property :data do
            key :'$ref', :User
          end

          property :meta do
            key :'$ref', :Meta
          end
        end
      end

      response 422 do
        key :description, 'Unprocessable Entity'
        schema do
          key :'$ref', :Error
        end
      end
    end
  end
end

We referenced to User, Meta, and Error classes to define the structure of the response payloads.

With the last change, rubocop warns us about too long blocks for the swagger classes, but since it’s the nature of the swagger-blocks DSL, we don’t need to maintain this rule in swagger related classes. You can disable the swagger folder from this rule as follows.

.rubocop.yml
inherit_from: .rubocop_todo.yml

# ...

Metrics/BlockLength:
  Exclude:
    - 'spec/**/*'
    - 'app/controllers/swagger/**/*'

Now, we can start working on the User payload.

app/controllers/swagger/models/user.rb
# frozen_string_literal: true

class Swagger::Models::User
  include Swagger::Blocks

  swagger_schema :User do
    key :type, :object
    key :required, %i[id email created_at updated_at]

    property :id do
      key :type, :integer
      key :format, :int64
    end

    property :email do
      key :type, :string
    end

    property :created_at do
      key :type, :string
      key :format, 'date-time'
    end

    property :updated_at do
      key :type, :string
      key :format, 'date-time'
    end
  end
end

We also need to create the Meta model that we will also use for other API endpoints.

app/controllers/swagger/models/meta.rb
# frozen_string_literal: true

class Swagger::Models::Meta
  include Swagger::Blocks

  swagger_schema :Meta do
    key :type, :object
    key :required, %i[resource count]

    property :resource do
      key :type, :string
    end

    property :count do
      key :type, :integer
    end
  end
end

Then, we can also define the Error payload for the swagger documentation.

app/controllers/swagger/models/error.rb
# frozen_string_literal: true

class Swagger::Models::Error
  include Swagger::Blocks

  swagger_schema :Error do
    key :required, [:errors]
    property :errors do
      key :type, :object

      property :field_name_one do
        key :type, :array

        items do
          key :type, :string
        end
      end

      property :field_name_two do
        key :type, :array

        items do
          key :type, :string
        end
      end
    end
  end
end

field_name_one and field_name_two refers to the attributes of the model. Let’s say for the User model email and password might replace the fields of the error keys.

We will also add those classes to the SWAGGERED_CLASSES in ApidocsController class.

app/controllers/apidocs_controller.rb
# frozen_string_literal: true

class ApidocsController < ActionController::Base
  # ...

  # A list of all classes that have swagger_* declarations.
  SWAGGERED_CLASSES = [
    Swagger::Controllers::UsersController,
    Swagger::Models::Error,
    Swagger::Models::Meta,
    Swagger::Models::User,
    Swagger::Models::UserInput,
    self
  ].freeze

  # ...
end

If you enter the http://localhost:3000/swagger address again, you must be able to see also the possible responses with payloads. Finally, the API documentation with Swagger UI is up and running.

Bookmarker API - Swagger UI

Summary

In this chapter, we completed the integration of Swagger UI and missing documentation classes that we use with swagger-blocks. You can find the source code of bookmarker application on github. You can also find the previous chapter here.

Thank you for reading! If you want to be notified when I publish a new chapter, you can follow me on twitter.