Paramore
Paramore is a small gem intended to make strong parameter definitions declarative and provide a unified way to format and sanitize their values outside of controllers.
Installation
In your Gemfile:
gem 'paramore'
In your terminal:
$ bundle
Usage
Without formatting/sanitizing
declare_params :item_params
item: [:name, :description, :for_sale, :price, metadata: [tags: []]]
This is completely equivalent (including return type) to
def item_params
@item_params ||= params
.require(:item)
.permit(:name, :description, :for_sale, :price, metadata: [tags: []])
end
With formatting/sanitizing
A common problem in app development is untrustworthy input given by clients. That input needs to be sanitized and potentially formatted and type-cast for further processing. A naive approach could be:
# items_controller.rb
def item_params
@item_params ||= begin
_params = params
.require(:item)
.permit(:name, :description, :price, metadata: [tags: []])
_params[:name] = _params[:name].strip.squeeze(' ') if _params[:name]
_params[:description] = _params[:description].strip.squeeze(' ') if _params[:description]
_params[:for_sale] = _params[:for_sale].in?('t', 'true', '1') if _params[:for_sale]
_params[:price] = _params[:price].to_d if _params[:price]
if _params.dig(:metadata, :tags)
_params[:metadata][:tags] =
_params[:metadata][:tags].map { |tag_id| Item.[tag_id.to_i] }
end
_params
end
end
This approach clutters controllers with procedures to clean data, which leads to repetition and difficulties refactoring. The next logical step is extracting those procedures - this is where Paramore steps in:
# app/controllers/items_controller.rb
declare_params :item_params
item: [:name, :description, :price, metadata: [tags: []]],
format: {
name: :Text,
description: :Text,
for_sale: :Boolean,
price: :Decimal,
metadata: {
tags: :ItemTags
}
}
# app/formatter/text.rb
module Formatter::Text
module_function
def run(input)
input.strip.squeeze(' ')
end
end
# app/formatter/boolean.rb
module Formatter::Boolean
TRUTHY_TEXT_VALUES = %w[t true 1]
module_function
def run(input)
input.in?(TRUTHY_TEXT_VALUES)
end
end
# app/formatter/decimal.rb
module Formatter::Decimal
module_function
def run(input)
input.to_d
end
end
# app/formatter/item_tags.rb
module Formatter::ItemTags
module_function
def run(input)
input.map { |tag_id| Item.[tag_id.to_i] }
end
end
Now, given params
are:
<ActionController::Parameters {
"unpermitted"=>"parameter",
"name"=>"Shoe \n",
"description"=>"Black, with laces",
"for_sale"=>"true",
"price"=>"39.99",
"metadata"=><ActionController::Parameters { "tags"=>["38", "112"] } permitted: false>
} permitted: false>
Calling item_params
will return:
<ActionController::Parameters {
"name"=>"Shoe",
"description"=>"Black, with laces",
"for_sale"=>true,
"price"=>39.99,
"metadata"=><ActionController::Parameters { "tags"=>[:shoe, :new] } permitted: true>
} permitted: true>
This is useful when the values are not used with Rails models, but are passed to simple functions for processing. The formatters can also be easily reused anywhere in the app, since they are completely decoupled from Rails.
Configuration
Running $ paramore
will generate a configuration file located in config/initializers/paramore.rb
.
config.formatter_namespace
- default isFormatter
. Set tonil
to have top level named formatters (this also allows specifying the formatter object itself, eg.:name: Formatter::Text
).config.formatter_method_name
- default isrun
. Don't set tonil
:D
Safety
- Formatters will not be called if their parameter is missing (no key in the param hash)
- Formatters are validated - all given formatter names must match actual modules/classes defined in the app
and must respond to the configured
formatter_method_name
. This means that all used formatters are loaded when the controller is loaded.
License
Paramore is released under the MIT license: