[RUBY] What I was addicted to when trying to properly openAPI/Swagger documentation with Rails + Grape + Grape Swagger

This article is the 12th day article of CBcloud Advent Calendar 2020.

I will introduce what I was addicted to when trying to actually generate an OpenAPI document in business and share it with members with Rails + Grape + Grape Swagger.

First review: What is OpenAPI, Swagger?

OpenAPI It is described as follows in the OpenAPI repository. https://github.com/OAI/OpenAPI-Specification

The OpenAPI Specification (OAS) defines a standard, programming language-agnostic interface description for HTTP APIs, which allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code, additional documentation, or inspection of network traffic.

In other words, OpenAPI is a specification for describing the interface of HTTP API that does not depend on the programming language. Documents written according to the specification can be read and understood by humans, and can be analyzed to provide a nice UI.

Swagger The Swagger page says: https://swagger.io/about/

Swagger is a powerful yet easy-to-use suite of API developer tools for teams and individuals, enabling development across the entire API lifecycle, from design and documentation, to test and deployment. ~ Abbreviation ~ Swagger started out as a simple, open source specification for designing RESTful APIs in 2010. Open source tooling like the Swagger UI, Swagger Editor and the Swagger Codegen were also developed to better implement and visualize APIs defined in the specification. ~ Abbreviation ~ In 2015, the Swagger project was acquired by SmartBear Software. The Swagger Specification was donated to the Linux foundation and renamed the OpenAPI

In other words, Swagger is a project that provides a set of tools for API developers. And the specification for Swagger's API definition was donated to the Linux foundation and renamed to OpenAPI.

Scope of this article

Based on the above, what I'm trying to do (and I'm addicted to) in this article is a project that is developing with a Rails + Grape configuration, and uses Grape Swagger's DSL to generate JSON that conforms to the OpenAPI/Swagger specification. Is to do. Also, when I read the generated JSON and see it with a tool that makes it easy to see, I aim to display it as the intended configuration (and I am addicted to it)

Gem used

The gems used to generate the document are as follows.

gem 'grape' #A framework with a DSL for developing RESTful APIs
gem 'grape-swagger' #Document generation from Grape API
gem 'grape-entity' #Add response formatting tools to the Grape framework
gem 'grape-swagger-entity' # grape-Document generation from entity

Please refer to each repository for setting and usage. https://github.com/ruby-grape/grape https://github.com/ruby-grape/grape-swagger https://github.com/ruby-grape/grape-entity https://github.com/ruby-grape/grape-swagger-entity

I was addicted to

1. The params of the desc block and the params of the same level as the desc are different.

Validation is done in params on the same level as desc

When using Grape, you may write something like the following. This means that user_name is a required parameter, and if you actually empty the request body and make a request, it will return with a 400 error as user_name is missing.

class Users < Grape::API
  resources :params_in_same_layer do
    desc 'When params are written in the same hierarchy as desc'
    params do
      requires :user_name, type: String, documentation: { desc: 'User name', type: 'string' }
      optional :address, type: String, documentation: { desc: 'Delivery information', type: 'string' }
    end
    post do
      present hoge: 'fuga'
    end
  end
end

The params in the desc block do not validate

Next, when you try to document with Grape Swagger, the following description method will appear. If you make a request without a request body as before, you will get 201 instead of 400 error this time.

class Users < Grape::API
  resources :params_whitin_desc_block do
    desc 'When params are written only in the desc block' do
      params SimpleUserParamsEntity.documentation
    end
    post do
      present hoge: 'fuga'
    end
  end
end

class SimpleUserParamsEntity < Grape::Entity
  expose :user_name, documentation: { desc: 'Username (defined by entity)', type: 'string', required: true }
  expose :address, documentation: { desc: 'Address (defined by entity)', type: 'string' }
end

However, when viewed from the UI, it looks almost the same.

Let's compare the difference when looking at the above in Swagger UI. In both cases, it can be expressed that the user name is required. But be aware that the second endpoint isn't really required.

