upgraded hooks to 0.3.3; integrated custom changes as made for 0.2.0

This commit is contained in:
Nico Hagenburger 2014-02-01 23:45:46 +01:00
parent 6ad90766a7
commit 247a152d39
25 changed files with 805 additions and 368 deletions

View file

@ -10,7 +10,7 @@ require 'active_support/json'
require 'active_support/core_ext/integer/inflections' require 'active_support/core_ext/integer/inflections'
# Simple callback library # Simple callback library
require 'vendored-middleman-deps/hooks-0.2.0/lib/hooks' require 'vendored-middleman-deps/hooks-0.3.3/lib/hooks'
# Our custom logger # Our custom logger
require 'middleman-core/logger' require 'middleman-core/logger'

View file

@ -1,9 +0,0 @@
h2. 0.2.0
h3. Changes
* Callback blocks are now executed on the instance using @instance_exec@. If you need to access the class (former context) use @self.class@.
h2. 0.1.4
h3. Bugfixes
* An uninitialized @inheritable_attr@ doesn't crash since it is not cloned anymore. Note that an uncloneable attribute value still causes an exception.

View file

@ -1,3 +0,0 @@
source :rubygems
gemspec

View file

@ -1,107 +0,0 @@
= Hooks
<em>Generic hooks with callbacks for Ruby.</em>
== Introduction
_Hooks_ lets you define hooks declaratively in your ruby class. You can add callbacks to your hook, which will be run as soon as _you_ run the hook!
It's almost like ActiveSupport::Callbacks but 76,6% less complex. Instead, it is not more than 60 lines of code, one method compilation, no +method_missing+ and no magic.
Also, you may pass additional arguments to your callbacks when invoking a hook.
== Example
Let's take... a cat.
require 'hooks'
class Cat
include Hooks
define_hook :after_dinner
Now you can add callbacks to your hook declaratively in your class.
after_dinner do
puts "Ice cream for #{self}!"
end
after_dinner :have_a_desert # => refers to Cat#have_a_desert
def have_a_desert
puts "Hell, yeah!"
end
This will run the block and <tt>#have_a_desert</tt> from above.
cat.run_hook :after_dinner
# => Ice cream for #<Cat:0x8df9d84>!
Hell, yeah!
Callback blocks and methods will be executed with instance context. Note how +self+ in the block refers to the Cat instance.
== Inheritance
Hooks are inherited, here's a complete example to put it all together.
class Garfield < Cat
after_dinner :want_some_more
def want_some_more
puts "Is that all?"
end
end
Garfield.new.run_hook :after_dinner
# => Ice cream for #<Cat:0x8df9d84>!
Hell, yeah!
Is that all?
Note how the callbacks are invoked in the order they were inherited.
== Options for Callbacks
You're free to pass any number of arguments to #run_callback, those will be passed to the callbacks.
cat.run_hook :before_dinner, cat, Time.now
The callbacks should be ready for receiving parameters.
before_dinner :wash_pawns
before_dinner do |who, when|
...
end
def wash_pawns(who, when)
Not sure why a cat should have ice cream for dinner. Beside that, I was tempted naming this gem _hooker_.
== Installation
gem install hooks
== Anybody using it?
* Hooks is already used in [Apotomo:http://github.com/apotonick/apotomo], a hot widget framework for Rails. Look at +lib/apotomo/widget.rb+ for examples and into +lib/apotomo/tree_node.rb+ to learn how modules-driven code might benefit from hooks.
== Similar libraries
* http://github.com/nakajima/aspectory
* http://github.com/auser/backcall
* http://github.com/mmcgrana/simple_callbacks
== License
Copyright (c) 2010, Nick Sutterer
Released under the MIT License.

View file

@ -1,141 +0,0 @@
require 'test_helper'
class HooksTest < Test::Unit::TestCase
class TestClass
include Hooks
def executed
@executed ||= [];
end
end
context "Hooks.define_hook" do
setup do
@klass = Class.new(TestClass)
@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
should "respond to Class.callbacks_for_hook" do
assert_equal [], @klass.callbacks_for_hook(:after_eight)
@klass.after_eight :dine
assert_equal [:dine], @klass.callbacks_for_hook(:after_eight)
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
should "be inherited" do
@klass.after_eight :dine
subklass = Class.new(@klass)
assert_equal [:dine], subklass._after_eight_callbacks
end
end
context "Hooks#run_hook" do
should "run without parameters" do
@mum.instance_eval do
def a; executed << :a; nil; 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
should "execute block callbacks in instance context" do
@mum.class.after_eight { executed << :c }
@mum.run_hook(:after_eight)
assert_equal [:c], @mum.executed
end
end
context "in class context" do
should "run a callback block" do
executed = []
@klass.after_eight do
executed << :klass
end
@klass.run_hook :after_eight
assert_equal [:klass], executed
end
should "run a class methods" do
executed = []
@klass.instance_eval do
after_eight :have_dinner
def have_dinner(executed)
executed << :have_dinner
end
end
@klass.run_hook :after_eight, executed
assert_equal [:have_dinner], executed
end
end
end
context "Deriving" do
setup do
@klass = Class.new(TestClass)
@mum = @klass.new
@mum.class.define_hook :after_eight
end
should "inherit the hook" do
@klass.class_eval do
after_eight :take_shower
def take_shower
executed << :take_shower
end
end
@kid = Class.new(@klass) do
after_eight :have_dinner
def have_dinner
executed << :have_dinner
end
end.new
assert_equal [:take_shower, :have_dinner], @kid.run_hook(:after_eight)
end
end
end

View file

@ -1,55 +0,0 @@
require 'test_helper'
class HooksTest < Test::Unit::TestCase
context "Hooks.define_hook" do
setup do
@klass = Class.new(Object) do
extend Hooks::InheritableAttribute
end
@mum = @klass.new
@klass.inheritable_attr :drinks
end
should "provide a reader with empty inherited attributes, already" do
assert_equal nil, @klass.drinks
end
should "provide a reader with empty inherited attributes in a derived class" do
assert_equal nil, Class.new(@klass).drinks
#@klass.drinks = true
#Class.new(@klass).drinks # TODO: crashes.
end
should "provide an attribute copy in subclasses" do
@klass.drinks = []
assert @klass.drinks.object_id != Class.new(@klass).drinks.object_id
end
should "provide a writer" do
@klass.drinks = [:cabernet]
assert_equal [:cabernet], @klass.drinks
end
should "inherit attributes" do
@klass.drinks = [:cabernet]
subklass_a = Class.new(@klass)
subklass_a.drinks << :becks
subklass_b = Class.new(@klass)
assert_equal [:cabernet], @klass.drinks
assert_equal [:cabernet, :becks], subklass_a.drinks
assert_equal [:cabernet], subklass_b.drinks
end
should "not inherit attributes if we set explicitely" do
@klass.drinks = [:cabernet]
subklass = Class.new(@klass)
subklass.drinks = [:merlot] # we only want merlot explicitely.
assert_equal [:merlot], subklass.drinks # no :cabernet, here
end
end
end

View file

@ -1,10 +0,0 @@
require 'rubygems'
# wycats says...
require 'bundler'
Bundler.setup
require 'test/unit'
require 'shoulda'
require 'hooks'
$:.unshift File.dirname(__FILE__) # add current dir to LOAD_PATHS

View file

@ -0,0 +1,2 @@
Gemfile.lock

View file

@ -0,0 +1,5 @@
language: ruby
rvm:
- 1.8.7
- 1.9.3
- 2.0.0

View file

@ -0,0 +1,33 @@
## 0.3.3
* Fix a bug where the hook writer method (e.g. `#after_dark`) wasn't available on the instance even when `InstanceHooks` was included.
## 0.3.2
* Added `Hooks::InstanceHooks` to add hooks and/or callbacks on instance level. Thanks to @mpapis for that suggestion.
## 0.3.1
* Fix a bug, string hook names are now treated as symbols.
## 0.3.0
* The callback chain can now be halted by configuring the hook as `halts_on_falsey: true` and returning `nil` or `false` from the callback.
* Internal refactorings: hooks are now encapsulated in `Hook` instances and run their callback chains.
## 0.2.2
* `#run_hook` now returns the list of callback results.
## 0.2.1
* You can now pass multiple hook names to `#define_hooks`.
## 0.2.0
h3. Changes
* Callback blocks are now executed on the instance using `instance_exec`. If you need to access the class (former context) use `self.class`.
## 0.1.4
* An uninitialized `inheritable_attr` doesn't crash since it is not cloned anymore. Note that an uncloneable attribute value still causes an exception.

View file

@ -0,0 +1,3 @@
source 'https://rubygems.org'
gemspec

View file

@ -0,0 +1,20 @@
Copyright (c) 2011-2013 Nick Sutterer
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,202 @@
# Hooks
_Generic hooks with callbacks for Ruby._
## Introduction
_Hooks_ lets you define hooks declaratively in your ruby class. You can add callbacks to your hook, which will be run as soon as _you_ run the hook!
It's almost like ActiveSupport::Callbacks but 76,6% less complex. Instead, it is not more than a few lines of code, one method compilation, no `method_missing` and no magic.
Also, you may pass additional arguments to your callbacks when invoking a hook.
## Example
Let's take... a cat.
```ruby
require 'hooks'
class Cat
include Hooks
define_hooks :before_dinner, :after_dinner
```
Now you can add callbacks to your hook declaratively in your class.
```ruby
before_dinner :wash_paws
after_dinner do
puts "Ice cream for #{self}!"
end
after_dinner :have_a_desert # => refers to Cat#have_a_desert
def have_a_desert
puts "Hell, yeah!"
end
```
This will run the block and `#have_a_desert` from above.
```ruby
cat.run_hook :after_dinner
# => Ice cream for #<Cat:0x8df9d84>!
Hell, yeah!
```
Callback blocks and methods will be executed with instance context. Note how `self` in the block refers to the Cat instance.
## Inheritance
Hooks are inherited, here's a complete example to put it all together.
```ruby
class Garfield < Cat
after_dinner :want_some_more
def want_some_more
puts "Is that all?"
end
end
Garfield.new.run_hook :after_dinner
# => Ice cream for #<Cat:0x8df9d84>!
Hell, yeah!
Is that all?
```
Note how the callbacks are invoked in the order they were inherited.
## Options for Callbacks
You're free to pass any number of arguments to #run_callback, those will be passed to the callbacks.
```ruby
cat.run_hook :before_dinner, cat, Time.now
```
The callbacks should be ready for receiving parameters.
```ruby
before_dinner :wash_pawns
before_dinner do |who, when|
...
end
def wash_pawns(who, when)
```
Not sure why a cat should have ice cream for dinner. Beside that, I was tempted naming this gem _hooker_.
## Running And Halting Hooks
Using `#run_hook` doesn't only run all callbacks for this hook but also returns an array of the results from each callback method or block.
```ruby
class Garfield
include Hooks
define_hook :after_dark
after_dark { "Chase mice" }
after_dark { "Enjoy supper" }
end
Garfield.new.run_hook :after_dark
# => ["Chase mice", "Enjoy supper"]
```
This is handy if you need to collect data from your callbacks without having to access a global (brrr) variable.
With the `:halts_on_falsey` option you can halt the callback chain when a callback returns `nil` or `false`.
```ruby
class Garfield
include Hooks
define_hook :after_dark, halts_on_falsey: true
after_dark { "Chase mice" }
after_dark { nil }
after_dark { "Enjoy supper" }
end
result = Garfield.new.run_hook :after_dark
# => ["Chase mice"]
```
This will only run the first two callbacks. Note that the result doesn't contain the `nil` value. You even can check if the chain was halted.
```ruby
result.halted? #=> true
```
## Instance Hooks
You can also define hooks and/or add callbacks per instance. This is helpful if your class should define a basic set of hooks and callbacks that are then extended by instances.
```ruby
class Cat
include Hooks
include Hooks::InstanceHooks
define_hook :after_dark
after_dark { "Chase mice" }
end
```
Note that you have to include `Hooks::InstanceHooks` to get this additional functionality.
See how callbacks can be added to a separate object, now.
```ruby
garfield = Cat.new
garfield.after_dark :sleep
garfield.run_hook(:after_dark) # => invoke "Chase mice" hook and #sleep
```
This will copy all callbacks from the `after_dark` hook to the instance and add a second hook. This all happens on the `garfield` instance, only, and leaves the class untouched.
Naturally, adding new hooks works like-wise.
```ruby
garfield.define_hook :before_six
garfield.before_six { .. }
```
This feature was added in 0.3.2.
## Installation
In your Gemfile, do
```ruby
gem "hooks"
```
## Anybody using it?
* Hooks is already used in [Apotomo](http://github.com/apotonick/apotomo), a hot widget framework for Rails.
* The [datamappify](https://github.com/fredwu/datamappify) gem uses hooks and the author Fred Wu contributed to this gem!
## Similar libraries
* http://github.com/nakajima/aspectory
* http://github.com/auser/backcall
* http://github.com/mmcgrana/simple_callbacks
## License
Copyright (c) 2013, Nick Sutterer
Released under the MIT License.

View file

@ -1,7 +1,7 @@
lib = File.expand_path('../lib/', __FILE__) lib = File.expand_path('../lib/', __FILE__)
$:.unshift lib unless $:.include?(lib) $:.unshift lib unless $:.include?(lib)
require 'hooks' require 'hooks/version'
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = "hooks" s.name = "hooks"
@ -9,14 +9,17 @@ Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY s.platform = Gem::Platform::RUBY
s.authors = ["Nick Sutterer"] s.authors = ["Nick Sutterer"]
s.email = ["apotonick@gmail.com"] s.email = ["apotonick@gmail.com"]
s.homepage = "http://nicksda.apotomo.de/tag/hooks" s.homepage = "http://nicksda.apotomo.de/2010/09/hooks-and-callbacks-for-ruby-but-simple/"
s.summary = %q{Generic hooks with callbacks for Ruby.} s.summary = %q{Generic hooks with callbacks for Ruby.}
s.description = %q{Declaratively define hooks, add callbacks and run them with the options you like.} s.description = %q{Declaratively define hooks, add callbacks and run them with the options you like.}
s.license = "MIT"
s.files = `git ls-files`.split("\n") s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.require_paths = ["lib"] s.require_paths = ["lib"]
s.license = 'MIT'
s.add_development_dependency "shoulda" s.add_development_dependency "minitest", ">= 5.0.0"
s.add_development_dependency "rake" s.add_development_dependency "rake"
s.add_development_dependency "pry"
end end

View file

@ -1,35 +1,41 @@
require File.join(File.dirname(__FILE__), "hooks/inheritable_attribute") require File.join(File.dirname(__FILE__), "hooks/inheritable_attribute")
require File.join(File.dirname(__FILE__), "hooks/hook")
# Almost like ActiveSupport::Callbacks but 76,6% less complex. # Almost like ActiveSupport::Callbacks but 76,6% less complex.
# #
# Example: # Example:
# #
# class CatWidget < Apotomo::Widget # class CatWidget < Apotomo::Widget
# define_hook :after_dinner # define_hooks :before_dinner, :after_dinner
# #
# Now you can add callbacks to your hook declaratively in your class. # Now you can add callbacks to your hook declaratively in your class.
# #
# after_dinner do puts "Ice cream!" end # before_dinner :wash_paws
# after_dinner { puts "Ice cream!" }
# after_dinner :have_a_desert # => refers to CatWidget#have_a_desert # 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. # Running the callbacks happens on instances. It will run the block and #have_a_desert from above.
# #
# cat.run_hook :after_dinner # cat.run_hook :after_dinner
module Hooks module Hooks
VERSION = "0.2.0"
def self.included(base) def self.included(base)
base.extend InheritableAttribute base.class_eval do
base.extend ClassMethods extend InheritableAttribute
extend ClassMethods
inheritable_attr :_hooks
self._hooks= HookSet.new
end
end end
module ClassMethods module ClassMethods
def define_hook(name) def define_hooks(*names)
accessor_name = "_#{name}_callbacks" options = extract_options!(names)
setup_hook_accessors(accessor_name) names.each do |name|
define_hook_writer(name, accessor_name) setup_hook(name, options)
end
end end
alias_method :define_hook, :define_hooks
# Like Hooks#run_hook but for the class. Note that +:callbacks+ must be class methods. # Like Hooks#run_hook but for the class. Note that +:callbacks+ must be class methods.
# #
@ -46,13 +52,7 @@ module Hooks
end end
def run_hook_for(name, scope, *args) def run_hook_for(name, scope, *args)
callbacks_for_hook(name).each do |callback| _hooks[name].run(scope, *args)
if callback.kind_of? Symbol
scope.send(callback, *args)
else
scope.instance_exec(*args, &callback)
end
end
end end
# Returns the callbacks for +name+. Handy if you want to run the callbacks yourself, say when # Returns the callbacks for +name+. Handy if you want to run the callbacks yourself, say when
@ -67,28 +67,37 @@ module Hooks
# #
# would run callbacks in the object _instance_ context, passing +self+ as block parameter. # would run callbacks in the object _instance_ context, passing +self+ as block parameter.
def callbacks_for_hook(name) def callbacks_for_hook(name)
send("_#{name}_callbacks") _hooks[name]
end end
private private
def setup_hook(name, options)
def define_hook_writer(hook, accessor_name) _hooks[name] = Hook.new(options)
self.send(:define_method, hook.to_sym) do |&block| define_hook_writer(name)
if self.class.respond_to?(hook)
self.class.send(hook.to_sym, &block)
end
end
instance_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{hook}(method=nil, &block)
#{accessor_name} << (block || method)
end
RUBY_EVAL
end end
def setup_hook_accessors(accessor_name) def define_hook_writer(name)
inheritable_attr(accessor_name) self.send(:define_method, name.to_sym) do |&block|
send("#{accessor_name}=", []) # initialize ivar. if self.class.respond_to?(name)
self.class.send(name.to_sym, &block)
end
end
instance_eval *hook_writer_args(name)
end
def hook_writer_args(name)
# DISCUSS: isn't there a simpler way to define a dynamic method? should the internal logic be handled by HooksSet instead?
str = <<-RUBY_EVAL
def #{name}(method=nil, &block)
_hooks[:#{name}] << (block || method)
end
RUBY_EVAL
[str, __FILE__, __LINE__ + 1]
end
def extract_options!(args)
args.last.is_a?(Hash) ? args.pop : {}
end end
end end
@ -106,4 +115,22 @@ module Hooks
def run_hook(name, *args) def run_hook(name, *args)
self.class.run_hook_for(name, self, *args) self.class.run_hook_for(name, self, *args)
end end
class HookSet < Hash
def [](name)
super(name.to_sym)
end
def []=(name, values)
super(name.to_sym, values)
end
def clone
super.tap do |cloned|
each { |name, callbacks| cloned[name] = callbacks.clone }
end
end
end
end end
require File.join(File.dirname(__FILE__), "hooks/instance_hooks")

View file

@ -0,0 +1,81 @@
module Hooks
class Hook < Array
def initialize(options)
super()
@options = options
end
# The chain contains the return values of the executed callbacks.
#
# Example:
#
# class Person
# define_hook :before_eating
#
# before_eating :wash_hands
# before_eating :locate_food
# before_eating :sit_down
#
# def wash_hands; :washed_hands; end
# def locate_food; :located_food; false; end
# def sit_down; :sat_down; end
# end
#
# result = person.run_hook(:before_eating)
# result.chain #=> [:washed_hands, false, :sat_down]
#
# If <tt>:halts_on_falsey</tt> is enabled:
#
# class Person
# define_hook :before_eating, :halts_on_falsey => true
# # ...
# end
#
# result = person.run_hook(:before_eating)
# result.chain #=> [:washed_hands]
def run(scope, *args)
inject(Results.new) do |results, callback|
executed = execute_callback(scope, callback, *args)
return results.halted! unless continue_execution?(executed)
results << executed
end
end
private
def execute_callback(scope, callback, *args)
if callback.kind_of?(Symbol)
scope.send(callback, *args)
else
scope.instance_exec(*args, &callback)
end
end
def continue_execution?(result)
@options[:halts_on_falsey] ? result : true
end
class Results < Array
# so much code for nothing...
def initialize(*)
super
@halted = false
end
def halted!
@halted = true
self
end
# Returns true or false based on whether all callbacks
# in the hook chain were successfully executed.
def halted?
@halted
end
def not_halted?
not @halted
end
end
end
end

View file

@ -0,0 +1,25 @@
module Hooks
module InstanceHooks
include ClassMethods
def run_hook(name, *args)
run_hook_for(name, self, *args)
end
private
def _hooks
@_hooks ||= self.class._hooks.clone # TODO: generify that with representable_attrs.
end
module ClassMethods
def define_hook_writer(name)
super
class_eval *hook_writer_args(name)
end
end
def self.included(base)
base.extend(ClassMethods)
end
end
end

View file

@ -0,0 +1,3 @@
module Hooks
VERSION = "0.3.3"
end

View file

@ -0,0 +1,31 @@
require 'test_helper'
class HookTest < MiniTest::Spec
subject { Hooks::Hook.new({}) }
it "exposes array behaviour for callbacks" do
subject << :play_music
subject << :drink_beer
subject.to_a.must_equal [:play_music, :drink_beer]
end
end
class ResultsTest < MiniTest::Spec
subject { Hooks::Hook::Results.new }
describe "#halted?" do
it "defaults to false" do
subject.halted?.must_equal false
end
it "responds to #halted!" do
subject.halted!
subject.halted?.must_equal true
end
it "responds to #not_halted?" do
subject.not_halted?.must_equal true
end
end
end

View file

@ -0,0 +1,216 @@
require 'test_helper'
class HooksTest < MiniTest::Spec
class TestClass
include Hooks
def executed
@executed ||= [];
end
end
describe "::define_hook" do
let(:klass) do
Class.new(TestClass) do
define_hook :after_eight
end
end
subject { klass.new }
it "respond to Class.callbacks_for_hook" do
assert_equal [], klass.callbacks_for_hook(:after_eight)
klass.after_eight :dine
assert_equal [:dine], klass.callbacks_for_hook(:after_eight)
end
it 'symbolizes strings when defining a hook' do
subject.class.define_hooks :before_one, 'after_one'
assert_equal [], klass.callbacks_for_hook(:before_one)
assert_equal [], klass.callbacks_for_hook(:after_one)
assert_equal [], klass.callbacks_for_hook('after_one')
end
it "accept multiple hook names" do
subject.class.define_hooks :before_ten, :after_ten
assert_equal [], klass.callbacks_for_hook(:before_ten)
assert_equal [], klass.callbacks_for_hook(:after_ten)
end
describe "creates a public writer for the hook that" do
it "accepts method names" do
klass.after_eight :dine
assert_equal [:dine], klass._hooks[:after_eight]
end
it "accepts blocks" do
klass.after_eight do true; end
assert klass._hooks[:after_eight].first.kind_of? Proc
end
it "be inherited" do
klass.after_eight :dine
subklass = Class.new(klass)
assert_equal [:dine], subklass._hooks[:after_eight]
end
# TODO: check if options are not shared!
end
describe "Hooks#run_hook" do
it "run without parameters" do
subject.instance_eval do
def a; executed << :a; nil; end
def b; executed << :b; end
self.class.after_eight :b
self.class.after_eight :a
end
subject.run_hook(:after_eight)
assert_equal [:b, :a], subject.executed
end
it "returns empty Results when no callbacks defined" do
subject.run_hook(:after_eight).must_equal Hooks::Hook::Results.new
end
it "accept arbitrary parameters" do
subject.instance_eval do
def a(me, arg); executed << arg+1; end
end
subject.class.after_eight :a
subject.class.after_eight lambda { |me, arg| me.executed << arg-1 }
subject.run_hook(:after_eight, subject, 1)
assert_equal [2, 0], subject.executed
end
it "execute block callbacks in instance context" do
subject.class.after_eight { executed << :c }
subject.run_hook(:after_eight)
assert_equal [:c], subject.executed
end
it "returns all callbacks in order" do
subject.class.after_eight { :dinner_out }
subject.class.after_eight { :party_hard }
subject.class.after_eight { :taxi_home }
results = subject.run_hook(:after_eight)
assert_equal [:dinner_out, :party_hard, :taxi_home], results
assert_equal false, results.halted?
assert_equal true, results.not_halted?
end
describe "halts_on_falsey: true" do
let(:klass) do
Class.new(TestClass) do
define_hook :after_eight, :halts_on_falsey => true
end
end
[nil, false].each do |falsey|
it "returns successful callbacks in order (with #{falsey.inspect})" do
ordered = []
subject.class.after_eight { :dinner_out }
subject.class.after_eight { :party_hard; falsey }
subject.class.after_eight { :taxi_home }
results = subject.run_hook(:after_eight)
assert_equal [:dinner_out], results
assert_equal true, results.halted?
end
end
end
describe "halts_on_falsey: false" do
[nil, false].each do |falsey|
it "returns all callbacks in order (with #{falsey.inspect})" do
ordered = []
subject.class.after_eight { :dinner_out }
subject.class.after_eight { :party_hard; falsey }
subject.class.after_eight { :taxi_home }
results = subject.run_hook(:after_eight)
assert_equal [:dinner_out, falsey, :taxi_home], results
assert_equal false, results.halted?
end
end
end
end
describe "in class context" do
it "runs callback block" do
executed = []
klass.after_eight do
executed << :klass
end
klass.run_hook(:after_eight)
executed.must_equal([:klass])
end
it "runs instance methods" do
executed = []
klass.instance_eval do
after_eight :have_dinner
def have_dinner(executed)
executed << :have_dinner
end
end
klass.run_hook(:after_eight, executed)
executed.must_equal([:have_dinner])
end
end
end
describe "Inheritance" do
let (:superclass) {
Class.new(TestClass) do
define_hook :after_eight
after_eight :take_shower
end
}
let (:subclass) { Class.new(superclass) do after_eight :have_dinner end }
it "inherits callbacks from the hook" do
subclass.callbacks_for_hook(:after_eight).must_equal [:take_shower, :have_dinner]
end
it "doesn't mix up superclass hooks" do
subclass.superclass.callbacks_for_hook(:after_eight).must_equal [:take_shower]
end
end
end
class HookSetTest < MiniTest::Spec
subject { Hooks::HookSet.new }
let (:first_hook) { Hooks::Hook.new(:halts_on_falsey => true) }
let (:second_hook) { Hooks::Hook.new(:halts_on_falsey => false) }
it "responds to #clone" do
subject[:after_eight] = [first_hook]
clone = subject.clone
clone[:after_eight] << second_hook
subject.must_equal(:after_eight => [first_hook])
clone.must_equal(:after_eight => [first_hook, second_hook])
end
# TODO: test if options get cloned.
end

View file

@ -0,0 +1,53 @@
require 'test_helper'
class HooksTest < MiniTest::Spec
describe "Hooks.define_hook" do
subject {
Class.new(Object) do
extend Hooks::InheritableAttribute
inheritable_attr :drinks
end
}
it "provides a reader with empty inherited attributes, already" do
assert_equal nil, subject.drinks
end
it "provides a reader with empty inherited attributes in a derived class" do
assert_equal nil, Class.new(subject).drinks
#subject.drinks = true
#Class.new(subject).drinks # TODO: crashes.
end
it "provides an attribute copy in subclasses" do
subject.drinks = []
assert subject.drinks.object_id != Class.new(subject).drinks.object_id
end
it "provides a writer" do
subject.drinks = [:cabernet]
assert_equal [:cabernet], subject.drinks
end
it "inherits attributes" do
subject.drinks = [:cabernet]
subklass_a = Class.new(subject)
subklass_a.drinks << :becks
subklass_b = Class.new(subject)
assert_equal [:cabernet], subject.drinks
assert_equal [:cabernet, :becks], subklass_a.drinks
assert_equal [:cabernet], subklass_b.drinks
end
it "does not inherit attributes if we set explicitely" do
subject.drinks = [:cabernet]
subklass = Class.new(subject)
subklass.drinks = [:merlot] # we only want merlot explicitely.
assert_equal [:merlot], subklass.drinks # no :cabernet, here
end
end
end

View file

@ -0,0 +1,55 @@
require "test_helper"
class InstanceHooksTest < HooksTest
describe "#define_hook" do
let(:klass) { Class.new(TestClass) do
include Hooks::InstanceHooks
end }
subject { klass.new.tap do |obj|
obj.instance_eval do
def dine; executed << :dine; end
end
end }
it "adds hook to instance" do
subject.define_hook :after_eight
assert_equal [], subject.callbacks_for_hook(:after_eight)
end
it "copies existing class hook" do
klass.define_hook :after_eight
klass.after_eight :dine
assert_equal [:dine], subject.callbacks_for_hook(:after_eight)
end
describe "#after_eight (adding callbacks)" do
before do
subject.define_hook :after_eight
subject.after_eight :dine
end
it "adds #after_eight hook" do
assert_equal [:dine], subject.callbacks_for_hook(:after_eight)
end
it "responds to #run_hook" do
subject.run_hook :after_eight
subject.executed.must_equal [:dine]
end
end
describe "#after_eight from class (no define_hook in instance)" do
it "responds to #after_eight" do
klass.define_hook :after_eight
subject.after_eight :dine
subject.run_hook :after_eight
subject.executed.must_equal [:dine]
end
end
end
end

View file

@ -0,0 +1,3 @@
require 'minitest/autorun'
require 'pry'
require 'hooks'