ValidateMyRoutes
Parameter Validation for Sinatra
REST specification defines how to consume web API, however it does not provide specification for parameters and API consumers are left with guessing how to use each specific endpoint.
This gem provides simple way to validate parameters for Sinatra routes.
Validation is done automatically before trying to execute route block, and if it fails, consumer
receives response with status code 4**
and validation failure message, explaining what was
expected from parameters.
Installation
ValidateMyRoutes can be installed via RubyGems:
$ gem install validate_my_routes
Or compiling and installing it yourself as:
$ gem build validate_my_routes.gemspec
$ gem install validate_my_routes-VERSION.gem
Usage
Using ValidateMyRoutes is quite straightforward and it basically requires two steps:
- register
ValidateMyRoutes::Validatable
extension in your Sinatra application - define parameters with validation
Example
require 'sinatra/base'
require 'validate_my_routes'
class MyApp < Sinatra::Base
# register validate my routes Sinatra extension
register ValidateMyRoutes::Validatable
# add validation rules on class level
extend ValidateMyRoutes::ValidationRules
param_validation :pet_type, from_enum(%w(cat dog))
get '/pet/:pet_type' do |pet_type|
"Here is your pet: #{pet_type}"
end
end
Rack::Handler::WEBrick.run MyApp.new
As a result consumer of such API will receive:
GET http://example.org/pet/dog
> HTTP/1.1 200 OK
> Here is your pet: dog
GET http://example.org/pet/bird
> HTTP/1.1 404 Not Found
> parameter <pet_type> was expected to have one of following values: <cat, dog>, but was <bird>
For more examples check out examples
folder in the repository that contains plenty more of
examples: here
Parameters validation
ValidateMyRoutes can validate parameters from the path by their name:
param_validation :order_id, of_type(Integer)
or validate all parameters for the route:
all_params_validation at_least_one_of(%i[order_type order_status])
Note: validating single parameter from path by name will validate all routes defined after such validation that contain defined name. i.e. if you define:
param_validation :id, of_type(Integer) get('/pet/:id') { '' } get('/order/:id') { '' }ValidateMyRoutes will validate both routes. If
order
route should not have the same validation forid
parameter make sure it has a different name (i.e.:order_id
instead of:id
)
Validation rules
Parameter validation is done by using validation rules:
param_validation :order_id, of_type(Integer)
get '/orders/:order_id' do
...
end
In the example you can see of_type(Integer)
validation rule to validate the type of parameter:
GET http://example.org/orders/5
> HTTP/1.1 200 OK
> Your order is waiting an aproval
GET http://example.org/orders/foo
> HTTP/1.1 404 Not Found
> was expected pet_id parameter to be of a type <Integer>, but was <foo>
The gem provides built-in validation rules for path parameters validation by name like of_type(Integer)
(you can find the full list with examples in autogenerated documentation)
And there are built-in validation rules for all parameters validation for single route like
required(:order_status)
(you can find the full list with examples in autogenerated documentation)
The gem provides built-in validation rules for all parameters validation:
-
required(param_name)
- fails if parameter was not provided -
only_one_of(list_of_names)
- mutually exclusive optional parameters -
exactly_one_of(list_of_names)
- mutually exclusive with at least one provided -
at_least_one_of(list_of_names)
- non-exclusive parameters with at least on provided
Rule combinators
To use multiple validation steps for single parameter ValidateMyRoutes provides combinators for rules, for example:
param_validation :id, of_type(Integer).and(eql('5'))
get '/pet/:id' do |id|
"Pet id #{id} is Integer and is equal to '5'"
end
You can find the full list with examples in autogenerated documentation
Types transformations for further validation
Some validation rules require parameter value to be converted to some type. For example
validation rule between(2, 10)
expects parameter value to be Integer
so it can be compared
with 2
and 10
.
For such case ValidateMyRoutes provides transformation rules:
-
value_as(TYPE, OTHER_RULE)
- transforms parameter value to typeTYPE
and then performs validation ofOTHER_RULE
with transformed value. In case when convertion to specified type fails validation will fail with type validation failure -
transform(TRANSFORMATION, OTHER_RULE)
- transforms parameter value by callingTRANSFORMATION
procedure with parameter value and performs validation ofOTHER_RULE
with transformed value
param_validation :order_id, value_as(Integer, greater_than(5))
and with custom transformation:
to_order = ->(order_type) { OrderTypesFactory.load(order_type) }
param_validation :order_type, transform(to_order, order_validator)
For more complete examples please check out here
Note: transformation is applied for parameter values only for validation. ValidateMyRoutes will not change the type or transform the value that will be passed in the route. For more information on why check out here
Conditional validation
In Sinatra, you can use conditions to determine what route to use depending on parameters value.
ValidateMyRoutes provides a special rule conditional(RULE)
to inform Sinatra to try another
route with the same path if validation failed instead of returning validation error to the consumer.
Example:
all_params_validation conditional(required(:q))
get '/pet' do
'request contains parameter <q> so it is a search request'
end
get '/pet' do
'no <q> parameter provided so we want to get a list of all pets'
end
In this example, Sinatra will try validation for the first route, and if parameter q
is not
present, it will try second route because validation for the parameter q
is conditional.
More information can be found in autogenerated documentation
Inline validation
It is possible to perform validation of any value when executing route code block. For this you need:
- use
ValidateMyRoutes::Validate.validate
with validation rule and value you want to validate, following by a block with action to perform if validation fails
Example:
post '/some_route' do
ValidateMyRoutes::Validate.validate(of_type(Integer), params['my_param']) do |msg|
halt 400, "my_param validation failed with: #{msg}"
end
'my_param is valid'
end
More information can be found in autogenerated documentation
Custom validation rules
ValidateMyRoutes provides a DSL for creating validation rules for single parameter validation:
extend ValidateMyRoutes::ValidationRules
def_single_param_validator :custom_eql do |expected|
validate { |actual, name| actual == expected }
end
Or for all parameters validation:
extend ValidateMyRoutes::ValidationRules
def_all_params_validator :custom_required do |expected|
validate { |params| params.key? expected }
end
More information can be found in autogenerated documentation
Extra notes
Architecture notes can be found here
Changelog can be found here
Thoughts behind ValidateMyRoutes and why is it design in this way can be found here