DEV Community

Yet Run
Yet Run

Posted on

A new gem to generate Swagger Doc in Rails

In reality, there are many gems that add parameter validation to Rails, and many add Swagger document generation, but there are very few that can support parameter validation, return value rendering, and Swagger document generation at the same time, and these capabilities are tightly combined. Almost gone.

Do you guys have these confusions during team development:

  • Lack of a more detailed API documentation, some do not even have documentation.
  • Documentation and implementation are often inconsistent. It is clearly stated in the document that there is this field, but it is found that there is no such field when it is actually called, or vice versa.
  • Documentation is difficult to implement specifications. For example, the fields of the parameter set and the fields of the return value need to be distinguished, and the fields of the return value in different scenarios should also be distinguished. Therefore, in reality, it is often only possible to throw out a large and comprehensive field table in the API document.

If so, you should really take a look at my gem: meta-api, it is born to solve these problems. We regard API documentation as a contract that both front-end and back-end developers need to abide by. Not only front-end call errors need to be reported, but back-end usage errors also need to be reported.

First glance

*Reminder: *Visit the repository web-frame-rails-example to see an example project. You can experience it now, or come back to it at any time.

Install

To use meta-api with Rails, follow the steps below.

The first step, add in Gemfile:

gem 'meta-api'
Enter fullscreen mode Exit fullscreen mode

Then execute bundle install.

Second step, Create a file in the config/initializers directory, such as meta_rails_plugin.rb, and write:

require 'meta/rails'

Meta::Rails.setup
Enter fullscreen mode Exit fullscreen mode

Step 3, Add under application_controller.rb:

class ApplicationController < ActionController::API
  # Import macro commands
  include Meta::Rails::Plugin

  # Handle parameter validation errors, the following is just an example, please write according to actual needs
  rescue_from Meta::Errors::ParameterInvalid do |e|
    render json: e.errors, status: :bad_request
  end
end
Enter fullscreen mode Exit fullscreen mode

After the above steps are completed, you can write macro commands in Rails to define parameters and return values.

Write an example

Let's write a UsersController as an example of using macro commands:

class UsersController < ApplicationController
   route '/users', :post do
     title 'Create User'
     params do
       param: user, required: true do
         param :name, type: 'string', description: 'name'
         param :age, type: 'integer', description: 'age'
       end
     end
     status 201 do
       expose: user do
         expose: id, type: 'integer', description: 'ID'
         expose :name, type: 'string', description: 'name'
         expose :age, type: 'integer', description: 'age'
       end
     end
   end
   def create
     user = User.create(params_on_schema[:user])
     render json_on_schema: { 'user' => user }, status: 201
   end
end
Enter fullscreen mode Exit fullscreen mode

The above first uses the route command to define a POST /users route, and provides a code block, all parameter definitions and return value definitions are in this code block. When all these definitions are in place, the actual execution will bring about two effects:

  1. params_on_schema: It returns the parameters parsed according to the definition, for example, if you pass such a parameter:

     {
       "user": {
         "name": "Jim",
         "age": "18",
         "foo": "foo"
       }
     }
    

    It will help you filter out unnecessary fields and do appropriate type conversion, so that the parameters actually obtained in the application become (via params_on_schema):

     {
       "user": {
         "name": "Jim",
         "age": 18
       }
     }
    

    This way you can safely pass it to the User.create! method without worrying about any errors.

  2. json_on_schema: The object passed through it will be parsed through the return value definition, because there are field filtering, type conversion and data verification, the backend can control which fields are returned to the front end, how to return them, and get reminder etc. Say your users table contains the following fields:

     id, name, age, password, created, updated
    

    By definition, it will only return the following fields:

     id, name, age
    

Generate Swagger documentation

Everything is centered on generating Swagger documentation.

If you want to generate documentation, follow my steps.

The first step is to figure out where to put your Swagger documents? For example, I create a new interface to return Swagger documents:

class SwaggerController < ApplicationController
   def index
     Rails.application.eager_load! # All controller constants need to be loaded early
     doc = Meta::Rails::Plugin.generate_swagger_doc(
       ApplicationController,
       info: {
         title: 'Web API Sample Project',
         version: 'current'
       },
       servers: [
         { url: 'http://localhost:9292', description: 'Web API Sample Project' }
       ]
     )
     render json: doc
   end
end
Enter fullscreen mode Exit fullscreen mode

The second step is to configure a route for it:

#config/routes.rb

get '/swagger_doc' => 'swagger#index'
Enter fullscreen mode Exit fullscreen mode

The third step is to start the application to view the effect of the Swagger document, enter in the browser:

http://localhost:3000/swagger_doc
Enter fullscreen mode Exit fullscreen mode

The Swagger documentation in JSON format comes into view.

If you are an old man using Swagger documentation, you should know what to do next. If you don't know, here's how to render a Swagger document:

  1. Add cross-domain support for the application:
