Parametric
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]. # => ['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
- Fork it ( http://github.com/ismasan/parametric/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request