Reputation: 18871
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
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 itCLASS_NAME
is misleading: it doesn't contain the name of the class, it contains the class itselfUpvotes: 1
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
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
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