ActiveSupport::Concern vs append_features - ruby

There is an article on ActiveSupport::Concern hooks. Here's a working implementation in Rails:
module ActionController
class Base < Metal
include AbstractController::Layouts
end
end
module AbstractController
module Layouts
extend ActiveSupport::Concern
include Rendering
included do
class_attribute :_layout, :_layout_conditions, :instance_accessor => false
self._layout = nil
self._layout_conditions = {}
_write_layout_method
end
module ClassMethods
...
end
end
end
module AbstractController
module Rendering
extend ActiveSupport::Concern
included do
class_attribute :protected_instance_variables
self.protected_instance_variables = []
end
module ClassMethods
...
end
end
end
How is the extend call in Layouts executed before the append_features ruby hook? The extend must be executed first. After all, the whole point of it is to hijack the append_features ruby default and reimplement it. However, according to Ruby documentation, append_features is executed right after you include this module (e.g. AbstractController::Layouts) in another (e.g. ActionController::Base). So there's confusion here for me. If that's the case, then the overwritten append_features of ActiveSupport::Concern will never be called.

Here's my take:
You can think of "include" like a method that takes a module as a parameter; in order to include a module, the module must already be loaded by the environment; otherwise the line would fail with a missing constant error.
Therefore, when include AbstractController::Layouts is called, Layouts must be loaded in the AbstractController or top-level namespace for this to work.
Peeking at the actionpack source code, it turns out that Layouts is autoloaded, which ensures that it will be loaded before the "include" line completes.
Since extend ActiveSupport::Concern is executed when Layouts is loaded, the append_features override will be available by the time "include" line in ActionController::Base has finished executing.
After this, the append_features method for Layouts will be executed with ActionController::Base as the argument.

Related

Is it dangerous to mix ActiveSupport::Concern modules with vanilla modules?

I know there's a dependency management system hidden behind ActiveSupport::Concern. I do not understand it completely (and I'm not sure I am ready to yet), but in a nutshell : is it possible to mix ActiveSupport::Concern with vanilla (non ActiveSupport::Concern) modules, or are there pitfalls ?
Here are some examples of the different usages I can think of
module Vanilla
module ModuleIncludedInASC
# Vanilla module
end
module ModuleIncludedInClass
# Vanilla module
end
module ASC
module ConcernIncludedInClass
extend ActiveSupport::Concern
...
end
module ConcernIncludedInASC
extend ActiveSupport::Concern
...
end
module ConcernIncludingVanillaModulesIncludedInClass
extend ActiveSupport::Concern
include Vanilla::VanillaConcernIncludedInASC
end
module ConcernIncludingASCConcernIncludedInASC
extend ActiveSupport::Concern
include ConcernIncludedInASC
end
end
class MyFoo
include Vanilla::ModuleIncludedInClass
include ASC::ConcernIncludedInClass
include ASC::ConcernIncludingVanillaModulesIncludedInClass
end
# Ans also possibly, ActiveSupport::Concern modules included in vanilla modules...?
Could that potentially lead to problems ?
It shouldn't be a problem.
I can't speak to the deep technical details, but I've never seen anyone mention it's risky, and I do this all the time. I have some models with 10+ includes. Some of those modules are using ActiveSupport::Concern, some aren't. Never encountered any issue.
I'd suggest trying it and post a new question if it does cause problems.
I haven't had any problem yet, but yes, theoretically there is a potential problem.
The methods that are defined in a normal module are appended to the method lookup chain if you include that module in a class. So the methods defined in the class will override the methods in the normal module.
The methods that are defined in a concern's included block will be added directly to the class that includes the concern.
The result is, even if you include concern earlier than a normal module, the methods in a concern will always override the methods in a normal module.
Example
module Normal
def foo
'normal'
end
end
module Concern
extend ActiveSupport::Concern
included do
def foo
'concern'
end
end
end
class Bar
include Concern # include concern first
include Normal # then include normal
end
Bar.new.foo #=> "concern"
Maybe you would expect "normal" to be returned, but that's never gonna happen.

Extending Modules in Cucumber

