Using ActiveSupport::Concern to overload a class method - ruby-on-rails

I have a class method that I would like a module to overload, such that the modules method can call super or another way to call the original class's implementation. This is similar to alias_method_chain, but the question is: is it possible to implement this via ActiveSupport::Concern?
Consider the following:
module CustomAction
CUSTOM_ACTIONS = {custom_action_one: "c1", custom_action_two: "c2"}
include ActiveSupport::Concern
module ClassMethods
def find(id)
CUSTOM_ACTIONS[id] || super
end
end
end
class Action
include CustomAction
ACTIONS = {action_one: "1", action_two: "2"}
def self.find(id)
ACTIONS[id]
end
end
So you can see here that the CustomAction class is adding actions and wants to overload/override the default #find action such that it looks up first in custom actions and if it doesn't find it there, will then fallback to the original find implementation.
However, this doesn't work. The original implementation will always be called. This is the case even if you put the include after the definition of the original #find.
I'm trying to avoid alias_method_chain because 1) isn't it outdated? where ActiveSupport::Concern is the new hotness 2) It would require having the include placed below the implementation which is kind of weird
Thoughts?

You cannot use ActiveSupport::Concern to override class method , because ActiveSupport::Concern use (ActiveSupport::Concern source code) :
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
It is same as :
class Action
extend CustomAction::ClassMethods
end
So , you should use prepend , insert CustomAction::ClassMethods before Action single instance . The result is :
module CustomAction
CUSTOM_ACTIONS = {custom_action_one: "c1", custom_action_two: "c2"}
module ClassMethods
def find(id)
CUSTOM_ACTIONS[id] || super
end
end
end
class Action
ACTIONS = {action_one: "1", action_two: "2"}
def self.find(id)
ACTIONS[id]
end
#prepend on the Action single instance , not on the instance
class << self
prepend CustomAction::ClassMethods
end
end

Related

Ruby include/extend Module: a class method - Beginner

