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?
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With