I know that in Cucumber that I can create a module and then include that module into the World object and all the methods that I have created within the newly created module are available globally
module MyModule
def my_method
end
end
World(MyModule)
Now anywhere in my cucumber tests I can call my_method and it will work
The issue I see here and an issue I have come across is duplication of method names, as the application gets bigger or other testers/developers work on the application.
So if I was to wrap everything up in its own module and create module methods like so
module MyModule
def self.my_method
page.find('#element')
end
end
World(MyModule)
MyModule.my_method
# This will return undefined variable or method 'page' for MyModule module
So being new to using modules I have read that you can extend other modules so that you can use those methods within another module
So to access the page method I would need to access Capybara::DSL
module MyModule
extend Capybara::DSL
def self.my_method
page.find('#element')
end
end
World(MyModule)
MyModule.my_method now works, but my question is rather than extend individual namespaces for every module that I need access to is there a way to extend/include everything or is this a bad practice?
Another example of where things fail are when I try to access instances of a class
module SiteCss
def login_page
Login.new
end
end
class Login < SitePrism::Page
element :username, "#username"
end
module MyModule
extend Capybara::DSL
def self.my_method
page.find('#element')
login_page.username.set('username')
end
end
World(MyModule)
So with this example if I was it try and call login_page.username I would get
undefined method `login_page`
I'm really looking for the correct way to be doing this.
In conclusion I am trying to understand how to use custom modules and classes in cucumber without having to load everything into the World object.
Yes, it's not pretty to extend a module multiple times, if that's really what you want to do, I can suggest a way you can improve it a bit:
base class which will inherit your page object framework and extend(include is probably the correct option here):
module Pages
class BasePage < SitePrism::Page
extend Capybara::DSL
end
end
Now your login class:
module Pages
class Login < BasePage
element :username, "#username"
def yourmethod
page.find('#element')
username.set('username')
end
end
end
Now the bits you are probably interested in, expose yourmethod to cucumber world:
module SiteCss
def page_object
#page_object ||= Pages::Login.new
end
end
World(SiteCss)
Now you should have access to yourmethod in a cleaner way...
Usage:
#page_object.yourmethod
Although the most important suggestion I could give you is run from SitePrism... Create your own page object framework... don't fall into SitePrism trap...

Can't redefine a class's instance method by including a module?

I'm having a herp-derpy moment. I could've sworn this worked:
module StubbedGreeting
def sayit
puts "StubbedGreeting"
end
end
module RegularGreeting
def sayit
puts "RegularGreeting"
end
end
class Greeting
def sayit
raise "Gotta catch me!"
end
end
class GreetingIncludes
include RegularGreeting
end
begin
Greeting.send(:include, StubbedGreeting)
Greeting.new.sayit
rescue Exception
puts "Exception raised"
end
GreetingIncludes.send(:include, StubbedGreeting)
GreetingIncludes.new.sayit
What happens here is Greeting.new.sayit results in in the rescue block being called, ignoring the attempted overwrite by StubbedGreeting.
However, GreetingIncludes.new.sayit results in "StubbedGreeting", not an exception.
So a module can overwrite another module's methods, but not methods already defined directly in the class?
I know how to finagle my way around this, I just found it weird.
When including a module, it's methods are put between the methods of the class and it's parent class in the method resolution order. So when resolving which actual method is to be called, ruby first searches for the method in the following order. If a matching method is found, the search is aborted and this method is used.
the singleton class of the object
modules included into the singleton class from last included to first included
the class of the object
modules included into the class
This then continues for each parent class until the Class class is reached.
As you can see, the modules can indeed not override methods defined on a class itself as modules come behind the actual class in the method resolution order. If you really need to override such a method, you can use alias_method or alias_method_chain to "rename" methods.
In the upcoming Ruby 2.0 there will be a prepend mechanism which will include modules before the class which will achieve what you want. But it's not released yet.

Making a module inherit from another module in Ruby

I'm making a small program for Rails that includes some of my methods I've built inside of a module inside of the ApplicationHelper module. Here's an example:
module Helper
def time
Time.now.year
end
end
module ApplicationHelper
# Inherit from Helper here...
end
I know that ApplicationHelper < Helper and include Helper would work in the context of a class, but what would you use for module-to-module inherits? Thanks.
In fact you can define a module inside of another module, and then include it within the outer one.
so ross$ cat >> mods.rb
module ApplicationHelper
module Helper
def time
Time.now.year
end
end
include Helper
end
class Test
include ApplicationHelper
def run
p time
end
self
end.new.run
so ross$ ruby mods.rb
2012
One potential gotcha is that if the included module attaches class methods, then those methods may be attached to the wrong object.
In some cases, it may be safer to include the 'parent' module directly on the base class, then include another module with the new methods. e.g.
module ApplicationHelper
def self.included(base)
base.class_eval do
include Helper
include InstanceMethods
end
end
module InstanceMethods
def new_method
#..
end
end
end
The new methods are not defined directly in ApplicationHelper as the include Helper would be run after the method definitions, causing them to be overwritten by Helper. One could alternatively define the methods inside the class_eval block

Why doesn't const_missing work without prefixing it with Object?

It looks like const_missing is an instance method of Object. If so, why doesn't this code work?
module Extensions
def const_missing(c)
puts c
end
end
class Object
include Extensions
end
NonExistent.new
In order to get it to function correctly, I have to change def const_missing to def Object.const_missing. Why?
This is just a consequence of the way method calls are resolved in Ruby.
First, singleton methods are checked. Then instance methods of the class, followed by the ancestors (which will be the included modules, then superclasses with their included modules).
So you could define Object.const_missing directly, or include your Module in the singleton class of Object:
class << Object
include Extensions
end
NonExistent # => prints "NonExistent"
You could also monkeypatch Module#const_missing.

Resources