image.png image.png

If you write params in both, the documentation in desc will take precedence if the keys match, but the ones that do not match will be output respectively.

So what if you write in both? I also added the keys age and blood_type, which are defined only for each.

class Users < Grape::API
  resources :params_in_same_layer_and_desc_block do
    desc 'When params are written both in the desc block and in the same hierarchy of desc' do
      params SimpleUserParamsEntity.documentation
    end
    params do
      requires :user_name, type: String, documentation: { desc: 'User name', type: 'string' }
      optional :address, type: String, documentation: { desc: 'Delivery information', type: 'string' }
      optional :age, type: Integer, documentation: { desc: 'age', type: 'string' }
    end
    post do
      present hoge: 'fuga'
    end
  end
end

class SimpleUserParamsEntity < Grape::Entity
  expose :user_name, documentation: { desc: 'Username (defined by entity)', type: 'string', required: true }
  expose :address, documentation: { desc: 'Address (defined by entity)', type: 'string' }
  expose :blood_type, documentation: { desc: 'Blood type (defined by entity)', type: 'string' }
end

Looking at the Swagger UI, it looks like this. If the keys match, the documentation in the desc takes precedence, otherwise it seems to be output respectively.

image.png

2. I tried to display the request of complicated configuration properly in the UI, but in the end it depends on the behavior of the UI used

Let's see what happens to the UI with a complex parameter pattern. Also, prepare an endpoint written in the same hierarchy as when it is written in the desc block.

class Users < Grape::API
  resources :complex_params_in_same_layer do
    desc 'When params are written in the same hierarchy as desc'
    params do
      requires :user_name, type: Integer, documentation: { desc: 'User name', type: 'string' }
      optional :addresses, type: Array[JSON], documentation: { desc: 'Delivery information', type: 'array', collectionFormat: 'multi' } do 
        requires :name, type: String, documentation: { desc: 'Delivery name', type: 'string' }
        requires :address, type: String, documentation: { desc: 'Delivery address', type: 'string' }
        requires :tags, type: Array[JSON], documentation: { desc: 'tag', type: 'array', collectionFormat: 'multi' } do
          optional :name, type: String, documentation: { desc: 'Tag name', type: 'string'}
        end
      end
    end
    post do
      present hoge: 'fuga'
    end
  end

  resources :complex_params_in_desc_block do
    desc 'When params are written only in the desc block' do
      params ComplexUserParamsEntity.documentation
    end
    post do
      present hoge: 'fuga'
    end
  end
end

class ComplexUserParamsEntity < Grape::Entity
  class TagEntity < Grape::Entity
    expose :name, documentation: { desc: 'Tag name (defined by entity)', type: 'string' }
  end

  class AddressEntity < Grape::Entity
    expose :name, documentation: { desc: 'Delivery name (defined by entity)', type: 'string' }
    expose :address, documentation: { desc: 'Delivery address (defined by entity)', type: 'string' }
    expose :tags, documentation: { desc: 'Tag (defined by entity)', type: 'array', is_array: true }, using: TagEntity
  end

  expose :user_name, documentation: { desc: 'Username (defined by entity)', type: 'string' }
  expose :addresses, documentation: { desc: 'Delivery information (defined by entity)', type: 'array', is_array: true }, using: AddressEntity
end

Let's check it with Swagger UI. One is not displayed in the array, the other is displayed, but the hierarchical structure is difficult to understand.

image.png image.png

I will try to make it easier to see. Try adding param_type:'body' to all parameters.

class Users < Grape::API
  resources :complex_params_in_same_layer do
    desc 'When params are written in the same hierarchy as desc'
    params do
      requires :user_name, type: Integer, documentation: { desc: 'User name', type: 'string', param_type: 'body' }
#The following is omitted

Check again in the Swagger UI. Then, the one defined by Entity still loses the hierarchical structure. The way of writing Entity may be bad. And on the other hand, the hierarchy is easier to understand, but it doesn't reflect the explanation.

image.png image.png

