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 #run 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 runned 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.run(:before_save) #=>
# class_callback
#
# my_object.run(: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. #run can be called
# on class or on instance. Also it is possible to pass block to run 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.run(:before_save) do
# puts "replaced text"
# end
# # will produce #=> replaced text
#
# MyClass.run(: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
class Runner
class << self
def singleton_hook(hook_object,hook_name)
class << hook_object
def hooks_runned(name=nil)
@hooks_runned ||=[]
@hooks_runned << name if name
@hooks_runned
end
end
hook_object.hooks_runned(hook_name)
end
def runned?(hook_object,hook_name)
if hook_object.respond_to?(:hooks_runned)
hook_object.hooks_runned.include?(hook_name)
end
end
def singleton_hooks
@singleton_hooks || {}
end
end
attr_accessor :hooks_run_scope, :given_callback_content
attr_writer :hooks_scope
def initialize(hook_class,hook_name, options)
@hook_class = hook_class
@hook_name = hook_name
@options = options
@options[:once] = @options[:once] == true ? @hook_class : @options[:once]
end
# Hooks scope is used to execute callbacks. By default it is class itself.
def hooks_scope
@hooks_scope || @hook_class
end
def run(&block)
if !@options[:once] || (@options[:once] && !self.class.runned?(@options[:once],@hook_name))
self.class.singleton_hook(@options[:once],@hook_name)
result = nil
in_hooks_scope(@options[:scope],@options[:run_scope]) do
callback = get_callback(@hook_name)
result = run_callback(callback,&block)
end
result
end
end
# Call callback block inside of run block.
# ====Example
# MyClass.run(:before_save) do
# do_stuff
# let_content # execute callback block(-s) in same scope as run is executed.
# end
def let_content
if self.given_callback_content.respond_to?(:call)
run_block(self.given_callback_content)
elsif self.given_callback_content
self.given_callback_content
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,run_scope=nil)
begin
this = self
self.hooks_scope=scope || @hook_class
self.hooks_scope.define_singleton_method(:let_content) do
this.let_content
end
if run_scope
run_scope.define_singleton_method(:let_content) do
this.let_content
end
end
self.hooks_run_scope = run_scope || self.hooks_scope
yield
ensure
self.hooks_scope = @hook_class
self.hooks_run_scope = self.hooks_scope
end
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] || {}
@hook_class.superclasses.each do |const_name|
scope_callbacks = @hook_class.collect_callbacks_from(name,const_name,scope_callbacks)
end
scope_callbacks
end
# Run callback. Each callback is Hash with :methods Array and :blocks Array
def run_callback(callback,&block)
method_results=run_methods(callback[:methods],&block)
block_results=run_blocks(callback[:blocks],&block)
method_results+block_results
end
# Run methods from methods Array
def run_methods methods, &block
result = ""
(methods||[]).each do |method_name|
result << (hooks_run_scope.__send__(method_name,&block)).to_s
end
result
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.
# After first run result of first block become #given_callback_content, and when next block
# call #let_content, this string will be returned for that block
def run_blocks blocks,&given_block
result=""
self.given_callback_content=block_given? ? given_block : nil
if blocks && !blocks.empty?
blocks.each do |block|
result << (run_block(block,&given_block)).to_s
self.given_callback_content=result
end
elsif block_given?
self.given_callback_content=nil
result << run_block(given_block).to_s
end
result
end
# Run block in scope.
def run_block block, &given_block
hooks_run_scope.instance_eval(&block)
end
end # end of Runner
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
def hooks_scope=(value)
@hooks_scope = value
end
def hooks_scope
@hooks_scope || self
end
# All hooks for class. This is Array of hook names.
def hooks
@hooks||=[]
@hooks
end
def all_hooks
@all_hooks||=self.ancestors.inject([]) do |result,const_name|
if const_name.respond_to?(:hooks)
result+=const_name.send(:hooks)
else
result
end
end
@all_hooks
end
# Reset all hooks and callbacks to defaults.
def clear_hooks
@hooks=[]
@callbacks={}
end
def add_hooks *names
add_hook *names
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
def in_hooks_scope(scope)
begin
self.hooks_scope = scope
yield
ensure
self.hooks_scope = self
end
end
# run 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.run(:before_save,:after_save,:scope=>MyClass.new)
# # this will call callbacks in MyClass instance scope, that means that self will be MyClass instance.
def run(hook_name,*args,&block)
options=args ? args.extract_options! : {}
raise Lolita::HookNotFound, "Hook #{hook_name} is not defined for #{self}." unless self.has_hook?(hook_name)
runner = Lolita::Hooks::Runner.new(self,hook_name,options)
runner.run(&block)
end
# Is hook with name is defined for class.
def has_hook?(name)
self.all_hooks.include?(name.to_sym)
end
# Try to recognize named run methods like
# MyClass.run_after_save # will call MyClass.run(:after_save)
def method_missing(*args, &block)
unless self.recognize_hook_methods(*args,&block)
super
end
end
# Set #method_missing
def recognize_hook_methods method_name, *args, &block
if method_name.to_s.match(/^run_(\w+)/)
self.run($1,*args,&block)
true
end
end
def collect_callbacks_from(name,const_name,scope_callbacks)
class_callbacks=const_name.callbacks[name.to_sym] || {}
[:methods,:blocks].each do |attr|
scope_callbacks[attr]=((class_callbacks[attr] || [])+(scope_callbacks[attr] || [])).uniq
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,&block)
super
end
end
end
end
end