Reputation: 3121
I have defined a module to extend ActiveRecord.
In my case I have to generate instance methods with the symbols given as arguments to the compound_datetime
class method. It works when class_eval
is called outside the each
block but not inside it; in the latter case I get an undefined method error.
Does anyone know what I am doing wrong?
module DateTimeComposer
mattr_accessor :attrs
@@attrs = []
module ActiveRecordExtensions
module ClassMethods
def compound_datetime(*attrs)
DateTimeComposer::attrs = attrs
include ActiveRecordExtensions::InstanceMethods
end
end
module InstanceMethods
def datetime_compounds
DateTimeComposer::attrs
end
def self.define_compounds(attrs)
attrs.each do |attr|
class_eval <<-METHODS
def #{attr.to_s}_to()
puts 'tes'
end
METHODS
end
end
define_compounds(DateTimeComposer::attrs)
end
end
end
class Account < ActiveRecord::Base
compound_datetime :sales_at, :published_at
end
When I try to access the method:
Account.new.sales_at_to
I get a MethodError: undefined method sales_at_to for #<Account:0x007fd7910235a8>
.
Upvotes: 0
Views: 897
Reputation: 17020
You are calling define_compounds(DateTimeComposer::attrs)
at the end of the InstanceMethods
module definition. At that point in the code, attrs
is still an empty array, and self
is the InstanceMethods
module.
This means no methods will be defined, and even if they were, they would be bound to InstanceMethods
's metaclass, making them class methods of that module, not instance methods of your Account
class.
This happens because method calls inside the InstanceMethods
module definition are evaluated as they are seen by the ruby interpreter, not when you call include ActiveRecordExtensions::InstanceMethods
. An implication of this is that it is possible to run arbitrary code in the most unusual of places, such as within a class definition.
To solve the problem, you could use the included
callback provided by ruby, which is called whenever a module is included in another:
module InstanceMethods
# mod is the Class or Module that included this module.
def included(mod)
DateTimeComposer::attrs.each do |attr|
mod.instance_eval <<-METHODS
def #{attr.to_s}_to
puts 'tes'
end
METHODS
end
end
end
As an additional suggestion, you should be able to achieve the same result by simply defining the methods when compound_datetime
is called, thus eliminating the dependence on the attrs
global class variable.
However, if you must have access to the fields which were declared as compound datetime, you should use class instance variables, which are unique to each class and not shared on the hierarchy:
module ClassMethods
def compound_datetime(*attrs)
@datetime_compounds = attrs
attrs.each do |attr|
instance_eval <<-METHODS
def #{attr.to_s}_to
puts 'tes'
end
METHODS
end
end
def datetime_compounds; @datetime_compounds; end;
end
class Account < ActiveRecord::Base
compound_datetime :sales_at, :published_at
end
class AnotherModel < ActiveRecord::Base
compound_datetime :attr1, :attr2
end
Account.datetime_compounds
=> [ :sales_at, :published_at ]
AnotherModel.datetime_compounds
=> [ :attr1, :attr2 ]
Upvotes: 3