README.md in pardner-0.1.1 vs README.md in pardner-0.1.2

- old
+ new

@@ -11,80 +11,273 @@ | .__/ \__,_|_| \__,_|_| |_|\___|_| |_| # pardner -A decorator library for ActiveRecord that has features to fit in nicely with the ActiveModel world +A decorator library for ActiveRecord that has features to fit in nicely +with the ActiveModel world ## Use cases 1. Presenters for views -2. Handle form params and translate them to what the modal understands -3. Creating or updating multiple ActiveRecord models atomically +2. Translate between form params and model attributes +3. Save multiple ActiveRecord models atomically 4. Adding optional validations 5. And more! -## 1. Presenters +## Usage -A presenter can be used as an alternative to a view helper to add logic -to a view. In this example we add a `description` method to a decorator -for the view: +``` ruby +# Decorate an ActiveRecord or ActiveModel class by creating a subclass +# of Pardner::Base. In this example we'll pretend a User active record +# class exists. - # app/models/conestoga_wagon.rb - class ConestogaWagon < ActiveRecord::Base - attr_accessor :wheels_count, :covered - end +class SilverMiner < Pardner::Base + howdy_pardner User +end - # app/presenters/conestoga_wagon_presenter.rb - class ConestogaWagonPresenter < Pardner::Base - howdy_pardner ConestogaWagon +# Instantiate it by calling `.new` and passing in a User object: +miner = SilverMiner.new User.find(123) +miner.new_record? # => true +miner.id # => 123 - def description - covered_string = covered ? "covered" : "uncovered" - "a #{wheels_count} wheeled #{covered_string} wagon" - end - end +# Behavior can be added to the decorator by defining methods: +class SilverMiner < Pardner::Base + # Add the title 'Silver miner' to the user name + def name + "Silver miner #{super}" + end +end - # app/view/conestoga_wagons/show.html.haml - ... - span.description= @conestoga_wagon.description - ... +miner.name # => 'Silver miner Sam' -## 2. Handle form params +# by adding callbacks: +class SilverMiner < Pardner::Base + before_destroy :retirement_party -In this example, the `GoldRush` model has separate `city` and `state` + private + + def retirement_party + years_worked = Time.now.year - self.start_year + Cake.create! candles_count: years_worked + end +end + +miner.destroy # creates a Cake + +# by adding validations: +class SilverMiner < Pardner::Base + validates_inclusion_of :favorite_ore, in: ['silver'] +end + +miner.favorite_ore = 'gold' +miner.valid? # => false +``` + +## More examples + +### Presenters + +A presenter a way to add logic to a view. It's an alternative to a view +helper. In this example a ConestogaWagon model is decorated to have a +`description` method. + +```ruby +# app/models/conestoga_wagon.rb +# The table has columns is_covered:boolean and wheels_count:integer +class ConestogaWagon < ActiveRecord::Base +end + +# app/presenters/conestoga_wagon_presenter.rb +class ConestogaWagonPresenter < Pardner::Base + howdy_pardner ConestogaWagon + + def description + covered_string = is_covered ? "covered" : "uncovered" + "a #{wheels_count} wheeled #{covered_string} wagon" + end +end + +# app/controllers/conestoga_wagons_controller.rb +class ConestogaWagonsController < ApplicationController + def show + @wagon = ConestogaWagonPresenter.new ConestogaWagon.find(params[:id]) + end +end +``` + +```haml +-# app/views/conestoga_wagons/show.html.haml +span.description= @conestoga_wagon.description +``` + +### Translating between form params and model attributes + +In this example, the `GoldRush` model has separate `city` and `territory` fields, but we want to present that to the user as a single form field. The decorator will split the incoming `location` param into `city` and -`state` fields. +`territory` fields, and vice versa. +```ruby # app/models/gold_rush.rb + # The table gold_rushes has columns city:string and territory:string class GoldRush < ActiveRecord::Base - attr_accessor :city, :state end # app/decorators/gold_rush_form.rb class GoldRushForm < Pardner::Base howdy_pardner GoldRush def location - "#{city}, #{state}" + "#{city}, #{territory}" end def location=(val) - self.city, self.state = val.split ',' + self.city, self.territory = val.split ',' end end # app/controllers/gold_rushes_controller.rb - def new - @gold_rush = GoldRushForm.new GoldRush.new + class GoldRushesController < ApplicationController + def new + @gold_rush = GoldRushForm.new GoldRush.new + end + + def create + @gold_rush = GoldRushForm.new GoldRush.new + @gold_rush.attributes = params[:gold_rush] + + if @gold_rush.save + flash[:notice] = "There's gold in them thar hills" + else + render :new + end + end end +``` - # app/view/gold_rushes/new.html.haml +```haml + -# app/view/gold_rushes/new.html.haml = form_for @gold_rush do |form| form.text :location + form.submit +``` +### Saving multiple ActiveRecord models atomically + +A decorator can be a convenient way to coordinate changes to several +models atomically. Model callbacks can also be used for this but have +some downsides: + +* sometimes its not clear which model should have the callback, +* decorators can opt-in more easily than callbacks, +* and extensive use of callbacks can lead to infinite loops. + +In this example, when a gold rush is declared a bunch of supporting +models need to be created. + +```ruby +# app/controllers/gold_rushes_controller.rb +class GoldRushesController < ApplicationController + def create + @gold_rush = GoldRushDeclared.new GoldRush.new + @gold_rush.attributes = params[:gold_rush] + + if @gold_rush.save + flash[:notice] = "There's gold in them thar hills" + else + render :new + end + end +end + +# app/services/gold_rush_declared.rb +class GoldRushDeclared < Pardner::Base + howdy_pardner GoldRush + before_validate :build_infrastructure + validate :mining_town_must_exist + validate :transport_must_exist + + private + + def build_infrastructure + if MiningTown.where(territory: self.territory).is_nearby(self).empty? + MiningTown.create! territory: self.territory, name: "Town near #{self.name}" + end + + if Railroad.is_nearby(self).empty? && WagonTrail.is_nearby(self).empty? + WagonTrail.create! territory: self.territory, destination: self.location + end + end + + def mining_town_must_exist + return if MiningTown.is_nearby(self) + errors.add :base, 'no mining town exists' + end + + def transport_must_exist + return if Railroad.is_nearby(self) || WagonTrail.is_nearby(self) + errors.add :base, 'no transport exists' + end +end +``` + +### Adding optional validations + +In this example, we have two controllers for the same resource. One +supports an admin interface and one is customer facing. We want the +customer facing one to do more validation than the admin one. + +```ruby +# app/decorators/small_posse.rb +class SmallPosse < Pardner::Base + howdy_pardner Posse + validate :must_be_small + + MAX_SIZE = 5 + + private + + def must_be_small + if deputies_count > MAX_SIZE + errors.add :deputies, "must be less than #{MAX_SIZE}" + end + end +end + +# app/controllers/posses_controller.rb +class PossesController < ApplicationController + def create + # This controller is customer facing so they can only create small posses + @posse = SmallPosse.new Posse.new + @posse.attributes = params[:posse] + + if @posse.save + flash[:notice] = 'Get a rope' + else + render :new + end + end +end + +# app/controllers/admin/posses_controller.rb +module Admin + class PossesController < ApplicationController + def create + # This controller is for admins so they can do what they want + @posse = Posse.new params[:posse] + + if @posse.save + flash[:notice] = 'Every day above ground is a good day' + else + render :new + end + end + end +end +``` + ## Installation Add this line to your application's Gemfile: ```ruby @@ -97,24 +290,25 @@ Or install it yourself as: $ gem install pardner -## Usage +## Similar projects -TODO: Write usage instructions here +* draper https://github.com/drapergem/draper +* informal https://github.com/joshsusser/informal +* more https://www.ruby-toolbox.com/categories/rails_presenters ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/pardner. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct. +Bug reports and pull requests are welcome on GitHub at https://github.com/ajh/pardner. ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). -