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 378 378: def args_for(method) 379: if method.arity == 0 380: "&block" 381: elsif method.arity > 0 382: "_" << (1 .. method.arity).to_a.join(", _") << ", &block" 383: elsif (method.arity + 1) < 0 384: "_" << (1 .. (method.arity).abs - 1).to_a.join(", _") << ", *args, &block" 385: else 386: "*args, &block" 387: end 388: 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 275 275: def define_advised_method(target_method, scope) 276: args = args_for(method_with_scope(target_method, scope)) 277: 278: renamed_target = hook_method_name(target_method, 'hookable_', 'before_advised') 279: 280: 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" 281: 282: if scope == :instance && !instance_methods(false).any? { |m| m.to_sym == target_method } 283: send(:alias_method, renamed_target, target_method) 284: 285: proxy_module = Module.new 286: proxy_module.class_eval(source, __FILE__, __LINE__) 287: self.send(:include, proxy_module) 288: else 289: source = %{alias_method :#{renamed_target}, :#{target_method}\n#{source}} 290: source = %{class << self\n#{source}\nend} if scope == :class 291: class_eval(source, __FILE__, __LINE__) 292: end 293: 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: before_hook_name = hook_method_name(target_method, 'execute_before', 'hook_stack') 239: after_hook_name = hook_method_name(target_method, 'execute_after', 'hook_stack') 240: 241: hooks[target_method][:in].class_eval "\#{scope == :class ? 'class << self' : ''}\n\nprivate\n\nremove_method :\#{before_hook_name} if instance_methods(false).any? { |m| m.to_sym == :\#{before_hook_name} }\ndef \#{before_hook_name}(*args)\n\#{before_hooks}\nend\n\nremove_method :\#{after_hook_name} if instance_methods(false).any? { |m| m.to_sym == :\#{after_hook_name} }\ndef \#{after_hook_name}(*args)\n\#{after_hooks}\nend\n\n\#{scope == :class ? 'end' : ''}\n", __FILE__, __LINE__ + 1 242: 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 263 263: def inline_call(method_info, scope) 264: name = method_info[:name] 265: 266: if scope == :instance 267: args = method_defined?(name) && instance_method(name).arity != 0 ? '*args' : '' 268: %(#{name}(#{args}) if self.class <= ObjectSpace._id2ref(#{method_info[:from].object_id})) 269: else 270: args = respond_to?(name) && method(name).arity != 0 ? '*args' : '' 271: %(#{name}(#{args}) if self <= ObjectSpace._id2ref(#{method_info[:from].object_id})) 272: end 273: end
— Add a hook —
# File lib/extlib/hook.rb, line 308 308: def install_hook(type, target_method, method_sym, scope, &block) 309: assert_kind_of 'target_method', target_method, Symbol 310: assert_kind_of 'method_sym', method_sym, Symbol unless method_sym.nil? 311: assert_kind_of 'scope', scope, Symbol 312: 313: if !block_given? and method_sym.nil? 314: raise ArgumentError, "You need to pass 2 arguments to \"#{type}\"." 315: end 316: 317: if method_sym.to_s[-1,1] == '=' 318: raise ArgumentError, "Methods ending in = cannot be hooks" 319: end 320: 321: unless [ :class, :instance ].include?(scope) 322: raise ArgumentError, 'You need to pass :class or :instance as scope' 323: end 324: 325: if registered_as_hook?(target_method, scope) 326: hooks = hooks_with_scope(scope) 327: 328: #if this hook is previously declared in a sibling or cousin we must move the :in class 329: #to the common ancestor to get both hooks to run. 330: if !(hooks[target_method][:in] <=> self) 331: before_hook_name = hook_method_name(target_method, 'execute_before', 'hook_stack') 332: after_hook_name = hook_method_name(target_method, 'execute_after', 'hook_stack') 333: 334: hooks[target_method][:in].class_eval "remove_method :\#{before_hook_name} if instance_methods(false).any? { |m| m.to_sym == :\#{before_hook_name} }\ndef \#{before_hook_name}(*args)\nsuper\nend\n\nremove_method :\#{after_hook_name} if instance_methods(false).any? { |m| m.to_sym == :\#{before_hook_name} }\ndef \#{after_hook_name}(*args)\nsuper\nend\n", __FILE__, __LINE__ + 1 335: 336: while !(hooks[target_method][:in] <=> self) do 337: hooks[target_method][:in] = hooks[target_method][:in].superclass 338: end 339: 340: define_hook_stack_execution_methods(target_method, scope) 341: hooks[target_method][:in].class_eval{define_advised_method(target_method, scope)} 342: end 343: else 344: register_hook(target_method, scope) 345: hooks = hooks_with_scope(scope) 346: end 347: 348: #if we were passed a block, create a method out of it. 349: if block 350: method_sym = "__hooks_#{type}_#{quote_method(target_method)}_#{hooks[target_method][type].length}".to_sym 351: if scope == :class 352: meta_class.instance_eval do 353: define_method(method_sym, &block) 354: end 355: else 356: define_method(method_sym, &block) 357: end 358: end 359: 360: # Adds method to the stack an redefines the hook invocation method 361: hooks[target_method][type] << { :name => method_sym, :from => self } 362: define_hook_stack_execution_methods(target_method, scope) 363: 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 390 390: def method_with_scope(name, scope) 391: case scope 392: when :class then method(name) 393: when :instance then instance_method(name) 394: else raise ArgumentError, 'You need to pass :class or :instance as scope' 395: end 396: 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 398 398: def quote_method(name) 399: name.to_s.gsub(/\?$/, '_q_').gsub(/!$/, '_b_').gsub(/=$/, '_eq_') 400: 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