Thiago Diniz
Thiago Diniz

Reputation: 3121

Class_eval not working inside each block

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

Answers (1)

Matheus Moreira
Matheus Moreira

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

Related Questions