Skip to content

dmgoeller/jsapi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Jsapi

Easily build OpenAPI compliant APIs with Rails.

Why Jsapi?

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.

Installation

Add the following line to Gemfile and run bundle install.

gem 'jsapi'

Getting started

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
end

Note 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
end

Note 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: 400

Then 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
end

The sources and OpenAPI documents of this example are here.

Jsapi DSL

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:

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
end

Furthermore, 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
end

All keywords except :ref, :schema and :type may also be specified by nested directives, for example:

parameter 'call', type: 'string' do
  existence true
end

Names and types can be specified as strings or symbols. Therefore,

parameter 'call', type: 'string'

is equivalent to

parameter :call, type: :string

Specifying operations

An 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
end

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

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.

Callbacks

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
end

The 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
end

A callback specified by an api_callback directive can be referred as below.

api_operation do
  callback ref: 'foo'
end
api_operation do
  callback 'foo'
end

Grouping operations by path

The 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
end

The 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.

Specifying parameters

A parameter of an operation is defined by a nested parameter directive, for example:

api_operation do
  parameter 'foo', type: 'string'
end

The 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.

Query Parameters

api_operation do
  parameter 'foo', type: 'string'
end
# => api_params.foo
api_operation do
  parameter 'query', in: 'querystring' do
    property 'foo', type: 'string'
  end
end
# => api_params.query.foo

Reusable parameters

If 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'
end
api_operation do
  parameter 'request_id'
end

Specifying request bodies

The 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
end

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

The :example, :examples and :openapi_extensions keywords are only used to describe the request body in generated OpenAPI documents.

Different types of content

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
end

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

These keywords are only used to describe a type of content in an OpenAPI document.

Reusable request bodies

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'
end

The 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'
end
api_operation do
  request_body 'foo'
end

Specifying responses

A 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
end

The optional positional argument specifies the response status. Possible values are:

  • HTTP status codes, for example: 200 or ":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

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
end

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

These keywords are only used to describe a type of content in generated OpenAPI documents.

Reusable responses

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'
end

The 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
end
api_operation do
  response 400, 'error'
end

Headers

The HTTP headers a response can have can be described by nested header directives, for example:

response do
  header 'foo', type: 'string'
end

If 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'
end
response do
  header 'foo'
end

Links

The operations that may follow after a response can be described by link directives, for example:

response do
  link 'foo', operation_id: 'bar'
end

The 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'
end
response do
  link 'foo'
end

Specifying properties

A 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
end

The 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 or Proc to 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 }

Specifying schemas

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

The :existence keyword combines the presence concepts of Rails and JSON Schema by four levels of existence:

  • :present or true - The parameter or property value must not be empty.
  • :allow_empty - The parameter or property value can be empty, for example ''.
  • :allow_nil or allow_null - The parameter or property value can be nil.
  • :allow_omitted or false - 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

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

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
end

The :format keyword

The :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 to Date.
  • "date-time" - values are casted to DateTime.
  • "duration" - values are casted to ActiveSupport::Duration.

All other formats are only used to describe the format of a string.

The :maximum keyword

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

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 }

Additional properties

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
end

The :source keyword specifies the sequence of methods or Proc to be called to read additional properties. The default source is :additional_properties.

Reusable schemas

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'
end

The 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'
end

Composition

All properties of another schema can be included by the all_of directive, for example:

api_schema 'Foo', type: 'object' do
  all_of 'Base'
end

The all_of directive corresponds to the allOf JSON Schema keyword. Note that there are no equivalent directives for the anyOf and oneOf keywords.

Polymorphism

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'
end

A 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'
end
api_schema 'Base', type: 'object' do
  discriminator property_name: 'type'
  property 'type', type: 'string', default: 'Foo'
end

Specifying metadata

Metadata 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.

Contact

The contact information are described by a nested contact directive, for example:

api_info do
  contact email: '[email protected]'
end

The 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.

Licence

The license of an API is described by a nested license directive, for example:

api_info do
  license name: 'MIT License', identifier: 'MIT'
end

The 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.

Specifying API locations

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.

Specifying security schemes and requirements

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'
end
api_operation do
  security_requirement do
    scheme 'basic_auth'
  end
end

Specifying examples

A 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'
end

Named sample values are specified by nested example directives, for example:

property 'foo', type: 'string' do
  example 'bar', value: 'value of bar'
end

The 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.

Reusable examples

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'
end

Specifying tags

A 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.

Specifying external docs

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'
end

Both directives take the following keywords:

  • :description - The description of the external documentation.
  • :url - The URI of the external documentation.

Specifying OpenAPI extensions

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.

Specifying rescue handlers and callbacks

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

The 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 :foo
api_on_rescue do |error|
  # ...
end

A callback can either be a method name or a block.

Specifying general default values

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.

Importing API definitions

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
  # ...
end

The 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'
end

Within 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'

Sharing API definitions

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'
end

In 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'
end

API controllers

An API controller class must either inherit from Jsapi::Controller::Base or include Jsapi::Controller::Methods.

class FooController < Jsapi::Controller::Base
  # ...
end
class FooController < ActionController::API
  include Jsapi::Controller::Methods
  # ...
end

Methods

The Jsapi::Controller::Methods module provides the following methods to deal with API operations:

The api_params method

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
  # ...
end

The api_response method

api_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.

The api_operation method

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 - The to_s representation of the object is rendered.
api_operation('foo', status: 200) do |api_params|
  raise BadRequest if api_params.invalid?

  # ...
end

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.

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

The api_operation! method

Like api_operation, except that a Jsapi::Controller::ParametersInvalid exception is raised on invalid request parameters.

api_operation!('foo') do |api_params|
  # ...
end

The errors instance method of Jsapi::Controller::ParametersInvalid returns all of the validation errors encountered.

The api_definitions method

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)

Strong parameters

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.

Authentication

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
end

The api_authenticate method

The api_authenticate class method registers a handler to authenticate requests according to one or more security schemes.

api_authenticate 'basic_auth', with: :authenticate

If 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
end

If the handler returns a truthy value, the request is assumed to be authenticated successfully.

Authenticating requests

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

Callbacks

The 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.

api_after_authentication callbacks

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
end

Note 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.

api_after_validation callbacks

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|
  # ...
end

When 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

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 }
end

When 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

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
end

The api_action method

api_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|
  # ...
end

api_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.

The api_action! method

Like api_action, except that api_operation! is used instead of api_operation.

API models

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
end

To 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
end
api_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'
end

A 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
end

Mass assignment

When 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
end

The 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

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages