sinatra-browse

Parameter declaration framework and browsable API for Sinatra.

What problem do we solve?

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.

How do we solve it?

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
end

Here 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

Parameter types

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'

Default values

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 }

Describing parameters

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."

Parameter validation

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: true

depends_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, :String

in 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"]

Minimum and maximum validation

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: 128

max The parameter must be lesser than or equal to this.

param :max_example, :Float, max: 66.666

In 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')

String validation

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: 5

max_length The string must be of this length or shorter.

param :five_or_shorter, :String, max_length: 5

Parameter transformation

You 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 }

Error handling

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.
end

If 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.

The error hash

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
end

The error hash contains the following keys:

  • :reason This tells you what validation failed. Possible values could be :in, :required, :format, etc.
  • :parameter The name of the faulty parameter.
  • :value The value our parameter had which caused it to fail validation.
  • :type The 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.

Overriding default error behaviour

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
end

The 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.

Removing undefined parameters

By default sinatra-browse removes all parameters that weren't defined. You can disable this behaviour with the following line.

disable :remove_undefined_parameters

You 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" ]

Named parameters in route patterns

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
end

You 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.