module Lolita
# Provide hook mechanism for Lolita. To use hooks for class start with including this in your own class.
# Next step is hook definition. This may be done using Lolita::Hooks#add_hook method.
# Hooks are stored in class @hooks variable, that is Hash and each key is hook name
# and each hook also is Hash that have :methods and :blocks
# keys. Both of those are Array, and each time you call callback method, like before_save and so on, block
# and/or methods is stored. Each time #fire is called all blocks and methods will be executed.
# It may look like this.
# class MyClass
# include Lolita::Hooks
# add_hook :before_save, :after_save
# end
# # This will define two hooks for MyClass.
# To add hook callback just call hook name on class and pass method(-s) or block.
# MyClass.after_save :write_log
# MyClass.before_save do
# validate(self)
# end
# ==Scopes
# Most times hook callbacks are defined for class like in previous example, but also it's possible to do it
# on class instances. Difference between calling it on class or on instance is that instance callbacks will
# be called only when event is fired on instance. Class callbacks will be called on class and also on instance
# callbacks.
# my_object=MyClass.new
# MyClass.before_save do
# puts "class callback"
# end
# my_object.before_save do
# puts "instance callback"
# end
#
# MyClass.fire(:before_save) #=>
# class_callback
#
# my_object.fire(:before_save) #=>
# class_callback
# instance_callback
# # As you can see, first class callbacks is called and after that instance callbacks.
#
# ==Firing events
# To execute callbacks, events should be called on object. Event names is same hooks names. #fire can be called
# on class or on instance. Also it is possible to pass block to fire event, that will replace callback block
# or if #let_content is called than it will work like wrapper, like this
# # this is continuation of previous code
# MyClass.fire(:before_save) do
# puts "replaced text"
# end
# # will produce #=> replaced text
#
# MyClass.fire(:before_save) do
# puts "before callback"
# let_content
# puts "after callback"
# end
# # this will produce #=>
# # before callback
# # class callback
# # after callback
# ==Named hooks
# See Lolita::Hooks::NamedHook for details.
module Hooks
def self.included(base)
base.extend(ClassMethods)
base.extend(CommonMethods)
base.class_eval{
include CommonMethods
include InstanceMethods
}
end
# Look for named hook with singular or plural name of method.
def self.method_missing method_name,*args, &block
if named_hook=(Lolita::Hooks::NamedHook.by_name(method_name))
named_hook[:_class]
else
super
end
end
# Shared methods between class and instance.
module CommonMethods
# All callbacks for class or instance.
def callbacks
var=self.instance_variable_get(:@callbacks)
unless var
var={}
self.instance_variable_set(:@callbacks,var)
end
instance_variable_get(:@callbacks)
end
end
module ClassMethods
# Setter for #hook_scope.
def hooks_scope=(object)
@hooks_scope=object
end
# Hooks scope is used to execute callbacks. By default it is class itself.
def hooks_scope
@hooks_scope||self
end
# Setter for #callback_content
def given_callback_content=(content)
@given_callback_content=content
end
# Callback content is used to let callback content executed insede of fire block.
def given_callback_content
@given_callback_content
end
# All hooks for class. This is Array of hook names.
def hooks
@hooks||=[]
@hooks
end
# Reset all hooks and callbacks to defaults.
def clear_hooks
@hooks=[]
@callbacks={}
end
# This method is used to add hooks for class. It accept one or more hook names.
# ====Example
# add_hook :before_save
# MyClass.add_hooks :after_save, :around_save
def add_hook(*names)
(names||[]).each{|hook_name|
self.class_eval <<-HOOK,__FILE__,__LINE__+1
def self.#{hook_name}(*methods,&block)
options=methods.extract_options!
in_hooks_scope(options[:scope]) do
register_callback(:"#{hook_name}",*methods,&block)
end
end
def #{hook_name}(*method,&block)
self.class.#{hook_name}(*method,:scope=>self,&block)
end
HOOK
register_hook(hook_name)
}
end
# Fire is used to execute callback. Method accept one or more hook_names and optional block.
# It will raise error if hook don't exist for this class. Also it accept :scope options, that
# is used to #get_callbacks and #run_callbacks.
# ====Example
# MyClass.fire(:before_save,:after_save,:scope=>MyClass.new)
# # this will call callbacks in MyClass instance scope, that means that self will be MyClass instance.
def fire(*hook_names,&block)
options=hook_names.extract_options!
(hook_names || []).each do |hook_name|
raise Lolita::HookNotFound, "Hook #{hook_name} is not defined for #{self}." unless self.has_hook?(hook_name)
in_hooks_scope(options[:scope]) do
callback=get_callback(hook_name)
run_callback(callback,&block)
end
end
end
# Is hook with name is defined for class.
def has_hook?(name)
self.hooks.include?(name.to_sym)
end
# Try to recognize named fire methods like
# MyClass.fire_after_save # will call MyClass.fire(:after_save)
def method_missing(*args, &block)
unless self.recognize_hook_methods(*args,&block)
super
end
end
# Call callback block inside of fire block.
# ====Example
# MyClass.fire(:before_save) do
# do_stuff
# let_content # execute callback block(-s) in same scope as fire is executed.
# end
def let_content
if content=self.given_callback_content
run_block(self.given_callback_content)
end
end
# Set #method_missing
def recognize_hook_methods method_name, *args, &block
if method_name.to_s.match(/^fire_(\w+)/)
self.fire($1,&block)
true
end
end
protected
# Switch between self and given scope. Block will be executed with scope.
# And after that it will switch back to self.
def in_hooks_scope(scope)
begin
self.hooks_scope=scope||self
yield
ensure
self.hooks_scope=self
end
end
# Run callback. Each callback is Hash with :methods Array and :blocks Array
def run_callback(callback,&block)
run_methods(callback[:methods],&block)
run_blocks(callback[:blocks],&block)
end
# Run methods from methods Array
def run_methods methods, &block
(methods||[]).each do |method_name|
hooks_scope.__send__(method_name,&block)
end
end
# Run blocks from blocks Array. Also it set #given_callback_content if block is given, this
# will allow to call #let_content. Each block is runned with #run_block.
def run_blocks blocks,&given_block
(blocks||[]).each do |block|
begin
if block_given?
self.given_callback_content=given_block
end
run_block(block,&given_block)
ensure
self.given_callback_content=nil
end
end
end
# Run block in scope.
def run_block block, &given_block
hooks_scope.instance_eval(&block)
end
# Return all callbacks
# If scope is not class then it merge class callbacks with scope callbacks. That means that
# class callbacks always will be called before scope callbacks.
def get_callback(name)
scope_callbacks=hooks_scope.callbacks[name.to_sym] || {}
unless hooks_scope==self
class_callbacks=self.callbacks[name.to_sym] || {}
[:methods,:blocks].each do |attr|
scope_callbacks[attr]=((class_callbacks[attr] || [])+(scope_callbacks[attr] || [])).uniq
end
end
scope_callbacks
end
# Register callback with given scope.
def register_callback(name,*methods,&block)
temp_callback=hooks_scope.callbacks[name]||{}
temp_callback[:methods]||=[]
temp_callback[:methods]+=(methods||[]).compact
temp_callback[:blocks]||=[]
temp_callback[:blocks]<< block if block_given?
hooks_scope.callbacks[name]=temp_callback
end
# Register hook for scope.
def register_hook(name)
self.hooks<self)
end
# See Lolita::Hooks::ClassMethods#let_content
def let_content
self.class.let_content
end
# See Lolita::Hooks::ClassMethods#method_missing
def method_missing(*args,&block)
unless self.class.recognize_hook_methods(*args,&block)
super
end
end
end
end
end