onfire 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +231 -0
- data/Rakefile +62 -0
- data/lib/onfire/event.rb +31 -0
- data/lib/onfire/event_table.rb +27 -0
- data/lib/onfire/version.rb +5 -0
- data/lib/onfire.rb +52 -0
- data/test/event_table_test.rb +30 -0
- data/test/event_test.rb +28 -0
- data/test/onfire_test.rb +260 -0
- data/test/test_helper.rb +11 -0
- metadata +67 -0
data/README.textile
ADDED
@@ -0,0 +1,231 @@
|
|
1
|
+
h1. Onfire
|
2
|
+
|
3
|
+
_Have bubbling events and observers in all your Ruby objects._
|
4
|
+
|
5
|
+
|
6
|
+
h2. Introduction
|
7
|
+
|
8
|
+
If you think "bubbling events" sounds awesome and should definitly be used in your project, you're lame. However, if you answer "yes" to at least one of the following requirements you're in. If not, go and use Ruby's great @Observable@ mixin.
|
9
|
+
|
10
|
+
*Do you...?*
|
11
|
+
* prefer *decoupled systems*, where observers don't wanna know the observed object (as @Observable#add_observer@ requires)?
|
12
|
+
* rather intend to *observe _events_*, not business objects alone?
|
13
|
+
* have a *tree-like* data structure? Bubbling events only make sense in a hierarchical environment where observers and event sources form a tree.
|
14
|
+
* miss a *stop!* command which prevents the event from further propagation?
|
15
|
+
|
16
|
+
h2. Example
|
17
|
+
|
18
|
+
Let's assume you have a set of @User@ objects with roles, like "CEO", "Manager", and "Developer". You just decided to implement some messaging system where developers can complain, managers can ignore, and the CEO is trying to control.
|
19
|
+
|
20
|
+
<pre>
|
21
|
+
CEO: bill
|
22
|
+
| |
|
23
|
+
Managers: mike matz
|
24
|
+
| |
|
25
|
+
Developers: dave didi
|
26
|
+
</pre>
|
27
|
+
|
28
|
+
If _dave_ would complain about a new policy (which implies exclusive usage of Win PCs only) it would bubble up to his manager _matz_ and then to _bill_, who'd fire _dave_ right away.
|
29
|
+
|
30
|
+
As _matz_ somehow likes his developers he would try to prevent his boss _bill_ from overhearing the conversation or make the complainment management-compatible. Good guy _matz_.
|
31
|
+
|
32
|
+
|
33
|
+
h2. Installation
|
34
|
+
|
35
|
+
<pre>
|
36
|
+
$ sudo gem install onfire
|
37
|
+
</pre>
|
38
|
+
|
39
|
+
|
40
|
+
h2. Usage
|
41
|
+
|
42
|
+
First, you extend your @User@ class to be "on fire".
|
43
|
+
|
44
|
+
<pre>
|
45
|
+
class User < ...
|
46
|
+
include Onfire
|
47
|
+
</pre>
|
48
|
+
|
49
|
+
|
50
|
+
As your @User@ objects don't have a tree structure *you* implement *@#parent@*. That's the *only requirement Onfire has* to the class it's mixed into.
|
51
|
+
|
52
|
+
*@#parent@* would return the boss object of the asked instance.
|
53
|
+
|
54
|
+
<pre>
|
55
|
+
dave.parent # => matz
|
56
|
+
matz.parent # => bill
|
57
|
+
bill.parent # => nil
|
58
|
+
</pre>
|
59
|
+
|
60
|
+
There's your hierarchical tree structure.
|
61
|
+
|
62
|
+
h2. Fireing events
|
63
|
+
|
64
|
+
Now _dave_ issues the bad circumstances in his office:
|
65
|
+
|
66
|
+
<pre>
|
67
|
+
dave.fire :thatSucks
|
68
|
+
</pre>
|
69
|
+
|
70
|
+
So far, nothing would happen as no one in the startup is observing that event.
|
71
|
+
|
72
|
+
h2. Responding to events
|
73
|
+
|
74
|
+
Anyway, a real CEO should respond to complainments from his subordinates.
|
75
|
+
|
76
|
+
<pre>
|
77
|
+
bill.on :thatSucks do puts "who's that?" end
|
78
|
+
</pre>
|
79
|
+
|
80
|
+
Now _bill_ would at least find out somebody's crying.
|
81
|
+
|
82
|
+
<pre>
|
83
|
+
> dave.fire :thatSucks
|
84
|
+
=> "who's that?" # by bill
|
85
|
+
</pre>
|
86
|
+
|
87
|
+
That's right, the *Onfire API* is just the two public methods
|
88
|
+
* *@#on@* for responding to events and
|
89
|
+
* *@#fire@* for triggering those
|
90
|
+
|
91
|
+
|
92
|
+
h2. Bubbling events
|
93
|
+
|
94
|
+
_matz_ being a good manager wants to mediate, so he takes part in the game:
|
95
|
+
|
96
|
+
<pre>
|
97
|
+
matz.on :thatSucks do puts "dave, sshhht!" end
|
98
|
+
</pre>
|
99
|
+
|
100
|
+
Which results in
|
101
|
+
|
102
|
+
<pre>
|
103
|
+
> dave.fire :thatSucks
|
104
|
+
=> "dave, sshhht!" # by matz
|
105
|
+
=> "who's that?" # by bill
|
106
|
+
</pre>
|
107
|
+
|
108
|
+
|
109
|
+
h2. Using the @Event@ object
|
110
|
+
|
111
|
+
Of course _bill_ wants to find out who's the subversive element, so he just askes the revealing *Event* object.
|
112
|
+
|
113
|
+
<pre>
|
114
|
+
bill.on :thatSucks do |event| event.source.fire! end
|
115
|
+
</pre>
|
116
|
+
|
117
|
+
That's bad for _dave_, as he's unemployed now.
|
118
|
+
|
119
|
+
|
120
|
+
h2. Intercepting events
|
121
|
+
|
122
|
+
As _dave_ has always been on time, _matz_ just swallows any offending messages for now.
|
123
|
+
|
124
|
+
<pre>
|
125
|
+
matz.on :thatSucks do |event| event.stop! end
|
126
|
+
</pre>
|
127
|
+
|
128
|
+
That leads to an event that's stopped at _matz_. It won't propagate further up to the big boss.
|
129
|
+
|
130
|
+
<pre>
|
131
|
+
> dave.fire :thatSucks
|
132
|
+
=> "dave, sshhht!" # first, by matz
|
133
|
+
=> nil # second, matz stops the event.
|
134
|
+
</pre>
|
135
|
+
|
136
|
+
h2. Organic event filtering
|
137
|
+
|
138
|
+
What happens if _mike_ wants to be a good manager, too?
|
139
|
+
|
140
|
+
<pre>
|
141
|
+
mike.on :thatSucks do puts "take it easy, dude!" end
|
142
|
+
</pre>
|
143
|
+
|
144
|
+
When _dave_ starts to cry, there's no _mike_ involved:
|
145
|
+
|
146
|
+
<pre>
|
147
|
+
> dave.fire :thatSucks
|
148
|
+
=> "dave, sshhht!" # by matz
|
149
|
+
=> nil
|
150
|
+
</pre>
|
151
|
+
|
152
|
+
Obviously, the @:thatSucks@ event triggered by _dave_ never passes _mike_ as he is on a completely different tree branch. The event travels from _dave_ to _matz_ up to _bill_.
|
153
|
+
|
154
|
+
That is dead simple, however it is a clear way to observe only *particular events*. When _mike_ calls @#on@ he limits his attention to events from his branch - his developers - only.
|
155
|
+
|
156
|
+
h2. Event source filtering
|
157
|
+
|
158
|
+
After a while discontent moves over to _didi_.
|
159
|
+
|
160
|
+
<pre>
|
161
|
+
> didi.fire :thatSucks
|
162
|
+
=> "dave, sshhht!" # first, by matz
|
163
|
+
=> nil
|
164
|
+
</pre>
|
165
|
+
|
166
|
+
_didi_ is a lamer and _matz_ always prefered working with _dave_ so he changes his tune.
|
167
|
+
|
168
|
+
<pre>
|
169
|
+
matz.on :thatSucks do |event| event.stop! if event.source == dave end
|
170
|
+
</pre>
|
171
|
+
|
172
|
+
That's unfair!
|
173
|
+
|
174
|
+
<pre>
|
175
|
+
> dave.fire :thatSucks
|
176
|
+
=> "dave, sshhht!" # by matz
|
177
|
+
=> nil # dave still got a job.
|
178
|
+
> didi.fire :thatSucks
|
179
|
+
=> "fired!" # didi's event travels up to boss who fires him.
|
180
|
+
</pre>
|
181
|
+
|
182
|
+
_matz_ is lazy, so he explicity lets Onfire handle the filtering:
|
183
|
+
|
184
|
+
<pre>
|
185
|
+
matz.on :thatSucks, :from => dave do |event| event.stop! end
|
186
|
+
</pre>
|
187
|
+
|
188
|
+
which will result in the same bad outcome for _didi_.
|
189
|
+
|
190
|
+
h2. Responding with instance methods
|
191
|
+
|
192
|
+
Nevertheless _matz_ is trying to keep himself clean, so he refactors the handler block to an instance method.
|
193
|
+
|
194
|
+
<pre>
|
195
|
+
matz.instance_eval do
|
196
|
+
def shield_dave(event)
|
197
|
+
event.stop!
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
matz.on :thatSucks, :from => dave, :do => :shield_dave
|
202
|
+
</pre>
|
203
|
+
|
204
|
+
Awesome shit!
|
205
|
+
|
206
|
+
h2. Who's using it?
|
207
|
+
* Right now, Onfire is used as clean, small event engine in "Apotomo":http://github.com/apotonick/apotomo, that's stateful widgets for Ruby and Rails.
|
208
|
+
|
209
|
+
h2. License
|
210
|
+
|
211
|
+
Copyright (c) 2010, Nick Sutterer
|
212
|
+
|
213
|
+
The MIT License
|
214
|
+
|
215
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
216
|
+
of this software and associated documentation files (the "Software"), to deal
|
217
|
+
in the Software without restriction, including without limitation the rights
|
218
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
219
|
+
copies of the Software, and to permit persons to whom the Software is
|
220
|
+
furnished to do so, subject to the following conditions:
|
221
|
+
|
222
|
+
The above copyright notice and this permission notice shall be included in
|
223
|
+
all copies or substantial portions of the Software.
|
224
|
+
|
225
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
226
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
227
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
228
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
229
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
230
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
231
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require File.join(File.dirname(__FILE__), 'lib', 'onfire', 'version')
|
6
|
+
|
7
|
+
desc 'Default: run unit tests.'
|
8
|
+
task :default => :test
|
9
|
+
|
10
|
+
desc 'Test the onfire library.'
|
11
|
+
Rake::TestTask.new(:test) do |test|
|
12
|
+
test.libs << ['lib', 'test']
|
13
|
+
test.pattern = 'test/*_test.rb'
|
14
|
+
test.verbose = true
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
# Gem managment tasks.
|
19
|
+
#
|
20
|
+
# == Bump gem version (any):
|
21
|
+
#
|
22
|
+
# rake version:bump:major
|
23
|
+
# rake version:bump:minor
|
24
|
+
# rake version:bump:patch
|
25
|
+
#
|
26
|
+
# == Generate gemspec, build & install locally:
|
27
|
+
#
|
28
|
+
# rake gemspec
|
29
|
+
# rake build
|
30
|
+
# sudo rake install
|
31
|
+
#
|
32
|
+
# == Git tag & push to origin/master
|
33
|
+
#
|
34
|
+
# rake release
|
35
|
+
#
|
36
|
+
# == Release to Gemcutter.org:
|
37
|
+
#
|
38
|
+
# rake gemcutter:release
|
39
|
+
#
|
40
|
+
begin
|
41
|
+
gem 'jeweler'
|
42
|
+
require 'jeweler'
|
43
|
+
|
44
|
+
Jeweler::Tasks.new do |spec|
|
45
|
+
spec.name = "onfire"
|
46
|
+
spec.version = ::Onfire::VERSION
|
47
|
+
spec.summary = %{Have bubbling events and observers in all your Ruby objects.}
|
48
|
+
spec.description = spec.summary
|
49
|
+
spec.homepage = "http://github.com/apotonick/onfire"
|
50
|
+
spec.authors = ["Nick Sutterer"]
|
51
|
+
spec.email = "apotonick@gmail.com"
|
52
|
+
|
53
|
+
spec.files = FileList["[A-Z]*", File.join(*%w[{lib,test} ** *]).to_s]
|
54
|
+
|
55
|
+
# spec.add_dependency 'activesupport', '>= 2.3.0' # Dependencies and minimum versions?
|
56
|
+
end
|
57
|
+
|
58
|
+
Jeweler::GemcutterTasks.new
|
59
|
+
rescue LoadError
|
60
|
+
puts "Jeweler - or one of its dependencies - is not available. " <<
|
61
|
+
"Install it with: sudo gem install jeweler -s http://gemcutter.org"
|
62
|
+
end
|
data/lib/onfire/event.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Onfire
|
2
|
+
# An Event is born in #fire and is passed up the ancestor chain of the triggering datastructure.
|
3
|
+
# It carries a <tt>type</tt>, the fireing widget <tt>source</tt> and arbitrary payload <tt>data</tt>.
|
4
|
+
class Event
|
5
|
+
|
6
|
+
attr_accessor :type, :source, :data
|
7
|
+
|
8
|
+
def initialize(type=nil, source=nil, data=nil)
|
9
|
+
@type = type
|
10
|
+
@source = source
|
11
|
+
@data = data
|
12
|
+
end
|
13
|
+
|
14
|
+
def stopped?
|
15
|
+
@stopped ||= false
|
16
|
+
end
|
17
|
+
|
18
|
+
# Stop event bubbling.
|
19
|
+
def stop!
|
20
|
+
@stopped = true
|
21
|
+
end
|
22
|
+
|
23
|
+
### FIXME: what about serialization? should we simply forget the source?
|
24
|
+
def _dump(depth)
|
25
|
+
""
|
26
|
+
end
|
27
|
+
def self._load(str)
|
28
|
+
::Onfire::Event.new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Onfire
|
2
|
+
# Keeps all event handlers attached to one object.
|
3
|
+
class EventTable
|
4
|
+
attr_accessor :source2evt
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@source2evt = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_handler(handler, opts)
|
11
|
+
event_type = opts[:event_type]
|
12
|
+
source_name = opts[:source_name] || nil
|
13
|
+
|
14
|
+
handlers_for(event_type, source_name) << handler
|
15
|
+
end
|
16
|
+
|
17
|
+
def handlers_for(event_type, source_name=nil)
|
18
|
+
evt_types = source2evt[source_name] ||= {}
|
19
|
+
evt_types[event_type] ||= []
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns all handlers, with :from first, then catch-all.
|
23
|
+
def all_handlers_for(event_type, source_name)
|
24
|
+
handlers_for(event_type, source_name) + handlers_for(event_type, nil)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/onfire.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'onfire/event'
|
2
|
+
require 'onfire/event_table'
|
3
|
+
|
4
|
+
module Onfire
|
5
|
+
def on(event_type, options={}, &block)
|
6
|
+
table_options = {}
|
7
|
+
table_options[:event_type] = event_type
|
8
|
+
table_options[:source_name] = options[:from] if options[:from]
|
9
|
+
|
10
|
+
if block_given?
|
11
|
+
return attach_event_handler(block, table_options)
|
12
|
+
end
|
13
|
+
|
14
|
+
attach_event_handler(options[:do], table_options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def fire(event_type)
|
18
|
+
bubble_event Event.new(event_type, self)
|
19
|
+
end
|
20
|
+
|
21
|
+
def bubble_event(event)
|
22
|
+
process_event(event) # locally process event, then climb up.
|
23
|
+
return if root?
|
24
|
+
|
25
|
+
parent.bubble_event(event)
|
26
|
+
end
|
27
|
+
|
28
|
+
def process_event(event)
|
29
|
+
local_event_handlers(event).each do |proc|
|
30
|
+
return if event.stopped?
|
31
|
+
proc.call(event)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def root?
|
36
|
+
!parent
|
37
|
+
end
|
38
|
+
|
39
|
+
def event_table
|
40
|
+
@event_table ||= Onfire::EventTable.new
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
def attach_event_handler(proc, table_options)
|
45
|
+
event_table.add_handler(proc, table_options)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Get all handlers from self for the passed event.
|
49
|
+
def local_event_handlers(event)
|
50
|
+
event_table.all_handlers_for(event.type, event.source)
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class EventTableTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context "An EventTable" do
|
6
|
+
setup do
|
7
|
+
@head = Onfire::EventTable.new
|
8
|
+
end
|
9
|
+
|
10
|
+
should "return an empty array when it can't find handlers" do
|
11
|
+
@head.add_handler :drink, :source_name => :stomach, :event_type => :thirsty
|
12
|
+
|
13
|
+
assert_equal [], @head.handlers_for( :hungry, :stomach)
|
14
|
+
assert_equal [], @head.all_handlers_for(:hungry, :stomach)
|
15
|
+
end
|
16
|
+
|
17
|
+
should "return handlers in the same order as they were added" do
|
18
|
+
@head.add_handler :drink, :source_name => :stomach, :event_type => :hungry
|
19
|
+
@head.add_handler :eat, :source_name => :stomach, :event_type => :hungry
|
20
|
+
@head.add_handler :sip, :source_name => :mouth, :event_type => :dry
|
21
|
+
@head.add_handler :have_desert, :event_type => :hungry
|
22
|
+
|
23
|
+
assert_equal [:sip], @head.handlers_for(:dry, :mouth)
|
24
|
+
assert_equal [:sip], @head.all_handlers_for(:dry, :mouth)
|
25
|
+
|
26
|
+
assert_equal [:drink, :eat], @head.handlers_for(:hungry, :stomach)
|
27
|
+
assert_equal [:drink, :eat, :have_desert], @head.all_handlers_for(:hungry, :stomach)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/test/event_test.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class EventTest < Test::Unit::TestCase
|
4
|
+
context "An event" do
|
5
|
+
should "accept its type and source in the constructor" do
|
6
|
+
event = Onfire::Event.new(:click, :source)
|
7
|
+
|
8
|
+
assert_equal :click, event.type
|
9
|
+
assert_equal :source, event.source
|
10
|
+
end
|
11
|
+
|
12
|
+
should "be fine without any parameters at all" do
|
13
|
+
event = Onfire::Event.new
|
14
|
+
|
15
|
+
assert_nil event.type
|
16
|
+
assert_nil event.source
|
17
|
+
assert_nil event.data
|
18
|
+
end
|
19
|
+
|
20
|
+
should "stop if needed" do
|
21
|
+
event = Onfire::Event.new
|
22
|
+
|
23
|
+
assert ! event.stopped?
|
24
|
+
event.stop!
|
25
|
+
assert event.stopped?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/test/onfire_test.rb
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class OnfireTest < Test::Unit::TestCase
|
4
|
+
def mock(name='my_mock')
|
5
|
+
obj = Class.new do
|
6
|
+
attr_accessor :list
|
7
|
+
attr_accessor :name
|
8
|
+
attr_accessor :parent
|
9
|
+
|
10
|
+
include Onfire
|
11
|
+
|
12
|
+
def initialize(name)
|
13
|
+
@name = name
|
14
|
+
@list = []
|
15
|
+
end
|
16
|
+
end.new(name)
|
17
|
+
end
|
18
|
+
|
19
|
+
context "including Onfire" do
|
20
|
+
should "provide event_table accessors to an emtpy table" do
|
21
|
+
table = mock.event_table
|
22
|
+
assert_kind_of Onfire::EventTable, table
|
23
|
+
assert_equal 0, table.size
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "#process_event" do
|
28
|
+
setup do
|
29
|
+
@event = Onfire::Event.new(:click, mock)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
should "not fill #list as there are no event handlers attached" do
|
34
|
+
obj = mock
|
35
|
+
obj.process_event(@event)
|
36
|
+
|
37
|
+
assert_equal [], obj.list
|
38
|
+
end
|
39
|
+
|
40
|
+
should "invoke exactly one proc and thus push `1` onto #list" do
|
41
|
+
obj = mock
|
42
|
+
obj.event_table.add_handler(lambda { obj.list << 1 }, :event_type => :click)
|
43
|
+
|
44
|
+
obj.process_event(@event)
|
45
|
+
|
46
|
+
assert_equal [1], obj.list
|
47
|
+
end
|
48
|
+
|
49
|
+
should "not invoke procs for another event_type" do
|
50
|
+
obj = mock
|
51
|
+
obj.event_table.add_handler(lambda{obj.list << 1}, :event_type => :click)
|
52
|
+
obj.event_table.add_handler(lambda{obj.list << 2}, :event_type => :drop) # don't call me!
|
53
|
+
|
54
|
+
obj.process_event(@event)
|
55
|
+
|
56
|
+
assert_equal [1], obj.list
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "calling #on" do
|
61
|
+
setup do
|
62
|
+
@obj = mock
|
63
|
+
@event = Onfire::Event.new(:click, @obj)
|
64
|
+
end
|
65
|
+
|
66
|
+
context "with a block" do
|
67
|
+
should "add a handler to the event_table when called with a block" do
|
68
|
+
@obj.on :click do
|
69
|
+
@obj.list << 1
|
70
|
+
end
|
71
|
+
|
72
|
+
@obj.process_event(@event)
|
73
|
+
assert_equal [1], @obj.list
|
74
|
+
end
|
75
|
+
|
76
|
+
should "invoke two handlers if called twice" do
|
77
|
+
@obj.on :click do @obj.list << 1 end
|
78
|
+
@obj.on :click do @obj.list << 2 end
|
79
|
+
|
80
|
+
@obj.process_event(@event)
|
81
|
+
assert_equal [1,2], @obj.list
|
82
|
+
end
|
83
|
+
|
84
|
+
should "receive the triggering event as parameter" do
|
85
|
+
@obj.on :click do |evt|
|
86
|
+
@obj.list << evt
|
87
|
+
end
|
88
|
+
|
89
|
+
@obj.process_event(@event)
|
90
|
+
assert_equal [@event], @obj.list
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "In the bar" do
|
96
|
+
setup do
|
97
|
+
@barkeeper = mock('barkeeper')
|
98
|
+
@nice_guest = mock('nice guest')
|
99
|
+
@bad_guest = mock('bad guest')
|
100
|
+
|
101
|
+
@nice_guest.parent = @barkeeper
|
102
|
+
@bad_guest.parent = @barkeeper
|
103
|
+
end
|
104
|
+
|
105
|
+
context "the #on method" do
|
106
|
+
context "with the :from option for filtering" do
|
107
|
+
setup do
|
108
|
+
@barkeeper.on(:order, :from => @nice_guest) {@barkeeper.list << 'be nice'}
|
109
|
+
@barkeeper.on(:order, :from => @bad_guest) {@barkeeper.list << 'ignore'}
|
110
|
+
@barkeeper.on(:order, :from => @bad_guest) {@barkeeper.list << 'throw out'}
|
111
|
+
end
|
112
|
+
|
113
|
+
should "invoke the handler for the nice guest only" do
|
114
|
+
@nice_guest.fire :order
|
115
|
+
assert_equal ['be nice'], @barkeeper.list
|
116
|
+
end
|
117
|
+
|
118
|
+
should "invoke both handlers for the bad guest only" do
|
119
|
+
@bad_guest.fire :order
|
120
|
+
assert_equal ['ignore', 'throw out'], @barkeeper.list
|
121
|
+
end
|
122
|
+
|
123
|
+
should "invoke an additional catch-all handler" do
|
124
|
+
@barkeeper.on(:order) {@barkeeper.list << 'have a drink yourself'}
|
125
|
+
@nice_guest.fire :order
|
126
|
+
assert_equal ['be nice', 'have a drink yourself'], @barkeeper.list
|
127
|
+
end
|
128
|
+
|
129
|
+
should "invoke another handler when :from is nil" do
|
130
|
+
@barkeeper.on(:order, :from => nil) {@barkeeper.list << 'have a drink yourself'}
|
131
|
+
@nice_guest.fire :order
|
132
|
+
assert_equal ['be nice', 'have a drink yourself'], @barkeeper.list
|
133
|
+
end
|
134
|
+
|
135
|
+
should "invoke :from handlers before it processes catch-all handlers" do
|
136
|
+
@barkeeper.on(:order) {@barkeeper.list << 'have a drink yourself'}
|
137
|
+
@barkeeper.on(:order, :from => @nice_guest) {@barkeeper.list << 'bring out toast'}
|
138
|
+
@nice_guest.fire :order
|
139
|
+
assert_equal ['be nice', 'bring out toast', 'have a drink yourself'], @barkeeper.list
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context "with a callable object" do
|
144
|
+
setup do
|
145
|
+
@callable = Class.new.new
|
146
|
+
@callable.instance_eval do
|
147
|
+
def call(event)
|
148
|
+
source = event.source
|
149
|
+
return source.list << 'order from barkeeper' if source.root?
|
150
|
+
source.parent.list << 'order from guest'
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
should "add a handler to the local event_table" do
|
156
|
+
@barkeeper.on :order, :do => @callable
|
157
|
+
|
158
|
+
@barkeeper.fire :order
|
159
|
+
assert_equal ['order from barkeeper'], @barkeeper.list
|
160
|
+
|
161
|
+
@nice_guest.fire :order
|
162
|
+
assert_equal ['order from barkeeper', 'order from guest'], @barkeeper.list
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
context "stopping events" do
|
169
|
+
should "not invoke any handler after the guest kills it" do
|
170
|
+
@nice_guest.on(:order) {@nice_guest.list << 'thirsty?'}
|
171
|
+
@nice_guest.on(:order) do |evt|
|
172
|
+
@nice_guest.list << 'money?'
|
173
|
+
evt.stop!
|
174
|
+
end
|
175
|
+
@barkeeper.on(:order) {@nice_guest.list << 'draw a beer'}
|
176
|
+
@nice_guest.fire :order
|
177
|
+
|
178
|
+
assert_equal ['thirsty?', 'money?'], @nice_guest.list
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
context "#event_table" do
|
184
|
+
should "expose the EventTable to the public" do
|
185
|
+
assert_kind_of ::Onfire::EventTable, @barkeeper.event_table
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
context "calling #fire" do
|
192
|
+
setup do
|
193
|
+
@obj = mock
|
194
|
+
end
|
195
|
+
|
196
|
+
should "be of no relevance when there are no handlers attached" do
|
197
|
+
@obj.fire :click
|
198
|
+
|
199
|
+
assert_equal [], @obj.list
|
200
|
+
end
|
201
|
+
|
202
|
+
should "invoke the attached matching handler" do
|
203
|
+
@obj.on :click do @obj.list << 1 end
|
204
|
+
@obj.fire :click
|
205
|
+
|
206
|
+
assert_equal [1], @obj.list
|
207
|
+
end
|
208
|
+
|
209
|
+
should "not invoke same handlers for :symbol or 'string' event names" do
|
210
|
+
@obj.on :click do @obj.list << 1 end
|
211
|
+
@obj.fire 'click'
|
212
|
+
|
213
|
+
assert_equal [], @obj.list
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
should "invoke handlers in the correct order when bubbling" do
|
218
|
+
# we use @obj for recording the chat.
|
219
|
+
bar = mock('bar')
|
220
|
+
guest = mock('guest')
|
221
|
+
|
222
|
+
guest.parent = bar
|
223
|
+
guest.on :thirsty do
|
224
|
+
@obj.list << 'A beer!'
|
225
|
+
guest.fire :order
|
226
|
+
end
|
227
|
+
guest.on :thirsty do
|
228
|
+
@obj.list << 'Hurry up, man!'
|
229
|
+
end
|
230
|
+
bar.on :thirsty do
|
231
|
+
@obj.list << 'Thanks.'
|
232
|
+
end
|
233
|
+
bar.on :order do
|
234
|
+
@obj.list << 'There you go.'
|
235
|
+
end
|
236
|
+
|
237
|
+
guest.fire :thirsty
|
238
|
+
|
239
|
+
assert_equal ['A beer!', 'There you go.', 'Hurry up, man!', 'Thanks.'], @obj.list
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|
243
|
+
|
244
|
+
|
245
|
+
context "#root?" do
|
246
|
+
setup do
|
247
|
+
@obj = mock
|
248
|
+
end
|
249
|
+
|
250
|
+
should "return false if we got parents" do
|
251
|
+
@obj.parent = :daddy
|
252
|
+
assert !@obj.root?
|
253
|
+
end
|
254
|
+
|
255
|
+
should "return true if we're at the top" do
|
256
|
+
assert @obj.root?
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: onfire
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nick Sutterer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-03-19 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Have bubbling events and observers in all your Ruby objects.
|
17
|
+
email: apotonick@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.textile
|
24
|
+
files:
|
25
|
+
- README.textile
|
26
|
+
- Rakefile
|
27
|
+
- lib/onfire.rb
|
28
|
+
- lib/onfire/event.rb
|
29
|
+
- lib/onfire/event_table.rb
|
30
|
+
- lib/onfire/version.rb
|
31
|
+
- test/event_table_test.rb
|
32
|
+
- test/event_test.rb
|
33
|
+
- test/onfire_test.rb
|
34
|
+
- test/test_helper.rb
|
35
|
+
has_rdoc: true
|
36
|
+
homepage: http://github.com/apotonick/onfire
|
37
|
+
licenses: []
|
38
|
+
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options:
|
41
|
+
- --charset=UTF-8
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "0"
|
49
|
+
version:
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
version:
|
56
|
+
requirements: []
|
57
|
+
|
58
|
+
rubyforge_project:
|
59
|
+
rubygems_version: 1.3.5
|
60
|
+
signing_key:
|
61
|
+
specification_version: 3
|
62
|
+
summary: Have bubbling events and observers in all your Ruby objects.
|
63
|
+
test_files:
|
64
|
+
- test/onfire_test.rb
|
65
|
+
- test/test_helper.rb
|
66
|
+
- test/event_table_test.rb
|
67
|
+
- test/event_test.rb
|