Project

pardner

0.0
No commit activity in last 3 years
No release in over 3 years
A decorator library for ActiveRecord that has features to fit in nicely with the ActiveModel world
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies
 Project Readme
      .-~-~-\-~\
     /       ~~ \
     |           ;
 ,--------,______|---.
/          \-----`    \ _
`.__________`-_______-'| |_ __   ___ _ __
   | '_ \ / _` | '__/ _` | '_ \ / _ \ '__|
   | |_) | (_| | | | (_| | | | |  __/ |
   | .__/ \__,_|_|  \__,_|_| |_|\___|_|
   |_|

pardner

A decorator library for ActiveRecord that has features to fit in nicely with the ActiveModel world

Use cases

  1. Presenters for views
  2. Translate between form params and model attributes
  3. Save multiple ActiveRecord models atomically
  4. Adding optional validations
  5. And more!

Usage

# 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.

class SilverMiner < Pardner::Base
  howdy_pardner User
end

# Instantiate it by calling `.new` and passing in a User object:
miner = SilverMiner.new User.find(123)
miner.new_record? # => true
miner.id          # => 123

# 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

miner.name # => 'Silver miner Sam'

# by adding callbacks:
class SilverMiner < Pardner::Base
  before_destroy :retirement_party

  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.

# 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
-# 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 territory fields, and vice versa.

    # app/models/gold_rush.rb
    # The table gold_rushes has columns city:string and territory:string
    class GoldRush < ActiveRecord::Base
    end

    # app/decorators/gold_rush_form.rb
    class GoldRushForm < Pardner::Base
      howdy_pardner GoldRush

      def location
        "#{city}, #{territory}"
      end

      def location=(val)
        self.city, self.territory = val.split ','
      end
    end

    # app/controllers/gold_rushes_controller.rb
    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
    = 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.

# 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.

# 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:

gem 'pardner'

And then execute:

$ bundle

Or install it yourself as:

$ gem install pardner

Similar projects

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.

Contributing

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.