So, I will try a UI that can interpret other OpenAPI. Click here to use. https://github.com/Redocly/redoc

Entity is still the same, but the explanation is a little better. And for those on the same level, the display is exactly what you want! !! (The explanation "Delivery information" attached to the key of the array is also displayed properly)

image.png

image.png

What you can see from this is that the actual display depends on the UI tool you use, so let's make sure that the tool you use will display it properly.

3. Workaround required to output example

Even if I specify example, it is not output, so I wondered why there were the following issues. Since it is open at the moment, it seems that it can be done by including the workaround posted in issues until it is resolved. https://github.com/ruby-grape/grape-swagger/issues/762

in conclusion

I still don't understand how to use the entity in the request parameter, so I would appreciate it if you could point out any mistakes.

Recommended Posts

What I was addicted to when trying to properly openAPI/Swagger documentation with Rails + Grape + Grape Swagger
What I was addicted to when implementing google authentication with rails
What I was addicted to when developing a Spring Boot application with VS Code
What I was addicted to when introducing the JNI library
What I fixed when updating to Spring Boot 1.5.12 ・ What I was addicted to
I was addicted to setting default_url_options with Rails devise introduction
What I was addicted to with the Redmine REST API
I was addicted to WSl when trying to build an android application development environment with Vue.js
Memorandum: What I was addicted to when I hit the accounting freee API
[Rails] I was addicted to the nginx settings when using Action Cable.
Problems I was addicted to when building the digdag environment with docker
I was addicted to doing onActivityResult () with DialogFragment
SpringSecurity I was addicted to trying to log in with a hashed password (solved)
A memo that I was addicted to when making batch processing with Spring Boot
What to do when you launch an application with rails
A story I was addicted to in Rails validation settings
The story I was addicted to when setting up STS
I was addicted to starting sbt
What I was addicted to when updating the PHP version of the development environment (Docker) from 7.2.11 to 7.4.x
A note when I was addicted to converting Ubuntu on WSL1 to WSL2
I was angry with proc_open (): fork failed when trying to composer update inside a Docker container
[Rails] How to solve ActiveSupport :: MessageVerifier :: InvalidSignature that I was addicted to when introducing twitter login [ActiveStorage]
[Rails] I want to add data to Params when transitioning with link_to
A story I was addicted to when testing the API using MockMVC
My.cnf configuration problem that I was addicted to when I was touching MySQL 8.0 like 5.7
I was addicted to the roll method
I was addicted to the Spring-Batch test
[Beginner] What I learned when trying to introduce bootstrap to the rails6 app without using a CDN [Asset pipeline]
I was addicted to using RXTX on Sierra
I was addicted to installing Ruby/Tk on MacOS
I want to play with Firestore from Rails
[Rails] I want to load CSS with webpacker
Webdrivers :: BrowserNotFound: Failed to find Chrome binary. When I was trying to test E2E with Docker + Rails for the first time, I got stuck in an error.
After installing'devise''bootstrap' of gemfile with rails, what to do when url is an error
When I tried to unit test with IntelliJ, I was told "java.lang.OutOfMemoryError: Java heap space"
When importing CSV with Rails, it was really easy to use the nkf command
A story I was addicted to when getting a key that was automatically tried on MyBatis
I was addicted to the NoSuchMethodError in Cloud Endpoints
I tried what I wanted to try with Stream softly.
I was addicted to the record of the associated model
I want to authenticate users to Rails with Devise + OmniAuth
What to do when rails creates a 〇〇 2.rb file
Addicted to the webpacker that comes standard with Rails 6
When I tried to scroll automatically with JScrollBar, the event handler was drawn only once.
A story I was addicted to before building a Ruby and Rails environment using Ubuntu (20.04.1 LTS)
When I personally developed with Rails, it was a painful story that Rails was hit very much
When I tried to run Azure Kinect DK with Docker, it was blocked by EULA
[Rails] What to do when the view collapses when a message is displayed with the errors method
A story I was addicted to with implicit type conversion of ActiveRecord during unit testing