Trajanson
Trajanson

Reputation: 441

Custom Hook/Callback/Macro Methods

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

Answers (3)

Phrogz
Phrogz

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:

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

Upvotes: 3

Cary Swoveland
Cary Swoveland

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

SteveTurczyn
SteveTurczyn

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

Related Questions