Parameter declaration framework and browsable API for Sinatra.
Sinatra has tools to define GET, POST, DELETE etc. routes. It doesn't have any tools to define their parameters.
There exist several frameworks that generate documentation based on comment blocks above routes. The problem is you have to update these every time something is changed in the code. We have seen programmers forget this in too many projects.
Parameter Declaration
We believe in using actual code as documentation. Take a look at the following example.
require "sinatra/base"
require "sinatra/browse"
class App < Sinatra::Base
register Sinatra::Browse
description "Creates a new user"
param :display_name, :String, required: true
param :type, :String, in: ["admin", "moderator", "user"], default: "user"
param :age, :Integer
param :gender, :String, in: ["m", "w", "M", "W"], transform: :upcase
param :activated, :Boolean, default: false
param :email, :String, format: /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/
post "/users" do
# ... some code that creates a new user
end
endHere we have a clear list of what parameters are expected and how they are validated. Since this is code and not a comment block, it will always be up to date with the behaviour of our application.
The syntax is inspired by the sinatra-param and thor projects.
Browsable API
Sinatra-browse automatically adds another route simply called browse. Surfing to it will display documentation generated from the parameter definitions.
http://<api_ip_address>:<api_port>/browse
At the time of writing four parameter types are available.
:String:Integer:Float:Boolean:DateTime
Remarks:
A Boolean parameter will be true for values: "1", "true", "t", "yes", "y" and false for values: "0", "false", "f", "no", "n"
A DateTime parameter relies on Ruby's DateTime#parse method and can thus handle all formats this method can.
Examples:
'Sat, 03 Feb 2001 04:05:06 GMT'
'Sat, 3 Feb 2001 04:05:06 +0700'
'2001-02-03T04:05:06+07:00'
'H13.02.03T04:05:06+07:00'
'2014/02/05'
'march 2nd'
You can set default values in your declarations. These will be used when the parameter in question wasn't provided in the request. You can either set the default value or provide a proc/lambda to generate it.
param :media_type, :String, default: "book"
param :year, :Integer, default: lambda { Time.now.year }Parameters declarations can have an optional arbitrary discription to be included in the generated documentation.
# Both of these syntaxes will work.
param :name, :Name, description: "What your mother calls you."
param :age, :Integer, desc: "The amount of years you've spent on this planet."You can write some quick validation logic directly in the parameter declaration. If the validation fails, either a standard 400 error will be returned or a custom error block will execute if provided.
required Fails if this parameter is not provided in the request.
param :you_must_include_me, :String, required: truedepends_on Some times there are parameters that are required by other parameters. Use depends_on to implement this. The example below will allow you to send post_something without any other parameters. If you send user_name though, you will be required to send password along with it.
param :post_something, :String
param :user_name, :String, depends_on: :password
param :password, :Stringin Fails if this parameter is not included in the values set by in. You can use anything that responds to .member? like an array or a range.
param :single_digit, :Integer, in: 1..9
param :small_prime_number, :Integer, in: Prime.take(10)
param :order, :String, in: ["ascending", "descending"]In the case of numeric values like :Integer, :Float and :DateTime, you can specify a minimum and/or maximum value.
min The parameter must be greater than or equal to this.
param :min_example, :Integer, min: 128max The parameter must be lesser than or equal to this.
param :max_example, :Float, max: 66.666In the case of :DateTime, min/max validators van be defined as either ruby's DateTime class or a String representation that Ruby's DateTime#parse method can handle.
param :string_min, :DateTime, min: '2014/02/05'
param :date_max, :DateTime, max: DateTime.ordinal(2005,34,4,5,6,'+7')The following parameter validators can only be used for parameters of type :String.
format The string must match this regular expression.
param :alphanumeric, :String, format: /^[0-9A-Za-z]*$/min_length The string must be of this length or longer.
param :five_or_longer, :String, min_length: 5max_length The string must be of this length or shorter.
param :five_or_shorter, :String, max_length: 5You can use transform to execute a quick method on any prameter provided. Anything that responds to to_proc will do.
param :only_caps, :String, transform: :upcase
param :power_of_two, :Integer, transform: proc { |n| n * n }When a validation fails, a standard 400 error will be returned. You can override this and do your own error handling using on_error.
param :lets_fail, :Integer, in: 1..9, on_error: proc { halt 400, "Must be between 1 and 9!" }
get 'example_route' do
# This is the scope that the on_error proc will be executed in.
endIf a request is made that fails validation on the lets_fail parameter, then the proc provided to on_error will be called in the same scope as your route. Therefore you have access to Sinatra keywords such as halt.
If you want to write a bit more intricate error handling, you can add the error hash as an argument to your on_error proc. This hash holds some extra information about what exactly went wrong.
param :lets_fail, :Integer, in: 1..9, required: true, on_error: proc { |error_hash|
case error_hash[:reason]
when :in
halt 400, "Must be between 1 and 9!"
when :required
halt 400, "Why u no give us lets_fail?"
end
}
get 'example_route' do
# Some code
endThe error hash contains the following keys:
:reasonThis tells you what validation failed. Possible values could be:in,:required,:format, etc.:parameterThe name of the faulty parameter.:valueThe value our parameter had which caused it to fail validation.:typeThe type of our parameter. Could be:String,:Integer, etc.- Any validation keys that were set in the parameter declaration will also be available in the error hash.
So we explained how to do error handling for single parameters. Now what if we wanted to set error handling for the entire application? You can do that with the default_on_error method.
default_on_error do |error_hash|
case error_hash[:reason]
when :required
halt 400, "#{error_hash[:parameter]} is required! provide it!"
else
_default_on_error(error_hash)
end
end
param :a, :String, in: ["a"], required: true
param :b, :String, format: /^bbb$/
get "/features/default_error_override" do
# Again this is the scope that default_on_error is executed in
params.to_json
endThe block we passed to the default_on_error method will be called or every parameter in our application that fails validation and does not have its own on_error block. Notice how inside our default_on_error
You might notice that in our example, the default_on_error method makes a call to _default_on_error. The latter is a fallback to sinatra-browse's standard error behaviour. It's available form both the default_on_error block and procs passed to on_error in parameter declarations.
By default sinatra-browse removes all parameters that weren't defined. You can disable this behaviour with the following line.
disable :remove_undefined_parametersYou can also set a allowed_undefined_parameters variable to allow for a select few parameters that aren't removed.
set allowed_undefined_parameters: [ "id", "username", "password" ]Unfortunately you are not able to use Sinatra-browse for named parameters in the route definition. Take the following example.
get 'foo/:bar' do
# some code
endYou will not be able to define the parameter bar. This is because Sinatra-browse does its thing in a before block and these parameters aren't added to the params hash until the route itself gets executed.
Some exta discussion of this problem can be found here.