Skip to content

Commit

Permalink
added Apotomo::Hooks to have simpler hooks and callbacks.
Browse files Browse the repository at this point in the history
  • Loading branch information
apotonick committed Sep 22, 2010
1 parent 69fa2d4 commit eba20ea
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 84 deletions.
2 changes: 1 addition & 1 deletion lib/apotomo/event_methods.rb
Expand Up @@ -12,7 +12,7 @@ def page_updates

def self.included(base)
base.extend(ClassMethods)
base.initialize_hooks << :add_class_event_handlers
base.after_initialize :add_class_event_handlers
end

def add_class_event_handlers(*)
Expand Down
65 changes: 65 additions & 0 deletions lib/apotomo/hooks.rb
@@ -0,0 +1,65 @@
module Apotomo
# Almost like ActiveSupport::Callbacks but 76,6% less complex.
#
# Example:
#
# class CatWidget < Apotomo::Widget
# define_hook :after_dinner
#
# Now you can add callbacks to your hook declaratively in your class.
#
# after_dinner do puts "Ice cream!" end
# after_dinner :have_a_desert # => refers to CatWidget#have_a_desert
#
# Running the callbacks happens on instances. It will run the block and #have_a_desert from above.
#
# cat.run_hook :after_dinner
module Hooks
def self.included(base)
base.extend ClassMethods
end

module ClassMethods
def define_hook(name)
accessor_name = "_#{name}_callbacks"

setup_hook_accessors(accessor_name)
define_hook_writer(name, accessor_name)
end

private
def define_hook_writer(hook, accessor_name)
instance_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{hook}(method=nil, &block)
callback = block_given? ? block : method
#{accessor_name} << callback
end
RUBY_EVAL
end

def setup_hook_accessors(accessor_name)
class_inheritable_array(accessor_name, :instance_writer => false)
send("#{accessor_name}=", []) # initialize ivar.
end

end

# Runs the callbacks (method/block) for the specified hook +name+. Additional arguments will
# be passed to the callback.
#
# Example:
#
# cat.run_hook :after_dinner, "i want ice cream!"
#
# will invoke the callbacks like
#
# desert("i want ice cream!")
# block.call("i want ice cream!")
def run_hook(name, *args)
self.class.send("_#{name}_callbacks").each do |callback|
send(callback, *args) and next if callback.kind_of? Symbol
callback.call(*args)
end
end
end
end
12 changes: 5 additions & 7 deletions lib/apotomo/tree_node.rb
Expand Up @@ -8,14 +8,10 @@ module TreeNode
attr_writer :content, :parent

def self.included(base)
base.after_initialize :initialize_tree_node_for
base.after_initialize :initialize_tree_node
end

# Constructor which expects the name of the node
#
# name of the node is expected to be unique across the
# tree.
def initialize_tree_node_for(name, *args)
def initialize_tree_node(*)
self.setAsRoot!

@childrenHash = Hash.new
Expand Down Expand Up @@ -49,7 +45,9 @@ def add(child)
@childrenHash[child.name] = child
@children << child
child.parent = self
return child

child.run_hook :after_add, self
child
end

# Removes the specified child node from the receiver node.
Expand Down
61 changes: 20 additions & 41 deletions lib/apotomo/widget.rb
Expand Up @@ -6,55 +6,38 @@
require 'apotomo/event_methods'
require 'apotomo/transition'
require 'apotomo/caching'
require 'apotomo/deep_link_methods'
require 'apotomo/widget_shortcuts'
require 'apotomo/rails/view_helper'
require 'apotomo/hooks'

### TODO: use load_hooks when switching to rails 3.
# wycats@gmail.com: ActiveSupport.run_load_hooks(:name)
# (21:01:17) wycats@gmail.com: ActiveSupport.on_load(:name) { … }
#require 'active_support/lazy_load_hooks'

module Apotomo
class Widget < Cell::Base
include Hooks

class_inheritable_array :initialize_hooks, :instance_writer => false
self.initialize_hooks = []
# Use this for setup code you're calling in every state. Almost like a +before_filter+ except that it's
# invoked after the initialization in #has_widgets.
#
# Example:
#
# class MouseWidget < Apotomo::Widget
# after_initialize :setup_cheese
#
# # we need @cheese in every state:
# def setup_cheese(*)
# @cheese = Cheese.find @opts[:cheese_id]
define_hook :after_initialize
define_hook :has_widgets
define_hook :after_add

attr_accessor :opts
attr_writer :visible

attr_writer :controller
attr_accessor :version

### DISCUSS: extract to has_widgets_methods for both Widget and Controller?
#class_inheritable_array :has_widgets_blocks

class << self
include WidgetShortcuts

def has_widgets_blocks
@has_widgets_blocks ||= []
end

def has_widgets(&block)
has_widgets_blocks << block
end

# Use this for setup code you're calling in every state. Almost like a +before_filter+ except that it's
# invoked after the initialization in #has_widgets.
#
# Example:
#
# class MouseWidget < Apotomo::Widget
# after_initialize :setup_cheese
#
# # we need @cheese in every state:
# def setup_cheese(*)
# @cheese = Cheese.find @opts[:cheese_id]
def after_initialize(method)
self.initialize_hooks << method
end
end

