README.md in decanter-0.5.4 vs README.md in decanter-0.5.5

- old
+ new

@@ -1,370 +1,346 @@ -ParamsTransformer +Decanter === -Installation +[![Code Climate](https://codeclimate.com/github/LaunchPadLab/decanter/badges/gpa.svg)](https://codeclimate.com/github/LaunchPadLab/decanter) [![Test Coverage](https://codeclimate.com/github/LaunchPadLab/decanter/badges/coverage.svg)](https://codeclimate.com/github/LaunchPadLab/decanter/coverage) --- -Add Gem: +What is Decanter? +--- + +Decanter is a Rails gem that makes it easy to manipulate form data before it hits the model. The basic idea is that form data entered by a user often needs to be processed before it is stored into the database. A typical example of this is a datepicker. A user selects January 15th, 2015 as the date, but this is going to come into our controller as a string like "01/15/2015", so we need to convert this string to a Ruby Date object before it is stored in our database. Many developers perform this conversion right in the controller, which results in errors and unnecessary complexity, especially as the application grows. + +Installation +--- + ```ruby gem "decanter" ``` ``` bundle ``` +Add the following to application.rb so we can load your decanters properly: +``` +config.paths.add "app/decanter", eager_load: true +config.to_prepare do + Dir[ File.expand_path(Rails.root.join("app/decanter/**/*.rb")) ].each do |file| + require_dependency file + end +end +``` + Basic Usage --- -Create a class to process your form and have it inherit from ParamsTransformer::Base. I typically create the following folder to hold my form classes: app/classes/forms +``` +rails g decanter Trip name:string start_date:date end_date:date +``` -**app/classes/forms/property.rb** +**app/decanter/decanters/trip_decanter.rb** ```ruby -class Forms::Property < ParamsTransformer::Base - - input :bedrooms, :integer - input :price, :float - input :has_air_conditioning, :boolean - input :has_parking, :boolean - input :has_laundry, :boolean - +class TripDecanter < Decanter::Base + input :name, :string + input :start_date, :date + input :end_date, :date end ``` In your controller: ```ruby def create - @form = Forms::Property.new(params) + @trip = Trip.decant_new(params[:trip]) - if @form.save + if @trip.save redirect_to trips_path else - set_index_variables - render "index" + render "new" end end + + def update + @trip = Trip.find(params[:id]) + + if @trip.decant_update(params[:trip]) + redirect_to trips_path + else + render "new" + end + end ``` -Inheritance +Basic Example --- -General pattern: +We have a form where users can create a new Trip, which has the following attributes: name, start_date, and end_date -- forms/property.rb -- forms/property/create.rb (inherits from forms/property.rb) -- forms/property/update.rb (inherits from forms/property.rb) +Without Decanter, here is what our create action may look like: - ```ruby -# app/classes/forms/property.rb -class Forms::Property < ParamsTransformer::Base +class TripsController < ApplicationController + def create + @trip = Trip.new(params[:trip]) + start_date = Date.strptime(params[:trip][:start_date], '%m/%d/%Y') + end_date = Date.strptime(params[:trip][:end_date], '%m/%d/%Y') + @trip.start_date = start_date + @trip.end_date = end_date - input :bedrooms, :integer - input :price, :float - input :referred_by, :string - + if @trip.save + redirect_to trips_path + else + render 'new' + end + end end +``` +We can see here that converting start_date and end_date to a Ruby date is creating complexity. Could you imagine the complexity involved with performing similar parsing with a nested resource? If you're curious how ugly it would get, we took the liberty of implementing an example here: [Nested Example (Without Decanter)](https://github.com/LaunchPadLab/decanter_demo/blob/master/app/controllers/nested_example/trips_no_decanter_controller.rb) -# app/classes/forms/property/create.rb -class Forms::Property::Create < Forms::Property +With Decanter installed, here is what the same controller action would look like: - # override any input, validation, or method here +```ruby +class TripsController < ApplicationController + def create + @trip = Trip.decant_new(params[:trip]) + if @trip.save + redirect_to trips_path + else + render 'new' + end + end end ``` -class Forms::Property::Update < Forms::Property +As you can see, we no longer need to parse the start and end date. Let's take a look at how we accomplished that. - # override any input, validation, or method here +From terminal we ran: -end ``` +rails g decanter Trip name:string start_date:date end_date:date +``` -Associations ---- +Which generates app/decanter/decanters/trip_decanter.rb: -Let's say we are creating a web app where teacher's can create Courses with assignments and resources attached to the courses. +```ruby +class TripDecanter < Decanter::Base + input :name, :string + input :start_date, :date + input :end_date, :date +end +``` -The modeling would look like: +You'll also notice that instead of ```@trip = Trip.new(params[:trip])``` we do ```@trip = Trip.decant_new(params[:trip])```. ```decant_new`` is where the magic happens. It is converting the params from this: -class Course < ActiveRecord::Base +```ruby +{ + name: "My Trip", + start_date: "01/15/2015", + end_date: "01/20/2015" +} +``` - has_many :assignments - has_many :resources +to this: -end +```ruby +{ + name: "My Trip", + start_date: Mon, 15 Jan 2015, + end_date: Mon, 20 Jan 2015 +} +``` +As you can see, the converted params hash has converted start_date and end_date to a Ruby Date object that is ready to be stored in our database. - -Why a Library for Parsing Forms? +Adding Custom Parsers --- -In my humble opinion, Rails is missing a tool for parsing forms on the backend. Currently the process looks something like this: +In the above example, start_date and end_date are ran through a DateParser that lives in Decanter. Let's take a look at the DateParser: -```html -# new.html.erb -<%= form_for @trip do |trip_builder| %> - <div class="field"> - <%= trip_builder.label :miles %> - <%= trip_builder.text_field :miles, placeholder: '50 miles' %> - </div> -<% end %> -``` - ```ruby - # trips_controller +class DateParser < Decanter::ValueParser::Base - def create - @trip = Trip.new(trip_params) + allow Date - if @trip.save - redirect_to trips_path, notice: "New trip successfully created." - else - render "new" - end + parser do |name, value, options| + parse_format = options.fetch(:parse_format, '%m/%d/%Y') + ::Date.strptime(value, parse_format) end +end ``` -The problem with this approach is that there is nothing that processes the user inputs before being saved into the database. For example, if the user types "1,000 miles" into the form, Rails would store 0 instead of 1000 into our "miles" column. +```allow Date``` basically tells Decanter that if the value comes in as a Date object, we don't need to parse it at all. Other than that, the parser is really just doing ```Date.strptime("01/15/2015", '%m/%d/%Y')```, which is just a vanilla date parse. -The "Services" concept that many Rails developers favor is on point. Every form should have a corresponding Ruby class whose responsibility is to process the inputs and get the form ready for storage. +You'll notice that the above ```parser do``` block takes a ```:parse_format``` option. This allows you to specify the format your date string will come in. For example, if you expect "2016-01-15" instead of "01/15/2016", you can adjust the TripDecanter like so: -However, this "Services" concept does not have a supporting framework. It is repetitive for every developer, for example, to write code to parse a decimal field that comes into our Ruby controller as a string (as in the example above). - -Here is how it may be done right now: - ```ruby -class Forms::Trip +# app/decanter/decanters/trip_decanter.rb - attr_accessor :trip - - def initialize(args = {}) - @trip = Trip.new - @trip.miles = parse_miles(args[:miles) - end - - def parse_miles(miles_input) - regex = /(\d|[.])/ # only care about numbers and decimals - miles_input.scan(regex).join.try(:to_f) - end - - def save - @trip.save - end - +class TripDecanter < Decanter::Base + input :name, :string + input :start_date, :date, parse_format: '%Y-%m-%d' + input :end_date, :date, parse_format: '%Y-%m-%d' end ``` -```ruby - # trips_controller.rb +You can add your own parser if you want more control over the logic, or if you have a peculiar format type we don't support. - def create - form = Forms::Trip.new(trip_params) - @trip = form.trip +``` +rails g parser Date +``` - if form.save - redirect_to trips_path, notice: "New trip successfully created." - else - render "new" - end +**app/decanter/parsers/date_parser** + +```ruby +class DateParser < Decanter::ValueParser::Base + parser do |name, value, options| + # your parsing logic here end +end ``` -While this process is not so bad with only one field and one model, it gets more complex with many different fields, types of inputs, and especially with nested attributes. +Nested Example +--- -A better solution is to have the form service classes inherit from a base "form parser" class that can handle the common parsing needs of the community. For example: +Let's say we have two models in our app: a Trip and a Destination. A trip has many destinations, and is prepared to accept nested attributes from the form. ```ruby -class Forms::Trip < FormParse::Base +# app/models/trip.rb - input :miles, :float - - def after_init(args) - @trip = Trip.new(to_hash) - end - +class Trip < ActiveRecord::Base + has_many :destinations + accepts_nested_attributes_for :destinations end ``` ```ruby - # trips_controller.rb - # same as above +# app/models/destination.rb - def create - form = Forms::Trip.new(trip_params) - @trip = form.trip - - if form.save - redirect_to trips_path, notice: "New trip successfully created." - else - render "new" - end - end +class Destination < ActiveRecord::Base + belongs_to :trip +end ``` -The FormParse::Base class that Forms::Trip inherits from by default performs the proper regex as seen in the original Forms::Trip service object above. We need only define the input key and the type of input and the base class takes care of the heavy lifting. +First, let's create our decanters for Trip and Destination. Note: decanters are automatically created whenever you run ```rails g resource```. -The "to_hash" method takes the inputs and converts the parsed values into a hash, which produces the following in this case: - -```ruby - { miles: 1000.0 } ``` +rails g decanter Trip name destinations:has_many +rails g decanter Destination city state arrival_date:date departure_date:date +``` -A more complex form would end up with a service class like below: +Which produces app/decanter/decanters/trip and app/decanter/decanters/destination: ```ruby -class Forms::Trip +class TripDecanter < Decanter::Base + input :name, :string + has_many :destinations +end +``` - attr_accessor :trip - - def initialize(args = {}) - @trip = Trip.new - @trip.miles = parse_miles(args[:miles) - @trip.origin_city = args[:origin_city] - @trip.origin_state = args[:origin_state] - @trip.departure_datetime = parse_datetime(args[:departure_datetime]) - @trip.destination_city = args[:destination_city] - @trip.destination_state = args[:destination_state] - @trip.arrival_datetime = parse_datetime(args[:arrival_datetime]) - end - - def parse_miles(miles_input) - regex = /(\d|[.])/ # only care about numbers and decimals - miles_input.scan(regex).join.try(:to_f) - end - - def parse_datetime(datetime_input) - Date.strptime(datetime, "%m/%d/%Y") - end - - def save - @trip.save - end - +```ruby +class DestinationDecanter < Decanter::Base + input :city, :string + input :state, :string + input :arrival_date, :date + input :departure_date, :date end ``` -With a framework, it would only involve the following: +With that, we can use the same vanilla create action syntax you saw in the basic example above: ```ruby -class Forms::Trip < FormParse::Base +class TripsController < ApplicationController + def create + @trip = Trip.decant_new(params[:trip]) - input :miles, :float - input :origin_city, :string - input :origin_state, :string - input :departure_datetime, :datetime - input :destination_city, :string - input :destination_state, :string - input :arrival_datetime, :datetime - - def after_init(args) - @trip = Trip.new(to_hash) + if @trip.save + redirect_to trips_path + else + render 'new' + end end - - # to_hash produces: - # { - # miles: 1000.0, - # origin_city: "Chicago", - # origin_state: "IL", - # departure_datetime: # DateTime object - # destination_city: "New York", - # destination_state: "NY", - # arrival_datetime: # DateTime object - # } - end ``` -Taking it a step further, if our form inputs are not database backed, we can even validate within our new service object like so: +Each of the destinations in our params[:trip] are automatically parsed according to the DestinationDecanter inputs set above. This means that ```arrival_date``` and ```departure_date``` are converted to Ruby Date objects for each of the destinations passed through the nested params. Yeehaw! -```ruby -class FormParse::Base - include ActiveModel::Model +Non Database-Backed Objects +--- - # ... base parsing code -end +Decanter will work for you non database-backed objects as well. We just need to call ```decant``` to parse our params according to our decanter logic. -class Form::Trip < FormParse::Base +Let's say we have a search filtering object called ```SearchFilter```. We start by generating our decanter: - input :miles, :float - input :origin_city, :string - input :origin_city, :string - input :destination_city, :string - input :destination_state, :string +``` +rails g decanter SearchFilter start_date:date end_date:date city:string state:string +``` - validates :miles, :origin_city, :origin_state, :destination_city, :destination_state, presence: true +```ruby +# app/decanter/decanters/search_filter_decanter.rb - def after_init(args) - @trip = Trip.new(to_hash) - end +class SearchFilterDecanter < Decanter::Base - def save - if valid? && trip.valid? - trip.save - else - return false - end - end - end ``` -Of course, the save method could be abstracted to our base class too, assuming we define which object the Form::Trip class is saving like so: - ```ruby -class Form::Trip +# app/controllers/search_controller.rb - def object - trip - end - - # ... above code here +def search + decanted_params = SearchFilterDecanter.decant(params[:search]) + # decanted_params is now parsed according to the parsers defined + # in SearchFilterDecanter end +``` -class FormParse::Base +Default Parsers +--- - def save - if valid? && object.valid? - object.save - else - return false - end - end -end +Decanter comes with the following parsers: +- boolean +- date +- datetime +- float +- integer +- phone +- string -``` +As an example as to how these parsers differ, let's consider ```float```. The float parser will perform a regex to find only characters that are digits or decimals. By doing that, your users can enter in commas and currency symbols without your backend throwing a hissy fit. -But where we really can see benefits from a "framework" for parsing our Rails forms is when we start to use nested attributes. For example, if we abstract the origin and destination fields to a "Location" model, we could build out a separate service class to handle parsing our "Location" form (even if it is in the context of a parent object like Trip). +We encourage you to create your own parsers for other needs in your app, or generate one of the above listed parsers to override its behavior. -```ruby -class Forms::Location < FormParse::Base +``` +rails g parser Zip +``` - input :city, :string - input :state, :string - input :departure_datetime, :datetime - input :arrival_datetime, :datetime +Squashing Inputs +--- - validates :city, :state :presence => true +Sometimes, you may want to take several inputs and combine them into one finished input prior to sending to your model. For example, if day, month, and year come in as separate parameters, but your database really only cares about start_date. +```ruby +class TripDecanter < Decanter::Base + input [:day, :month, :year], :squash_date, key: :start_date end +``` -class Forms::Trip < FormParse::Base +``` +rails g parser SquashDate +``` - input :miles, :float - input :origin_attributes, :belongs_to, class: 'location' - input :destination_attributes, :belongs_to, class: 'location' +```ruby +# app/decanter/squashers/date_squasher.rb - validates :miles, presence: true - - def after_init(args) - @trip = Trip.new(to_hash) +class SquashDateParser < Decanter::Parser::Base + parser do |name, values, options| + day = values[0] + month = values[1] + year = values[2] + Date.new(year, month, day) end end ``` - -The FormParse::Base class will recoginze the :belongs_to relationship and utilize the Location service class to parse the part of the params hash that correspond with our destination and origin models. This is great because if there is anywhere that the user can update just the origin or destination of the trip, we could just reuse that Location service object to parse the form.