Backo
Backo

Reputation: 18871

How to "dynamically" open a class so to add to it a scope method that makes use of a local variable?

I am using Ruby on Rails v3.2.2. In a module I am trying to "dynamically" open a class so to add to it a Ruby on Rails "scope method" that makes use of a local variable, this way:

module MyModule
  extend ActiveSupport::Concern

  included do
    # Note: The `CLASS_NAME` is not the class where `MyModule` is included. That
    # is, for instance, if the including class of `MyModule` is `Article` then
    # the `CLASS_NAME` is `User`.
    CLASS_NAME           = self.get_class_name.constantize # => User
    counter_cache_column = self.get_counter_cache          # => "counter_count"

    class CLASS_NAME
      def self.order_by_counter
        order("#{counter_cache_column} DESC")
      end
    end
  end
end

If I run the above code, I get the following error:

NameError
undefined local variable or method `counter_cache_column' for #<Class:0x0000010775c558>

It happens because the counter_cache_column in not called in the context of the module. How should I properly state the order_by_counter scope method?


Bonus: What do you advice about the above "so dynamic" implementation?

Upvotes: 1

Views: 1742

Answers (4)

J&#246;rg W Mittag
J&#246;rg W Mittag

Reputation: 369556

counter_cache_column is a local variable. Local variable are local to the scope they are defined in (that's why they are called local variables).

In this case, the scope is the block passed to included.

The class definition and the method definition create a new empty scope. Only blocks create nested scopes, so, you need to use a block to defined your method. Thankfully, there is a way to do so: by passing a block to define_method:

module MyModule
  extend ActiveSupport::Concern

  included do
    klass                = get_class_name.constantize # => User
    counter_cache_column = get_counter_cache          # => "counter_count"

    klass.define_singleton_method(:order_by_counter) {
      order("#{counter_cache_column} DESC")
    }
  end
end

I made some other style improvements:

  • self is the implicit receiver in Ruby, there is no need to specify it
  • CLASS_NAME is misleading: it doesn't contain the name of the class, it contains the class itself
  • also, I don't see why it would need to be a constant

Upvotes: 1

Boris Stitnicky
Boris Stitnicky

Reputation: 12578

There are many quick and dirty ways of achieving what you want. For example, if you want the symbol 'counter_cache_column' to mean something outside its scope, you could declare it as a method rather than a local variable:

included do
  CLASS_NAME           = self.get_class_name.constantize # => User
  def counter_cache_column; get_counter_cache end        # => "counter_count"

  class CLASS_NAME
    def self.order_by_counter
      order("#{counter_cache_column} DESC")
    end
  end
end

Upvotes: -1

simonmenke
simonmenke

Reputation: 2880

Local variables are not passed on to reopened classes.

module MyModule
  extend ActiveSupport::Concern

  included do
    counter_cache_column = self.get_counter_cache # => "counter_count"

    class_eval <<-RUBY, __FILE__, __LINE__+1
      def self.order_by_counter               # def self.order_by_counter
        order("#{counter_cache_column} DESC") #   order("counter_count DESC")
      end                                     # end
    RUBY
  end
end

Upvotes: 0

rossta
rossta

Reputation: 11494

The included block provided by ActiveSupport::Concern is evaluated within the scope of the including class. In other words, you've "reopened" the class within this block. If the including class inherits from ActiveRecord::Base, you can use any AR class macros, e.g. scope, has_many, attr_accessible, etc.:

module MyModule
  extend ActiveSupport::Concern

  included do
    scope :order_by_counter, order("#{self.get_counter_cache} DESC")
  end

end

This assumes that 'get_counter_cache` is already defined as a class method in the including classes (though this isn't clear from the code you've shown).

Upvotes: 3

Related Questions