# Gemfile
gem 'rack-cors'

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'openapi.yet.run'
resource "*", headers: :any, methods: :all
end
end
  1. If you are using Google Chrome browser, you need to do some extra settings to support cross domain of localhost domain name. Or you need to hang it under a normal domain name.

  2. Open http://openapi.yet.run/playground and enter http://localhost:3000/swagger_doc in the input box.

At this point, all the initial test experience is completed, the front-end can obtain a well-defined document, and the back-end is also strictly implemented according to this document. Back-end developers don't need to do extra work, it just defines the parameters, defines the return value, and then, everything is complete.

Respond to different fields in different occasions

This chapter provides a more adequate topic, how meta-api handles field distinctions in different situations. Different occasions, such as:

  1. Some fields are only used as parameters, and some fields are only used as return values.
  2. When used as a return value, some interfaces return fields a, b, c, while some interfaces return fields a, b, c, d.
  3. Same as 2 when used as parameters, some interfaces use fields a, b, c, and some interfaces use fields a, b, c, d.
  4. How to handle the semantic distinction between PUT and PATCH requests in HTTP.

Here meta-api plug-in provides the concept of entity, we can put fields into entity, and then refer to it in interface. Entities are only defined once, without any repeated definitions, and most importantly, the documentation and your definitions can always be consistent. Please read on!

Use entities to simplify the definition of parameters and return values

We summarize the parameters and return values ​​in the above example into one entity:

class UserEntity < Meta::Entity
   property :id, type: 'integer', description: 'ID', param: false
   property :name, type: 'string', description: 'name'
   property :age, type: 'integer', description: 'age'
end
Enter fullscreen mode Exit fullscreen mode

Then both parameter and return value definitions can refer to this entity:

route '/users', :post do
   params do
     param: user, required: true, ref: UserEntity
   end
   status 201 do
     expose :user, ref: UserEntity
   end
end
Enter fullscreen mode Exit fullscreen mode

Note that in the entity definition, the id attribute has a param: false option, which means that this field can only be used as a return value and not as a parameter. This is consistent with the previous definition.

In addition to restricting a field to be used only as a return value, it can also be restricted to be used only as a parameter. Add a password field as a parameter in UserEntity, which can be defined as follows:

class UserEntity < Meta::Entity
   # omit other fields
   property :password, type: 'string', description: 'password', render: false
end
Enter fullscreen mode Exit fullscreen mode

*In short, only write the entity once, and use it as a parameter and a return value at the same time. *

How to return different fields according to different scenarios

In the actual application of the interface, the field categories returned by different scenarios are often different, and those interface implementations that return the same fields regardless of the occasion are wrong. The different exit scenarios mentioned here, for example:

  1. The basic fields are returned on the list page, and more fields are returned on the details page.
  2. Ordinary users can only see part of the fields when viewing, and administrators can view all fields when viewing.

These can be distinguished by the scope mechanism provided by meta-api. I only use the list page interface as an example.

Suppose the /articles interface returns a list of articles, and each article only needs to return the title field; the /articles/:id interface needs to return article details, and each article needs to return the title, content fields . Defining two entities would be cumbersome and confusing, just define one entity and use the scope: option to differentiate:

class ArticleEntity < Meta::Entity
   property:title
   property: content, scope: 'full'
end
Enter fullscreen mode Exit fullscreen mode

At this time, the list page interface and detail page interface are implemented in the following way:

class ArticlesController < ApplicationController
   route '/articles', :get do
     status 200 do
       expose: articles, type: 'array', ref: ArticleEntity
     end
   end
   def index
     articles = Article.all
     render json_on_schema: { 'articles' => articles }
   end

   route '/articles/:id', :get do
     status 200 do
       expose: article, type: 'object', ref: ArticleEntity
     end
   end
   def show
     article = Article. find(params[:id])
     render json_on_schema: { 'article' => article }, scope: 'full'
   end
end
Enter fullscreen mode Exit fullscreen mode

The only change from the regular implementation is this line of code used when rendering data in the show method:

render json_on_schema: { 'article' => article }, scope: 'full'
Enter fullscreen mode Exit fullscreen mode

It passes an option scope: 'full', which tells the entity to also render the field marked as full by scope (that is, the content field).

In addition to passing specific options when calling, there is another trick here, using the locking technique provided by meta-api to lock the referenced entity on specific options.

Let's modify the definition and implementation of the show method in the above example, as follows:

class ArticlesController < ApplicationController
   route '/articles/:id', :get do
     status 200 do
       expose : article, type: 'object', ref: ArticleEntity. locked(scope: 'full')
     end
   end
   def show
     article = Article. find(params[:id])
     render json_on_schema: { 'article' => article }
   end
end
Enter fullscreen mode Exit fullscreen mode

There are two changes here:

  1. When referencing an entity, we do ArticleEntity.locked(scope: 'full') on the entity, which returns the new locked entity.
  2. When rendering data, the option scope: 'full' does not need to be passed.

Don't underestimate this small change. When we lock the entity when defining it, we actually define the scope of the entity at the interface design stage. My point is that design takes precedence over implementation, so I prefer the second option. In addition, locking will also affect the generation of documents. Entities in documents will only render the fields that are locked scope, and will not generate other fields that will not be returned. This is more friendly to the front end when viewing documents.

