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.
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.
# 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.
<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.
# 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.
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.
inherit_from: .rubocop_todo.yml
# ...
Metrics/BlockLength:
Exclude:
- 'spec/**/*'
- 'app/controllers/swagger/**/*'
Now, we can start working on the User
payload.
# 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.
# 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.
# 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.
# 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.
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.