Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overriding setter method for attr_accessor when including InstanceMethods module

I have an ActiveRecord extension (abbreviated):

module HasPublishDates
  def self.included(base)
    base.send :extend, ClassMethods
  end

  module ClassMethods
    def has_publish_dates(*args)
      attr_accessor :never_expire

      include InstanceMethods
    end
  end

  module InstanceMethods
    def never_expire=(value)
      @never_expire = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
    end

    def another_instance_method
      'something to return'
    end
  end
end

ActiveSupport.on_load(:active_record) do
  include HasPublishDates
end

which can be called like this:

class MyModel < ActiveRecord::Base
  has_publish_dates
  ...
end

The idea is that never_expire= should override the setter defined by attr_accessor :never_expire. However, it doesn't seem to be working:

m = MyModel.new
m.never_expire            #=> nil
m.never_expire = '1'      #=> '1'
m.never_expire            #=> '1' should be true if never_expire= has been overridden
m.another_instance_method #=> 'something to return' works as expected

As you can see, another_instance_method is being included and is working as expected but never_expire= is not overriding the setter as I expected.

If I change HasPublishDates to use class_eval then it works as expected:

module HasPublishDates
  ...
  module ClassMethods
    def has_publish_dates(*args)
      ...
      class_eval do
        def never_expire=(value)
          @never_expire = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
        end

        def another_instance_method
          'something to return'
        end
      end
    end
  end
end
...

m = MyModel.new
m.never_expire            #=> nil
m.never_expire = '1'      #=> true
m.never_expire            #=> true
m.another_instance_method #=> 'something to return'

I imagine that this is because InstanceMethods is defined before attr_accessor :never_expire is called by has_publish_dates.

Though I think that class_eval is an elegant way of doing things I also like the idea of having my instance methods exposed for documentation so there's no "magic" when another developer is trying to use my code.

Is there anyway I can use the include InstanceMethods approach in this scenario?

like image 822
tristanm Avatar asked Nov 07 '11 21:11

tristanm


2 Answers

Call order in Ruby starts with normal instance methods before proceeding to methods of included modules and superclass methods. The never_expire= method created by attr_accessor winds up being an instance method, so it's called rather than the InstanceMethods module's method. If you use attr_reader instead, so that no never_expire= instance method gets defined, it will work as you intend.

That said, you're making things more complicated than they need to be with those extra ClassMethods and InstanceMethods modules. Just use modules like they were intended:

module HasPublishDates
  attr_reader :never_expire

  def never_expire=(value)
    @never_expire = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
  end
end

class MyModel < ActiveRecord::Base
  include HasPublishDates
end
like image 122
John Avatar answered Sep 27 '22 19:09

John


Well, you could just not bother with the attr_accessor... after all, you'd just need to add:

def never_expire
  @never_expire
end

and it'd work just fine without that.

If it's an actual AR column on the db, though, I'd recommend using

set_attribute(:never_expire, ActiveRecord::ConnectionAdapters::Column....

rather than the @never_expire variable. You'd also not need the attr_accessor in that case.

A a final option, you could use class-eval just on the include statement eg:

module ClassMethods
  def has_publish_dates(*args)
    attr_accessor :never_expire

    class_eval do
      include InstanceMethods
    end
  end
end
like image 33
Taryn East Avatar answered Sep 27 '22 19:09

Taryn East