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'
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
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
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
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:
-
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. -
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
The second step is to configure a route for it:
#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:
- 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
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.
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:
- Some fields are only used as parameters, and some fields are only used as return values.
- When used as a return value, some interfaces return fields
a, b, c
, while some interfaces return fieldsa, b, c, d
. - Same as 2 when used as parameters, some interfaces use fields
a, b, c
, and some interfaces use fieldsa, b, c, d
. - How to handle the semantic distinction between
PUT
andPATCH
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
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
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
*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:
- The basic fields are returned on the list page, and more fields are returned on the details page.
- 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
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
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'
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
There are two changes here:
- When referencing an entity, we do
ArticleEntity.locked(scope: 'full')
on the entity, which returns the new locked entity. - 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
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
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
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:
- 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 anil
value. - 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
If the user request only passes { "name": "Jim" }
, what will call params_on_schema
return? yes
{ "name": "Jim", "age": null }
still
{ "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.
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 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
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
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
enumeration
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:
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)