ZRO: OpenApi 3 JSON-Doc Generator for Rails
Concise DSL for generating OpenAPI Specification 3 (OAS3, formerly Swagger3) JSON documentation for Rails application.
class Api::ExamplesController < ApiController
api :update, 'POST update some thing' do
path :id, Integer
query :token, String, desc: 'api token', length: 16
form data: { phone: String }
end
end
Contributing
Hi, here is ZhanDao = ▽ =
It may be a very useful tool if you want to write API document clearly.
I'm looking forward to your issue and PR!
(Test cases are rich, like: api DSL and schema Obj)
Table of Contents
About OAS
Everything about OAS3 is on OpenAPI Specification
You can getting started from swagger.io
I suggest you should understand the basic structure of OAS3 at least.
such as component (can help you reuse DSL code, when your apis are used with the
same data structure).
Installation
Add this line to your Rails's Gemfile:
gem 'zero-rails_openapi'
gem 'zero-rails_openapi', github: 'zhandao/zero-rails_openapi'
And then execute:
$ bundle
Configure
Create an initializer, configure ZRO and define your OpenApi documents.
This is the simplest example:
require 'open_api'
OpenApi::Config.class_eval do
self.file_output_path = 'public/open_api'
open_api :doc_name, base_doc_classes: [ApiDoc]
info version: '1.0.0', title: 'Homepage APIs'
end
Part 1: configs of this gem
file_output_path
(required): The location where .json doc file will be output.default_run_dry
: defaults to run dry blocks even if the dry
method is not called in the (Basic) DSL block. defaults to false
.doc_location
: give regular expressions for file or folder paths. Dir[doc_location]
will be require
before document generates.
this option is only for not writing spec in controllers.rails_routes_file
: give a txt's file path (which's content is the copy of rails routes
's output). This will speed up document generation.model_base
: The parent class of models in your application. This option is for auto loading schema from database.file_format
Part 2: config (DSL) for generating OpenApi info
open_api doc_name, base_doc_classes: []
info version:, title:, desc: '', **addition
See all the DSLs: config_dsl.rb
DSL Usage
There are two kinds of DSL for this gem: basic and inside basic.
- Basic DSLs are class methods which is for declaring your APIs, components, and spec code DRYing ...
- DSLs written inside the block of Basic DSLs, is for declaring the parameters, responses (and so on) of the specified API and component.
First of all, include OpenApi::DSL
in your base class (which is for writing spec):
For example:
class ApiController < ActionController::API
include OpenApi::DSL
end
DSL Usage Example
Here is the simplest usage:
class Api::ExamplesController < ApiController
api :index, 'GET list' do
query :page, Integer
query :rows, Integer
end
end
Basic DSL
source code
(1) route_base
[required if you're not writing DSL in controller]
route_base path
route_base 'api/v1/examples'
Usage: write the DSL somewhere else to simplify the current controller.
(2) doc_tag
[optional]
doc_tag name: nil, **tag_info
doc_tag name: 'ExampleTagName', description: "ExamplesController's APIs"
This method allows you to set the Tag (which is a node of OpenApi Object)
of all the APIs in the class.
Tag's name defaults to controller_name.
(3) components
[optional]
components(&block)
components do
schema :DogSchema => [ { id: Integer, name: String }, dft: { id: 1, name: 'pet' } ]
query! :UidQuery => [ :uid, String, desc: 'uid' ]
response :BadRqResp => [ 'bad request', :json ]
end
api :action do
query :doge, :DogSchema
param_ref :UidQuery
response_ref :BadRqResp
end
Each RefObj is associated with components through component key.
We suggest that component keys should be camelized, and must be Symbol.
(4) api
[required]
For defining API (or we could say controller action).
api action_name, summary = '', id: nil, tag: nil, http: nil, dry: Config.default_run_dry, &block
api :index, '(SUMMARY) this api blah blah ...',
Parameters explanation:
- action_name: must be the same as controller action name
- id: operationId
- http: HTTP method (like: 'GET' or 'GET|POST')
(5) api_dry
[optional]
This method is for DRYing.
The blocks passed to api_dry
will be executed to the specified APIs which are having the actions or tags in the class.
api_dry action_or_tags = :all, &block
api_dry :all, 'common response'
api_dry :index
api_dry :TagA
api_dry [:index, :show] do
query
end
And then you should call dry
method (detailed info) for executing the declared dry blocks:
api :index do
dry
end
DSLs written inside api and api_dry's block
source code
These following methods in the block describe the specified API action: description, valid?,
parameters, request body, responses, securities and servers.
(Here corresponds to OAS Operation Object)
(1) this_api_is_invalid!
, and its aliases:
this_api_is_expired!
this_api_is_unused!
this_api_is_under_repair!
this_api_is_invalid!(*)
this_api_is_invalid! 'cause old version'
After that, deprecated
field of this API will be set to true.
(2) desc
: description for the current API
desc string
desc "current API's description"
(3) param
family methods (OAS - Parameter Object)
To define parameter for APIs.
param # 1. normal usage
param_ref # 2. links sepcified RefObjs (by component keys) to current parameters.
header, path, query, cookie # 3. passes specified parameter location (like header) to `param`
header!, path!, query!, cookie! # 4. bang method of above methods
in_* by: { parameter_definations } # 5. batch definition, such as `in_path`, `in_query`
examples # 6. examples of parameters
The bang method and param_name (which's name is end of a exclamation point !
) means this param is required. Without !
means optional. THE SAME BELOW.
param param_type, param_name, schema_type, required, schema = { }
param :query, :page, Integer, :req, range: { gt: 0, le: 5 }, desc: 'page number'
param_ref *component_key
param_ref :IdPath
header param_name, schema_type = nil, **schema
query! param_name, schema_type = nil, **schema
header :'X-Token', String
query! :readed, Boolean, default: false
param :query, :readed, Boolean, :req, default: false
in_query **params_and_schema
in_query(
search_type: String,
search_val: String,
export!: { type: Boolean, desc: 'export as pdf' }
)
query :search_type, String
query :search_val, String
query! :export, Boolean, desc: 'export as pdf'
examples exp_params = :all, examples_hash
examples(
right_input: [ 1, 'user', 26 ],
wrong_input: [ 2, 'resu', 35 ]
)
examples [:id, :name], {
right_input: [ 1, 'user' ],
wrong_input: [ 2, 'resu' ]
}
[A] OpenAPI 3.0 distinguishes between the following parameter types based on the parameter location:
header, path, query, cookie. more info
[B] If param_type
is path, for example: if the API path is /good/:id
, you have to declare a path parameter named id
(4) request_body
family methods (OAS - Request Body Object)
OpenAPI 3.0 uses the requestBody keyword to distinguish the payload from parameters.
Notice: Each API has only ONE request body object. Each request body object can has multiple media types.
It means: call request_body
multiple times, (schemas) will be deeply merged (let's call it fusion) into a request body object.
request_body # 1. normal usage
body_ref # 2. it links sepcified RefObjs (by component keys) to the body.
body, body! # 3. alias of request_body
form, form! # 4. to define a multipart/form-data request_body
json, json! # 5. to define a application/json request_body
data # 5. to define [a] property in the form-data request_body
Bang methods(!) means the specified media-type body is required.
request_body required, media_type, data: { }, desc: '', **options
request_body :opt, :form, data: {
id!: Integer,
name: { type: String, desc: 'name' }
}, desc: 'a form-data'
body_ref component_key
body_ref :UpdateUserBody
body! media_type, data: { }, **options
body :json
def form data:, **options
body :form, data: data, **options
end
form! data: {
name!: String,
password: { type: String, pattern: /[0-9]{6,10}/ },
}
json data: { name!: String }
data name, type = nil, schema = { }
data :password!, String, pattern: /[0-9]{6,10}/
How fusion works:
- Difference media types will be merged into
requestBody["content"]
form data: { }
body :json, data: { }
- The same media-types will be deeply merged together, including their
required
array:
(So that you can call form
multiple times)
data :param_a!, String
data :param_b, Integer
form data: { :param_a! => String }
form data: { :param_b => Integer }
(5) response
family methods (OAS - Response Object)
To define the response for APIs.
response # 1. aliases: `resp` and `error`
response_ref # 2. it links sepcified RefObjs (by component keys) to the response.
response code, desc, media_type = nil, headers: { }, data: { }, **options
resp 200, 'success', :json, data: { name: 'test' }
response 200, 'query result', :pdf, data: File
response :success, 'succ', :json, headers: { 'X-Request-Start': String }, data: { }
response_ref code_and_compkey_hash
response_ref 700 => :AResp, 800 => :BResp
About Callbacks
In OpenAPI 3 specs, you can define callbacks – asynchronous, out-of-band requests that your service will send to some other service in response to certain events. This helps you improve the workflow your API offers to clients.
A typical example of a callback is a subscription functionality ... you can define the format of the “subscription” operation as well as the format of callback messages and expected responses to these messages.
This description will simplify communication between different servers and will help you standardize use of webhooks in your API.
Complete YAML Example
The structure of Callback Object:
callbacks:
Event1:
path1:
...
path2:
...
Event2:
...
callback
method is for defining callbacks.
callback event_name, http_method, callback_url, &block
callback :myEvent, :post, 'localhost:3000/api/goods' do
query :name, String
data :token, String
response 200, 'success', :json, data: { name: String, description: String }
end
Use runtime expressions in callback_url:
callback :myEvent, :post, '{body callback_addr}/api/goods/{query id}'
(7) Authentication and Authorization
First of all, please make sure that you have read one of the following documents:
OpenApi Auth
or securitySchemeObject
Define Security Scheme
Use these DSL in your initializer config 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)
security_scheme scheme_name, other_info
security_scheme :BasicAuth, { type: 'http', scheme: 'basic', desc: 'basic auth' }
base_auth scheme_name, other_info = { }
bearer_auth scheme_name, format = 'JWT', other_info = { }
api_key scheme_name, field:, in:, **other_info
base_auth :BasicAuth, desc: 'basic auth'
bearer_auth :Token
api_key :ApiKeyAuth, field: 'X-API-Key', in: 'header', desc: 'pass api key to header'
Apply Security
# Use in initializer (Global effectiveness)
global_security_require # alias: global_security & global_auth
# Use in `api`'s block (Only valid for the current controller)
security_require # alias security & auth_with
security_require scheme_name, scopes: [ ]
global_auth :Token
auth_with :OAuth, scopes: %w[ read_example admin ]
(8) Overriding Global Servers by server
server url, desc: ''
server 'http://localhost:3000', desc: 'local'
(9) dry
You have to call dry
method inside api
block, or pass dry: true
as parameter of api
,
for executing the dry blocks you declared before. Otherwise nothing will happen.
dry only: nil, skip: nil, none: false
dry
dry skip: [:id, :name]
dry only: [:id]
DSLs written inside components's block
code source (Here corresponds to OAS Components Object)
Inside components
's block,
you can use the same DSLs as DSLs written inside api
and api_dry
's block.
But notice there are two differences:
(1) Each method needs to pass one more parameter component_key
(as the first parameter),
it will be used as the reference name for the component.
query! :UidQuery, :uid, String, desc: 'it is a component'
query! :UidQuery => [:uid, String, desc: '']
(2) You can use schema
to define a Schema Component.
schema component_key, type = nil, **schema
schema :Dog => [ String, desc: 'doge' ]
schema :Dog => [
{
id!: Integer,
name: { type: String, desc: 'doge name' }
}, default: { id: 1, name: 'pet' }
]
schema :Dog, { id!: Integer, name: String }, default: { id: 1, name: 'pet' }
schema User
To enable load schema from database, you must set model base correctly.
Schema and Type
schema and type -- contain each other
(Schema) Type
Support all data types in OAS.
- String / 'binary' / 'base64' / 'uri'
- Integer / Long / 'int32' / 'int64' / Float / Double
- File (it will be converted to
{ type: 'string', format: Config.file_format }
) - Date / DateTime
- 'boolean'
- Array / Array[<Type>] (like:
Array[String]
, [String]
) - Nested Array (like:
[[[Integer]]]
) - Object / Hash (Object with properties)
Example: { id!: Integer, name: String }
- Nested Hash:
{ id!: Integer, name: { first: String, last: String } }
- Nested Array[Nested Hash]:
[[{ id!: Integer, name: { first: String, last: String } }]]
- Symbol Value: it will generate a Schema Reference Object link to the component correspond to ComponentKey, like: :IdPath, :NameQuery
Notice that Symbol is not allowed in all cases except 11.
Schema
OAS Schema Object
and source code
Schema (Hash) is for defining properties of parameters, responses and request bodies.
The following property keys will be process slightly:
- desc / description / d
- enum / in / values / allowable_values
should be Array or Range - range: allow value in this continuous range
should be Range or like { gt: 0, le: 5 }
- length / size / lth
should be an Integer, Integer Array, Integer Range,
or the following format Symbol: :gt_
, :ge_
, :lt_
, :le_
(:ge_5 means "greater than or equal 5"; :lt_9 means "lower than 9") - pattern / regxp
- additional_properties / add_prop / values_type
- example
- examples
- format
- default: default value
- type
The other keys will be directly merged. Such as:
title: 'Property Title'
myCustomKey: 'Value'
Combined Schema
Very easy to use:
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 ] ]
OAS: link1,
link2
Run! - Generate JSON Documentation File
Use OpenApi.write_docs
:
OpenApi.write_docs
if
option is used to control whether a JSON document is generated or not.
Then the JSON files will be written to the directories you set. (Each API a file.)
Use Swagger UI(very beautiful web page) to show your Documentation
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
Tricks
Trick1 - Write the DSL Somewhere Else
Does your documentation take too many lines?
Do you want to separate documentation from controller to simplify both?
Very easy! Just follow
base_doc_classes: [ApiDoc]
require 'open_api/dsl'
class ApiDoc < Object
include OpenApi::DSL
end
class V1::ExamplesDoc < ApiDoc
route_base 'api/v1/examples'
api :index do
end
end
Explain: These four steps are necessary:
- create a class, like ApiDoc, and make it include OpenApi::DSL (then it could be the base class for writing Api spec).
- set the specified Api spec's base_doc_classes to ApiDoc.
- let your doc class (like V1::ExamplesDoc) inherit the base_doc_classes (ApiDoc).
- set the route_base (to route path api/v1/examples of that controller Api::V1::ExamplesController) inside V1::ExamplesDoc.
Notes: file name ends in _doc.rb
by default, but you can change it by setting Config.doc_location
(it should be file paths, defaults to ./app/**/*_doc.rb
).
Trick2 - Global DRYing
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.
Trick3 - Auto Generate Description from Enum
Just use enum!
:
query :search_type, String, desc: 'search field, allows:<br/>', enum!: %w[name creator category price]
"search field, allows:<br/>1/ name<br/>2/ creator,<br/>3/ category<br/>4/ price<br/>"
Or Hash 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,
}
Troubleshooting
-
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 file config/routes.txt
. - Put
c.rails_routes_file = 'config/routes.txt'
to your ZRO config.
About OpenApi.docs
and OpenApi.routes_index
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.
Development
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.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Zero-RailsOpenApi project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.