Module: Extlib::Hook::ClassMethods
- Includes:
- Assertions
- Defined in:
- lib/extlib/hook.rb
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 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.
-
108 109 110 |
# File 'lib/extlib/hook.rb', line 108 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.
-
76 77 78 |
# File 'lib/extlib/hook.rb', line 76 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 —
381 382 383 384 385 386 387 388 389 390 391 |
# File 'lib/extlib/hook.rb', line 381 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.
-
92 93 94 |
# File 'lib/extlib/hook.rb', line 92 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.
-
61 62 63 |
# File 'lib/extlib/hook.rb', line 61 def before_class_method(target_method, method_sym = nil, &block) install_hook :before, target_method, method_sym, :class, &block end |
#class_hooks ⇒ Object
153 154 155 |
# File 'lib/extlib/hook.rb', line 153 def class_hooks self.const_get("CLASS_HOOKS") end |
#define_advised_method(target_method, scope) ⇒ Object
280 281 282 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 |
# File 'lib/extlib/hook.rb', line 280 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).
231 232 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 |
# File 'lib/extlib/hook.rb', line 231 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.
201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/extlib/hook.rb', line 201 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
145 146 147 148 149 150 151 |
# File 'lib/extlib/hook.rb', line 145 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.
268 269 270 271 272 273 274 275 276 277 278 |
# File 'lib/extlib/hook.rb', line 268 def inline_call(method_info, scope) name = method_info[:name] if scope == :instance args = method_defined?(name) && instance_method(name).arity != 0 ? '*args' : '' %(#{name}(#{args}) if self.class <= ObjectSpace._id2ref(#{method_info[:from].object_id})) else args = respond_to?(name) && method(name).arity != 0 ? '*args' : '' %(#{name}(#{args}) if self <= ObjectSpace._id2ref(#{method_info[:from].object_id})) end end |
#install_hook(type, target_method, method_sym, scope, &block) ⇒ Object
— Add a hook —
312 313 314 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 |
# File 'lib/extlib/hook.rb', line 312 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
157 158 159 |
# File 'lib/extlib/hook.rb', line 157 def instance_hooks self.const_get("INSTANCE_HOOKS") end |
#method_with_scope(name, scope) ⇒ Object
393 394 395 396 397 398 399 |
# File 'lib/extlib/hook.rb', line 393 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
215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/extlib/hook.rb', line 215 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
401 402 403 |
# File 'lib/extlib/hook.rb', line 401 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.
-
120 121 122 |
# File 'lib/extlib/hook.rb', line 120 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.
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/extlib/hook.rb', line 170 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.
-
132 133 134 |
# File 'lib/extlib/hook.rb', line 132 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.
194 195 196 |
# File 'lib/extlib/hook.rb', line 194 def registered_as_hook?(target_method, scope) ! hooks_with_scope(scope)[target_method].nil? end |
#reset_hook!(target_method, scope) ⇒ Object
Not yet implemented
137 138 139 |
# File 'lib/extlib/hook.rb', line 137 def reset_hook!(target_method, scope) raise NotImplementedError end |