I've been reading this article on the difference between include & extend in ruby.
If I have this module, I understand how the first and second methods of the module will be used in the class. What I don't understand is how the class << self will be used by include or extend.
module Direction
def straight
puts "going straight!"
end
def turn
puts "turning!"
end
class << self
def stop
puts "stopping!"
end
end
end
# This will work because `include` brings them in as instance methods
class Car
include Direction
end
Car.new.straight
Car.new.turn
# ---------------------
# Now this will also work because `extend` brings them in as class methods
class Car
extend Direction
end
Car.straight
Car.turn
# ---------------------
Now, the issue is, doing Car.stop or Car.new.stop will always result in an error:
/Users/<name>/Projects/ruby-testing/main.rb:34:in `<main>': undefined method `stop' for Car:Class (NoMethodError)
Why are class methods not carried over via include and extend?
I started thinking about this because of my research into the [forwardable source code at line 119].(https://github.com/ruby/ruby/blob/master/lib/forwardable.rb#L119)
Thank you for any help you may have!
Update from Answer Below
The following was an example given:
module Direction
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def stop
puts 'stopping!'
end
end
def straight
puts "going straight!"
end
def turn
puts "turning!"
end
end
class Car
include Direction
end
This I understand now, and I understand how I can implement class methods from a module into a class using def self.included(base). My question is, if we used extend inside of Car instead of include, would we still be able to get at those class methods using def self.included(base)?
When you define a method with class << self you are defining a class method. It's the same as defining the methed like this:
class Foo
def self.foo
puts 'foo'
end
# the above definition is the same as doing:
class << self
def foo
puts 'foo'
end
end
end
The above shows 2 ways of defining class methods which are called directly on the class and not on instances of the class. You might use the 2nd syntax if you want to define only class methods or several of them inside of the class << self block. But either style has the same result.
Since you've defined a class method on the Direction module, include or extend will not inherit the class method of that module. This is the expected behavior.
If you want to use inheritance with class methods from a module, you should do it like this which is explained further down in the article you've linked
module Direction
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def stop
puts 'stopping!'
end
end
def straight
puts "going straight!"
end
def turn
puts "turning!"
end
end
class Car
include Direction
end
Now calling class methods on Car will inherit as defined in the Direction class.
Car.stop
stopping!
=>nil # calling a method will return nil unless the method returns a value.
However always be careful using inheritance of any kind as Ruby is a dynamic language. So if you do the above code and then later redefine this method:
module Direction
module ClassMethods
def stop
puts 'go!'
end
end
end
Guess what will happen if you do this:
Car.stop
Since the method was defined inside Direction module, when the method gets called on Car it will be calling the method from the Direction module.
Car.stop
go!
=>nil
Updated based on comments:
If you prefer to use extend vs include you would need to do this instead:
module Direction
def self.extended(base)
base.extend(ClassMethods)
end
module ClassMethods
def stop
puts 'stopping!'
end
end
end
class Car
extend Direction
end
In this example, all the methods which were inherited from the module are "copied" to the class extending them. This avoids the problem of possible result of redefining the module method which I warned about when using include previously in my answer.
But you may want to look at answers to this question for ideas about when and why to use either case.

Ruby extend & include tracing code

I'm confused about using "include" vs "extend, after searching for hours all I got is that module methods used with instance of the class including the module, and module methods used with the class itself when the class extending the module of those methods.
but this didn't help me to figure out, why this code give error when commenting the extend module line in "#extend Inventoryable"
while work when uncomment it, here's the code
module Inventoryable
def create(attributes)
object = new(attributes)
instances.push(object)
return object
end
def instances
#instances ||= []
end
def stock_count
#stock_count ||= 0
end
def stock_count=(number)
#stock_count = number
end
def in_stock?
stock_count > 0
end
end
class Shirt
#extend Inventoryable
include Inventoryable
attr_accessor :attributes
def initialize(attributes)
#attributes = attributes
end
end
shirt1 = Shirt.create(name: "MTF", size: "L")
shirt2 = Shirt.create(name: "MTF", size: "M")
puts Shirt.instances.inspect
the output is
store2.rb:52:in `<main>': undefined method `create' for Shirt:Class (NoMethodError)
while when uncomment the "extend Inventoryable" to make the code work:
module Inventoryable
def create(attributes)
object = new(attributes)
instances.push(object)
return object
end
def instances
#instances ||= []
end
def stock_count
#stock_count ||= 0
end
def stock_count=(number)
#stock_count = number
end
def in_stock?
stock_count > 0
end
end
class Shirt
extend Inventoryable
include Inventoryable
attr_accessor :attributes
def initialize(attributes)
#attributes = attributes
end
end
shirt1 = Shirt.create(name: "MTF", size: "L")
shirt2 = Shirt.create(name: "MTF", size: "M")
puts Shirt.instances.inspect
makes the code work and output the following
[#<Shirt:0x0055792cb93890 #attributes={:name=>"MTF", :size=>"L"}>, #<Shirt:0x0055792cb937a0 #attributes={:name=>"MTF", :size=>"M"}>]
it's kinda confusing, but all I need to know, is why I need to extend the module in order to avoid the error ?, and how to edit this code to make it work without the extend method ? , what's left in the code that still depends on the extend ?
When you extend a module, the methods in that module become "class methods"**. So, when you extend Inventoryable, create becomes available as a method on the Shirt class.
When you include a module, the methods in that module become "instance methods"**. So, when you include Inventoryable, create is not available on the Shirt class (but is available on an instance of Shirt).
To make create available on the Shirt class when using include, you can use the included hook. That might look something like:
module Inventoryable
module ClassMethods
def create
puts "create!"
end
end
module InstanceMethods
end
def self.included(receiver)
receiver.extend ClassMethods
receiver.include InstanceMethods
end
end
Then if you do:
class Shirt
include Invetoryable
end
You can do:
> Shirt.create
create!
=> nil
** The ruby purists in the crowd will correctly point out that, in ruby, everything is an instance method and that there are no class methods. That is formally 100% correct, but we'll use the colloquial meaning of class and instance methods here.
When you extend a module in a class, you get the module's methods exposed as class methods but if you include the module then you get the module's method as instance methods, in your example for you to be able to call create method of Inventoryable class you need to invoke it using an instance of Shirt class (if you include the module)
shirt1 = Shirt.new(attributes).create(attributes)
Without more info I can't tell what you are trying to do but you need to redesign the initialize and create methods to decide where or what to do in those methods.
I'll try to explain it using a simple example
module A
def test
puts "ok"
end
end
class B
include A
end
class C
extend A
end
puts C.test # here you invoke the method against the class itself
puts B.new.test #here you create an instance to do it
Hope it helps.
At the end of the day, it's really simple:
C.include(M) makes the current superclass of C the superclass of M and M the superclass of C. In other words, it inserts M into C's ancestry chain.
obj.extend(M) is (roughly) the same as obj.singleton_class.include(M).

Rails: Controller can not find method in according helper file

Controllers
-concerns
-application_controller.rb
-display_controller.rb
Helpers
-application_helper.rb
-display_controller.rb
In display_helper.rb
module DisplayHelper
def is_c
a + b
end
end
In display_controller.rb
class DisplayController < ApplicationController
include ApplicationHelper
include DisplayHelper
def update
#c = is_c
end
end
The problem is if I want #c = is_c in update action work , I must do include DisplayHelper in Controller, otherwise the result of is_c can not be assigned to #c. Normally method in helper file can be used in accroding controller without including, but for this resource, why it does not work ?
If you are using your module like that, you have to call is_c on an instance. This means that you will have to include DisplayHelper in the class associated with that instance.
if you don't want to call it on an instance, then you can define is_c on the SELF:
Try this:
module DisplayHelper
def self.is_c
a + b
end
end
In the controller:
DisplayHelper.is_c
If you don't understand just comment and i'll try to explain. I hope all of that makes sense.

Overriding methods in an ActiveSupport::Concern module which are defined by a class method in the same module

I have an ActiveSupport::Concern module which looks roughly like the following:
module MyModel
module Acceptance
extend ActiveSupport::Concern
included do
enum status: [:declined, :accepted]
end
def declined!
self.status = :declined
# some extra logic
self.save!
end
def accepted!
self.status = :accepted
# some extra logic
self.save!
end
end
end
This is only ever going to be included into ActiveRecord classes, hence the use of enum. Basically, I'm overriding the declined! and accepted! methods that are created by ActiveRecord::Enum.enum with some extra, custom logic of my own.
The problem is, this doesn't work, because when I call #model.declined! it justs call the original implementation of declined! and ignores my custom method.
Looks like my custom methods are being included into the calling class before the included block is being run - meaning my custom methods are being overridden by the ones defined by enum, instead of the other way around.
There some easy workarounds in this particular situation (e.g. I could move the call enum back into the including class and make sure it's above the line include MyModel::Acceptance, but I'm wondering if there's a way I can solve this problem while keeping it all in the same module.
Is there any way I can call a class method within included that defines an instance method, then override that instance method from within the same Concern module?
I think you're looking for define_method.
module MyModel
module Acceptance
extend ActiveSupport::Concern
included do
enum status: [:declined, :accepted]
define_method :declined! do
self.status = :declined
# some extra logic
self.save!
end
define_method :accepted! do
self.status = :accepted
# some extra logic
self.save!
end
end
end
end

Call module subclass without specify the name

I would like to access my subclass using only the name of my module.
module MyModule
class UselessName
include OtherModel
# only self method
def self.x
end
end
# No other class
end
And I would like to write MyModule.x and not MyModule::UselessName.x
I could transform my module in class, but I use RoR Helpers, and I would prefer that MyModule remains a module and not a class.
Is there a way to do this ?
Thanks ;)
OK, let's split problem into two - getting list of such methods and making proxies in the module.
Getting list might be a little tricky:
MyModule::UselessName.public_methods(false) - MyModule::UselessName.superclass.public_methods(false)
Here we start with list of all public class methods and subtract list of all superclass's public class methods from it.
Now, assuming we know method's name, we need to make proxy method.
metaclass = class << MyModule; self; end
metaclass.send(:define_method, :x) do |*args, &block|
MyModule::UselessName.send(:x, *args, &block)
end
This code will just make equivalent of following definition at runtime.
module MyModule
def x(*args, &block)
MyModule::UselessName.send(:x, *args, &block)
end
end
So let's put it together in simple function.
def make_proxies(mod, cls)
methods = cls.public_methods(false) - cls.superclass.public_methods(false)
metaclass = class << mod; self; end
methods.each do |method|
metaclass.send(:define_method, method) do |*args, &block|
cls.send(method, *args, &block)
end
end
end
So now you'll just need to call it for needed modules and classes. Note that "destination" module can be different from "source" module owning the class, so you can slurp all methods (given they have different names or you'll prefix them using class name) to one module. E.g. for your case just make following call.
make_proxies(MyModule, MyModule::UselessName)
Ok, I've found a VERY DIRTY way to accomplish what I THINK you mean:
module MyModule
class UselessName
include OtherModule
# have whatever you want here
end
def self.x
# whatever
end
end
So somewhere in your code you can do, and I repeat THIS IS VERY, VERY DIRTY!
MyModule.methods(false).each do |m|
# m = method
# now you can redefine it in your class
# as an instance method. Not sure if this
# is what you want though
MyModule::UselessName.send(:define_method, :m) do
# in this NEW (it's not the same) method you can
# call the method from the module to get the same
# behaviour
MyModule.send(m)
end
end
I don't know if this overwrites an instance method with the same name if it's in the class before or if it throws an exception, you have to try that.
In my opinion you should overthink your app design, because this is not the way it should be, but here you go...

Resources