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 and 3.1.
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 app/api_defs/echo.rb
:
# app/api_defs/echo.rb
operation path: '/echo' do
parameter 'call', type: 'string', existence: true
response 200, 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! status: 200 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 app/api_defs/echo.rb
:
# app/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'
# app/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.
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 200, type: 'object' do
property 'echo', type: 'string'
end
response 400, 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 200, type: 'object' do
property 'echo', type: 'string'
end
response 400, 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
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:
: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 summary of the operation.:tags
- The tags to group operations in an OpenAPI document.
All keywords except :model
, :parameters
, :request_body
and :responses
are only used to
describe the operation in an OpenAPI document. 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
operation '{$request.query.bar}', path: '/bar'
end
end
The one and only positional argument specifies the mandatory name of the callback. The nested
operation
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
operation '{$request.query.bar}', path: '/bar'
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
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:
:example
,:examples
- See Specifying examples.:in
- The location of the parameter. Possible locations are"header"
,"path"
and"query"
. The default location is"query"
.:openapi_extensions
- See Specifying OpenAPI extensions.:ref
- Refers a reusable parameter.
The :example
, examples
and :openapi_extensions
keywords are only used to describe a
parameter in an OpenAPI document.
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
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:
: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 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'
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
A response that may be produced by an operation is defined by a nested response
directive,
for example:
api_operation do
response 200 do
property 'foo', type: 'string'
end
end
The optional positional argument specifies the response status. 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.:locale
- The locale to be used when rendering a response.:openapi_extensions
- See Specifying OpenAPI extensions.:ref
- Refers a reusable response.
The :locale
keyword allows to produce responses in different languages depending on status
code. This can especially be used to return error responses in English regardless of the
language of regular responses.
The :example
, :examples
, :headers
, :links
and :openapi_extensions
keywords are only
used to describe a response in an OpenAPI document.
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 400, ref: 'error'
end
api_operation do
response 400, 'error'
end
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
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
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 orProc
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 }
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 an OpenAPI or JSON Schema document. 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:
:present
ortrue
- The parameter or property value must not be empty.:allow_empty
- The parameter or property value can be empty, for example''
.:allow_nil
orallow_null
- The parameter or property value can benil
.:allow_omitted
orfalse
- 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
end
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 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
end
The :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'
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
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.
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
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.
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.
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.
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'
end
api_operation do
security_requirement do
scheme 'basic_auth'
end
end
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
- Specifies whethervalue
is the URI of an external example.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'
end
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.:name
- The name 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'
end
Both 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, a rescue handler can be defined by
an api_rescue_from
directive, for example:
api_rescue_from Jsapi::Controller::ParametersInvalid, with: 400
The one and only positional argument specifies the exception class to be rescued. The :with
keyword specifies the status 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.
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 apps/api_defs
. Directives
within files are specified as in api_definitions
blocks without prefix api_
, for example:
# app/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, app/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.
# app/api_defs/foo/bar.rb
import 'foo/shared'
# app/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'
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
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
Note that Jsapi::Controller::Base
inherits from ActionController::API
and includes
Jsapi::DSL
as well as Jsapi::Controller::Methods
.
The 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.
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
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. status
specifies the HTTP status code of the response to be selected.
If status
is not present, the default response of the API operation is selected.
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.
This method implicitly renders the JSON representation of the object returned by the block when the content type is a JSON MIME type. This can be a hash or an object providing corresponding methods for all properties of the response.
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. status
specifies the HTTP status code of
the response to be selected. If status
is not present, the default response of the API
operation is selected.
If an exception is raised while performing the block, an error response according to the first matching rescue handler is rendered and all of the callbacks are called. If no matching rescue handler could be found, 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|
# ...
end
The 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
.
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