Module | Extlib::Hook::ClassMethods |
In: |
lib/extlib/hook.rb
|
Inject code that executes after the target instance method.
@param target_method<Symbol> the name of the instance method to inject after @param method_sym<Symbol> the name of the method to run after the
target_method
@param block<Block> the code to run after the target_method
@note
Either method_sym or block is required.
- @api public
# File lib/extlib/hook.rb, line 102 102: def after(target_method, method_sym = nil, &block) 103: install_hook :after, target_method, method_sym, :instance, &block 104: end
Inject code that executes after the target class method.
@param target_method<Symbol> the name of the class method to inject after @param method_sym<Symbol> the name of the method to run after the target_method @param block<Block> the code to run after the target_method
@note
Either method_sym or block is required.
- @api public
# File lib/extlib/hook.rb, line 70 70: def after_class_method(target_method, method_sym = nil, &block) 71: install_hook :after, target_method, method_sym, :class, &block 72: end
— Helpers —
# File lib/extlib/hook.rb, line 360 360: def args_for(method) 361: if method.arity == 0 362: "&block" 363: elsif method.arity > 0 364: "_" << (1 .. method.arity).to_a.join(", _") << ", &block" 365: elsif (method.arity + 1) < 0 366: "_" << (1 .. (method.arity).abs - 1).to_a.join(", _") << ", *args, &block" 367: else 368: "*args, &block" 369: end 370: end
Inject code that executes before the target instance method.
@param target_method<Symbol> the name of the instance method to inject before @param method_sym<Symbol> the name of the method to run before the
target_method
@param block<Block> the code to run before the target_method
@note
Either method_sym or block is required.
- @api public
# File lib/extlib/hook.rb, line 86 86: def before(target_method, method_sym = nil, &block) 87: install_hook :before, target_method, method_sym, :instance, &block 88: end
Inject code that executes before the target class method.
@param target_method<Symbol> the name of the class method to inject before @param method_sym<Symbol> the name of the method to run before the
target_method
@param block<Block> the code to run before the target_method
@note
Either method_sym or block is required.
- @api public
# File lib/extlib/hook.rb, line 55 55: def before_class_method(target_method, method_sym = nil, &block) 56: install_hook :before, target_method, method_sym, :class, &block 57: end
# File lib/extlib/hook.rb, line 147 147: def class_hooks 148: self.const_get("CLASS_HOOKS") 149: end
# File lib/extlib/hook.rb, line 269 269: def define_advised_method(target_method, scope) 270: args = args_for(method_with_scope(target_method, scope)) 271: 272: renamed_target = hook_method_name(target_method, 'hookable_', 'before_advised') 273: 274: source = "def \#{target_method}(\#{args})\nretval = nil\ncatch(:halt) do\n\#{hook_method_name(target_method, 'execute_before', 'hook_stack')}(\#{args})\nretval = \#{renamed_target}(\#{args})\n\#{hook_method_name(target_method, 'execute_after', 'hook_stack')}(retval, \#{args})\nretval\nend\nend\n" 275: 276: if scope == :instance && !instance_methods(false).map { |m| m.to_s }.include?(target_method.to_s) 277: send(:alias_method, renamed_target, target_method) 278: 279: proxy_module = Module.new 280: proxy_module.class_eval(source, __FILE__, __LINE__) 281: self.send(:include, proxy_module) 282: else 283: source = %{alias_method :#{renamed_target}, :#{target_method}\n#{source}} 284: source = %{class << self\n#{source}\nend} if scope == :class 285: class_eval(source, __FILE__, __LINE__) 286: end 287: end
Defines two methods. One method executes the before hook stack. The other executes the after hook stack. This method will be called many times during the Class definition process. It should be called for each hook that is defined. It will also be called when a hook is redefined (to make sure that the arity hasn‘t changed).
# File lib/extlib/hook.rb, line 225 225: def define_hook_stack_execution_methods(target_method, scope) 226: unless registered_as_hook?(target_method, scope) 227: raise ArgumentError, "#{target_method} has not be registered as a hookable #{scope} method" 228: end 229: 230: hooks = hooks_with_scope(scope) 231: 232: before_hooks = hooks[target_method][:before] 233: before_hooks = before_hooks.map{ |info| inline_call(info, scope) }.join("\n") 234: 235: after_hooks = hooks[target_method][:after] 236: after_hooks = after_hooks.map{ |info| inline_call(info, scope) }.join("\n") 237: 238: source = %{ 239: private 240: 241: def #{hook_method_name(target_method, 'execute_before', 'hook_stack')}(*args) 242: #{before_hooks} 243: end 244: 245: def #{hook_method_name(target_method, 'execute_after', 'hook_stack')}(*args) 246: #{after_hooks} 247: end 248: } 249: 250: source = %{class << self\n#{source}\nend} if scope == :class 251: 252: hooks[target_method][:in].class_eval(source, __FILE__, __LINE__ - 12) 253: end
Generates names for the various utility methods. We need to do this because the various utility methods should not end in = so, while we‘re at it, we might as well get rid of all punctuation.
# File lib/extlib/hook.rb, line 195 195: def hook_method_name(target_method, prefix, suffix) 196: target_method = target_method.to_s 197: 198: case target_method[-1,1] 199: when '?' then "#{prefix}_#{target_method[0..-2]}_ques_#{suffix}" 200: when '!' then "#{prefix}_#{target_method[0..-2]}_bang_#{suffix}" 201: when '=' then "#{prefix}_#{target_method[0..-2]}_eq_#{suffix}" 202: # I add a _nan_ suffix here so that we don't ever encounter 203: # any naming conflicts. 204: else "#{prefix}_#{target_method[0..-1]}_nan_#{suffix}" 205: end 206: end
Returns the correct HOOKS Hash depending on whether we are working with class methods or instance methods
# File lib/extlib/hook.rb, line 139 139: def hooks_with_scope(scope) 140: case scope 141: when :class then class_hooks 142: when :instance then instance_hooks 143: else raise ArgumentError, 'You need to pass :class or :instance as scope' 144: end 145: end
Returns ruby code that will invoke the hook. It checks the arity of the hook method and passes arguments accordingly.
# File lib/extlib/hook.rb, line 257 257: def inline_call(method_info, scope) 258: name = method_info[:name] 259: 260: if scope == :instance 261: args = method_defined?(name) && instance_method(name).arity != 0 ? '*args' : '' 262: %(#{name}(#{args}) if self.class <= ObjectSpace._id2ref(#{method_info[:from].object_id})) 263: else 264: args = respond_to?(name) && method(name).arity != 0 ? '*args' : '' 265: %(#{name}(#{args}) if self <= ObjectSpace._id2ref(#{method_info[:from].object_id})) 266: end 267: end
— Add a hook —
# File lib/extlib/hook.rb, line 302 302: def install_hook(type, target_method, method_sym, scope, &block) 303: assert_kind_of 'target_method', target_method, Symbol 304: assert_kind_of 'method_sym', method_sym, Symbol unless method_sym.nil? 305: assert_kind_of 'scope', scope, Symbol 306: 307: if !block_given? and method_sym.nil? 308: raise ArgumentError, "You need to pass 2 arguments to \"#{type}\"." 309: end 310: 311: if method_sym.to_s[-1,1] == '=' 312: raise ArgumentError, "Methods ending in = cannot be hooks" 313: end 314: 315: unless [ :class, :instance ].include?(scope) 316: raise ArgumentError, 'You need to pass :class or :instance as scope' 317: end 318: 319: if registered_as_hook?(target_method, scope) 320: hooks = hooks_with_scope(scope) 321: 322: #if this hook is previously declared in a sibling or cousin we must move the :in class 323: #to the common ancestor to get both hooks to run. 324: if !(hooks[target_method][:in] <=> self) 325: hooks[target_method][:in].class_eval( 326: %{def #{hook_method_name(target_method, 'execute_before', 'hook_stack')}(*args);super;end\n} + 327: %{def #{hook_method_name(target_method, 'execute_after', 'hook_stack')}(*args);super;end}, 328: __FILE__,__LINE__ - 2 329: ) 330: while !(hooks[target_method][:in] <=> self) do 331: hooks[target_method][:in] = hooks[target_method][:in].superclass 332: end 333: define_hook_stack_execution_methods(target_method, scope) 334: hooks[target_method][:in].class_eval{define_advised_method(target_method, scope)} 335: end 336: else 337: register_hook(target_method, scope) 338: hooks = hooks_with_scope(scope) 339: end 340: 341: #if we were passed a block, create a method out of it. 342: if block 343: method_sym = "__hooks_#{type}_#{quote_method(target_method)}_#{hooks[target_method][type].length}".to_sym 344: if scope == :class 345: meta_class.instance_eval do 346: define_method(method_sym, &block) 347: end 348: else 349: define_method(method_sym, &block) 350: end 351: end 352: 353: # Adds method to the stack an redefines the hook invocation method 354: hooks[target_method][type] << { :name => method_sym, :from => self } 355: define_hook_stack_execution_methods(target_method, scope) 356: end
# File lib/extlib/hook.rb, line 151 151: def instance_hooks 152: self.const_get("INSTANCE_HOOKS") 153: end
# File lib/extlib/hook.rb, line 372 372: def method_with_scope(name, scope) 373: case scope 374: when :class then method(name) 375: when :instance then instance_method(name) 376: else raise ArgumentError, 'You need to pass :class or :instance as scope' 377: end 378: end
This will need to be refactored
# File lib/extlib/hook.rb, line 209 209: def process_method_added(method_name, scope) 210: hooks_with_scope(scope).each do |target_method, hooks| 211: if hooks[:before].any? { |hook| hook[:name] == method_name } 212: define_hook_stack_execution_methods(target_method, scope) 213: end 214: 215: if hooks[:after].any? { |hook| hook[:name] == method_name } 216: define_hook_stack_execution_methods(target_method, scope) 217: end 218: end 219: end
# File lib/extlib/hook.rb, line 380 380: def quote_method(name) 381: name.to_s.gsub(/\?$/, '_q_').gsub(/!$/, '_b_').gsub(/=$/, '_eq_') 382: end
Register a class method as hookable. Registering a method means that before hooks will be run immediately before the method is invoked and after hooks will be called immediately after the method is invoked.
@param hookable_method<Symbol> The name of the class method that should
be hookable
- @api public
# File lib/extlib/hook.rb, line 114 114: def register_class_hooks(*hooks) 115: hooks.each { |hook| register_hook(hook, :class) } 116: end
Registers a method as hookable. Registering hooks involves the following process
# File lib/extlib/hook.rb, line 164 164: def register_hook(target_method, scope) 165: if scope == :instance && !method_defined?(target_method) 166: raise ArgumentError, "#{target_method} instance method does not exist" 167: elsif scope == :class && !respond_to?(target_method) 168: raise ArgumentError, "#{target_method} class method does not exist" 169: end 170: 171: hooks = hooks_with_scope(scope) 172: 173: if hooks[target_method].nil? 174: hooks[target_method] = { 175: # We need to keep track of which class in the Inheritance chain the 176: # method was declared hookable in. Every time a child declares a new 177: # hook for the method, the hook stack invocations need to be redefined 178: # in the original Class. See #define_hook_stack_execution_methods 179: :before => [], :after => [], :in => self 180: } 181: 182: define_hook_stack_execution_methods(target_method, scope) 183: define_advised_method(target_method, scope) 184: end 185: end
Register aninstance method as hookable. Registering a method means that before hooks will be run immediately before the method is invoked and after hooks will be called immediately after the method is invoked.
@param hookable_method<Symbol> The name of the instance method that should
be hookable
- @api public
# File lib/extlib/hook.rb, line 126 126: def register_instance_hooks(*hooks) 127: hooks.each { |hook| register_hook(hook, :instance) } 128: end
Is the method registered as a hookable in the given scope.
# File lib/extlib/hook.rb, line 188 188: def registered_as_hook?(target_method, scope) 189: ! hooks_with_scope(scope)[target_method].nil? 190: end