A new gem to generate Swagger Doc in Rails
Tags: ruby, rails, swagger, opensource
Published: 2023-04-06 08:01:14

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](https://github.com/yetrun/web-frame), 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](https://github.com/yetrun/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:

```ruby
gem 'meta-api'
```

Then execute `bundle install`.

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

```ruby
require 'meta/rails'

Meta::Rails.setup
```

**Step 3,** Add under `application_controller.rb`:

```ruby
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
```

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:

```ruby
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
```

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:

     ```json
     {
       "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`):

     ```json
     {
       "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:

```ruby
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
```

The second step is to configure a route for it:

```ruby
#config/routes.rb

get '/swagger_doc' => 'swagger#index'
```

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
```

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:
>
> ```ruby
> # 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
> ```
>
> 2. 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.
>
> 3. 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:

```ruby
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
```

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

```ruby
route '/users', :post do
   params do
     param: user, required: true, ref: UserEntity
   end
   status 201 do
     expose :user, ref: UserEntity
   end
end
```

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:

```ruby
class UserEntity < Meta::Entity
   # omit other fields
   property :password, type: 'string', description: 'password', render: false
end
```

**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:

```ruby
class ArticleEntity < Meta::Entity
   property:title
   property: content, scope: 'full'
end
```

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

```ruby
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
```

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

```ruby
render json_on_schema: { 'article' => article }, scope: 'full'
```

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:

```ruby
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
```

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`:

```ruby
class ArticleEntity < Meta::Entity
   property:title
   property: content, scope: 'full'
   property :foo, scope: 'foo'
end
```

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:

```ruby
class ExampleEntity < Meta::Entity
   property:name
   property: age
   property: password, scope: 'master'
   property :created_at, scope: 'admin'
   property :updated_at, scope: 'admin'
end
```

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:

```ruby
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
```

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:

```ruby
class UserEntity < Meta::Entity
   property:name
   property: age
end
```

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

```json
{ "name": "Jim", "age": null }
```

still

```json
{ "name": "Jim" }
```

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.

```ruby
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
```

**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](https://github.com/yetrun/web-frame/blob/master/docs/%E6%95%99% E7%A8%8B.md) 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:

```ruby
class UserEntity < Meta::Entity
   property :age, type: 'integer', default: 18
end
```

### data verification

There are some built-in data validators such as:

```ruby
class UserEntity < Meta::Entity
   property :birth_date, type: 'string', format: /\d\d\d\d-\d\d-\d\d/
end
```

or custom:

```ruby
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
```

### enumeration

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

### 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](https://github.com/yetrun/web-frame/issues).

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.