include TreeNode
Expand All @@ -64,15 +47,15 @@ def after_initialize(method)

include Transition
include Caching

include DeepLinkMethods
include WidgetShortcuts

helper Apotomo::Rails::ViewHelper




def add_has_widgets_blocks(*)
self.class.has_widgets_blocks.each { |block| block.call(self) }
run_hook :has_widgets, self
end
after_initialize :add_has_widgets_blocks

Expand All @@ -89,11 +72,7 @@ def initialize(id, start_state, opts={})

@cell = self

process_initialize_hooks(id, start_state, opts)
end

def process_initialize_hooks(*args)
self.class.initialize_hooks.each { |method| send(method, *args) }
run_hook(:after_initialize, id, start_state, opts)
end

def last_state
Expand Down
64 changes: 64 additions & 0 deletions test/unit/hooks_test.rb
@@ -0,0 +1,64 @@
require File.join(File.dirname(__FILE__), '..', 'test_helper')

class HooksTest < ActiveSupport::TestCase
context "Hooks.define_hook" do
setup do
@klass = Class.new(Object) do
include Apotomo::Hooks

def executed
@executed ||= [];
end
end

@mum = @klass.new
@mum.class.define_hook :after_eight
end

should "provide accessors to the stored callbacks" do
assert_equal [], @klass._after_eight_callbacks
@klass._after_eight_callbacks << :dine
assert_equal [:dine], @klass._after_eight_callbacks
end

context "creates a public writer for the hook that" do
should "accepts method names" do
@klass.after_eight :dine
assert_equal [:dine], @klass._after_eight_callbacks
end

should "accepts blocks" do
@klass.after_eight do true; end
assert @klass._after_eight_callbacks.first.kind_of? Proc
end
end

context "Hooks.run_hook"do
should "run without parameters" do
@mum.instance_eval do
def a; executed << :a; end
def b; executed << :b; end

self.class.after_eight :b
self.class.after_eight :a
end

@mum.run_hook(:after_eight)

assert_equal [:b, :a], @mum.executed
end

should "accept arbitrary parameters" do
@mum.instance_eval do
def a(me, arg); executed << arg+1; end
end
@mum.class.after_eight :a
@mum.class.after_eight lambda { |me, arg| me.executed << arg-1 }

@mum.run_hook(:after_eight, @mum, 1)

assert_equal [2, 0], @mum.executed
end
end
end
end
65 changes: 30 additions & 35 deletions test/unit/widget_test.rb
@@ -1,7 +1,7 @@
require File.join(File.dirname(__FILE__), '..', 'test_helper')

class WidgetTest < ActiveSupport::TestCase
context "#has_widgets in class context" do
context "Widget.has_widgets" do
setup do
@mum = Class.new(MouseCell) do
has_widgets do |me|
Expand All @@ -17,8 +17,35 @@ class WidgetTest < ActiveSupport::TestCase
assert_kind_of Apotomo::StatefulWidget, @mum['baby']
end

should "not inherit trees for now" do
assert_equal [], @kid.children
should "inherit trees for now" do
assert_equal 1, @mum.children.size
assert_kind_of Apotomo::StatefulWidget, @mum['baby']
end
end

context "Widget.after_add" do
setup do
@mum = Class.new(MouseCell) do
after_add do |parent|
parent << widget('mouse_cell', 'kid', :squeak)
end
end.new('mum', :squeak)

@root = mouse_mock('root')
end

should "be invoked after mum is added" do
assert_equal [], @root.children
@root << @mum

assert_equal ['mum', 'kid'], @root.children.collect { |w| w.name }
end

should "inherit callbacks for now" do
@berry = Class.new(@mum.class).new('berry', :squeak)
@root << @berry

assert_equal ['berry', 'kid'], @root.children.collect { |w| w.name }
end
end

Expand Down Expand Up @@ -84,37 +111,5 @@ class WidgetTest < ActiveSupport::TestCase
should "respond to the WidgetShortcuts methods, like #widget" do
assert_respond_to @mum, :widget
end

context "with initialize_hooks" do
should "expose its class_inheritable_array with #initialize_hooks" do
@mum = mouse_class_mock.new('mum', :eating)
@mum.class.instance_eval { self.initialize_hooks << :initialize_mouse }
assert ::Apotomo::StatefulWidget.initialize_hooks.size + 1 == @mum.class.initialize_hooks.size
end

should "execute the initialize_hooks in the correct order in #process_initialize_hooks" do
@mum = mouse_class_mock.new('mum', :eating)
@mum.class.instance_eval do
define_method(:executed) { |*args| @executed ||= [] }
define_method(:setup) { |*args| executed << :setup }
define_method(:configure) { |*args| executed << :configure }
initialize_hooks << :setup
initialize_hooks << :configure
end

assert_equal [:setup, :configure], @mum.class.new('zombie', nil).executed
end

should "provide after_initialize" do
@mum = mouse_class_mock.new('mum', :eat)
@mum.class.instance_eval do
after_initialize :first
after_initialize :second
end

assert_equal @mum.class.initialize_hooks[-1], :second
assert_equal @mum.class.initialize_hooks[-2], :first
end
end
end
end

0 comments on commit eba20ea

Please sign in to comment.