public
Description: Handy ruby snippets to get the job done quickly!
Homepage: http://cheind.blogspot.com
Clone URL: git://github.com/cheind/ruby-snippets.git
ruby-snippets / util / following.rb
100644 122 lines (112 sloc) 4.415 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#
# Project:: Ruby-Snippets
#
# Author:: Christoph Heindl (mailto:christoph.heindl@gmail.com)
# Homepage:: http://cheind.blogspot.com
#
# == Overview
#
# Implements <tt>following</tt> and <tt>following!</tt>
# utility methods to hook into instance methods and execute
# custom code after hooked methods are called.
#
# See discussion at
# http://cheind.blogspot.com/2008/12/method-hooks-in-ruby.html
#
 
# Contains methods to hook method calls
module FollowingHook
  
  module ClassMethods
    
    private
    
    # Hook the provided instance methods so that the block
    # is executed directly after the specified methods have been invoked.
    #
    # There can only be one hook for a single instance method. Further attempts
    # to hook already hooked methods will result in an ArgumentError
    #
    # +syms+:: is list of method symbols or stringified names to hook on
    # +block+:: is the block to execute after hooked method has been invoked.
    #
    # +block+ can receive two arguments: the receiver of the method and an argument hash
    # which contains the invoked method name <tt>:method</tt>, the arguments passed to
    # the method <tt>:args</tt> and the return value of the method <tt>:return</tt>.
    # See documentation of <tt>__hook__</tt> for a detailed discussion.
    #
    # class Object
    # include FollowingHook
    # following :system do |receiver, args|
    # p "#{args[:method]} called with arguments #{args[:args].join(",")}"
    # p "return value was #{args[:return]}"
    # end
    # end
    #
    # system('ruby --version')
    # # => ruby 1.8.6 (2008-08-11 patchlevel 287) [i386-mswin32]
    # # => "system called with arguments ruby --version"
    # # => "return value was true"
    # # => true
    #
    def following(*syms, &block)
      syms.each do |sym|
        backup_name = ::FollowingHook.hook_name_for_method(sym)
        raise ArgumentError.new("Method #{sym} already hooked.") if __hooked__?(backup_name)
        __backup__(sym, backup_name)
        __hook__(sym, backup_name, &block)
      end
    end
    
    # Identical to <tt>following</tt> except that if method has
    # already been hooked, hook will be overridden which call
    # to given block
    #
    # See documentation of <tt>following</tt>
    #
    def following!(*syms, &block)
      syms.each do |sym|
        backup_name = ::FollowingHook.hook_name_for_method(sym)
        __backup__(sym, backup_name) unless __hooked__?(backup_name)
        __hook__(sym, backup_name, &block)
      end
    end
    
    # This method will backup an existing instance method (create a copy)
    # and privatize it.
    #
    def __backup__(sym, backup_name)
      alias_method backup_name, sym # Backup original method
      private backup_name # Make backup private
    end
    
    # Defines or overrides a a method with a call to the original
    # method and an invokation of the block.
    #
    # sym:: method name to define/override
    # backup_name:: name of the orginal method
    # block:: block to call after hooked method is called.
    # In case the block has
    # - 0 arguments: nothing is passed as argument to the block
    # - 1 arguments: the receiver of the method call is passed
    # - 2 arguments: the receiver and a hash of options containing the method call arguments is passed.
    #
    def __hook__(sym, backup_name, &block)
      define_method sym do |*args| # Replace method
        ret = __send__ backup_name, *args # Invoke backup
        case block.arity # Based on the number of block arguments
        when 0 : yield
        when 1 : yield(self)
        when 2 : yield(self, :method => sym, :args => args, :return => ret)
        end
        ret # Forward return value of method
      end
    end
    
    # Test if method has already been hooked.
    def __hooked__?(backup_name)
      private_instance_methods.include?(backup_name)
    end
  end
  
  # Standard identifier for the backup of a hooked method.
  def FollowingHook.hook_name_for_method(sym)
    RUBY_VERSION >= '1.9.0' ? "__#{sym}__hooked__".to_sym : "__#{sym}__hooked__"
  end
  
  # On inclusion, we extend the receiver by the defined class-methods
  # This is an ruby idiom for defining class methods within a module.
  def FollowingHook.included(base)
    base.extend(ClassMethods)
  end
end