Tuesday, March 12, 2013

First Cut at ActsAsEvent Module

This post is one in a series on how I achieved composite classes and multi-table inheritance. For background, see Composite classes and multi-table inheritance. The last post in the series was Create EventBase.
For proper attribution, I'll note that a whole lot of the code in this post came directly from or was inspired by Multiple Table Inheritance with ActiveRecord.
 
In this post, I'm going to be taking a first cut at the ActsAsEvent module. If you don't know what modules are or how to create and use them, do some Googling.

Including Your Modules

One thing to remember is that you have to require your modules in order for Rails to find them correctly. In my config/initializers file, I include the line:
Dir[File.join(Rails.root, "lib", "*.rb")].each {|l| require l }

which causes all files in my lib directory ending in .rb to be required.

The Basic ActsAsEvent Module

In my lib directory, I created an acts_as_event.rb file for my ActsAsEvent module. I edited the file to create a basic module structure:

module ActsAsEvent
  def acts_as_event
    class_eval do
      include InstanceMethods
      extend ClassMethods
    end
  end
  
  module InstanceMethods
  end # InstanceMethods

  module ClassMethods
  end # ClassMethods
end

ActiveRecord::Base.extend ActsAsEvent

At this point, the module includes one method declaration (acts_as_event) and two module declarations (InstanceMethods and ClassMethods).

The class_eval block (within acts_as_event) is a metaprogramming construct that allows anything defined in the  block to be included in another class as if it had been defined in that class. Within this block, I included the include InstanceMethod and extend ClassMethods lines which cause any class that calls acts_as_event to include and extend the instance and class methods (respectively).

To the acts_as_event method, I added two lines:
  def acts_as_event
    class_eval do
      include InstanceMethods
      extend ClassMethods
      has_one :event_base, as: :eventable, :autosave => true, :dependent => :destroy
      alias_method_chain :event_base, :build
    end
  end

The has_one :event_base line creates an association between any class calling the acts_as_event method and the EventBase class that I created in a previous post. If you are unfamiliar with Rails associations, see A Guide to ActiveRecord Associations.

The alias_method_chain :event_base, :build line ensures that an EventBase record is created each time the composing object (in my case, Event or Trip) is created. This line calls an event_base_with_build instance method which I declared as:
  module InstanceMethods
    def event_base_with_build
      event_base_without_build || build_event_base
    end
  end # InstanceMethods

Mix the Method Into the Trip Class

Finally, I call the acts_as_event method (that I defined in the ActsAsEvent module) from within my Trip class:
class Trip < ActiveRecord::Base
# ==========================================================================================
#  Mixins
# ==========================================================================================
  acts_as_event

end

When I call acts_as_events, it causes the class_eval block to be called which in turn 'mixes in' everything within that block.

Testing the Mixed In Module

At this point, I should have a functioning mixed in module. So, I wrote a simple test in spec/models/trip_spec.rb:
require 'spec_helper'

describe Trip do
  describe "functions properly" do
    it "  - saves EventBase correctly" do
      tcount_before = Trip.find(:all).count
      ebcount_before = EventBase.find(:all).count
      @t = Trip.create
      tid = @t.id
      @t.respond_to?("event_base").should be_true
      @t.event_base.description = "test description"
      @t.save.should be_true
      @t = nil
      @dbt = Trip.find(tid)
      @dbt.event_base.description.should == "test description"
      Trip.find(:all).count.should == tcount_before + 1
      EventBase.find(:all).count.should == ebcount_before + 1
      @dbt.destroy
      Trip.find(:all).count.should == tcount_before
      EventBase.find(:all).count.should == ebcount_before
    end
  end
end

Here's what's going on in the test:
  • To begin, I record how many Trip and EventBase records there are in the database.
  • Then, I create a new Trip record and save its ID to tid.
  • I check that the new Trip responds to "event_base" - making sure the association is working correctly.
  • Then, I assign a description to the EventBase associated with the Trip.
  • And make sure it saves correctly. To be sure the save works, I set @t to nil and then retrieve the record from the database using tid.
  • Next, I make sure the record counts make sense.
  • Then, I destroy the record I just created.
  • And, again, make sure the record counts make sense.
  At the console, I ran:
$ rspec trip_spec.rb --format documentation

And I got:
Trip
  functions properly
    - saves EventBase correctly

Finished in 0.30754 seconds
1 example, 0 failures

Right on! It works.

Next Up: Mixin Validations

So far, this seems pretty good. But, there's a problem lurking in here. I want to be able to have EventBase validations so that my classes (Event and Trip) only save if my EventBase validations pass. So, let's say I add a validation to EventBase that ensures that there is a description, like this:
class EventBase < ActiveRecord::Base

  # ==========================================================================================
  #  Validations
  # ==========================================================================================  
    validates_presence_of :description

  # ==========================================================================================
  #  Associations
  # ==========================================================================================  
    belongs_to  :recur_rule, 
                dependent: :destroy  
    belongs_to  :eventable, polymorphic: true, dependent: :destroy
end

And, I update my test like this:
require 'spec_helper'

describe Trip do
  describe "responds to" do
  end
  
  describe "functions properly" do
    it "  - saves EventBase correctly" do
      tcount_before = Trip.find(:all).count
      ebcount_before = EventBase.find(:all).count
      @t = Trip.create
      tid = @t.id
      @t.respond_to?("event_base").should be_true
      @t.save.should be_false
      @t.event_base.description = "test description"
      @t.save.should be_true
      @dbt = Trip.find(@t.id)
      @dbt.event_base.description.should == "test description"
      Trip.find(:all).count.should == tcount_before + 1
      EventBase.find(:all).count.should == ebcount_before + 1
      @dbt.destroy
      Trip.find(:all).count.should == tcount_before
      EventBase.find(:all).count.should == ebcount_before
    end
  end
end

Note the line that says @t.save.should be_false. This says that my save should fail because event_base.description is nil. When I run:
$ rspec trip_spec.rb --format documentation

I get:
Trip
  functions properly
    - saves EventBase correctly (FAILED - 1)

Failures:

  1) Trip functions properly   - saves EventBase correctly
     Failure/Error: @t.save.should be_false
       expected: false value
            got: true
     # ./trip_spec.rb:25:in `block (3 levels) in <top (required)>'

Finished in 0.08088 seconds
1 example, 1 failure

Oh no! @t is saving even though the validation condition is not met. I'll fix this in the next post - Mixin Validations.

No comments:

Post a Comment