We add a new unused field inside ArticleEntity:

class ArticleEntity < Meta::Entity
   property:title
   property: content, scope: 'full'
   property :foo, scope: 'foo'
end
Enter fullscreen mode Exit fullscreen mode

Then ArticleEntity.locked(scope: 'full') will not only return the fields title and content when it is implemented, but also only the fields title and content will appear when the document is rendered.

**The idea of this section is: only need to write the entity once, and use it in different occasions. **This idea is also the idea of the next section.

Lock on parameters: distinguish different occasions on parameters

Like the return value, parameters also need to modify different fields according to different occasions. We define a parameter entity:

class ExampleEntity < Meta::Entity
   property:name
   property: age
   property: password, scope: 'master'
   property :created_at, scope: 'admin'
   property :updated_at, scope: 'admin'
end
Enter fullscreen mode Exit fullscreen mode

It is set with two scopes, and the fields that can be modified are different in the two occasions. When we implement, we can use locking technology when defining parameters:

class Admin::UsersController < ApplicationController
   route '/admin/users', :put do
     params do
       param :user, ref: ExampleEntity. locked(scope: 'admin')
     end
   end
   def update
   end
end

class UsersController < ApplicationController
   route '/user', :put do
     params do
       param :user, ref: ExampleEntity. locked(scope: 'master')
     end
   end
   def example
   end
end
Enter fullscreen mode Exit fullscreen mode

This is both responsive in implementation and documentation.

Lock on parameters: handle missing parameters

First declare an HTTP-related background. For missing parameters, HTTP provides two semantic methods:

  1. A PUT request needs to provide a complete entity, including all fields. If a field is missing, it will be treated as if the field passed a nil value.
  2. The PATCH request only provides the fields that need to be updated. If a field is missing, it will indicate that this field does not need to be updated.

What is involved here is how to deal with missing values in parameters. In other words, for entities:

class UserEntity < Meta::Entity
   property:name
   property: age
end
Enter fullscreen mode Exit fullscreen mode

If the user request only passes { "name": "Jim" }, what will call params_on_schema return? yes

{ "name": "Jim", "age": null }
Enter fullscreen mode Exit fullscreen mode

still

{ "name": "Jim" }
Enter fullscreen mode Exit fullscreen mode

Woolen cloth?

The difference between the two is that the former sets missing values to nil, while the latter discards missing values. Note that these two kinds of data are passed as parameters to the user.update! method, and their effects are different.

The way to control this effect is also by locking setting the discard_missing: option.

class UsersController < ApplicationController
   # PUT effect, by default, the parameter always returns the complete entity
   route '/users/:id', :put do
     params do
       param :user, ref: UserEntity # or UserEntity.locked(discard_missing: false), equivalent
     end
   end
   def replace
   end

   # PATCH effect, parameters discard unpassed fields
   route '/users/:id', :patch do
     params do
       param :ref: UserEntity. locked(discard_missing: true)
     end
   end
   def update
   end
end
Enter fullscreen mode Exit fullscreen mode

*Summary: You only need to write the entity once, and use it on different occasions. *

Dive into details

Finally, let's talk about some details about meta-api. It has basic parameter verification, default value and conversion logic, for detailed content, please refer to tutorial relevant part. If there is an unfriendly part in the document, you are welcome to submit an ISSUE improvement.

Defaults

You can provide default values for parameters (or fields within entities), like so:

class UserEntity < Meta::Entity
   property :age, type: 'integer', default: 18
end
Enter fullscreen mode Exit fullscreen mode

data verification

There are some built-in data validators such as:

class UserEntity < Meta::Entity
   property :birth_date, type: 'string', format: /\d\d\d\d-\d\d-\d\d/
end
Enter fullscreen mode Exit fullscreen mode

or custom:

class UserEntity < Meta::Entity
   property : birthday,
     type: 'string',
     validate: ->(value) { raise Meta::JsonSchema::ValidationError('The date format is incorrect') unless value =~ /\d\d\d\d-\d\d-\d\d/}
end
Enter fullscreen mode Exit fullscreen mode

enumeration

class UserEntity < Meta::Entity
   property :age, type: 'integer', allowable: [1,2,3,4,5,6] # only allow children under 6 years old
end
Enter fullscreen mode Exit fullscreen mode

Polymorphism

Really, it has the ability to handle polymorphism, as provided in the Swagger documentation. I don't want to explain it here, but please believe that it really has the ability to handle polymorphism.

Final remarks

Say nothing, my project address is currently at:

http://github.com/yetrun/web-frame

All comments and suggestions are useful to me.

If you encounter bugs, or want new features, please file ISSUE.

If this project is useful to you, please give it a star, it is much appreciated.

If you are interested in contributing to this project, a PR is welcome.

My hope is that plugins for Rails will be provided in a way that doesn't interfere with your existing projects and will help with documentation.

Top comments (0)