Reputation: 441
How do I create a Custom Hook Method in a Subclass?
No need to duplicate Rails, of course -- the simpler, the better.
My goal is to convert:
class SubClass
def do_this_method
first_validate_something
end
def do_that_method
first_validate_something
end
private
def first_validate_something; end
end
To:
class ActiveClass; end
class SubClass < ActiveClass
before_operations :first_validate_something, :do_this_method, :do_that_method
def do_this_method; end
def do_that_method; end
private
def first_validate_something; end
end
Example in Module: https://github.com/PragTob/after_do/blob/master/lib/after_do.rb
Rails #before_action: http://apidock.com/rails/v4.0.2/AbstractController/Callbacks/ClassMethods/before_action
Upvotes: 3
Views: 1579
Reputation: 303361
Here's a solution that uses prepend
. When you call before_operations
for the first time it creates a new (empty) module and prepends it to your class. This means that when you call method foo
on your class, it will look first for that method in the module.
The before_operations
method then defines simple methods in this module that first invoke your 'before' method, and then use super
to invoke the real implementation in your class.
class ActiveClass
def self.before_operations(before_method,*methods)
prepend( @active_wrapper=Module.new ) unless @active_wrapper
methods.each do |method_name|
@active_wrapper.send(:define_method,method_name) do |*args,&block|
send before_method
super(*args,&block)
end
end
end
end
class SubClass < ActiveClass
before_operations :first_validate_something, :do_this_method, :do_that_method
def do_this_method(*args,&block)
p doing:'this', with:args, and:block
end
def do_that_method; end
private
def first_validate_something
p :validating
end
end
SubClass.new.do_this_method(3,4){ |x| p x }
#=> :validating
#=> {:doing=>"this", :with=>[3, 4], :and=>#<Proc:0x007fdb1301fa18@/tmp.rb:31>}
If you want to make the idea by @SteveTurczyn work you must:
define_method
, not as arguments to it.before_operations
AFTER your methods have been defined if you want to be able to alias them.
class ActiveClass
def self.before_operations(before_method, *methods)
methods.each do |meth|
raise "No method `#{meth}` defined in #{self}" unless method_defined?(meth)
orig_method = "_original_#{meth}"
alias_method orig_method, meth
define_method(meth) do |*args,&block|
send before_method
send orig_method, *args, &block
end
end
end
end
class SubClass < ActiveClass
def do_this_method(*args,&block)
p doing:'this', with:args, and:block
end
def do_that_method; end
before_operations :first_validate_something, :do_this_method, :do_that_method
private
def first_validate_something
p :validating
end
end
SubClass.new.do_this_method(3,4){ |x| p x }
#=> :validating
#=> {:doing=>"this", :with=>[3, 4], :and=>#<Proc:0x007fdb1301fa18@/tmp.rb:31>}
Upvotes: 3
Reputation: 110725
This is a way of writing the code that does not make use of aliases. It includes a class method validate
that specifies the validator method and the methods that are to call the validator method. This method validate
can be invoked multiple times to change the validator and validatees dynamically.
class ActiveClass
end
Place all the methods other than the validators in a subclass of ActiveClass
named (say) MidClass
.
class MidClass < ActiveClass
def do_this_method(v,a,b)
puts "this: v=#{v}, a=#{a}, b=#{b}"
end
def do_that_method(v,a,b)
puts "that: v=#{v}, a=#{a}, b=#{b}"
end
def yet_another_method(v,a,b)
puts "yet_another: v=#{v}, a=#{a}, b=#{b}"
end
end
MidClass.instance_methods(false)
#=> [:do_this_method, :do_that_method, :yet_another_method]
Place the validators, together with a class method validate
, in a subclass of MidClass
named (say) SubClass
.
class SubClass < MidClass
def self.validate(validator, *validatees)
superclass.instance_methods(false).each do |m|
if validatees.include?(m)
define_method(m) do |v, *args|
send(validator, v)
super(v, *args)
end
else
define_method(m) do |v, *args|
super(v, *args)
end
end
end
end
private
def validator1(v)
puts "valid1, v=#{v}"
end
def validator2(v)
puts "valid2, v=#{v}"
end
end
SubClass.methods(false)
#=> [:validate]
SubClass.private_instance_methods(false)
#=> [:validator1, :validator2]
The class method validate
passes symbols for the validation method to use and the methods to be validated. Let's try it.
sc = SubClass.new
SubClass.validate(:validator1, :do_this_method, :do_that_method)
sc.do_this_method(1,2,3)
# valid1, v=1
# this: v=1, a=2, b=3
sc.do_that_method(1,2,3)
# valid1, v=1
# that: v=1, a=2, b=3
sc.yet_another_method(1,2,3)
# yet_another: v=1, a=2, b=3
Now change the validation.
SubClass.validate(:validator2, :do_that_method, :yet_another_method)
sc.do_this_method(1,2,3)
# this: v=1, a=2, b=3
sc.do_that_method(1,2,3)
# valid2, v=1
# that: v=1, a=2, b=3
sc.yet_another_method(1,2,3)
# valid2, v=1
# yet_another: v=1, a=2, b=3
When super
is called without arguments from a normal method, all arguments and a block, if there is one, are passed to super. If the method was created with define_method
, however, no arguments (and no block) are passed to super. In the latter case the arguments must be explicit.
I wanted to pass a block or proc on to super
if there is one, but have been using the wrong secret sauce. I would welcome advice for doing that.
Upvotes: 1
Reputation: 36860
You can alias the original method to a different name (so :do_this_something
becomes :original_do_this_something
) and then define a new :do_this_something
method that calls :first_validate_something
and then the original version of the method Something like this...
class ActiveClass
def self.before_operations(before_method, *methods)
methods.each do |method|
alias_method "original_#{method.to_s}".to_sym, method
define_method(method, *args, &block) do
send before_method
send "original_#{method.to_s}", *args, &block
end
end
end
end
Upvotes: 3