一套简洁的 DSL,用于为 Rails 应用生成 OpenAPI Specification 3 (OAS3, 旧称「Swagger3」) 标准的 JSON 文档。
(你还可以使用 Swagger-UI 3.2.0 以上版本来可视化所生成的文档。)
这里是栈刀 = ▽ =
如果你在寻找能清晰书写 OAS API 文档的 DSL 工具,俺这个还挺不错的 ~
你还可以复用其所产出来写一些扩展,比如参数自动校验什么的(我有写哦)。
有什么想法敬请 PR,谢过!
另外,走过路过不妨来个 star?
另外,如果对其行为表现有任何疑惑,请先阅读测试代码,这其中已表明我的大多数考量。
可一读:api DSL 以及 schema Obj。
- 关于 OAS (OpenAPI Specification)
- 安装
- 配置
- DSL 介绍及用例
- 执行文档生成
- 使用 Swagger-UI 可视化所生成的文档
- 技巧
- 问题集
- 有关
OpenApi.docs
和OpenApi.routes_index
有关 OAS3 的所有内容请看 OpenAPI Specification
你也可以看这份文档做初步的了解 swagger.io
建议你应该至少了解 OAS3 的基本结构,比如说 component(组件)—— 这能帮助你进一步减少书写文档 DSL 的代码(如果其中有很多可复用的数据结构的话)。
选一行添加到 Gemfile:
gem 'zero-rails_openapi'
# or
gem 'zero-rails_openapi', github: 'zhandao/zero-rails_openapi'
命令行执行:
$ bundle
新建一个 initializer, 用来配置 ZRO 并定义你的文档。
这是一个简单的示例:
# config/initializers/open_api.rb
require 'open_api'
OpenApi::Config.tap do |c|
# [REQUIRED] The output location where .json doc file will be written to.
c.file_output_path = 'public/open_api'
c.open_api_docs = {
# 对文档 `homepage` 进行定义
homepage: {
# [REQUIRED] ZRO will scan all the descendants of base_doc_classes, then generate their docs.
base_doc_classes: [Api::V1::BaseController],
# [REQUIRED] OAS Info Object: The section contains API information.
info: {
# [REQUIRED] The title of the application.
title: 'Homepage APIs',
# Description of the application.
description: 'API documentation of Rails Application. <br/>' \
'Optional multiline or single-line Markdown-formatted description ' \
'in [CommonMark](http://spec.commonmark.org/) or `HTML`.',
# [REQUIRED] The version of the OpenAPI document
# (which is distinct from the OAS version or the API implementation version).
version: '1.0.0'
}
}
}
end
除了直接使用 Hash,你还可以使用 DSL 来定义文档的基本信息:
# config/initializers/open_api.rb
require 'open_api'
OpenApi::Config.tap do |c|
c.file_output_path = 'public/open_api'
c.instance_eval do
open_api :homepage_api, base_doc_classes: [ApiDoc]
info version: '1.0.0', title: 'Homepage APIs'
end
end
更多更详尽的配置和文档信息定义示例: open_api.rb 所有你可以配置的项目: config.rb 所有你可以使用的文档信息 DSL: config_dsl.rb
# app/controllers/api/api_controller.rb
class ApiController < ActionController::API
include OpenApi::DSL
end
一个最简单的实例:
class Api::ExamplesController < ApiController
api :index, 'GET list' do
query :page, Integer#, desc: 'page, greater than 1', range: { ge: 1 }, dft: 1
query :rows, Integer#, desc: 'per page', range: { ge: 1 }, default: 10
end
end
更多更详细的实例: goods_doc.rb、 examples_controller.rb,以及 这里。
基本的 DSL (source code)
# method signature
route_base(path)
# usage
route_base 'api/v1/examples'
其默认设定为 controller_path
。
这个技巧 展示如何使用 route_base
来让你将 DSL 写在他处(与控制器分离),来简化你的控制器。
# method signature
doc_tag(name: nil, desc: '', external_doc_url: nil)
# usage
doc_tag name: 'ExampleTagName', desc: "ExamplesController's APIs"
该方法可以设置当前类中声明的 API 的 Tag (Tag 是一个 OpenApi Object节点)。
Tag 的名字默认为 controller_name,除了名字,还可以设置可选参数 desc 和 external_doc_url。
# method signature
components(&block)
# usage
components do
# DSL for defining components
schema :DogSchema => [ { id: Integer, name: String }, dft: { id: 1, name: 'pet' } ]
query! :UidQuery => [ :uid, String, desc: 'uid' ]
resp :BadRqResp => [ 'bad request', :json ]
end
# to use component
api :action, 'summary' do
query :doge, :DogSchema # to use a Schema component
param_ref :UidQuery # to use a Parameter component
response_ref :BadRqResp # to use a Response component
end
Component 用以简化你的 DSL 代码 (即通过 *_ref
形式的方法,来引用已定义的 Component 对象)。
每个 RefObj 都是通过 component key 来关联指定的 component。 我们建议 component key 规范为驼峰命名法,且必须是 Symbol。
顾名思义,此方法用于 DRY。
# method signature
api_dry(action = :all, desc = '', &block)
# usage
api_dry :all, 'common response' # block ...
api_dry :index # block ...
api_dry [:index, :show] do
query! #...
end
如你所觉,传给该方法的块,将会 eval 到指定的 API 的开头。
定义指定 API (或者说是一个 controller action).
# method signature
api(action, summary = '', http: nil, skip: [ ], use: [ ], &block)
# usage
api :index, '(SUMMARY) this api blah blah ...', # block ...
use
和 skip
: 指定使用或者跳过在 api_dry
中声明的参数。
api :show, 'summary', use: [:id] # 将会从 dry 块中声明的参数中挑出 id 这个参数用于 API :show
These following methods in the block describe the specified API action: description, valid?, parameters, request body, responses, securities, servers.
(Here corresponds to OAS Operation Object)
this_api_is_expired!
this_api_is_unused!
this_api_is_under_repair!
# method signature
this_api_is_invalid!(explain = '')
# usage
this_api_is_invalid! 'this api is expired!'
Then deprecated
of this API will be set to true.
# method signature
desc(desc, param_descs = { })
# usage
desc 'current API\'s description',
id: 'desc of the parameter :id',
email: 'desc of the parameter :email'
You can of course describe the input in it's DSL method (like query! :done ...
, this line),
but that will make it long and ugly. We recommend that unite descriptions in this place.
In addition, when you want to dry the same parameters (each with a different description), it will be of great use.
(3) param
family methods (OAS - Parameter Object)
Define the parameters for the API (action).
param
param_ref # for reuse component,
# it links sepcified RefObjs (by component keys) to current parameters.
header, path, query, cookie # will pass specified parameter location to `param`
header!, path!, query!, cookie! # bang method of above methods
do_* by: { parameter_definations } # batch definition parameters, such as do_path, do_query
order # order parameters by names array you passed
examples # define examples of parameters
The bang method (which's name is end of a exclamation point !
) means this param is required, so without !
means optional.
THE SAME BELOW.
# `param_type` just is the location of parameter, like: query, path
# `schema_type` is the type of parameter, like: String, Integer (must be a constant)
# For more explanation, please click the link below ↓↓↓
# method signature
param(param_type, param_name, schema_type, is_required, schema_info = { })
# usage
param :query, :page, Integer, :req, range: { gt: 0, le: 5 }, desc: 'page'
# method signature
param_ref(component_key, *component_keys) # should pass at least 1 key
# usage
param_ref :IdPath
param_ref :IdPath, :NameQuery, :TokenHeader
### method signature
header(param_name, schema_type = nil, **schema_info)
header!(param_name, schema_type = nil, **schema_info)
query!(param_name, schema_type = nil, **schema_info)
# ...
### usage
header! 'Token', String
query! :readed, Boolean, must_be: true, default: false
# The same effect as above, but not simple
param :query, :readed, Boolean, :req, must_be: true, default: false
#
# When schema_type is a Object
# (describe by hash, key means prop's name, value means prop's schema_type)
query :good, { name: String, price: Float, spec: { size: String, weight: Integer } }, desc: 'good info'
# Or you can use `type:` to sign the schema_type, maybe this is clearer for describing object
query :good, type: { name: String, price: Float, spec: { size: String, weight: Integer } }, desc: 'good info'
#
query :good_name, type: String # It's also OK, but some superfluous
query :good_name, String # recommended
# About Combined Schema (`one_of` ..), see the link below.
# method signature
do_query(by:)
# usage
do_query by: {
search_type: String,
search_val: String,
export!: Boolean
}
# The same effect as above, but a little bit repetitive
query :search_type, String
query :search_val, String
query! :export, Boolean
# method signature
# `exp_params` (select_example_by): choose the example fields.
examples(exp_params = :all, examples_hash)
# usage
# it defines 2 examples by using parameter :id and :name
# if pass :all to `exp_params`, keys will be all the parameter's names.
examples [:id, :name], {
:right_input => [ 1, 'user'], # == { id: 1, name: 'user' }
:wrong_input => [ -1, '' ]
}
This trick show you how to define combined schema (by using one_of
..)
(4) request_body
family methods (OAS - Request Body Object)
OpenAPI 3.0 uses the requestBody keyword to distinguish the payload from parameters.
request_body
body_ref # for reuse component,
# it links sepcified RefObjs (by component keys) to current body.
body, body! # alias of request_body
form, form! # define a multipart/form-data body
data # define [a] property in the form-data body
file, file! # define a File media-type body
Bang methods(!) means the specified media-type body is required.
# method signature
request_body(required, media_type, data: { }, **options)
# usage
# (1) `data` contains all the attributes required by this request body.
# (2) `param_name!` means it is required, otherwise without '!' means optional.
request_body :opt, :form, data: { id!: Integer, name: { type: String, desc: 'name' } }, desc: 'form-data'
# method signature
body_ref(component_key)
# usage
body_ref :UpdateDogeBody
# method signature
body!(media_type, data: { }, **options)
# usage
body :json
# method implement
def form data:, **options
body :form, data: data, **options
end
# usage
form! data: {
name: String,
password: String,
password_confirmation: String
}
# advance usage
form data: {
:name! => { type: String, desc: 'user name' },
:password! => { type: String, pattern: /[0-9]{6,10}/, desc: 'password' },
# optional
:remarks => { type: String, desc: 'remarks' },
}, exp_params: %i[ name password ],
examples: { # ↓ ↓
:right_input => [ 'user1', '123456' ],
:wrong_input => [ 'user2', 'abc' ]
},
desc: 'for creating a user'
# method implement
def data name, type = nil, schema_info = { }
schema_info[:type] = type if type.present?
form data: { name => schema_info }
end
# usage: please look at the 4th point below
# about `file`
def file! media_type, data: { type: File }, **options
body! media_type, data: data, **options
end
media_type
: we provide some mapping from symbols to real media-types.schema_info
: as above (see param).exp_params
andexamples
: for the above example, the following has the same effect:examples: { :right_input => { name: 'user1', password: '123456' }, :wrong_input => { name: 'user2', password: 'abc' } }
- [IMPORTANT] Each request bodies you declared will FUSION together.
(1) Media-Types will be merged torequestBody["content"]
(2) The same media-types will fusion, but not merge:form data: { }, desc: 'desc' body :json, data: { }, desc: 'desc' # will generate: "content": { "multipart/form-data": { }, "application/json": { } }
(So that you can writeform
separately, and makedata
method possible.)data :param_a!, String data :param_b, Integer # or same as: form data: { :param_a! => String } form data: { :param_b => Integer } # will generate: { "param_a": { "type": "string" }, "param_b": { "type": "integer" } } (call it X) # therefore: # "content": { "multipart/form-data": # { "schema": { "type": "object", "properties": { X }, "required": [ "param_a" ] } # }
(5) response
family methods (OAS - Response Object)
Define the responses for the API (action).
response # aliases: `resp` and `error`
response_ref
# method signature
response(code, desc, media_type = nil, data: { }, type: nil)
# usage
resp 200, 'json response', :json, data: { name: 'test' }
response 200, 'query result', :pdf, type: File
# same as:
response 200, 'query result', :pdf, data: File
# method signature
response_ref(code_compkey_hash)
# usage
response_ref 700 => :AResp, 800 => :BResp
practice: Automatically generate responses based on the agreed error class. AutoGenDoc
First of all, please make sure that you have read one of the following documents: OpenApi Auth or securitySchemeObject
Use these DSL in your initializer or components
block:
security_scheme # alias `auth_scheme`
base_auth # will call `security_scheme`
bearer_auth # will call `security_scheme`
api_key # will call `security_scheme`
It's very simple to use (if you understand the above document)
# method signature
security_scheme(scheme_name, other_info)
# usage
security_scheme :BasicAuth, { type: 'http', scheme: 'basic', desc: 'basic auth' }
# method signature
base_auth(scheme_name, other_info = { })
bearer_auth(scheme_name, format = 'JWT', other_info = { })
api_key(scheme_name, field:, in:, **other_info)
# usage
base_auth :BasicAuth, desc: 'basic auth' # the same effect as ↑↑↑
bearer_auth :Token
api_key :ApiKeyAuth, field: 'X-API-Key', in: 'header', desc: 'pass api key to header'
# In initializer
# Global effectiveness
global_security_require
global_security # alias
global_auth # alias
# In `api`'s block
# Only valid for the current controller
security_require
security # alias
auth # alias
need_auth # alias
Name is different, signature and usage is similar.
# method signature
security_require(scheme_name, scopes: [ ])
# usage
global_auth :Token
need_auth :Token
auth :OAuth, scopes: %w[ read_example admin ]
# method signature
server(url, desc: '')
# usage
server 'http://localhost:3000', desc: 'local'
DSL methods inside components's block (code source)
(Here corresponds to OAS Components Object)
Inside components
's block,
you can use the same DSL as [DSL methods inside api
and api_dry
's block].
But there are two differences:
(1) Each method needs to pass one more parameter component_key
(in the first parameter position),
this will be used as the reference name for the component.
query! :UidQuery, :uid, String
This writing is feasible but not recommended, because component's key and parameter's name seem easy to confuse. The recommended writing is:
query! :UidQuery => [:uid, String]
(2) You can use schema
to define a Schema Component.
# method signature
schema(component_key, type = nil, **schema_info)
# usage
schema :Dog => [ String, desc: 'dogee' ] # <= schema_type is `String`
# advance usage
schema :Dog => [
{
id!: Integer,
name: { type: String, must_be: 'name', desc: 'name' }
}, # <= this hash is schema type[1]
dft: { id: 1, name: 'pet' },
desc: 'dogee'
]
# or (unrecommended)
schema :Dog, { id!: Integer, name: String }, dft: { id: 1, name: 'pet' }, desc: 'dogee'
#
# pass a ActiveRecord class constant as `component_key`,
# it will automatically read the db schema to generate the component.
schema User # easy! And the component_key will be :User
[1] see: Type
Use OpenApi.write_docs
:
# initializer
OpenApi.write_docs generate_files: !Rails.env.production?
# or run directly in console
OpenApi.write_docs # will generate json doc files
Then the JSON files will be written to the directories you set. (Each API a file.)
Download Swagger UI (version >= 2.3.0 support the OAS3) to your project, change the default JSON file path(url) in index.html. In order to use it, you may have to enable CORS, see
Does your documentation take too many lines?
Do you want to separate documentation from business controller to simplify both?
Very easy! Just follow
# config/initializers/open_api.rb
# in your configuration
base_doc_classes: [ApiDoc]
# app/api_doc/api_doc.rb
require 'open_api/dsl'
class ApiDoc < Object
include OpenApi::DSL
end
# app/api_doc/v1/examples_doc.rb
class V1::ExamplesDoc < ApiDoc
route_base 'api/v1/examples'
api :index do
# ...
end
end
Notes: file name ends in _doc.rb
by default, but you can change via Config.doc_location
(it should be file paths, defaults to ./app/**/*_doc.rb
).
Method api_dry
is for DRY but its scope is limited to the current controller.
I have no idea of best practices, But you can look at this file.
The implementation of the file is: do api_dry
when inherits the base controller inside inherited
method.
You can use sort
to specify the order of parameters.
desc 'api desc',
search_type!: 'search field, allows:<br/>'
query :search_type, String, enum: %w[name creator category price]
# or
query :search_type, String, desc!: 'search field, allows:<br/>',
enum: %w[name creator category price]
Notice !
use (search_type!
, desc!
), it tells ZRO to append
information that analyzed from definitions (enum, must_be ..) to description automatically.
Any one of above will generate:
search field, allows:
1/ name
2/ creator,
3/ category
4/ price
You can also use Hash to define enum
:
query :view, String, desc: 'allows values<br/>', enum!: {
'all goods (default)': :all,
'only online': :online,
'only offline': :offline,
'expensive goods': :get,
'cheap goods': :borrow,
}
Read this file to learn more.
Pass skip: []
and use: []
to api
like following code:
api :index, 'desc', skip: [ :Token ]
Look at this file to learn more.
Use method load_schema
in api_dry
.
See this file for uasge information.
query :combination, one_of: [ :GoodSchema, String, { type: Integer, desc: 'integer input' } ]
form data: {
:combination_in_form => { any_of: [ Integer, String ] }
}
schema :PetSchema => [ not: [ Integer, Boolean ] ]
-
You wrote document of the current API, but not find in the generated json file?
Check your routing settings. -
Report error when require
routes.rb
?*- Run
rails routes
. - Copy the output to a file, for example
config/routes.txt
. Ignore the fileconfig/routes.txt
. - Put
c.rails_routes_file = 'config/routes.txt'
to your ZRO config.
- Run
After OpenApi.write_docs
, the above two module variables will be generated.
OpenApi.docs
: A Hash with API names as keys, and documents of each APIs as values.
documents are instances of ActiveSupport::HashWithIndifferentAccess.
OpenApi.routes_index
: Inverted index of controller path to API name mappings.
Like: { 'api/v1/examples' => :homepage_api }
It's useful when you want to look up a document based on a controller and do something.
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the Zero-OpenApi project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.