hooks 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -0
- data/.travis.yml +5 -0
- data/CHANGES.textile +5 -0
- data/Gemfile +1 -1
- data/README.md +165 -0
- data/hooks.gemspec +5 -4
- data/lib/hooks.rb +45 -40
- data/lib/hooks/hook.rb +81 -0
- data/test/hook_test.rb +31 -0
- data/test/hooks_test.rb +146 -104
- data/test/inheritable_attribute_test.rb +36 -38
- data/test/test_helper.rb +2 -9
- metadata +27 -7
- data/README.rdoc +0 -109
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGES.textile
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
h2 0.3.0
|
2
|
+
|
3
|
+
* The callback chain can now be halted by configuring the hook as @halts_on_falsey: true@ and returning @nil@ or @false@ from the callback.
|
4
|
+
* Internal refactorings: hooks are now encapsulated in @Hook@ instances and run their callback chains.
|
5
|
+
|
1
6
|
h2. 0.2.2
|
2
7
|
|
3
8
|
* `#run_hook` now returns the list of callback results.
|
data/Gemfile
CHANGED
data/README.md
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# Hooks
|
2
|
+
|
3
|
+
_Generic hooks with callbacks for Ruby._
|
4
|
+
|
5
|
+
|
6
|
+
## Introduction
|
7
|
+
|
8
|
+
_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!
|
9
|
+
|
10
|
+
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.
|
11
|
+
|
12
|
+
Also, you may pass additional arguments to your callbacks when invoking a hook.
|
13
|
+
|
14
|
+
## Example
|
15
|
+
|
16
|
+
Let's take... a cat.
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
require 'hooks'
|
20
|
+
|
21
|
+
class Cat
|
22
|
+
include Hooks
|
23
|
+
|
24
|
+
define_hooks :before_dinner, :after_dinner
|
25
|
+
```
|
26
|
+
|
27
|
+
Now you can add callbacks to your hook declaratively in your class.
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
before_dinner :wash_paws
|
31
|
+
|
32
|
+
after_dinner do
|
33
|
+
puts "Ice cream for #{self}!"
|
34
|
+
end
|
35
|
+
|
36
|
+
after_dinner :have_a_desert # => refers to Cat#have_a_desert
|
37
|
+
|
38
|
+
def have_a_desert
|
39
|
+
puts "Hell, yeah!"
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
This will run the block and `#have_a_desert` from above.
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
cat.run_hook :after_dinner
|
47
|
+
# => Ice cream for #<Cat:0x8df9d84>!
|
48
|
+
Hell, yeah!
|
49
|
+
```
|
50
|
+
|
51
|
+
Callback blocks and methods will be executed with instance context. Note how `self` in the block refers to the Cat instance.
|
52
|
+
|
53
|
+
|
54
|
+
## Inheritance
|
55
|
+
|
56
|
+
Hooks are inherited, here's a complete example to put it all together.
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class Garfield < Cat
|
60
|
+
|
61
|
+
after_dinner :want_some_more
|
62
|
+
|
63
|
+
def want_some_more
|
64
|
+
puts "Is that all?"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
Garfield.new.run_hook :after_dinner
|
70
|
+
# => Ice cream for #<Cat:0x8df9d84>!
|
71
|
+
Hell, yeah!
|
72
|
+
Is that all?
|
73
|
+
```
|
74
|
+
|
75
|
+
Note how the callbacks are invoked in the order they were inherited.
|
76
|
+
|
77
|
+
|
78
|
+
## Options for Callbacks
|
79
|
+
|
80
|
+
You're free to pass any number of arguments to #run_callback, those will be passed to the callbacks.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
cat.run_hook :before_dinner, cat, Time.now
|
84
|
+
```
|
85
|
+
|
86
|
+
The callbacks should be ready for receiving parameters.
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
before_dinner :wash_pawns
|
90
|
+
before_dinner do |who, when|
|
91
|
+
...
|
92
|
+
end
|
93
|
+
|
94
|
+
def wash_pawns(who, when)
|
95
|
+
```
|
96
|
+
|
97
|
+
Not sure why a cat should have ice cream for dinner. Beside that, I was tempted naming this gem _hooker_.
|
98
|
+
|
99
|
+
|
100
|
+
## Running And Halting Hooks
|
101
|
+
|
102
|
+
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.
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
class Garfield
|
106
|
+
include Hooks
|
107
|
+
define_hook :after_dark
|
108
|
+
|
109
|
+
after_dark { "Chase mice" }
|
110
|
+
after_dark { "Enjoy supper" }
|
111
|
+
end
|
112
|
+
|
113
|
+
Garfield.new.run_hook :after_dark
|
114
|
+
# => ["Chase mice", "Enjoy supper"]
|
115
|
+
```
|
116
|
+
|
117
|
+
This is handy if you need to collect data from your callbacks without having to access a global (brrr) variable.
|
118
|
+
|
119
|
+
With the `:halts_on_falsey` option you can halt the callback chain when a callback returns `nil` or `false`.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
class Garfield
|
123
|
+
include Hooks
|
124
|
+
define_hook :after_dark, halts_on_falsey: true
|
125
|
+
|
126
|
+
after_dark { "Chase mice" }
|
127
|
+
after_dark { nil }
|
128
|
+
after_dark { "Enjoy supper" }
|
129
|
+
end
|
130
|
+
|
131
|
+
result = Garfield.new.run_hook :after_dark
|
132
|
+
# => ["Chase mice"]
|
133
|
+
```
|
134
|
+
|
135
|
+
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.
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
result.halted? #=> true
|
139
|
+
```
|
140
|
+
|
141
|
+
## Installation
|
142
|
+
|
143
|
+
In your Gemfile, do
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
gem hooks
|
147
|
+
```
|
148
|
+
|
149
|
+
## Anybody using it?
|
150
|
+
|
151
|
+
* Hooks is already used in [Apotomo](http://github.com/apotonick/apotomo), a hot widget framework for Rails.
|
152
|
+
* The [datamappify](https://github.com/fredwu/datamappify) gem uses hooks and the author Fred Wu contributed to this gem!
|
153
|
+
|
154
|
+
## Similar libraries
|
155
|
+
|
156
|
+
* http://github.com/nakajima/aspectory
|
157
|
+
* http://github.com/auser/backcall
|
158
|
+
* http://github.com/mmcgrana/simple_callbacks
|
159
|
+
|
160
|
+
|
161
|
+
## License
|
162
|
+
|
163
|
+
Copyright (c) 2013, Nick Sutterer
|
164
|
+
|
165
|
+
Released under the MIT License.
|
data/hooks.gemspec
CHANGED
@@ -9,14 +9,15 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.platform = Gem::Platform::RUBY
|
10
10
|
s.authors = ["Nick Sutterer"]
|
11
11
|
s.email = ["apotonick@gmail.com"]
|
12
|
-
s.homepage = "http://nicksda.apotomo.de/
|
12
|
+
s.homepage = "http://nicksda.apotomo.de/2010/09/hooks-and-callbacks-for-ruby-but-simple/"
|
13
13
|
s.summary = %q{Generic hooks with callbacks for Ruby.}
|
14
14
|
s.description = %q{Declaratively define hooks, add callbacks and run them with the options you like.}
|
15
|
-
|
15
|
+
|
16
16
|
s.files = `git ls-files`.split("\n")
|
17
17
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
18
|
s.require_paths = ["lib"]
|
19
|
-
|
20
|
-
s.add_development_dependency "
|
19
|
+
|
20
|
+
s.add_development_dependency "minitest", ">= 5.0.0"
|
21
21
|
s.add_development_dependency "rake"
|
22
|
+
s.add_development_dependency "pry"
|
22
23
|
end
|
data/lib/hooks.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "hooks/inheritable_attribute"
|
2
|
+
require "hooks/hook"
|
2
3
|
|
3
4
|
# Almost like ActiveSupport::Callbacks but 76,6% less complex.
|
4
5
|
#
|
@@ -12,50 +13,50 @@ require "hooks/inheritable_attribute"
|
|
12
13
|
# before_dinner :wash_paws
|
13
14
|
# after_dinner { puts "Ice cream!" }
|
14
15
|
# after_dinner :have_a_desert # => refers to CatWidget#have_a_desert
|
15
|
-
#
|
16
|
+
#
|
16
17
|
# Running the callbacks happens on instances. It will run the block and #have_a_desert from above.
|
17
18
|
#
|
18
19
|
# cat.run_hook :after_dinner
|
19
20
|
module Hooks
|
20
|
-
VERSION = "0.
|
21
|
-
|
21
|
+
VERSION = "0.3.0"
|
22
|
+
|
22
23
|
def self.included(base)
|
23
|
-
base.
|
24
|
-
|
24
|
+
base.class_eval do
|
25
|
+
extend InheritableAttribute
|
26
|
+
extend ClassMethods
|
27
|
+
inheritable_attr :_hooks
|
28
|
+
self._hooks= HookSet.new
|
29
|
+
end
|
25
30
|
end
|
26
|
-
|
31
|
+
|
27
32
|
module ClassMethods
|
28
33
|
def define_hooks(*names)
|
34
|
+
options = extract_options!(names)
|
35
|
+
|
29
36
|
names.each do |name|
|
30
|
-
setup_hook(name)
|
37
|
+
setup_hook(name, options)
|
31
38
|
end
|
32
39
|
end
|
33
40
|
alias_method :define_hook, :define_hooks
|
34
|
-
|
41
|
+
|
35
42
|
# Like Hooks#run_hook but for the class. Note that +:callbacks+ must be class methods.
|
36
43
|
#
|
37
44
|
# Example:
|
38
45
|
#
|
39
46
|
# class Cat
|
40
47
|
# after_eight :grab_a_beer
|
41
|
-
#
|
48
|
+
#
|
42
49
|
# def self.grab_a_beer(*) # and so on...
|
43
|
-
#
|
50
|
+
#
|
44
51
|
# where <tt>Cat.run_hook :after_eight</tt> will call the class method +grab_a_beer+.
|
45
52
|
def run_hook(name, *args)
|
46
53
|
run_hook_for(name, self, *args)
|
47
|
-
end
|
48
|
-
|
54
|
+
end
|
55
|
+
|
49
56
|
def run_hook_for(name, scope, *args)
|
50
|
-
|
51
|
-
if callback.kind_of? Symbol
|
52
|
-
scope.send(callback, *args)
|
53
|
-
else
|
54
|
-
scope.instance_exec(*args, &callback)
|
55
|
-
end
|
56
|
-
end
|
57
|
+
_hooks[name].run(scope, *args)
|
57
58
|
end
|
58
|
-
|
59
|
+
|
59
60
|
# Returns the callbacks for +name+. Handy if you want to run the callbacks yourself, say when
|
60
61
|
# they should be executed in another context.
|
61
62
|
#
|
@@ -68,33 +69,29 @@ module Hooks
|
|
68
69
|
#
|
69
70
|
# would run callbacks in the object _instance_ context, passing +self+ as block parameter.
|
70
71
|
def callbacks_for_hook(name)
|
71
|
-
|
72
|
+
_hooks[name]
|
72
73
|
end
|
73
|
-
|
74
|
+
|
74
75
|
private
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
setup_hook_accessors(accessor_name)
|
80
|
-
define_hook_writer(name, accessor_name)
|
76
|
+
def setup_hook(name, options)
|
77
|
+
_hooks[name] = Hook.new(options)
|
78
|
+
define_hook_writer(name)
|
81
79
|
end
|
82
|
-
|
83
|
-
def define_hook_writer(
|
80
|
+
|
81
|
+
def define_hook_writer(name)
|
84
82
|
instance_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
85
|
-
def #{
|
86
|
-
|
83
|
+
def #{name}(method=nil, &block)
|
84
|
+
_hooks[:#{name}] << (block || method)
|
87
85
|
end
|
88
86
|
RUBY_EVAL
|
89
87
|
end
|
90
|
-
|
91
|
-
def
|
92
|
-
|
93
|
-
|
94
|
-
end
|
88
|
+
|
89
|
+
def extract_options!(args)
|
90
|
+
args.last.is_a?(Hash) ? args.pop : {}
|
91
|
+
end
|
95
92
|
end
|
96
|
-
|
97
|
-
# Runs the callbacks (method/block) for the specified hook +name+. Additional arguments will
|
93
|
+
|
94
|
+
# Runs the callbacks (method/block) for the specified hook +name+. Additional arguments will
|
98
95
|
# be passed to the callback.
|
99
96
|
#
|
100
97
|
# Example:
|
@@ -102,10 +99,18 @@ module Hooks
|
|
102
99
|
# cat.run_hook :after_dinner, "i want ice cream!"
|
103
100
|
#
|
104
101
|
# will invoke the callbacks like
|
105
|
-
#
|
102
|
+
#
|
106
103
|
# desert("i want ice cream!")
|
107
104
|
# block.call("i want ice cream!")
|
108
105
|
def run_hook(name, *args)
|
109
106
|
self.class.run_hook_for(name, self, *args)
|
110
107
|
end
|
108
|
+
|
109
|
+
class HookSet < Hash
|
110
|
+
def clone
|
111
|
+
super.tap do |cloned|
|
112
|
+
each { |name, callbacks| cloned[name] = callbacks.clone }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
111
116
|
end
|
data/lib/hooks/hook.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
module Hooks
|
2
|
+
class Hook < Array
|
3
|
+
def initialize(options)
|
4
|
+
super()
|
5
|
+
@options = options
|
6
|
+
end
|
7
|
+
|
8
|
+
# The chain contains the return values of the executed callbacks.
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# class Person
|
13
|
+
# define_hook :before_eating
|
14
|
+
#
|
15
|
+
# before_eating :wash_hands
|
16
|
+
# before_eating :locate_food
|
17
|
+
# before_eating :sit_down
|
18
|
+
#
|
19
|
+
# def wash_hands; :washed_hands; end
|
20
|
+
# def locate_food; :located_food; false; end
|
21
|
+
# def sit_down; :sat_down; end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# result = person.run_hook(:before_eating)
|
25
|
+
# result.chain #=> [:washed_hands, false, :sat_down]
|
26
|
+
#
|
27
|
+
# If <tt>:halts_on_falsey</tt> is enabled:
|
28
|
+
#
|
29
|
+
# class Person
|
30
|
+
# define_hook :before_eating, :halts_on_falsey => true
|
31
|
+
# # ...
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# result = person.run_hook(:before_eating)
|
35
|
+
# result.chain #=> [:washed_hands]
|
36
|
+
def run(scope, *args)
|
37
|
+
inject(Results.new) do |results, callback|
|
38
|
+
executed = execute_callback(scope, callback, *args)
|
39
|
+
|
40
|
+
return results.halted! unless continue_execution?(executed)
|
41
|
+
results << executed
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def execute_callback(scope, callback, *args)
|
47
|
+
if callback.kind_of?(Symbol)
|
48
|
+
scope.send(callback, *args)
|
49
|
+
else
|
50
|
+
scope.instance_exec(*args, &callback)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def continue_execution?(result)
|
55
|
+
@options[:halts_on_falsey] ? result : true
|
56
|
+
end
|
57
|
+
|
58
|
+
class Results < Array
|
59
|
+
# so much code for nothing...
|
60
|
+
def initialize(*)
|
61
|
+
super
|
62
|
+
@halted = false
|
63
|
+
end
|
64
|
+
|
65
|
+
def halted!
|
66
|
+
@halted = true
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns true or false based on whether all callbacks
|
71
|
+
# in the hook chain were successfully executed.
|
72
|
+
def halted?
|
73
|
+
@halted
|
74
|
+
end
|
75
|
+
|
76
|
+
def not_halted?
|
77
|
+
not @halted
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/test/hook_test.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class HookTest < MiniTest::Spec
|
4
|
+
subject { Hooks::Hook.new({}) }
|
5
|
+
|
6
|
+
it "exposes array behaviour for callbacks" do
|
7
|
+
subject << :play_music
|
8
|
+
subject << :drink_beer
|
9
|
+
|
10
|
+
subject.to_a.must_equal [:play_music, :drink_beer]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class ResultsTest < MiniTest::Spec
|
15
|
+
subject { Hooks::Hook::Results.new }
|
16
|
+
|
17
|
+
describe "#halted?" do
|
18
|
+
it "defaults to false" do
|
19
|
+
subject.halted?.must_equal false
|
20
|
+
end
|
21
|
+
|
22
|
+
it "responds to #halted!" do
|
23
|
+
subject.halted!
|
24
|
+
subject.halted?.must_equal true
|
25
|
+
end
|
26
|
+
|
27
|
+
it "responds to #not_halted?" do
|
28
|
+
subject.not_halted?.must_equal true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/test/hooks_test.rb
CHANGED
@@ -1,156 +1,198 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
|
-
class HooksTest <
|
3
|
+
class HooksTest < MiniTest::Spec
|
4
4
|
class TestClass
|
5
5
|
include Hooks
|
6
|
-
|
6
|
+
|
7
7
|
def executed
|
8
8
|
@executed ||= [];
|
9
9
|
end
|
10
10
|
end
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
@mum.class.define_hook :after_eight
|
19
|
-
end
|
20
|
-
|
21
|
-
should "provide accessors to the stored callbacks" do
|
22
|
-
assert_equal [], @klass._after_eight_callbacks
|
23
|
-
@klass._after_eight_callbacks << :dine
|
24
|
-
assert_equal [:dine], @klass._after_eight_callbacks
|
11
|
+
|
12
|
+
|
13
|
+
describe "Hooks.define_hook" do
|
14
|
+
let(:klass) do
|
15
|
+
Class.new(TestClass) do
|
16
|
+
define_hook :after_eight
|
17
|
+
end
|
25
18
|
end
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
assert_equal [
|
19
|
+
|
20
|
+
subject { klass.new }
|
21
|
+
|
22
|
+
it "respond to Class.callbacks_for_hook" do
|
23
|
+
assert_equal [], klass.callbacks_for_hook(:after_eight)
|
24
|
+
klass.after_eight :dine
|
25
|
+
assert_equal [:dine], klass.callbacks_for_hook(:after_eight)
|
31
26
|
end
|
32
27
|
|
33
|
-
|
34
|
-
|
35
|
-
assert_equal [],
|
36
|
-
assert_equal [],
|
28
|
+
it "accept multiple hook names" do
|
29
|
+
subject.class.define_hooks :before_ten, :after_ten
|
30
|
+
assert_equal [], klass.callbacks_for_hook(:before_ten)
|
31
|
+
assert_equal [], klass.callbacks_for_hook(:after_ten)
|
37
32
|
end
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
assert_equal [:dine],
|
33
|
+
|
34
|
+
describe "creates a public writer for the hook that" do
|
35
|
+
it "accepts method names" do
|
36
|
+
klass.after_eight :dine
|
37
|
+
assert_equal [:dine], klass._hooks[:after_eight]
|
43
38
|
end
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
assert
|
39
|
+
|
40
|
+
it "accepts blocks" do
|
41
|
+
klass.after_eight do true; end
|
42
|
+
assert klass._hooks[:after_eight].first.kind_of? Proc
|
48
43
|
end
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
subklass = Class.new(
|
53
|
-
|
54
|
-
assert_equal [:dine], subklass.
|
44
|
+
|
45
|
+
it "be inherited" do
|
46
|
+
klass.after_eight :dine
|
47
|
+
subklass = Class.new(klass)
|
48
|
+
|
49
|
+
assert_equal [:dine], subklass._hooks[:after_eight]
|
55
50
|
end
|
51
|
+
# TODO: check if options are not shared!
|
56
52
|
end
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
53
|
+
|
54
|
+
describe "Hooks#run_hook" do
|
55
|
+
it "run without parameters" do
|
56
|
+
subject.instance_eval do
|
61
57
|
def a; executed << :a; nil; end
|
62
58
|
def b; executed << :b; end
|
63
|
-
|
59
|
+
|
64
60
|
self.class.after_eight :b
|
65
61
|
self.class.after_eight :a
|
66
62
|
end
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
assert_equal [:b, :a],
|
63
|
+
|
64
|
+
subject.run_hook(:after_eight)
|
65
|
+
|
66
|
+
assert_equal [:b, :a], subject.executed
|
71
67
|
end
|
72
|
-
|
73
|
-
|
74
|
-
|
68
|
+
|
69
|
+
it "accept arbitrary parameters" do
|
70
|
+
subject.instance_eval do
|
75
71
|
def a(me, arg); executed << arg+1; end
|
76
72
|
end
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
assert_equal [2, 0],
|
73
|
+
subject.class.after_eight :a
|
74
|
+
subject.class.after_eight lambda { |me, arg| me.executed << arg-1 }
|
75
|
+
|
76
|
+
subject.run_hook(:after_eight, subject, 1)
|
77
|
+
|
78
|
+
assert_equal [2, 0], subject.executed
|
83
79
|
end
|
84
80
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
assert_equal [:c],
|
81
|
+
it "execute block callbacks in instance context" do
|
82
|
+
subject.class.after_eight { executed << :c }
|
83
|
+
subject.run_hook(:after_eight)
|
84
|
+
assert_equal [:c], subject.executed
|
89
85
|
end
|
90
86
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
87
|
+
it "returns all callbacks in order" do
|
88
|
+
subject.class.after_eight { :dinner_out }
|
89
|
+
subject.class.after_eight { :party_hard }
|
90
|
+
subject.class.after_eight { :taxi_home }
|
91
|
+
|
92
|
+
results = subject.run_hook(:after_eight)
|
95
93
|
|
96
|
-
results = @mum.run_hook(:after_eight)
|
97
94
|
assert_equal [:dinner_out, :party_hard, :taxi_home], results
|
95
|
+
assert_equal false, results.halted?
|
96
|
+
assert_equal true, results.not_halted?
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "halts_on_falsey: true" do
|
100
|
+
let(:klass) do
|
101
|
+
Class.new(TestClass) do
|
102
|
+
define_hook :after_eight, :halts_on_falsey => true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
[nil, false].each do |falsey|
|
107
|
+
it "returns successful callbacks in order (with #{falsey.inspect})" do
|
108
|
+
ordered = []
|
109
|
+
|
110
|
+
subject.class.after_eight { :dinner_out }
|
111
|
+
subject.class.after_eight { :party_hard; falsey }
|
112
|
+
subject.class.after_eight { :taxi_home }
|
113
|
+
|
114
|
+
results = subject.run_hook(:after_eight)
|
115
|
+
|
116
|
+
assert_equal [:dinner_out], results
|
117
|
+
assert_equal true, results.halted?
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "halts_on_falsey: false" do
|
123
|
+
[nil, false].each do |falsey|
|
124
|
+
it "returns all callbacks in order (with #{falsey.inspect})" do
|
125
|
+
ordered = []
|
126
|
+
|
127
|
+
subject.class.after_eight { :dinner_out }
|
128
|
+
subject.class.after_eight { :party_hard; falsey }
|
129
|
+
subject.class.after_eight { :taxi_home }
|
130
|
+
|
131
|
+
results = subject.run_hook(:after_eight)
|
132
|
+
|
133
|
+
assert_equal [:dinner_out, falsey, :taxi_home], results
|
134
|
+
assert_equal false, results.halted?
|
135
|
+
end
|
136
|
+
end
|
98
137
|
end
|
99
138
|
end
|
100
|
-
|
101
|
-
|
102
|
-
|
139
|
+
|
140
|
+
describe "in class context" do
|
141
|
+
it "run a callback block" do
|
103
142
|
executed = []
|
104
|
-
|
143
|
+
klass.after_eight do
|
105
144
|
executed << :klass
|
106
145
|
end
|
107
|
-
|
108
|
-
|
146
|
+
klass.run_hook(:after_eight)
|
147
|
+
|
109
148
|
assert_equal [:klass], executed
|
110
149
|
end
|
111
|
-
|
112
|
-
|
150
|
+
|
151
|
+
it "run a class methods" do
|
113
152
|
executed = []
|
114
|
-
|
153
|
+
klass.instance_eval do
|
115
154
|
after_eight :have_dinner
|
116
|
-
|
155
|
+
|
117
156
|
def have_dinner(executed)
|
118
157
|
executed << :have_dinner
|
119
158
|
end
|
120
159
|
end
|
121
|
-
|
122
|
-
|
160
|
+
klass.run_hook(:after_eight, executed)
|
161
|
+
|
123
162
|
assert_equal [:have_dinner], executed
|
124
163
|
end
|
125
164
|
end
|
126
165
|
end
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
@mum.class.define_hook :after_eight
|
134
|
-
end
|
135
|
-
|
136
|
-
should "inherit the hook" do
|
137
|
-
@klass.class_eval do
|
166
|
+
|
167
|
+
describe "Inheritance" do
|
168
|
+
let (:superclass) {
|
169
|
+
Class.new(TestClass) do
|
170
|
+
define_hook :after_eight
|
171
|
+
|
138
172
|
after_eight :take_shower
|
139
|
-
|
140
|
-
def take_shower
|
141
|
-
executed << :take_shower
|
142
|
-
end
|
143
173
|
end
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
174
|
+
}
|
175
|
+
|
176
|
+
let (:subclass) { Class.new(superclass) do after_eight :have_dinner end }
|
177
|
+
|
178
|
+
it "inherits callbacks from the hook" do
|
179
|
+
subclass.callbacks_for_hook(:after_eight).must_equal [:take_shower, :have_dinner]
|
180
|
+
end
|
181
|
+
|
182
|
+
it "doesn't mix up superclass hooks" do
|
183
|
+
subclass.superclass.callbacks_for_hook(:after_eight).must_equal [:take_shower]
|
154
184
|
end
|
155
185
|
end
|
156
186
|
end
|
187
|
+
|
188
|
+
class HookSetTest < MiniTest::Spec
|
189
|
+
subject { Hooks::HookSet.new }
|
190
|
+
|
191
|
+
it "responds to #clone" do
|
192
|
+
subject[:after_eight] = [:drink_beer]
|
193
|
+
clone = subject.clone
|
194
|
+
clone[:after_eight] << :open_fridge
|
195
|
+
|
196
|
+
subject.must_equal(:after_eight => [:drink_beer])
|
197
|
+
end
|
198
|
+
end
|
@@ -1,53 +1,51 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
|
-
class HooksTest <
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
class HooksTest < MiniTest::Spec
|
4
|
+
describe "Hooks.define_hook" do
|
5
|
+
subject {
|
6
|
+
Class.new(Object) do
|
7
7
|
extend Hooks::InheritableAttribute
|
8
|
+
inheritable_attr :drinks
|
8
9
|
end
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
should "provide a reader with empty inherited attributes, already" do
|
15
|
-
assert_equal nil, @klass.drinks
|
10
|
+
}
|
11
|
+
|
12
|
+
it "provides a reader with empty inherited attributes, already" do
|
13
|
+
assert_equal nil, subject.drinks
|
16
14
|
end
|
17
|
-
|
18
|
-
|
19
|
-
assert_equal nil, Class.new(
|
20
|
-
|
21
|
-
#Class.new(
|
15
|
+
|
16
|
+
it "provides a reader with empty inherited attributes in a derived class" do
|
17
|
+
assert_equal nil, Class.new(subject).drinks
|
18
|
+
#subject.drinks = true
|
19
|
+
#Class.new(subject).drinks # TODO: crashes.
|
22
20
|
end
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
assert
|
21
|
+
|
22
|
+
it "provides an attribute copy in subclasses" do
|
23
|
+
subject.drinks = []
|
24
|
+
assert subject.drinks.object_id != Class.new(subject).drinks.object_id
|
27
25
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
assert_equal [:cabernet],
|
26
|
+
|
27
|
+
it "provides a writer" do
|
28
|
+
subject.drinks = [:cabernet]
|
29
|
+
assert_equal [:cabernet], subject.drinks
|
32
30
|
end
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
subklass_a = Class.new(
|
31
|
+
|
32
|
+
it "inherits attributes" do
|
33
|
+
subject.drinks = [:cabernet]
|
34
|
+
|
35
|
+
subklass_a = Class.new(subject)
|
38
36
|
subklass_a.drinks << :becks
|
39
|
-
|
40
|
-
subklass_b = Class.new(
|
41
|
-
|
42
|
-
assert_equal [:cabernet],
|
37
|
+
|
38
|
+
subklass_b = Class.new(subject)
|
39
|
+
|
40
|
+
assert_equal [:cabernet], subject.drinks
|
43
41
|
assert_equal [:cabernet, :becks], subklass_a.drinks
|
44
42
|
assert_equal [:cabernet], subklass_b.drinks
|
45
43
|
end
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
subklass = Class.new(
|
50
|
-
|
44
|
+
|
45
|
+
it "does not inherit attributes if we set explicitely" do
|
46
|
+
subject.drinks = [:cabernet]
|
47
|
+
subklass = Class.new(subject)
|
48
|
+
|
51
49
|
subklass.drinks = [:merlot] # we only want merlot explicitely.
|
52
50
|
assert_equal [:merlot], subklass.drinks # no :cabernet, here
|
53
51
|
end
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hooks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,10 +9,26 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-05-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
15
|
+
name: minitest
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 5.0.0
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 5.0.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
16
32
|
requirement: !ruby/object:Gem::Requirement
|
17
33
|
none: false
|
18
34
|
requirements:
|
@@ -28,7 +44,7 @@ dependencies:
|
|
28
44
|
- !ruby/object:Gem::Version
|
29
45
|
version: '0'
|
30
46
|
- !ruby/object:Gem::Dependency
|
31
|
-
name:
|
47
|
+
name: pry
|
32
48
|
requirement: !ruby/object:Gem::Requirement
|
33
49
|
none: false
|
34
50
|
requirements:
|
@@ -51,17 +67,21 @@ executables: []
|
|
51
67
|
extensions: []
|
52
68
|
extra_rdoc_files: []
|
53
69
|
files:
|
70
|
+
- .gitignore
|
71
|
+
- .travis.yml
|
54
72
|
- CHANGES.textile
|
55
73
|
- Gemfile
|
56
|
-
- README.
|
74
|
+
- README.md
|
57
75
|
- Rakefile
|
58
76
|
- hooks.gemspec
|
59
77
|
- lib/hooks.rb
|
78
|
+
- lib/hooks/hook.rb
|
60
79
|
- lib/hooks/inheritable_attribute.rb
|
80
|
+
- test/hook_test.rb
|
61
81
|
- test/hooks_test.rb
|
62
82
|
- test/inheritable_attribute_test.rb
|
63
83
|
- test/test_helper.rb
|
64
|
-
homepage: http://nicksda.apotomo.de/
|
84
|
+
homepage: http://nicksda.apotomo.de/2010/09/hooks-and-callbacks-for-ruby-but-simple/
|
65
85
|
licenses: []
|
66
86
|
post_install_message:
|
67
87
|
rdoc_options: []
|
@@ -81,7 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
101
|
version: '0'
|
82
102
|
requirements: []
|
83
103
|
rubyforge_project:
|
84
|
-
rubygems_version: 1.8.
|
104
|
+
rubygems_version: 1.8.25
|
85
105
|
signing_key:
|
86
106
|
specification_version: 3
|
87
107
|
summary: Generic hooks with callbacks for Ruby.
|
data/README.rdoc
DELETED
@@ -1,109 +0,0 @@
|
|
1
|
-
= Hooks
|
2
|
-
|
3
|
-
<em>Generic hooks with callbacks for Ruby.</em>
|
4
|
-
|
5
|
-
|
6
|
-
== Introduction
|
7
|
-
|
8
|
-
_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!
|
9
|
-
|
10
|
-
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.
|
11
|
-
|
12
|
-
Also, you may pass additional arguments to your callbacks when invoking a hook.
|
13
|
-
|
14
|
-
== Example
|
15
|
-
|
16
|
-
Let's take... a cat.
|
17
|
-
|
18
|
-
require 'hooks'
|
19
|
-
|
20
|
-
class Cat
|
21
|
-
include Hooks
|
22
|
-
|
23
|
-
define_hooks :before_dinner, :after_dinner
|
24
|
-
|
25
|
-
Now you can add callbacks to your hook declaratively in your class.
|
26
|
-
|
27
|
-
before_dinner :wash_paws
|
28
|
-
|
29
|
-
after_dinner do
|
30
|
-
puts "Ice cream for #{self}!"
|
31
|
-
end
|
32
|
-
|
33
|
-
after_dinner :have_a_desert # => refers to Cat#have_a_desert
|
34
|
-
|
35
|
-
def have_a_desert
|
36
|
-
puts "Hell, yeah!"
|
37
|
-
end
|
38
|
-
|
39
|
-
This will run the block and <tt>#have_a_desert</tt> from above.
|
40
|
-
|
41
|
-
cat.run_hook :after_dinner
|
42
|
-
# => Ice cream for #<Cat:0x8df9d84>!
|
43
|
-
Hell, yeah!
|
44
|
-
|
45
|
-
Callback blocks and methods will be executed with instance context. Note how +self+ in the block refers to the Cat instance.
|
46
|
-
|
47
|
-
|
48
|
-
== Inheritance
|
49
|
-
|
50
|
-
Hooks are inherited, here's a complete example to put it all together.
|
51
|
-
|
52
|
-
class Garfield < Cat
|
53
|
-
|
54
|
-
after_dinner :want_some_more
|
55
|
-
|
56
|
-
def want_some_more
|
57
|
-
puts "Is that all?"
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
|
62
|
-
Garfield.new.run_hook :after_dinner
|
63
|
-
# => Ice cream for #<Cat:0x8df9d84>!
|
64
|
-
Hell, yeah!
|
65
|
-
Is that all?
|
66
|
-
|
67
|
-
Note how the callbacks are invoked in the order they were inherited.
|
68
|
-
|
69
|
-
|
70
|
-
== Options for Callbacks
|
71
|
-
|
72
|
-
You're free to pass any number of arguments to #run_callback, those will be passed to the callbacks.
|
73
|
-
|
74
|
-
cat.run_hook :before_dinner, cat, Time.now
|
75
|
-
|
76
|
-
The callbacks should be ready for receiving parameters.
|
77
|
-
|
78
|
-
before_dinner :wash_pawns
|
79
|
-
before_dinner do |who, when|
|
80
|
-
...
|
81
|
-
end
|
82
|
-
|
83
|
-
def wash_pawns(who, when)
|
84
|
-
|
85
|
-
|
86
|
-
Not sure why a cat should have ice cream for dinner. Beside that, I was tempted naming this gem _hooker_.
|
87
|
-
|
88
|
-
|
89
|
-
== Installation
|
90
|
-
|
91
|
-
gem install hooks
|
92
|
-
|
93
|
-
|
94
|
-
== Anybody using it?
|
95
|
-
|
96
|
-
* 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.
|
97
|
-
|
98
|
-
== Similar libraries
|
99
|
-
|
100
|
-
* http://github.com/nakajima/aspectory
|
101
|
-
* http://github.com/auser/backcall
|
102
|
-
* http://github.com/mmcgrana/simple_callbacks
|
103
|
-
|
104
|
-
|
105
|
-
== License
|
106
|
-
|
107
|
-
Copyright (c) 2010, Nick Sutterer
|
108
|
-
|
109
|
-
Released under the MIT License.
|