use of extend ActiveSupport::Concern - ruby

I'm working through CodeSchool's RubyBits and I've come to an exercise I'm just not comprehending: "Make sure the AtariLibrary class includes only the LibraryUtils module and let ActiveSupport::Concern take care of loading its dependencies. Then, refactor the self.included method on LibraryUtils to use the included method."
module LibraryLoader
extend ActiveSupport::Concern
module ClassMethods
def load_game_list
end
end
end
module LibraryUtils
def self.included(base)
base.load_game_list
end
end
class AtariLibrary
include LibraryLoader
include LibraryUtils
end
Based on the solution (below) it seems like ActiveSupport::Concern doesn't take care of loading the dependencies - you need to include LibraryLoader inside of LibraryUtils.
Can you help me understand just what ActiveSupport::Concern is doing, and why it needs to be called via extend in both modules?
module LibraryLoader
extend ActiveSupport::Concern
module ClassMethods
def load_game_list
end
end
end
module LibraryUtils
extend ActiveSupport::Concern
include LibraryLoader
#result of refactoring the self.included method
included do
load_game_list
end
end
class AtariLibrary
include LibraryUtils
end
Thanks!

When you call extend ActiveSupport::Concern it will look for a ClassMethods inner-module and will extend your 'host' class with that. Then it will provide you with an included method which you can pass a block to:
included do
some_function
end
The included method will be run within the context of the included class. If you have a module that requires functions in another module, ActiveSupport::Concern will take care of the dependencies for you.

Related

How to include redefined methods in a class with include

I wrote this module:
module Hooks
module ExecutionHooks
def before_action(hook, *method_names)
method_names.each do |method_name|
method = method(method_name)
define_singleton_method(method_name) do |*args, &block|
method(hook).call(*args)
method.call(*args, &block)
end
end
end
end
def self.included(base)
base.send(:extend, Hooks::ExecutionHooks)
end
end
This module allows other modules or classes to define a hook which should be called before a particular action similar to a before_action in Rails.
Then I included this module in my HTTParty module:
module HTTParty
include Hooks
before_action :perform_action, :get
def self.perform_action
puts "performed"
end
end
There is a class which includes the HTTParty module:
class TestClient
include HTTParty
...
end
When I try to access the get method in TestClient, it doesn't call perform_action. The get method being included here is original one, not the redefined one.
Is there a way to include the redefined get method in the TestClient class?
Your code almost works, but get isn't actually defined directly on HTTParty, which you didn't expect, and HTTParty's included class method adds get to your class through another path.
HTTParty has a module called HTTParty::ClassMethods that contains get, etc. It puts them in two places: on HTTParty itself so you can call HTTParty.get, and on any class with include HTTParty, via the included hook. When you open up module HTTParty and include Hooks, you're inserting hooks on HTTParty.get, which is a different lookup chain that when you call TestClient.get. Leaving your Hooks::ExecutionHooks module alone, I recommend making a HookedHTTParty module instead of monkeypatching HTTParty. That will make it more clear what's going on and avoid the complexity of HTTParty's internals, which we shouldn't really be fiddling with.
# hooked_httparty.rb
require 'httparty'
require 'hooks'
module HookedHTTParty
module ClassMethods
def global_perform(*args)
puts "Running global perform with args #{args.inspect}"
end
end
def self.included(base)
base.include(HTTParty)
base.include(Hooks)
base.extend(HookedHTTParty::ClassMethods)
base.before_action :global_perform, :get
end
end
This makes sure HTTParty and Hooks are available on base, and then extends it with the global_perform hook on every get. The primary different from your initial code is that before_action gets called on base (TestClient) instead of on HTTParty, so we catch the right get method. You'll also notice global_perform accepts *args, since you're calling it that way when you generate the hook.
Because we include Hooks, you now also have access to before_action in TestClient itself, so you can also define more specific before_actions:
class TestClient
include HookedHTTParty
before_action :local_perform, :get
def self.local_perform(*args)
puts "Running local perform with args #{args.inspect}"
end
end
Running get looks like this:
> TestClient.get 'https://www.stackoverflow.com'
Running local perform with args ["https://www.stackoverflow.com"]
Running global perform with args ["https://www.stackoverflow.com"]
=> #<HTTParty::Response:0x7fa523a009e0 ... >
If you really need anything including HTTParty to get your hooks (perhaps because you don't have control of the thing including it), you might need to monkeypatch HTTParty::ClassMethods directly, since that's the bottleneck where get is defined, but that's getting into even darker territory. As long as you're injecting code around, you could also OtherThing.include(HookedHTTParty) to make it more explicit and keep it just a bit more encapsulated.

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...

What does this extend do?

Given this example:
module A
module B
def foo
puts 'foo'
end
extend A::B
end
end
What does this extend A::B do?
It extends module A::B with itself, essentially make method foo available on module object A::B itself.
Without that line, you are not able to call A::B.foo in your code.
You may want to read more on Ruby extend aModule vs include aModule.

ActiveSupport::Concern vs append_features

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.

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

Resources