3

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

Trajanson
  • 441
  • 3
  • 11
  • Do you want `first_validate_something` to be called before *any* method is called (even, say, `to_s`)? If not, how is it supposed to decide which methods to "hook"? – Jordan Running Mar 19 '16 at 00:34
  • first_validate is called only before the methods listed as arguments, so in this case #do_this_method, and #do_that method This is what the method definition would look like, to steal from SteveTurczyn: before_operations(before_method, *methods) Does that answer your question? – Trajanson Mar 19 '16 at 03:30
  • Yes it does. My apologies, I'm on mobile and didn't scroll right far enough to see the second and third arguments! – Jordan Running Mar 19 '16 at 03:37
  • These are some obvious libraries that pull this off: RSpec(#before): https://github.com/rspec/rspec-core/blob/5598c131bbaf3197881780e2b779f447da71eea4/lib/rspec/core/hooks.rb Rails: https://github.com/rails/rails/blob/master/activesupport/lib/active_support/callbacks.rb#L366 But I'm struggling to understand them at the moment. – Trajanson Mar 19 '16 at 21:53

3 Answers3

3

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
SteveTurczyn
  • 36,057
  • 6
  • 41
  • 53
  • 1
    This looks like its definitely on the right track, but when I add it to simplified sample code, I get an error, first this: "both block arg and actual block given" then I delete &block from the define method line and get this error "undefined method `do_this_method' for class `Context::SubClass'" any thoughts, on what went wrong? – Trajanson Mar 19 '16 at 03:34
  • I've added a portion to my answer that is inspired by this idea, but fixes it to actually work. – Phrogz Mar 21 '16 at 17:50
3

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:

  1. receive the args params in the block of define_method, not as arguments to it.
  2. call 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>}
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • any thoughts on how to move before_operations above the methods listed? – Trajanson Mar 20 '16 at 02:04
  • 1
    Have `before_operations` maintain a list of methods, override `method_added` to check whether the method currently being defined is in that list. That's how Rake's `desc` method works, for example. You can see an example in these two answers of mine: http://stackoverflow.com/a/2948977/2988 http://stackoverflow.com/a/3161693/2988 (Note to self: update to use `Module#prepend`!) – Jörg W Mittag Mar 21 '16 at 01:07
  • I've edited my answer to add a new, prepend-based solution to the problem. This allows `before_operations` to be invoked before you have defined the methods, and also does not use any aliasing nonsense. – Phrogz Mar 21 '16 at 17:49
1

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.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • that's cool, but does require that you define the validees in the super class? – SteveTurczyn Mar 21 '16 at 22:11
  • @SteveTurczyn, If the validees were in `SubClass` I don't see a way of modifying them without using aliases. By having them in `MidClass`, but calling them on an instance of `SubClass`, the instance method with the same name in `SubClass` need only contain a line calling a validator and `super`, which is easy to create with `define_method`. – Cary Swoveland Mar 22 '16 at 16:54
  • I see where your coming from, and it does seem the easiest way to meet the requirements. – SteveTurczyn Mar 22 '16 at 17:07