Module: Extlib::Hook::ClassMethods
Instance Method Summary collapse
-
#after(target_method, method_sym = nil, &block) ⇒ Object
Inject code that executes after the target instance method.
-
#after_class_method(target_method, method_sym = nil, &block) ⇒ Object
Inject code that executes after the target class method.
-
#args_for(method) ⇒ Object
— Helpers —.
-
#before(target_method, method_sym = nil, &block) ⇒ Object
Inject code that executes before the target instance method.
-
#before_class_method(target_method, method_sym = nil, &block) ⇒ Object
Inject code that executes before the target class method.
- #class_hooks ⇒ Object
- #define_advised_method(target_method, scope) ⇒ Object
-
#define_hook_stack_execution_methods(target_method, scope) ⇒ Object
Defines two methods.
-
#hook_method_name(target_method, prefix, suffix) ⇒ Object
Generates names for the various utility methods.
-
#hooks_with_scope(scope) ⇒ Object
Returns the correct HOOKS Hash depending on whether we are working with class methods or instance methods.
-
#inline_call(method_info, scope) ⇒ Object
Returns ruby code that will invoke the hook.
-
#install_hook(type, target_method, method_sym, scope, &block) ⇒ Object
— Add a hook —.
- #instance_hooks ⇒ Object
- #method_with_scope(name, scope) ⇒ Object
-
#process_method_added(method_name, scope) ⇒ Object
This will need to be refactored.
- #quote_method(name) ⇒ Object
-
#register_class_hooks(*hooks) ⇒ Object
Register a class method as hookable.
-
#register_hook(target_method, scope) ⇒ Object
Registers a method as hookable.
-
#register_instance_hooks(*hooks) ⇒ Object
Register aninstance method as hookable.
-
#registered_as_hook?(target_method, scope) ⇒ Boolean
Is the method registered as a hookable in the given scope.
-
#reset_hook!(target_method, scope) ⇒ Object
Not yet implemented.
Methods included from LocalObjectSpace
Methods included from Assertions
Instance Method Details
#after(target_method, method_sym = nil, &block) ⇒ Object
Either method_sym or block is required.
Inject code that executes after the target instance method.
-
110 111 112 |
# File 'lib/extlib/hook.rb', line 110 def after(target_method, method_sym = nil, &block) install_hook :after, target_method, method_sym, :instance, &block end |
#after_class_method(target_method, method_sym = nil, &block) ⇒ Object
Either method_sym or block is required.
Inject code that executes after the target class method.
-
78 79 80 |
# File 'lib/extlib/hook.rb', line 78 def after_class_method(target_method, method_sym = nil, &block) install_hook :after, target_method, method_sym, :class, &block end |
#args_for(method) ⇒ Object
— Helpers —
384 385 386 387 388 389 390 391 392 393 394 |
# File 'lib/extlib/hook.rb', line 384 def args_for(method) if method.arity == 0 "&block" elsif method.arity > 0 "_" << (1 .. method.arity).to_a.join(", _") << ", &block" elsif (method.arity + 1) < 0 "_" << (1 .. (method.arity).abs - 1).to_a.join(", _") << ", *args, &block" else "*args, &block" end end |
#before(target_method, method_sym = nil, &block) ⇒ Object
Either method_sym or block is required.
Inject code that executes before the target instance method.
-
94 95 96 |
# File 'lib/extlib/hook.rb', line 94 def before(target_method, method_sym = nil, &block) install_hook :before, target_method, method_sym, :instance, &block end |
#before_class_method(target_method, method_sym = nil, &block) ⇒ Object
Either method_sym or block is required.
Inject code that executes before the target class method.
-
63 64 65 |
# File 'lib/extlib/hook.rb', line 63 def before_class_method(target_method, method_sym = nil, &block) install_hook :before, target_method, method_sym, :class, &block end |
#class_hooks ⇒ Object
155 156 157 |
# File 'lib/extlib/hook.rb', line 155 def class_hooks self.const_get("CLASS_HOOKS") end |
#define_advised_method(target_method, scope) ⇒ Object
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'lib/extlib/hook.rb', line 283 def define_advised_method(target_method, scope) args = args_for(method_with_scope(target_method, scope)) renamed_target = hook_method_name(target_method, 'hookable_', 'before_advised') source = <<-EOD def #{target_method}(#{args}) retval = nil catch(:halt) do #{hook_method_name(target_method, 'execute_before', 'hook_stack')}(#{args}) retval = #{renamed_target}(#{args}) #{hook_method_name(target_method, 'execute_after', 'hook_stack')}(retval, #{args}) retval end end EOD if scope == :instance && !instance_methods(false).any? { |m| m.to_sym == target_method } send(:alias_method, renamed_target, target_method) proxy_module = Module.new proxy_module.class_eval(source, __FILE__, __LINE__) self.send(:include, proxy_module) else source = %{alias_method :#{renamed_target}, :#{target_method}\n#{source}} source = %{class << self\n#{source}\nend} if scope == :class class_eval(source, __FILE__, __LINE__) end end |
#define_hook_stack_execution_methods(target_method, scope) ⇒ Object
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).
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/extlib/hook.rb', line 233 def define_hook_stack_execution_methods(target_method, scope) unless registered_as_hook?(target_method, scope) raise ArgumentError, "#{target_method} has not be registered as a hookable #{scope} method" end hooks = hooks_with_scope(scope) before_hooks = hooks[target_method][:before] before_hooks = before_hooks.map{ |info| inline_call(info, scope) }.join("\n") after_hooks = hooks[target_method][:after] after_hooks = after_hooks.map{ |info| inline_call(info, scope) }.join("\n") before_hook_name = hook_method_name(target_method, 'execute_before', 'hook_stack') after_hook_name = hook_method_name(target_method, 'execute_after', 'hook_stack') hooks[target_method][:in].class_eval <<-RUBY, __FILE__, __LINE__ + 1 #{scope == :class ? 'class << self' : ''} private remove_method :#{before_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} } def #{before_hook_name}(*args) #{before_hooks} end remove_method :#{after_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{after_hook_name} } def #{after_hook_name}(*args) #{after_hooks} end #{scope == :class ? 'end' : ''} RUBY end |
#hook_method_name(target_method, prefix, suffix) ⇒ Object
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.
203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/extlib/hook.rb', line 203 def hook_method_name(target_method, prefix, suffix) target_method = target_method.to_s case target_method[-1,1] when '?' then "#{prefix}_#{target_method[0..-2]}_ques_#{suffix}" when '!' then "#{prefix}_#{target_method[0..-2]}_bang_#{suffix}" when '=' then "#{prefix}_#{target_method[0..-2]}_eq_#{suffix}" # I add a _nan_ suffix here so that we don't ever encounter # any naming conflicts. else "#{prefix}_#{target_method[0..-1]}_nan_#{suffix}" end end |
#hooks_with_scope(scope) ⇒ Object
Returns the correct HOOKS Hash depending on whether we are working with class methods or instance methods
147 148 149 150 151 152 153 |
# File 'lib/extlib/hook.rb', line 147 def hooks_with_scope(scope) case scope when :class then class_hooks when :instance then instance_hooks else raise ArgumentError, 'You need to pass :class or :instance as scope' end end |
#inline_call(method_info, scope) ⇒ Object
Returns ruby code that will invoke the hook. It checks the arity of the hook method and passes arguments accordingly.
270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/extlib/hook.rb', line 270 def inline_call(method_info, scope) Extlib::Hook::ClassMethods.hook_scopes << method_info[:from] name = method_info[:name] if scope == :instance args = method_defined?(name) && instance_method(name).arity != 0 ? '*args' : '' %(#{name}(#{args}) if self.class <= Extlib::Hook::ClassMethods.object_by_id(#{method_info[:from].object_id})) else args = respond_to?(name) && method(name).arity != 0 ? '*args' : '' %(#{name}(#{args}) if self <= Extlib::Hook::ClassMethods.object_by_id(#{method_info[:from].object_id})) end end |
#install_hook(type, target_method, method_sym, scope, &block) ⇒ Object
— Add a hook —
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 |
# File 'lib/extlib/hook.rb', line 315 def install_hook(type, target_method, method_sym, scope, &block) assert_kind_of 'target_method', target_method, Symbol assert_kind_of 'method_sym', method_sym, Symbol unless method_sym.nil? assert_kind_of 'scope', scope, Symbol if !block_given? and method_sym.nil? raise ArgumentError, "You need to pass 2 arguments to \"#{type}\"." end if method_sym.to_s[-1,1] == '=' raise ArgumentError, "Methods ending in = cannot be hooks" end unless [ :class, :instance ].include?(scope) raise ArgumentError, 'You need to pass :class or :instance as scope' end if registered_as_hook?(target_method, scope) hooks = hooks_with_scope(scope) #if this hook is previously declared in a sibling or cousin we must move the :in class #to the common ancestor to get both hooks to run. if !(hooks[target_method][:in] <=> self) before_hook_name = hook_method_name(target_method, 'execute_before', 'hook_stack') after_hook_name = hook_method_name(target_method, 'execute_after', 'hook_stack') hooks[target_method][:in].class_eval <<-RUBY, __FILE__, __LINE__ + 1 remove_method :#{before_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} } def #{before_hook_name}(*args) super end remove_method :#{after_hook_name} if instance_methods(false).any? { |m| m.to_sym == :#{before_hook_name} } def #{after_hook_name}(*args) super end RUBY while !(hooks[target_method][:in] <=> self) do hooks[target_method][:in] = hooks[target_method][:in].superclass end define_hook_stack_execution_methods(target_method, scope) hooks[target_method][:in].class_eval{define_advised_method(target_method, scope)} end else register_hook(target_method, scope) hooks = hooks_with_scope(scope) end #if we were passed a block, create a method out of it. if block method_sym = "__hooks_#{type}_#{quote_method(target_method)}_#{hooks[target_method][type].length}".to_sym if scope == :class .instance_eval do define_method(method_sym, &block) end else define_method(method_sym, &block) end end # Adds method to the stack an redefines the hook invocation method hooks[target_method][type] << { :name => method_sym, :from => self } define_hook_stack_execution_methods(target_method, scope) end |
#instance_hooks ⇒ Object
159 160 161 |
# File 'lib/extlib/hook.rb', line 159 def instance_hooks self.const_get("INSTANCE_HOOKS") end |
#method_with_scope(name, scope) ⇒ Object
396 397 398 399 400 401 402 |
# File 'lib/extlib/hook.rb', line 396 def method_with_scope(name, scope) case scope when :class then method(name) when :instance then instance_method(name) else raise ArgumentError, 'You need to pass :class or :instance as scope' end end |
#process_method_added(method_name, scope) ⇒ Object
This will need to be refactored
217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/extlib/hook.rb', line 217 def process_method_added(method_name, scope) hooks_with_scope(scope).each do |target_method, hooks| if hooks[:before].any? { |hook| hook[:name] == method_name } define_hook_stack_execution_methods(target_method, scope) end if hooks[:after].any? { |hook| hook[:name] == method_name } define_hook_stack_execution_methods(target_method, scope) end end end |
#quote_method(name) ⇒ Object
404 405 406 |
# File 'lib/extlib/hook.rb', line 404 def quote_method(name) name.to_s.gsub(/\?$/, '_q_').gsub(/!$/, '_b_').gsub(/=$/, '_eq_') end |
#register_class_hooks(*hooks) ⇒ Object
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.
-
122 123 124 |
# File 'lib/extlib/hook.rb', line 122 def register_class_hooks(*hooks) hooks.each { |hook| register_hook(hook, :class) } end |
#register_hook(target_method, scope) ⇒ Object
Registers a method as hookable. Registering hooks involves the following process
-
Create a blank entry in the HOOK Hash for the method.
-
Define the methods that execute the before and after hook stack. These methods will be no-ops at first, but everytime a new hook is defined, the methods will be redefined to incorporate the new hook.
-
Redefine the method that is to be hookable so that the hook stacks are invoked approprietly.
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/extlib/hook.rb', line 172 def register_hook(target_method, scope) if scope == :instance && !method_defined?(target_method) raise ArgumentError, "#{target_method} instance method does not exist" elsif scope == :class && !respond_to?(target_method) raise ArgumentError, "#{target_method} class method does not exist" end hooks = hooks_with_scope(scope) if hooks[target_method].nil? hooks[target_method] = { # We need to keep track of which class in the Inheritance chain the # method was declared hookable in. Every time a child declares a new # hook for the method, the hook stack invocations need to be redefined # in the original Class. See #define_hook_stack_execution_methods :before => [], :after => [], :in => self } define_hook_stack_execution_methods(target_method, scope) define_advised_method(target_method, scope) end end |
#register_instance_hooks(*hooks) ⇒ Object
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.
-
134 135 136 |
# File 'lib/extlib/hook.rb', line 134 def register_instance_hooks(*hooks) hooks.each { |hook| register_hook(hook, :instance) } end |
#registered_as_hook?(target_method, scope) ⇒ Boolean
Is the method registered as a hookable in the given scope.
196 197 198 |
# File 'lib/extlib/hook.rb', line 196 def registered_as_hook?(target_method, scope) ! hooks_with_scope(scope)[target_method].nil? end |
#reset_hook!(target_method, scope) ⇒ Object
Not yet implemented
139 140 141 |
# File 'lib/extlib/hook.rb', line 139 def reset_hook!(target_method, scope) raise NotImplementedError end |