Project

paramore

0.0
Low commit activity in last 3 years
No release in over a year
Paramore lets you declare which parameters are permitted and what object is responsible for typing/sanitizing/type-casting them before they are passed along to your models/domain. It is intended to reduce the amount of imperative code in controllers.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 2.0
>= 0

Runtime

>= 5.0
 Project Readme

Paramore

Paramore allows you to declare a typed schema for your params, so that any downstream code can work with the data it expects.

Installation

In your Gemfile:

gem 'paramore'

In your terminal:

$ bundle

Usage

Without typing

param_schema :item_params,
  item: [:name, :description, :for_sale, :price, metadata: [tags: []]]

This is completely equivalent (including the return type) to

def item_params
  @item_params ||= params
    .require(:item)
    .permit(:name, :description, :for_sale, :price, metadata: [tags: []])
end

With typing

A common problem in app development is untrustworthy input given by clients. That input needs to be sanitized and typecast for further processing. A naive approach could be:

# items_controller.rb

def item_params
  @item_params ||= begin
    _params = params
      .require(:item)
      .permit(:name, :description, :for_sale, :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.tags[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

class ItemsController < ApplicationController
  def create
    Item.create(item_params)
  end

  param_schema :item_params,
    item: Paramore.field({
      name: Paramore.field(Paramore::SanitizedString),
      description: Paramore.field(Paramore::StrippedString, null: true),
      for_sale: Paramore.field(Paramore::Boolean, default: false),
      price: Paramore.field(Paramore::Decimal),
      metadata: Paramore.field({
        tags: Paramore.field([Types::ItemTag], compact: true)
      })
    })
end

Types::ItemTag could be your own type:

# app/types/item_tag.rb

module Types::ItemTag
  module_function
  def [](input)
    Item.tags[input.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 types can also be easily reused anywhere in the app, since they are completely decoupled from Rails.

Notice that the Paramore::StrippedString does not perform .squeeze(' '), only Paramore::SanitizedString does.

nil

Types are non-nullable by default and raise exceptions if the param hash misses any. This can be disabled for any type by declaring Paramore.field(Paramore::Int, null: true).

nils will usually not reach any of the type classes - if some parameter is nullable, the class will not be called. If a parameter is non-nullable, then a Paramore::NilParameter error will be raised before calling the class. If an incoming array contains nils, they will get passed to type classes. If you wish to get rid of empty array elements, declare Paramore.field(Paramore::Int, compact: true).

Configuration

Running $ paramore will generate a configuration file located in config/initializers/paramore.rb.

  • config.type_method_name - default is [], to allow using, for example, SuperString["foo"] syntax. Note that changing this value will preclude you from using built in types.

Safety

  • Types will not be called if their parameter is missing (no key in the param hash)
  • All given types must respond to the configured type_method_name and an error will be raised when controllers are loaded if they don't.

License

Paramore is released under the MIT license: