Parametric

Build Status Gem Version

DSL for declaring allowed parameters with options, regexp pattern and default values.

Useful for building self-documeting APIs, search or form objects.

Usage

Declare your parameters

class OrdersSearch
  include Parametric::Params
  param :q, 'Full text search query'
  param :page, 'Page number', default: 1
  param :per_page, 'Items per page', default: 30
  param :status, 'Order status', options: ['checkout', 'pending', 'closed', 'shipped'], multiple: true
end

Populate and use. Missing keys return defaults, if provided.

order_search = OrdersSearch.new(page: 2, q: 'foobar')
order_search.params[:page] # => 2
order_search.params[:per_page] # => 30
order_search.params[:q] # => 'foobar'
order_search.params[:status] # => nil

Undeclared keys are ignored.

order_search = OrdersSearch.new(page: 2, foo: 'bar')
order_params.params.has_key?(:foo) # => false
order_search = OrderParams.new(status: 'checkout,closed')
order_search.params[:status] #=> ['checkout', 'closed']

Search object pattern

A class that declares allowed params and defaults, and builds a query.

class OrdersSearch
  include Parametric::Params
  param :q, 'Full text search query'
  param :page, 'Page number', default: 1
  param :per_page, 'Items per page', default: 30
  param :status, 'Order status', options: ['checkout', 'pending', 'closed', 'shipped'], multiple: true
  param :sort, 'Sort', options: ['updated_on-desc', 'updated_on-asc'], default: 'updated_on-desc'

  def results
    query = Order.sort(params[:sort])
    query = query.where(["code LIKE ? OR user_name LIKE ?", params[:q]]) if params[:q]
    query = query.where(status: params[:status]) if params[:status].any?
    query = query.paginate(page: params[:page], per_page: params[:per_page])
  end
end

:match

Pass a regular expression to match parameter value. Non-matching values will be ignored or use default value, if available.

class OrdersSearch
  include Parametric::Params
  param :email, 'Valid email address', match: /\w+@\w+\.\w+/
end

:options array

Declare allowed values in an array. Values not in the options will be ignored or use default value.

class OrdersSearch
  include Parametric::Params
  param :sort, 'Sort', options: ['updated_on-desc', 'updated_on-asc'], default: 'updated_on-desc'
end

:multiple values

:multiple values are separated on "," and treated as arrays.

class OrdersSearch
  include Parametric::Params
  param :status, 'Order status', multiple: true
end

search = OrdersSearch.new(status: 'closed,shipped,abandoned')
search.params[:status] # => ['closed', 'shipped', 'abandoned']

If :options array is declared, values outside of the options will be filtered out.

class OrdersSearch
  include Parametric::Params
  param :status, 'Order status', options: ['checkout', 'pending', 'closed', 'shipped'], multiple: true
end

search = OrdersSearch.new(status: 'closed,shipped,abandoned')
search.params[:status] # => ['closed', 'shipped']

When using :multiple, results and defaults are always returned as an array, for consistency.

class OrdersSearch
  include Parametric::Params
  param :status, 'Order status', multiple: true, default: 'closed'
end

search = OrdersSearch.new
search.params[:status] # => ['closed']

:nullable fields

In same cases you won't want Parametric to provide nil or empty keys for attributes missing from the input. For example when missing keys has special meaning in your application.

In those cases you can add the :nullable option to said param definitions:

class OrdersSearch
  include Parametric::Params
  param :query, 'Search query. optional', nullable: true
  param :tags, 'Tags', multiple: true
end

search = OrdersSearch.new({})
search.params # {tags: []}

available_params

#available_params returns the subset of keys that were populated (including defaults). Useful for building query strings.

order_search = OrdersSearch.new(page: 2, foo: 'bar')
order_search.available_params # => {page: 2, per_page: 50}

schema

#schema returns a data structure including meta-data on each parameter, such as "label" and "options". Useful for building forms or self-documented Hypermedia APIs (or maybe json-schema endpoints).

order_search.schema[:q].label # => 'Full text search query'
order_search.schema[:q].value # => ''

order_search.schema[:page].label # => 'Page number'
order_search.schema[:page].value # => 1

order_search.schema[:status].label # => 'Order status'
order_search.schema[:status].value # => ['pending']
order_search.schema[:status].options # => ['checkout', 'pending', 'closed', 'shipped']
order_search.schema[:status].multiple # => true
order_search.schema[:status].default # => 'closed'

Coercing values

Param definitions take an optional :coerce option with a symbol or proc to coerce resulting values.

class UsersSearch
  include Parametric::Params
  param :age, 'User age', coerce: :to_i
  param :name, 'User name', coerce: lambda{|name| "Mr. #{name}"}
end

search = UsersSearch.new(age: '36', name: 'Ismael')

search.available_params[:age] # => 36
search.available_params[:name] # => 'Mr. Ismael'

Parametric::TypedParams

The Parametric::TypedParams module includes extra DSL methods to coerce values to standard Ruby types.

class UsersSearch
  include Parametric::TypedParams
  integer :age, 'User age'
  array :accounts
  string :country_code
  # you can still use :coerce
  param :name, 'User name', coerce: lambda{|name| "Mr. #{name}"}
end

Parametric::Hash

The alternative Parametric::Hash class makes your objects quack like a hash, instead of exposing the #params object directly.

class OrdersParams < Parametric::Hash
  param :q, 'Full text search query'
  integer :page, 'Page number', default: 1
  integer :per_page, 'Items per page', default: 30
  array :status, 'Order status', options: ['checkout', 'pending', 'closed', 'shipped']
end
order_params = OrdersParams.new(page: 2, q: 'foobar')
order_params[:page] # => 2
order_params[:per_page] # => 30
order_params.each{|key, value| ... }

Nested structures

You can also nest parameter definitions. This is useful if you need to model POST payloads, for example.

class AccountPayload
  include Parametric::Params
  param :status, 'Account status', default: 'pending', options: ['pending', 'active', 'cancelled']
  param :users, 'Users in this account', multiple: true do
    param :name, 'User name'
    param :title, 'Job title', default: 'Employee'
    param :email, 'User email', match: /\w+@\w+\.\w+/
  end
  param :owner, 'Owner user' do
    param :name, 'User name'
    param :email, 'User email', match: /\w+@\w+\.\w+/
  end
end

The example above expects a data structure like the following:

{
  status: 'active',
  users: [
    {name: 'Joe Bloggs', email: '[email protected]'},
    {name: 'jane Bloggs', email: '[email protected]', title: 'CEO'}
  ],
  owner: {
    name: 'Olivia Owner',
    email: '[email protected]'
  }
}

Use cases

In Rails

You can use one-level param definitions in GET actions

def index
  @search = OrdersSearch.new(params)
  @results = @search.results
end

I use this along with Oat in API projects:

def index
  search = OrdersSearch.new(params)
  render json: OrdersSerializer.new(search)
end

You can use nested definitions on POST/PUT actions, for example as part of your own strategy objects.

def create
  @payload = AccountPayload.new(params)
  if @payload.save
    render json: AccountSerializer.new(@payload.order)
  else
    render json: ErrorSerializer.new(@payload.errors), status: 422
  end
end

You can also use the #schema metadata to build Hypermedia "actions" or forms.

# /accounts/new.json
def new
  @payload = AccountPayload.new
  render json: JsonSchemaSerializer.new(@payload.schema)
end

Installation

Add this line to your application's Gemfile:

gem 'parametric'

And then execute:

$ bundle

Or install it yourself as:

$ gem install parametric

Contributing

  1. Fork it ( http://github.com/ismasan/parametric/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request