Easily build OpenAPI compliant APIs with Rails.
Without Jsapi, complex API applications typically use in-memory models to read requests and serializers to write responses. When using OpenAPI for documentation purposes, this is done separatly.
Jsapi brings all this together. The models to read requests, serialization of objects and optional OpenAPI documentation base on the same API definition. This significantly reduces the workload and ensures that the OpenAPI documentation is consistent with the server-side implementation of the API.
Jsapi supports OpenAPI 2.0, 3.0, 3.1 and 3.2.
Add the following line to Gemfile and run bundle install.
gem 'jsapi'Start by adding a route for the API endpoint. For example, a non-resourceful route for a simple echo endpoint can be defined as below.
# config/routes.rb
get 'echo', to: 'echo#index'Specify the operation to be bound to the API endpoint in jsapi/api_defs/echo.rb:
# jsapi/api_defs/echo.rb
operation path: '/echo' do
parameter 'call', type: 'string', existence: true
response type: 'object' do
property 'echo', type: 'string'
end
response 400, type: 'object' do
property 'status', type: 'integer'
property 'message', type: 'string'
end
endNote that existence: true declares the call parameter to be required.
Create a controller that inherits from Jsapi::Controller::Base:
# app/controllers/echo_controller.rb
class EchoController < Jsapi::Controller::Base
def index
api_operation! do |api_params|
{
echo: "#{api_params.call}, again"
}
end
end
endNote that api_operation! renders the JSON representation of the object returned by the block.
This can be a hash or an object providing corresponding methods for all properties of the
response.
When calling GET /echo?call=Hello, a response with HTTP status code 200 and the following
body is produced:
{
"echo": "Hello, again"
}When the required call parameter is missing or the value of call is empty, api_operation!
raises a Jsapi::Controller::ParametersInvalid error. To rescue such exceptions, add an
rescue_from directive to jsapi/api_defs/echo.rb:
# jsapi/api_defs/echo.rb
rescue_from Jsapi::Controller::ParametersInvalid, with: 400Then a response with HTTP status code 400 and the following body is produced:
{
"status": 400,
"message": "'call' can't be blank."
}To produce an OpenAPI document describing the API, add another route, an info directive and
a controller action matching the route, for example:
# config/routes.rb
get 'echo/openapi', to: 'echo#openapi'# jsapi/api_defs/echo.rb
info title: 'Echo', version: '1'# app/controllers/echo_controller.rb
class EchoController < Jsapi::Controller::Base
def openapi
render(json: api_definitions.openapi_document(params[:version]))
end
endThe sources and OpenAPI documents of this example are here.
Everything needed to build an API is defined by a DSL whose vocabulary bases on OpenAPI and
JSON Schema. This DSL can be used in any controller inheriting from Jsapi::Controller::Base
as well as any class extending Jsapi::DSL. To avoid naming conflicts with other libraries,
all top-level directives start with api_. These are:
- api_base_path
- api_callback
- api_default
- api_definitions
- api_example
- api_external_docs
- api_header
- api_host
- api_import
- api_include
- api_info
- api_link
- api_on_rescue
- api_operation
- api_parameter
- api_request_body
- api_rescue_from
- api_response
- api_schema
- api_scheme
- api_security_requirement
- api_security_scheme
- api_server
- api_tag
When using top-level directives, the example in Getting started looks like:
# app/controllers/echo_controller.rb
class EchoController < Jsapi::Controller::Base
api_info title: 'Echo', version: '1'
api_rescue_from Jsapi::Controller::ParametersInvalid, with: 400
api_operation path: '/echo' do
parameter 'call', type: 'string', existence: true
response type: 'object' do
property 'echo', type: 'string'
end
response '4xx', type: 'object' do
property 'status', type: 'integer'
property 'message', type: 'string'
end
end
endFurthermore, API definitions can be specified within an api_definitions block as below.
# app/controllers/echo_controller.rb
class EchoController < Jsapi::Controller::Base
api_definitions do
info title: 'Echo', version: '1'
rescue_from Jsapi::Controller::ParametersInvalid, with: 400
operation path: '/echo' do
parameter 'call', type: 'string', existence: true
response type: 'object' do
property 'echo', type: 'string'
end
response '4xx', type: 'object' do
property 'status', type: 'integer'
property 'message', type: 'string'
end
end
end
endAll keywords except :ref, :schema and :type may also be specified by nested directives,
for example:
parameter 'call', type: 'string' do
existence true
endNames and types can be specified as strings or symbols. Therefore,
parameter 'call', type: 'string'is equivalent to
parameter :call, type: :stringAn operation is defined by an api_operation directive, for example:
api_operation 'foo' do
parameter 'bar', type: 'string'
response type: 'object' do
property 'foo', type: 'string'
end
endThe one and only positional argument specifies the name of the operation. It can be omitted if
the controller handles one operation only. The api_operation directive takes the following
keywords:
:callbacks- See Callbacks.:deprecated- Specifies whether or not the operation is deprecated.:description- The description of the operation.:external_docs- See Specifying external docs.:method- The HTTP verb of the operation,"GET"by default.:model- See API models.:openapi_extensions- See Specifying OpenAPI extensions.:parameters- See Specifying parameters.:path- The relative path of the operation.:request_body- See Specifying request bodies.:responses- See Specifying responses.:schemes- The transfer protocols supported by the operation.:security_requirements- See Specifying security schemes and requirements.:servers- See Specifying API locations.:summary- The short description of the operation.:tags- The tags to group operations in an OpenAPI document.
All keywords except :model, :parameters, :request_body, :responses and
:security_requirements are only used to describe the operation in generated OpenAPI
documents.
The relative path of an operation is derived from the controller name, unless it is
explictly specified by the :path keyword.
The callbacks that may be initiated by an operation can be described by nested callback
directives, for example:
api_operation do
callback 'foo' do
expression '{$request.query.bar}' do
operation 'get'
end
end
endThe one and only positional argument specifies the mandatory name of the callback. The nested
expression directives maps expressions to operations.
If a callback is associated with multiple operations, it can be specified once by an
api_callback directive, for example:
api_callback 'foo' do
expression '{$request.query.bar}' do
operation 'get'
end
endA callback specified by an api_callback directive can be referred as below.
api_operation do
callback ref: 'foo'
endapi_operation do
callback 'foo'
endThe api_path directive can be used to group operations by path, for example:
api_path 'foos' do
operation 'foos'
operation 'create_foo', method: 'post'
path '{id}' do
parameter 'id', type: 'integer'
response 404, 'ErrorResponse'
operation 'read_foo'
operation 'update_foo', method: 'patch'
operation 'delete_foo', method: 'delete'
end
endThe one and only positional argument specifies the relative path. The api_path directive
takes the following keywords:
:description- The description that applies to all operations in this path.:model- The default model of all operations in this path. See API models for further information.:parameters- The parameters that apply to all operations in this path. See Specifying parameters for further information.:request_body- The request body used by default by all operations in this path. See Specifying request bodies for further information.:responses- The responses that can be produced by all operations in this path. See Specifying responses for further information.:security_requirements- The security requirements that apply to all operations in this path. See Specifying security schemes and requirements for further information.:servers- The servers that apply by default to all operations in this path. See Specifying API locations for further information.:summary- The summary that applies to all operations in this path.:tags- The tags that apply to all operations in this path.
The :description, :servers, :summary and :tags keywords are only used to describe
the path in generated OpenAPI documents.
A parameter of an operation is defined by a nested parameter directive, for example:
api_operation do
parameter 'foo', type: 'string'
endThe one and only positional argument specifies the mandatory parameter name. The parameter
directive takes all keywords described in Specifying schemas to define the schema of a
parameter. Additionally, the following keywords may be specified:
:content_type- The content type of a complex parameter.:example,:examples- See Specifying examples.:in- The location of the parameter. Possible locations are"header","path","query"and"querystring". The default location is"query".:openapi_extensions- See Specifying OpenAPI extensions.:ref- Refers a reusable parameter.
The :content_type, :example, examples and :openapi_extensions keywords are only used
to describe a parameter in generated OpenAPI documents.
api_operation do
parameter 'foo', type: 'string'
end
# => api_params.fooapi_operation do
parameter 'query', in: 'querystring' do
property 'foo', type: 'string'
end
end
# => api_params.query.fooIf a parameter is provided by multiple operations, it can be defined once by an api_parameter
directive, for example:
api_parameter 'request_id', type: 'string'The one and only positional argument specifies the mandatory name of the reusable parameter.
A reusable parameter can be referred as below.
api_operation do
parameter ref: 'request_id'
endapi_operation do
parameter 'request_id'
endThe optional request body of an operation is defined by a nested request_body directive,
for example:
api_operation do
request_body type: 'object' do
property 'foo', type: 'string'
end
endThe request_body directive takes all keywords described in Specifying schemas to define the
schema of the request body. Additionally, the following keywords may be specified:
:content_type- The content type a request body,"application/json"by default.:example,:examples- See Specifying examples.:openapi_extensions- See Specifying OpenAPI extensions.:ref- Refers a reusable request body.
The :example, :examples and :openapi_extensions keywords are only used to describe the
request body in generated OpenAPI documents.
Different types of content can be specified by nested content directives, for example:
api_operation do
request_body do
content 'application/json', type: 'object' do
property 'foo', type: 'string'
end
content 'text/*', type: 'string'
end
endThe one and only positional argument specifies the media range of the content. The default
media range is "application/json". The content directive takes all keywords described
in Specifying schemas to define the schema of the content. Additionally, the following
keywords may be specified:
:example,:examples- See Specifying examples.:openapi_extensions- See Specifying OpenAPI extensions.
These keywords are only used to describe a type of content in an OpenAPI document.
If multiple operations have the same request body, this request body can be defined once by
an api_request_body directive, for example:
api_request_body 'foo', type: 'object' do
property 'bar', type: 'string'
endThe one and only positional argument specifies the mandatory name of the reusable request body.
A reusable request body can be referred as below.
api_operation do
request_body ref: 'foo'
endapi_operation do
request_body 'foo'
endA response that may be produced by an operation is defined by a nested response directive,
for example:
api_operation do
response 'default' do
property 'foo', type: 'string'
end
endThe optional positional argument specifies the response status. Possible values are:
- HTTP status codes, for example:
200or":ok" - ranges of HTTP status codes, for example:
"2xx"or"2XX" "default"
The default response status is "default".
The response directive takes all keywords described in Specifying schemas to define the
schema of the response. Additionally, the following keywords may be specified:
:content_type- The content type of the response,"application/json"by default.:example,:examples- See Specifying examples.:headers- See Headers.:links- See Links.:nodoc- Prevents response to be described in generated OpenAPI documents.:locale- The locale to be used when rendering a response.:openapi_extensions- See Specifying OpenAPI extensions.:ref- Refers a reusable response.:summary- The short description of the response.
:locale allows producing responses with different status codes in different languages.
This can especially be used to return error responses in English regardless of the
language of regular responses.
The :example, :examples, :headers, :links :openapi_extensions and summary keywords
are only used to describe a response in generated OpenAPI documents.
Different types of content can be specified by nested content directives, for example:
api_operation do
response 200 do
content 'application/json', type: 'object' do
property 'items', type: 'array' do
property 'foo', type: 'string'
end
end
content 'application/json-seq', type: 'array' do
property 'foo', type: 'string'
end
end
endThe one and only positional argument specifies the media type of the content. The default
media type is "application/json". The content directive takes all keywords described
in Specifying schemas to define the schema of the content. Additionally, the following
keywords may be specified:
:example,:examples- See Specifying examples.:openapi_extensions- See Specifying OpenAPI extensions.
These keywords are only used to describe a type of content in generated OpenAPI documents.
If a response may be produced by multiple operations, it can be defined once by an
api_response directive, for example:
api_response 'error', type: 'object' do
property 'status', type: 'integer'
property 'detail', type: 'string'
endThe one and only positional argument specifies the mandatory name of the reusable response.
A reusable response can be referred as below.
api_operation do
response '4xx', ref: 'error', locale: :en
response '5xx', ref: 'error', locale: :en, nodoc: true
endapi_operation do
response 400, 'error'
endThe HTTP headers a response can have can be described by nested header directives,
for example:
response do
header 'foo', type: 'string'
endIf a header belongs to multiple responses, it can be specified once by an api_header
directive, for example:
api_header 'foo', type: 'string'The one and only positional argument specifies the mandatory name of the header. The header
directive takes all keywords described in Specifying schemas to define the schema of the
header.
A header specified by an api_header directive can be referred as below.
response do
header ref: 'foo'
endresponse do
header 'foo'
endThe operations that may follow after a response can be described by link directives, for
example:
response do
link 'foo', operation_id: 'bar'
endThe one and only positional argument specifies the mandatory name of the link. The link
directive take the following keywords:
:description- The description of the link.:operation_id- The ID of the operation to be linked.:parameters- The parameters to be passed.:request_body- The request body to be passed.:server- The server providing the operation.
If an operation is linked to multiple responses, the link can be specified once by an api_link
directive, for example:
api_link 'foo', operation_id: 'bar'A link specified by a api_link directive can be referred as below.
response do
link ref: 'foo'
endresponse do
link 'foo'
endA property of a parameter, request body or response is defined by a nested property directive,
for example:
api_operation do
parameter 'foo', type: 'object' do
property 'bar', type: 'string'
end
endThe one and only positional argument specifies the mandatory property name. The property
directive takes all keywords described in Specifying schemas to define the schema of a
property. Additionally, the following keywords may be specified:
:read_only- Specifies whether or not the property is read only.:source- The sequence of methods orProcto be called to read property values.:write_only- Specifies whether or not the property is write only.
The source can be a string, a symbol, an array or a Proc, for example:
property 'foo', source: 'bar.foo'property 'foo', source: %i[bar foo]property 'foo', source: ->(bar) { bar.foo }The following keywords are provided to define the schema of a parameter, request body, response or property.
:additional_properties- See Additional properties.:conversion- See The :conversion keyword.:default- The default value.:deprecated- Specifies whether or not it is deprecated.:description- The description of the parameter, request body, response or property.:enum- The valid values.:example,:examples- One or more sample values.:existence- See The :existence keyword.:format- See The :format keyword.:items- See The :items keyword.:max_items- The maximum length of an array.:max_length- The maximum length of a string.:maximum- See The :maximum keyword.:min_items- The minimum length of an array.:min_length- The minimum length of a string.:minimum- See The :minimum keyword.:model- See API models.:multiple_of- The value an integer or a number must be a multiple of.:openapi_extensions- See Specifying OpenAPI extensions.:pattern- The regular expression a string must match.:properties- See Specifying properties.:schema- See Reusable schemas.:title- The title of the parameter, request body, response or property.:type- The type of a parameter, response or property. Possible values are"array","boolean","integer","number","object"and"string". The default type is"object".
The :deprecated, :description, :example, :examples, and :title keywords are only
used to describe a schema in generated OpenAPI and JSON Schema documents. Note that examples
of a parameter, request body and response differ from other schemas because they are compliant
to the OpenAPI specification, whereas in all other cases examples are compliant to the JSON
Schema specification.
The :existence keyword combines the presence concepts of Rails and JSON Schema by four levels
of existence:
:presentortrue- The parameter or property value must not be empty.:allow_empty- The parameter or property value can be empty, for example''.:allow_nilorallow_null- The parameter or property value can benil.:allow_omittedorfalse- The parameter or property can be omitted.
The default level of existence is false.
Note that existence: :present slightly differs from Rails present? as it treats false
to be present.
The conversion keyword specifies a method or Proc to convert integers, numbers and strings
when consuming requests or producing responses, for example:
# Conversion by method
property 'foo', type: 'string', conversion: :upcase# Conversion by proc
property 'foo', type: 'string', conversion: ->(value) { value.upcase }The :items keyword defines the schema of the items that can be contained in an array,
for example:
property 'foo', type: 'array', items: { type: 'string' }property 'foo', type: 'array' do
items type: 'object' do
property 'bar', type: 'string'
end
endThe :format keyword specifies the format of a string. If the format is "date", "date-time"
or "duration", parameter and property values are implicitly casted as below.
"date"- values are casted toDate."date-time"- values are casted toDateTime."duration"- values are casted toActiveSupport::Duration.
All other formats are only used to describe the format of a string.
The :maximum keyword specifies the maximum value an integer or a number can be, for example:
# Allow negative integers only
parameter 'foo', type: 'integer', maximum: -1# Allow negative numbers only
parameter 'bar', type: 'number', maximum: { value: 0, exclusive: true }The :minimum keyword specifies the minimum value an integer or a number can be, for example:
# Allow positive integers only
parameter 'foo', type: 'integer', minimum: 1# Allow positive numbers only
parameter 'bar', type: 'number', minimum: { value: 0, exclusive: true }The schema of properties that are not explictly specified is defined by an
additional_properties directive, for example:
schema 'foo' do
additional_properties type: 'string', source: :bar
endThe :source keyword specifies the sequence of methods or Proc to be called to read
additional properties. The default source is :additional_properties.
If a schema is used multiple times, it can be defined once by an api_schema directive,
for example:
api_schema 'Foo', type: 'object' do
property 'id', type: 'integer', read_only: true
property 'bar', type: 'string'
endThe one and only positional argument of the api_schema directive specifies the mandatory
name of the reusable schema.
A schema defined by api_schema can be referred as below.
api_operation 'create_foo', method: 'post' do
request_body schema: 'Foo'
response schema: 'Foo'
endAll properties of another schema can be included by the all_of directive, for example:
api_schema 'Foo', type: 'object' do
all_of 'Base'
endThe all_of directive corresponds to the allOf JSON Schema keyword. Note that there are no
equivalent directives for the anyOf and oneOf keywords.
api_schema 'Base', type: 'object' do
discriminator property_name: 'type' do
mapping 'foo', 'Foo'
mapping 'bar', 'Bar'
end
property 'type', type: 'string', existence: true
end
api_schema 'Foo', type: 'object' do
all_of 'Base'
property 'foo', type: 'string'
end
api_schema 'Bar', type: 'object' do
all_of 'Base'
property 'bar', type: 'string'
endA default mapping can either be specified by the :default_mapping keyword or the default
value of the discriminating property, for example:
api_schema 'Base', type: 'object' do
discriminator property_name: 'type', default_mapping: 'Foo'
property 'type', type: 'string'
endapi_schema 'Base', type: 'object' do
discriminator property_name: 'type'
property 'type', type: 'string', default: 'Foo'
endMetadata about an API is specified by an api_info directive, for example:
api_info title: 'Foo', version: '1'The api_info directive takes the following keywords:
:contact- See Contact.:description- The description of the API.:license- See License.:summary- The short summary of the API.:terms_of_service- The URL pointing to the terms of service.:title- The mandatory title of the API.:version- The mandatory version of the API.
The contact information are described by a nested contact directive, for example:
api_info do
contact email: '[email protected]'
endThe contact directive takes the following keywords:
:email- The email address of the contact.:name- The name of the contact.:url- The URL of the contact.
The license of an API is described by a nested license directive, for example:
api_info do
license name: 'MIT License', identifier: 'MIT'
endThe license directive takes the following keywords:
:identifier- The SPDX identifier of the license.:name- The name of the license.:url- The URL of the license.
Note that :identifier and :url are mutually exlusive.
The location of an API can either be specified by an api_server directive or the api_scheme,
api_host and api_base_path directives, for example:
api_server 'https://foo.bar/foo'api_scheme 'https'
api_host 'foo.bar'
api_base_path '/foo'The api_server directive corresponds to the Server object introduced with OpenAPI 3.0. The
positional argument must be an absolute or relative URI.
The api_scheme, api_host and api_base_path directives correspond to the scheme, host
and basePath fields in OpenAPI 2.0.
A security scheme is described by an api_scurity_scheme directive, for example:
api_security_scheme 'basic_auth', type: 'http', scheme: 'basic'The one and only positional argument specifies the name of the security scheme. The :type
keyword specifies the type of the security scheme. Possible types are:
"api_key""basic""http""oauth2""open_id_connect"
Security schemes are linked by api_security_requirement or nested security_requirement
directives, for example:
api_security_requirement 'http_basic' do
scheme 'basic_auth'
endapi_operation do
security_requirement do
scheme 'basic_auth'
end
endA single sample value can be specified by an example keyword, for example:
property 'foo', type: 'string', example: 'bar'property 'foo', type: 'string' do
example 'bar'
endNamed sample values are specified by nested example directives, for example:
property 'foo', type: 'string' do
example 'bar', value: 'value of bar'
endThe example directive takes the following keywords:
description- The description of the example.external_value- The URI of an external sample value.serialized_value- The serialized form of the sample value.summary- The short summary of the example.value- The sample value.
If an example matches multiple parameters, request bodies or responses, it can be specified once
by an api_example directive, for example:
api_example 'foo', value: 'bar'An example specified by a api_example directive can be referred as below.
property 'foo', type: 'string' do
example ref: 'foo'
endA tag is specified by an api_tag directive, for example:
api_tag name: 'foo', description: 'Lorem ipsum'The api_tag directive takes the following keywords:
:external_docs- See Specifying external docs.:description- The description of the tag.:kind- The category of the tag.:name- The name of the tag.:summary- The short summary of the tag.
External documentations are described by api_external_docs or nested external_docs
directives, for example:
api_external_docs url: 'https://foo.bar'api_operation do
external_docs url: 'https://foo.bar'
endBoth directives take the following keywords:
:description- The description of the external documentation.:url- The URI of the external documentation.
OpenAPI extensions are specified by nested openapi_extension directives, for example:
openapi_extension 'foo', 'bar'The first argument specifies the name of the extension. The second argument specifies the
assigned value. Note that the prefix x- is added automatically when producing an OpenAPI
document.
To rescue exceptions raised while performing an operation, one or more rescue handlers can be
defined by api_rescue_from directives, for example:
api_rescue_from Jsapi::Controller::ParametersInvalid, with: 400
api_rescue_from StandardError, with: 500The one and only positional argument specifies the exception class to be rescued. The :with
keyword specifies the status code of the error response to be produced.
To notice exceptions caught by a rescue handler, a callback can be added by an api_on_rescue
directive, for example:
api_on_rescue :fooapi_on_rescue do |error|
# ...
endA callback can either be a method name or a block.
The general default values for a type can be defined by an api_default directive, for example:
api_default 'array', within_requests: [], within_responses: []api_default takes the following keywords:
:within_requests- The general default value of parameters when consuming requests.:within_responses- The general default value of properties when producing responses.
API definitions can also be specified in separate files located in jsapi/api_defs. Directives
within files are specified as in api_definitions blocks without prefix api_, for example:
# jsapi/api_defs/foo.rb
operation 'foo' do
# ...
endThe API definitions specified in a file are automatically imported into a controller if the
file name matches the controller name. For example, jsapi/api_defs/foo.rb is automatically
imported into FooController. Other files can be imported as below.
class FooController < Jsapi::Controller::Base
api_import 'bar'
endWithin a file, other files can be imported as below.
# jsapi/api_defs/foo/bar.rb
import 'foo/shared'# jsapi/api_defs/foo/bar.rb
import_relative 'shared'The location of API definitions can be changed by an initializer:
# config/initializers/jsapi.rb
Jsapi.configuration.api_defs_path = 'app/foo'API components can be used in multiple classes by inheritance or inclusion. A controller class inherits all API components from the parent class, for example:
class FooController < Jsapi::Controller::Base
api_schema 'Foo'
end
class BarController < FooController
api_response 'Bar', schema: 'Foo'
endIn addition, API components from other classes can be included as below.
class FooController < Jsapi::Controller::Base
api_schema 'Foo'
end
class BarController < Jsapi::Controller::Base
api_include FooController
api_response 'Bar', schema: 'Foo'
endAn API controller class must either inherit from Jsapi::Controller::Base or include
Jsapi::Controller::Methods.
class FooController < Jsapi::Controller::Base
# ...
endclass FooController < ActionController::API
include Jsapi::Controller::Methods
# ...
endThe Jsapi::Controller::Methods module provides the following methods to deal with
API operations:
api_params can be used to read request parameters as an instance of an operation's model
class. The request parameters are casted according the operation's parameter and
request_body specifications. Parameter names are converted to snake case.
params = api_params('foo')The one and only positional argument specifies the name of the API operation. It can be
omitted if the controller handles one API operation only. If no operation could be found for
this name, an Jsapi::Controller::OperationNotFound exception is raised.
Note that each call of api_params returns a newly created instance. Thus, the instance
returned by api_params must be locally stored when validating request parameters,
for example:
if (params = api_params).valid?
# ...
else
full_messages = params.errors.full_messages
# ...
endapi_response can be used to serialize an object according to one of the API operation's
response specifications.
render(json: api_response(foo, 'foo', status: 200))The object to be serialized is passed as the first positional argument. The second
positional argument specifies the name of the API operation. It can be omitted if the
controller handles one API operation only. If no operation could be found for this name,
an Jsapi::Controller::OperationNotFound exception is raised.
:status specifies the HTTP status code of the response to be produced.
api_operation performs an API operation by calling the given block.
The request parameters are passed as an instance of the operation's model class to the block. Parameter names are converted to snake case.
The object returned by the block is implicitly rendered or streamed according to the most appropriate +response+ specification if the media type of that response is one of:
application/json,text/json,\*/\*+json- The JSON representation of the object is rendered.application/json-seq- The object is streamed in JSON sequence text format.text/plain- Theto_srepresentation of the object is rendered.
api_operation('foo', status: 200) do |api_params|
raise BadRequest if api_params.invalid?
# ...
endThe one and only positional argument specifies the name of the API operation. It can be
omitted if the controller handles one API operation only. If no operation could be found for
this name, an Jsapi::Controller::OperationNotFound exception is raised.
:status specifies the HTTP status code of the response to be produced.
If an exception is raised while performing the operation, an error response according to the first matching rescue handler is rendered. If no rescue handler matches, the exception is raised again.
Like api_operation, except that a Jsapi::Controller::ParametersInvalid exception is raised
on invalid request parameters.
api_operation!('foo') do |api_params|
# ...
endThe errors instance method of Jsapi::Controller::ParametersInvalid returns all of the
validation errors encountered.
api_definitions returns the API definitions of the controller class. In particular, this
method can be used to create an OpenAPI document.
render(json: api_definitions.openapi_document)The api_operation, api_operation! and api_params methods take a :strong option that
specifies whether or not request parameters that can be mapped are accepted only.
api_params('foo', strong: true)The model returned is invalid if there are any request parameters that cannot be mapped to a parameter or a request body property of the API operation. For each parameter that can't be mapped an error is added to the model. The pattern of error messages can be customized using I18n as below.
# config/en.yml
en:
jsapi:
errors:
forbidden: "{name} is forbidden"The default pattern is {name} isn't allowed.
If the optional Jsapi::Controller::Authentication module is included, requests can be
authenticated according to the security requirements associated with an API operation.
class FooController < Jsapi::Controller::Base
include Jsapi::Controller::Authentication
api_authenticate 'basic_auth' do |credentials|
credentials.username == 'api_user' &&
credentials.password == 'secret'
end
api_security_scheme 'basic_auth', type: 'http', scheme: 'basic'
api_security_requirement do
scheme 'basic_auth'
end
endThe api_authenticate class method registers a handler to authenticate requests according to
one or more security schemes.
api_authenticate 'basic_auth', with: :authenticateIf no security schemes are specified, the handler is used as fallback for all security schemes for which no handler is registered.
The :with option specifies the method or Proc to be called. Alternatively, a block can be
given as handler.
api_authenticate 'basic_auth' do |credentials|
# Implement handler here
endIf the handler returns a truthy value, the request is assumed to be authenticated successfully.
The api_authenticated? method can be used to authenticate requests, for example:
head :unauthorized && return unless api_authenticated?If a controller class includes the Jsapi::Controller:Authentication module the api_operation
and api_operation! methods implicitly authenticate requests before performing the operation.
When the current request could not be authenticated, a Jsapi::Controller::Unauthorized
exception is raised. Such exceptions can be rescued as below to produce an error response.
api_rescue_from Jsapi::Controller::Unauthorized, with: 401The following class methods can be used to register callbacks:
When registering a callback, the following keyword arguments can be specifed:
:if- The conditions under which the callback is triggered only.:unless- The conditions under which the callback isn't triggered.:only- The operations on which the callback is triggered only.:except- The operations on which the callback isn't triggered.
:if and :unless can be a symbol, a Proc or an array of symbols and Procs.
Callbacks are triggered in the same order as they are registered. Callbacks inherited from superclasses are triggered before callbacks that are registered in the actual class.
If a controller class includes the Jsapi::Controller:Authentication module, all of the
registered api_after_authentication callbacks are triggered after a request has been
authenticated.
An api_after_authentication callback can be used to check whether or not a request is
authorized to perform an API operation, for example:
api_after_authentication do |operation_name|
head :forbidden unless authorized?(operation_name)
end
def authorized?(operation_name)
# Implement authorization here
endNote that in real-word scenarios it is more common to raise an exception instead of using
head.
When calling an api_after_authentication, the name of the API operation is passed if the
callback takes one positional argument.
An api_after_validation callback is triggered by api_operation! after the parameters have
been validated successfully.
api_after_validation do |operation_name, api_params|
# ...
endWhen calling an api_after_validation callback, the name of the API operation is passed if
the callback takes one positional argument. Additionally, the parameters are passed if the
callback takes two positional arguments.
api_before_rendering callbacks are triggered by api_operation and api_operation! before
the response body is rendered. They are typically used to enrich responses, for example:
api_before_rendering do |result, _api_operation, api_params|
{ request_id: api_params.request_id, payload: result }
endWhen calling an api_before_rendering callback, the object to render the response body from
is passed as the first positional argument. This object is replaced by the object returned
by the callback.
If the callback takes at least two positional arguments, the name of API operation is passed as the second positional argument. If the callback takes three positional arguments, the parameters are passed as the third positional argument.
API actions can be implemented even more easily using the api_action or api_action!
method provided by the Jsapi::Controller::Actions module. This module is already
included in Jsapi::Controller::Base.
class FooController < Jsapi::Controller::Base
api_operation 'foo' do
# Define API operation here
end
api_action :foo, action: :index
def foo(api_params)
# Implement API operation here
end
endapi_action defines a controller action that performs an API operation by wrapping the
given method or block by api_operation.
# Invoke :foo to perform :bar
api_action :foo, :bar
def foo(api_params)
# ...
end# Call the given block to perform :bar
api_action :bar do |api_params|
# ...
endapi_action takes up to two positional arguments. If no block is given, the first
argument specifies the method to be invoked. The optional second argument specifies
the API operation to be performed. If only one argument is given, it specifies the
method to be invoked as well as the API operation to be performed. If a block is
given, the positional argument specifies the API operation to be performed.
The :action option specifies the name of the controller action to be defined.
The default is :index.
All other options are passed to api_operation.
Like api_action, except that api_operation! is
used instead of api_operation.
By default, the parameters returned by the params method of a controller are wrapped by
an instance of Jsapi::Model::Base. Parameter names are converted to snake case. This allows
parameter values to be read by Ruby-stylish methods, even if parameter names are represented
in camel case.
Additional model methods can be added by a model block, for example:
api_schema 'IntegerRange' do
property 'range_begin', type: 'integer'
property 'range_end', type: 'integer'
model do
def range
@range ||= (range_begin..range_end)
end
end
endTo use additional model methods in multiple API components, a subclass of Jsapi::Model::Base
can be use as below.
class BaseRange < Jsapi::Model::Base
def range
@range ||= (range_begin..range_end)
end
endapi_schema 'IntegerRange', model: BaseRange do
property 'range_begin', type: 'integer'
property 'range_end', type: 'integer'
end
api_schema 'DateRange', model: BaseRange do
property 'range_begin', type: 'string', format: 'date'
property 'range_end', type: 'string', format: 'date'
endA model class may also have validations, for example:
class BaseRange < Jsapi::Model::Base
validate :end_greater_than_or_equal_to_begin
private
def end_greater_than_or_equal_to_begin
return if range_begin.blank? || range_end.blank?
if range_end < range_begin
errors.add(:range_end, :greater_than_or_equal_to, count: range_begin)
end
end
endWhen creating or updating an ActiveRecord object, attributes can be set using the
serializable_hash method provided by Jsapi::Model::Base, for example:
def create
api_operation!('create_foo') do |api_params|
Foo.create! api_params.foo.serializable_hash
end
endThe attributes returned by serializable_hash can be filtered by :only or :except.
An update action that only updates the passed attributes only looks like:
def update
api_operation!('update_foo') do |api_params|
foo = Foo.find api_params.id
foo.update! api_params.foo.serializable_hash(only: params[:foo].keys)
end
end