In this line of thought, strong_parameters lacks a few awesome validations that this gem provides.
For example, let's say you want your operation create
to require a Fixnum
parameter between 1 and 99. With strong_parameters, you're out of luck. With this gem, you simply write in your controller:
class Api::PeopleController < Api::BaseController
def create
validated_params = validate_params params do
param :age, type: Fixnum, allow: (1..99)
end
# Do something with the validated parameters.
end
end
So when you use this controller:
> app.post 'api/people', age: 12 # validated_params = { age: 12 }
> app.post 'api/people', age: 100 # throws a ParameterSchema::InvalidParameters
- You prefer a procedural approach (via a DSL) over a declarative one.
- You want more control over the parameters of your API, at the type and format level.
- You want to do things differently, you darn hipster ;)
Add in your Gemfile:
gem 'parameters_schema'
Add in your project:
require 'parameters_schema'
The schema is the heart of this gem. It provides a simple DSL to express an operation's parameters.
Creating a schema:
schema = ParametersSchema::Schema.new do
# Define parameters here...
# ... but an empty schema is also valid.
end
Validating parameters against a schema:
params = { potatoe: 'Eramosa' }
schema.validate!(params)
The minimal representation of a parameter is:
param :potatoe
This represents a required
parameter of type String
accepting any characters and which doesn't allow nil or empty values.
The valid options for a parameter are:
* required # Whether the parameter is required. Default: true.
* type # The type of the parameter. Default: String.
* allow # The allowed values of the parameter. Default: :any.
* deny # The denied values of the parameter. Default: :none.
* array # Whether the parameter is an array. Default: false.
The available types are:
* String
* Symbol
* Fixnum
* Float
* Date
* DateTime
* Array # An array of :any types.
* Hash # An object which members are not validated further.
* :boolean # See options for accepted values.
* :any # Accepts any value type.
To accept more than one type, you can do:
param :potatoe, type: [Boolean, String] # Accepts a boolean or string value.
To accept an array of a specific type, you can do:
param :potatoes, type: { Array => String } # Accepts an array of strings.
To deeper refine the schema of an object, you pass a block to the parameter:
param :potatoe do # Implicitly of type Hash
param :variety
param :origin
end
As you have seen above, a parameter can be of type Array
but can also have the option array
. Confusing, right? This option was introduced to simplify the type
syntax. For example:
param :potatoes, type: String, array: true # This is equivalent...
param :potatoes, type: { Array => String } # ... to this.
But this parameter truly shine with an array of objects:
param :potatoes, array: true do
param :variety
param :origin
end
# This syntax is also valid but less sexy:
param :potatoes, type: { Array => Hash } do
param :variety
param :origin
end
- A
Float
value can be passed to aFixnum
parameter but will loose its precision. - Some types accepts more than one representation. Example:
Symbol
accepts any type that respond to:to_sym
. - If you define multiple types (ex:
[Symbol, String]
), values are interpreted in this order. So the value'a'
will be cast to:a
. - Defining the type
{ Fixnum => Date }
doesn't make sense so it falls back toFixnum
(the key). { Array => Array }
is accepted. It means a 2D array of:any
.{ Array => Array => ... }
is not yet supported. Did I hear pull request?
By default, the value of a parameter can be any one in the spectrum of a type, with the exception of nil and empty. The allow
and deny
options can be used to further refine the accepted values.
To accept nil or empty values:
param :potatoe, allow: :nil
# => accepts nil, 'Kennebec' but not ''.
param :potatoe, allow: :empty
# => accepts '', 'Kennebec' but not nil.
param :potatoe, allow: [:nil, :empty]
# => accepts nil, '' and 'Kennebec'
Of course, this nil or empty restriction doesn't make sense for all the types so it will only be applied when it does.
To accept predefined values:
param :potatoe, allow: ['Superior', 'Ac Belmont', 'Eramosa'] # this is case-sensitive.
# Gotcha: this will allow empty values even if you wanted to accept the value 'empty'. You can redefine keywords in the options.
param :potatoe, type: Symbol, allow: [:superior, :ac_belmont, :empty]
To accept a value matching a regex:
param :potatoe, allow: /^[a-zA-Z]*$/
# Gotcha: even though the regex above allows empty values, it must be explicitly stated:
param :potatoe, allow: [:empty, /^[a-zA-Z]*$/]
To accept a value in a range:
param :potatoe, type: Fixnum, allow: (1..3)
# => accepts 1, 2, 3 but will fail on any other value.
The deny
option is conceptually identical to allow
but a value will fail the validation if a match is found:
param :potatoe, type: Fixnum, deny: (1..3)
# => accepts any value except 1, 2, 3.
The options allow
and deny
are validated independently. So beware to not define allow
and deny
options that encompass all the possible values of the parameter!
When the validation fails, an instance of ParametersSchema::InvalidParameters
is raised. This exception contains the attribute errors
which is an hash of { key: error_code }
that you can work with.
Simple case:
ParametersSchema::Schema.new do
param :potatoe
end.validate!({})
# => ParametersSchema::InvalidParameters
# @errors = { potatoe: :missing }
The validation process tries to accumulate as many errors as possible before raising the exception, so you can have a precise picture of what went wrong:
ParametersSchema::Schema.new do
param :potatoe do
param :name
param :type, allow: ['Atlantic']
end
end.validate!(potatoe: { type: 'Conestoga' })
# => ParametersSchema::InvalidParameters
# @errors = { potatoe: { name: :missing, type: :disallowed } }
The possible error codes are (in the order the are validated):
* :unknown # The parameter is provided but not defined in the schema.
* :missing # The parameter is required but is missing.
* :nil # The value cannot be nil but is nil.
* :empty # The value cannot be empty but is empty.
* :disallowed # The value has an invalid format (type/allow) other than nil/empty.
This gem can be used outside of Rails but was created with Rails in mind. For example, the parameters controller, action, format
are skipped by default (see Options section to override this behavior) and the parameters are defined in a Hash
. However, this gem doesn't insinuate itself in your project so you must manually add it in your controllers or anywhere else that make sense to you. Here is a little recipe to add validation in your API pipeline:
In the base controller of your API, add this helper:
# Validate the parameters of an action, using a schema.
# Returns the validated parameters and throw exceptions on invalid input.
def validate_params(¶meters_schema)
schema = ParametersSchema::Schema.new(¶meters_schema)
schema.validate!(params)
end
In the base controller of your API, add this exception handler:
# Handle errors related to invalid parameters.
rescue_from ParametersSchema::InvalidParameters do |e|
# Do something with the exception (ex: log it).
# Render the response.
render json: ..., status: :bad_request
end
Now in any controller where you want to validate the parameters, you can do:
def operation
validated_params = validate_params do
# ...
end
# ...
end
Options can be specified on the module ParametersSchema::Options
. Example:
ParametersSchema::Options.skip_parameters = [:internal_stuff]
Available options:
skip_parameters
an array of first-level parameters to skip. Default:[:controller, :action, :format]
.empty_keyword
the keyword used to represent an empty value. Default::empty
.any_keyword
the keyword used to represent any value. Default::any
.none_keyword
the keyword used to represent no value. Default::none
.boolean_keyword
the keyword used to represent a boolean value. Default::boolean
.nil_keyword
the keyword used to represent a nil value. Default::nil
.boolean_true_values
the accepted boolean true values. Not case-sensitive. Default:true
,'t'
,'true'
,'1'
,1
,1.0
.boolean_false_values
the accepted boolean false values. Not case-sensitive. Default:false
,'f'
,'false'
,'0'
,0
,0.0
.
Yes, please. Bug fixes, new features, refactoring, unit tests. Send your precious pull requests.
- Array of arrays of ...
- Min/Max for numeric values
- More
allow
options - Better refine error codes
Parameters Schema is released under the MIT License.