Parameters Schema
This gem is an alternative to strong_parameters to validate data at the controller level.
For example, let's say you want your operation create to require a Fixnum parameter between 1 and 99:
class Api::PeopleController < Api::BaseController
def create
validated_params = validate_params params do
param :age, type: Fixnum, allow: (1..99)
end
@person = Person.create!(validated_params)
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
Why use this gem instead of strong_parameters:
- You want more control over the parameters of your API, at the type and format level.
- You want a strict API that will only accept well-formed requests.
- You don't grasp the strong_parameters syntax and wants something more like a DSL.
- You want to validate data outside of Rails.
Installation
Add in your Gemfile:
gem 'parameters_schema'
Add in your project (not required for Rails):
require 'parameters_schema'
Quick start
Read this then Integrate with Rails if you want to use this gem quickly.
Let's take the example at the beginning of this README, with more details:
class Api::PeopleController < Api::BaseController
def create
# 1. For each request, you define a schema (i.e. the parameters of the request).
schema = ParametersSchema::Schema.new do
param :age, type: Fixnum, allow: (1..99)
end
# 2. When a user makes a request, you validate it against the schema.
# - When the request is invalid, an exception is raised.
# - When the request is valid, you receive a sanitized hash.
validated_params = schema.validate!(params)
@person = Person.create!(validated_params)
end
end
Now, Integrate with Rails gives pointers on how to simplify all this.
Schema
The schema is the heart of this gem. With a simple DSL, you define the parameters of a request.
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.
* default # Default value when the parameter is missing. Default: nil.
Notes on default option:
- When set, the
requiredoption becomes automaticallyfalse. - When set, the provided value is always kept in sanitized hash, even if the value is
nil. - Cannot be used on a
arrayorhashtype (but can be used on the fields of an hash!).
Parameter types
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
Gotchas
- A
Floatvalue can be passed to aFixnumparameter but will loose its precision. - Some types accepts more than one representation. Example:
Symbolaccepts 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?
The allow and deny options
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!
Exceptions
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.
Integrate with Rails
This gem can be used outside of Rails but was created with Rails in mind. For example, the parameters controller, action and 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.
# Note: this helper could be refined to cache the schemas.
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
Options can be specified on the module ParametersSchema::Options. Example:
ParametersSchema::Options.skip_parameters = [:internal_stuff]
Available options:
skip_parametersan array of first-level parameters to skip. Default:[:controller, :action, :format].empty_keywordthe keyword used to represent an empty value. Default::empty.any_keywordthe keyword used to represent any value. Default::any.none_keywordthe keyword used to represent no value. Default::none.boolean_keywordthe keyword used to represent a boolean value. Default::boolean.nil_keywordthe keyword used to represent a nil value. Default::nil.boolean_true_valuesthe accepted boolean true values. Not case-sensitive. Default:true,'t','true','1',1,1.0.boolean_false_valuesthe accepted boolean false values. Not case-sensitive. Default:false,'f','false','0',0,0.0.
Contribute
Yes, please. Bug fixes, new features, refactoring, unit tests. Send your precious pull requests.
Developing this gem
- Fork this repository.
- Clone your fork.
- (Optional) Prepare it with
rbenvorRVM. Ex:echo 2.3.0 > .ruby-version. bundle installto install the gems.rake testto launch the tests suite.- Make your changes and send me pull requests.
Ideas
- Array of arrays of ...
minandmaxoptions for numeric values (instead ofallow: (min..max))- More
allowoptions - Better refine error codes
License
Parameters Schema is released under the MIT License.