I'm playing with ruby's metaprogramming features, and I'm finding it a bit hairy. I'm trying to wrap a method call using a module. Currently, I'm doing this:
module Bar
module ClassMethods
def wrap(method)
class_eval do
old_method = "wrapped_#{method}".to_sym
unless respond_to? old_method
alias_method old_method, method
define_method method do |*args|
send old_method, *args
end
end
end
end
end
def self.included(base)
base.extend ClassMethods
end
end
class Foo
include Bar
def bar(arg = 'foo')
puts arg
end
wrap :bar
end
Three questions:
Is there any way to do this without renaming the method, so as to allow the use of super? Or something cleaner/shorter?
Is there a clean way to set the default values?
Is there a means to move the wrap :bar call further up?
1) Cleaner/shorter
module ClassMethods
def wrap(method)
old = "_#{method}".to_sym
alias_method old, method
define_method method do |*args|
send(old, *args)
end
end
end
class Foo
extend ClassMethods
def bar(arg = 'foo')
puts arg
end
wrap :bar
end
As far as I know there is no way to achieve this without renaming. You could try to call super inside the define_method block. But first of all, a call to super from within a define_method will only succeed if you specify arguments explicitly, otherwise you receive an error. But even if you call e.g. super(*args), self in that context would be an instance of Foo. So a call to bar would go to the super classes of Foo, not be found and ultimately result in an error.
2) Yes, like so
define_method method do |def_val='foo', *rest|
send(old, def_val, *rest)
end
However, in Ruby 1.8 it is not possible to use a block in define_method, but this has been fixed for 1.9. If you are using 1.9, you could also use this
define_method method do |def_val='foo', *rest, &block|
send(old, def_val, *rest, &block)
end
3) No, unfortunately. alias_method requires the existence of the methods that it takes as input. As Ruby methods come into existence as they are parsed, the wrap call must be placed after the definition of bar otherwise alias_method would raise an exception.
Related
I have a module I'd like to be able to wrap every method in a class, whether it be an instance method or a class method, but am having a bit of trouble trying to get it to work in both circumstances. Below is what I have right now, it works for wrapping instance methods but calling class methods doesn't seem to work. I assume the module proxy isn't installed correctly for overriding class methods, but I'm not sure what to do to fix that.
I'm limited to ruby 2.7, btw.
module Wrapper
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
module Proxy
end
def wrap_method(name)
puts "wrapping #{name}"
Proxy.define_method(name) do |*args|
puts "in wrap_method, about to call #{name}"
super *args
end
end
def wrap_methods
# wrap any methods that were defined before this was called
self.instance_methods(false).each {|name| wrap_method(name) }
self.singleton_methods(false).each {|name| wrap_method(name) }
# wrap any methods that are defined after this is called
self.class.define_method(:singleton_method_added) do |name|
if respond_to?(:wrap_method)
wrap_method(name)
end
end
self.class.define_method(:method_added) do |name|
if respond_to?(:wrap_method)
wrap_method(name)
end
end
self.prepend(Proxy)
end
end
end
module Top
module Second
class Main
include Wrapper
wrap_methods
def self.first_class_method
puts "in self.first_class_method"
end
def self.second_class_method
puts "in self.second_class_method"
end
def some_instance_method
puts "in some_instance_method"
end
end
end
end
Top::Second::Main.first_class_method
Top::Second::Main.second_class_method
Top::Second::Main.new.some_instance_method
Output:
wrapping first_class_method
wrapping second_class_method
wrapping some_instance_method
in self.first_class_method
in self.second_class_method
in wrap_method, about to call some_instance_method
in some_instance_method
You prepend Proxy to the class itself, and this modifies the method resolution for instances of the class, but not for the class itself.
To achieve the second, you have to
self.singleton_class.prepend(Proxy)
in addition.
I am creating a helper module to initialize the object before calling its methods
module Initialized
extend ActiveSupport::Concern
class_methods do
def run(*args)
new(*args).run
end
def call(*args)
new(*args).call
end
def execute(*args)
new(*args).create
end
end
end
So instead of defining run, call, and execute in my helper module I need to receive any method name and check if it exists on the main class after initializing it, then call the requested instance method if exists in the main class or raise an error if not
I would say my targeted code would be something like this
module Initialized
extend ActiveSupport::Concern
class_methods do
def _(*args, methodname)
new(*args).try(:send, "#{methodname}") || raise 'Method not exist'
end
end
end
Sample usage would be
class MyClass
include Initialized
def initialize(param1)
#param1 = param1
end
def call
puts "call '#{#param1}'"
end
end
then calling
MyClass.call('method param')
I found these links but couldn't find my answer yet:
meta-dynamic-generic-programming-in-ruby
ruby-module-that-delegates-methods-to-an-object
template-methods-in-ruby
Despite the fact method_missing would do the job, I'd probably avoid it in this case in favor of a more explicit delegation. Simple and dirty example:
module InstanceDelegator
def delegate_to_instance(*methods)
methods.each do |method_name|
define_singleton_method method_name do |*args|
new(*args).public_send(method_name)
end
end
end
end
class Foo
extend InstanceDelegator
delegate_to_instance :bar # <- define explicitly which instance methods
# should be mirrored by the class ones
def bar
puts "bar is called"
end
def baz
puts "baz is called"
end
end
# and then
Foo.bar # => bar is called
Foo.baz # NoMethodError ...
# reopening works too
class Foo
delegate_to_instance :baz
end
Foo.baz # baz is called
Pros:
you don't need to redefine method_missing (less magic -> less pain when you debug the code)
you control precisely which instance methods to be wrapped with the class level "shorthand" (fewer chances to delegate something you don't want to - more robust code)
(minor) no need to raise NoMethodError explicitly - you can fully rely on the core dispatching as it is...
I found another solution instead of using a module,
I can use the class method self.method_missing
def self.method_missing(method_name, *args, &block)
obj = new(*args)
raise NoMethodError, "undefined method `#{method_name}' for #{self}:Class" unless obj.respond_to?(method_name)
obj.send(method_name, &block)
end
But the limitation is that I have to copy it into every class whenever I need to use this feature
In Rails we can define a class like:
class Test < ActiveRecord::Base
before_initialize :method
end
and when calling Test.new, method() will be called on the instance. I'm trying to learn more about Ruby and class methods like this, but I'm having trouble trying to implement this in plain Ruby.
Here's what I have so far:
class LameAR
def self.before_initialize(*args, &block)
# somehow store the symbols or block to be called on init
end
def new(*args)
## Call methods/blocks here
super(*args)
end
end
class Tester < LameAR
before_initialize :do_stuff
def do_stuff
puts "DOING STUFF!!"
end
end
I'm trying to figure out where to store the blocks in self.before_initialize. I originally tried an instance variable like #before_init_methods, but that instance variable wouldn't exist in memory at that point, so I couldn't store or retrieve from it. I'm not sure how/where could I store these blocks/procs/symbols during the class definition, to later be called inside of new.
How could I implement this? (Either having before_initialize take a block/proc/list of symbols, I don't mind at this point, just trying to understand the concept)
For a comprehensive description, you can always check the Rails source; it is itself implemented in 'plain Ruby', after all. (But it handles lots of edge cases, so it's not great for getting a quick overview.)
The quick version is:
module MyCallbacks
def self.included(klass)
klass.extend(ClassMethods) # we don't have ActiveSupport::Concern either
end
module ClassMethods
def initialize_callbacks
#callbacks ||= []
end
def before_initialize(&block)
initialize_callbacks << block
end
end
def initialize(*)
self.class.initialize_callbacks.each do |callback|
instance_eval(&callback)
end
super
end
end
class Tester
include MyCallbacks
before_initialize { puts "hello world" }
end
Tester.new
Left to the reader:
arguments
calling methods by name
inheritance
callbacks aborting a call and supplying the return value
"around" callbacks that wrap the original invocation
conditional callbacks (:if / :unless)
subclasses selectively overriding/skipping callbacks
inserting new callbacks elsewhere in the sequence
... but eliding all of those is what [hopefully] makes this implementation more approachable.
One way would be by overriding Class#new:
class LameAR
def self.before_initialize(*symbols_or_callables, &block)
#before_init_methods ||= []
#before_init_methods.concat(symbols_or_callables)
#before_init_methods << block if block
nil
end
def self.new(*args, &block)
obj = allocate
#before_init_methods.each do |symbol_or_callable|
if symbol_or_callable.is_a?(Symbol)
obj.public_send(symbol_or_callable)
else
symbol_or_callable.(obj)
end
end
obj.__send__(:initialize, *args, &block)
end
end
class Tester < LameAR
before_initialize :do_stuff
def do_stuff
puts "DOING STUFF!!"
end
end
How to define an original name scope in module/class with Ruby
I want to implement class like the following:
module SomeModule
extend OriginalNameScope
scope(:some) do
def method1
puts 1
end
def method2
puts 2
end
end
end
class SomeClass
include SomeModule
end
c = SomeClass.new
# I want to call methods like the following:
c.some_method1
c.some_method2
How to implement the OriginalNameScope module? I found out to get the method definitions in this method, but I don't know how to redefine methods with a prefix scope.
module OriginalNameScope
def scope(name, &method_definition)
puts method_definition.class
# => Proc
end
end
This is actually just a combination of some simple standard Ruby metaprogramming patterns and idioms:
module OriginalNameScope
def scope(name)
singleton_class.prepend(Module.new do
define_method(:method_added) do |meth|
if name && !#__recursion_guard__
#__recursion_guard__ = meth
method = instance_method(meth)
undef_method(meth)
define_method(:"#{name}_#{meth}") do |*args, &block|
method.bind(self).(*args, &block)
end
end
#__recursion_guard__ = nil
super(meth)
end
end)
yield
end
end
I just slapped this together, there's probably a lot that can be improved (e.g. use Refinements) and simplified.
I have a subclass that I would like to implement some default behavior for a set of methods, for example if I know I need to override the method, but just want to print a warning for now if the method is not yet supported on this particular subclass.
I can override each method in the subclass, like so, but I was wondering if there was a easier way, maybe using delegate or method_missing?
class Foo
def method_a
# do foo stuff
end
def method_b
# do foo stuff
end
def method_c
# do foo stuff
end
end
class Bar < Foo
def not_yet_supported
puts "warnnig not yet supported"
end
def method_b
not_yet_supported
end
end
Using alias or alias_method as suggested in the comments is an easy route, and would look like
alias method_b not_yet_supported
alias_method :method_c, :not_yet_supported
Note the syntactic differences, since alias is a keyword and alias_method is a method.
If you want to declare a whole list of unsupported methods, a little metaprogramming gets you there:
class Foo
def self.not_yet_supported(*method_names)
method_names.each do |method_name|
define_method method_name do
puts "#{method_name} is not yet supported on #{self.class.name}"
end
end
end
not_yet_supported :bar, :baz
end
A cleaner approach would be to use method_missing:
class Foo
UNIMPLEMENTED_METHODS = [:method_a, :method_b, :method_etc]
def method_missing(method_id)
UNIMPLEMENTED_METHODS.include?(method_id) ? warn("#{method_id} has not been implemented") : raise(NoMethodError)
end
end
A friendlier explanation of method_missing can also be found here