How to "Nest" the Inclusion of Modules When Using the Ruby on Rails Activesupport::Concern Feature

How to nest the inclusion of modules when using the Ruby on Rails ActiveSupport::Concern feature?

If you include MyModuleB in the "body" of MyModuleA, then it is the module itself that is extended with B's functionality. If you include it in the included block, then it is included on the class that mixes in MyModuleA.

That is:

module MyModuleA
extend ActiveSupport::Concern
include MyModuleB
end

produces something like:

MyModuleA.send :include, MyModuleB
class Foo
include MyModuleA
end

while

module MyModuleA
extend ActiveSupport::Concern
included do
include MyModuleB
end
end

produces something like:

class Foo
include MyModuleA
include MyModuleB
end

The reason for this is that ActiveSupport::Concern::included is analogous to:

def MyModuleA
def self.included(klass, &block)
klass.instance_eval(&block)
end
end

The code in the included block is run in the context of the including class, rather than the context of the module. Thus, if MyModuleB needs access to the class it's being mixed-in to, then you'd want to run it in the included block. Otherwise, it's effectively the same thing.

By means of demonstration:

module A
def self.included(other)
other.send :include, B
end
end

module B
def self.included(other)
puts "B was included on #{other.inspect}"
end
end

module C
include B
end

class Foo
include A
end

# Output:
# B was included on C
# B was included on Foo

How to make methods added to a class by including nested modules to be instance methods of that class when using the ActiveSupport::Concern feature?

Methods in modules that get mixed into a class become instance methods on that class. While putting them in the included block would likely work, there's no need to do it. This, by extension, works with modules, since you can include ModuleB in ModuleA and all its instance methods become instance methods on ModuleA, and once ModuleA is included on class Foo, all its instance methods (including those mixed in from B) become instance methods on Foo.

A "traditional" mix-in looks like this:

module Mixin
def self.included(klass)
klass.send :extend, ClassMethods
klass.some_class_method
end

module ClassMethods
def some_class_method
puts "I am a class method on #{self.inspect}"
end
end

def some_instance_method
puts "I am an instance method on #{self.inspect}"
end
end

class Foo
include Mixin
end

Foo.new.some_instance_method

# Output:
# I am a class method on Foo
# I am an instance method on #<Foo:0x00000002b337e0>

ActiveSupport::Concern just pretties this up a bit by automatically including a module named ClassMethods and by running the included block in the context of the including class, so the equivalent is:

module Mixin
extend ActiveSupport::Concern

included do
some_class_method
end

module ClassMethods
def some_class_method
puts "I am a class method on #{self.inspect}"
end
end

def some_instance_method
puts "I am an instance method on #{self.inspect}"
end
end

class Foo
include Mixin
end

Foo.new.some_instance_method

# Output:
# I am a class method on Foo
# I am an instance method on #<Foo:0x000000034d7cd8>

Extending ActiveSupport::Concern

You shouldn't override or extend directly the concern. With a simple module it would be maybe useful, but concerns are set up to be explicitly extended:

module MyFoo
extend ActiveSupport::Concern
extend Foo

included do
#some other stuff
end

def method_b
end
end

included do ..... end (wrong no of arguments) error

oh.... got it fixed...

Stupid typo mistake

extend ActiveSupport::Concerns should be Concern

Getting around ruby-units conflict with activesupport

The answer to this is to require the ruby-units library before the rails library in the Gemfile:

gem 'ruby-units'
gem 'rails'

Obviously you then won't be able to use .to() on strings to access the ruby-units conversion.

Understanding the singleton class when aliasing a instance method

I recommend Yehuda Katz's post on metaprogamming on Ruby's self. Here's my humble summary in response to your question:

In Ruby, all objects have a singleton class (also known as metaclass). Objects inherit first from their singleton class invisibly, then from their explicit class. Ruby classes themselves have their own singleton classes since classes are objects as well. The class << idiom is simply Ruby's syntax for accessing the scope of an object's singleton class.

 class Person
class << self
# self in this scope is Person's singleton class
end
end

person = Person.new
person_singleton_class = class << person; self; end

Your version of Rails actually provides singleton_class as a shortcut. Since singleton_class is an available method, you don't need to assign it to a variable in the expression singleton_class = class << self; self end:

Person.singleton_class 

person = Person.new
person.singleton_class

Since a class inherits directly from its singleton class, this is where we want to add class methods dynamically when metaprogramming. Ruby provides a few ways to open up the scope of an object while maintaining access to the surrounding scope: class_eval and instance_eval. There are subtle differences in the way these behave (Yehuda's post explains this), but you may use either to enter the scope of your singleton class, resolve methods on the singleton class as self and still have access to my_method_name from the surrounding scope.

All that said, you could make a few small changes to your module:

module MyModule
extend ActiveSupport::Concern

included do
# Builds the instance method name.
my_method_name = build_method_name.to_sym # => :my_method

# Defines the :my_method instance method in the including class of MyModule.
define_singleton_method(my_method_name) do |*args|
# ...
end

singleton_class.class_eval do
# method resolution in scope of singleton class
alias_method :my_new_method, my_method_name
end

end

end

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

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

Code refactoring by injecting a Hash

What you should write in the inject block:

result.update(key => IVar.new(key, opts, args.keys)

But no need to use inject or each_with_object to build a hash, we have Hash[pairs]:

@i_vars = Hash[(args || {}).map { |k, opts| [k, IVar.new(k, opts, (args || {}).keys)] }

However, I wouldn't obsess with one-liners, I'd write an equivalent but more clear code:

hargs = args || {}
@i_vars = Hash[hargs.map do |key, opts|
[key, IVar.new(key, opts, hargs.keys)]
end]


Related Topics



Leave a reply



Submit