seh 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ module Seh
2
+ # EventBindDisconnector should not be used directly. Event#bind and Event#bind_once return instances of EventBindDisconnector.
3
+ class EventBindDisconnector
4
+ def initialize disconnect_proc
5
+ @disconnect_proc = disconnect_proc
6
+ end
7
+
8
+ # Disconnect this bind from its EventTarget
9
+ def disconnect
10
+ unless @disconnect_proc.nil?
11
+ @disconnect_proc.call
12
+ @disconnect_proc = nil
13
+ end
14
+ nil
15
+ end
16
+ end
17
+ end
@@ -1,36 +1,44 @@
1
+ require_relative 'event_bind'
2
+ require_relative 'event_bind_disconnector'
3
+
1
4
  module Seh
2
5
  module EventTarget
3
- def bind( event_type=nil, &block )
4
- raise "EventTarget::bind expects a block" unless block_given?
6
+ def bind event_type=nil, &block
7
+ raise "expected a block" unless block_given?
5
8
  @_binds ||= []
6
- bind = Private::EventBind.new( event_type, &block )
9
+ bind = EventBind.new event_type, &block
7
10
  @_binds << bind
8
- ->{ @_binds.delete bind }
11
+ EventBindDisconnector.new ->{ @_binds.delete bind; nil }
9
12
  end
10
13
 
11
- def bind_once( event_type=nil, &block )
14
+ def bind_once event_type=nil, &block
12
15
  return unless block_given?
13
- disconnect = self.bind(event_type) { |event| disconnect.call; block.call event }
14
- disconnect
16
+ bind_disconnector = self.bind(event_type) { |event| bind_disconnector.disconnect; block.call event }
17
+ end
18
+
19
+ # @private
20
+ # Internal use only. Used in Seh::Event.
21
+ # Override #observers as needed.
22
+ # @return [EventTarget] - array of other EventTargets observing this EventTarget
23
+ def observers
24
+ []
15
25
  end
16
26
 
17
- def each_bind
27
+ # @private
28
+ # Internal use only. Used in Seh::Event.
29
+ # For each bind matching the passed event_types, yield the bind's callback
30
+ # @param event_types - Array of event types
31
+ # @yield each bind callback matching the passed event types
32
+ # @return nil
33
+ def each_matching_callback event_types
18
34
  @_binds ||= []
19
- @_binds.dup.each { |b| yield b }
35
+ @_binds.each { |bind| yield bind.block if bind.event_type.match event_types }
20
36
  nil
21
37
  end
22
- end
23
38
 
24
- # @private
25
- module Private
26
- class EventBind
27
- attr_reader :event_type, :block
28
-
29
- def initialize( event_type, &block )
30
- event_type = EventType.new event_type unless event_type.is_a? EventType
31
- @event_type = event_type
32
- @block = block
33
- end
34
- end # EventBind
35
- end # Private
39
+ # An empty class that includes EventTarget
40
+ class Default
41
+ include EventTarget
42
+ end
43
+ end
36
44
  end
@@ -1,56 +1,46 @@
1
1
  module Seh
2
2
  class EventType
3
- attr_reader :type
4
-
5
- def initialize( type )
3
+ def initialize type
6
4
  @type = type
7
5
  end
8
6
 
9
- def match(types)
10
- types = [types] unless types.respond_to? :each
11
- _match types
12
- end
13
-
14
- private
15
- def _match(types)
16
- return true if not self.type or types.include? self.type
17
- false
7
+ # @param types - an Array of types
8
+ def match types
9
+ return true if @type.nil? # define nil to match everything
10
+ types.include? @type
18
11
  end
19
12
 
20
13
  class And < EventType
21
- def initialize(*types)
14
+ def initialize *types
22
15
  @types = []
23
16
  types.each { |t| t = EventType.new t unless t.kind_of? EventType ; @types << t }
24
17
  end
25
18
 
26
- private
27
- def _match(types)
19
+ def match types
28
20
  @types.each { |t| return false unless t.match types }
29
21
  true
30
22
  end
31
23
  end # And
32
24
 
33
25
  class Or < EventType
34
- def initialize(*types)
26
+ def initialize *types
35
27
  @types = []
36
28
  types.each { |t| t = EventType.new t unless t.kind_of? EventType ; @types << t }
37
29
  end
38
30
 
39
- private
40
- def _match(types)
31
+ def match types
41
32
  @types.each { |t| return true if t.match types }
42
33
  false
43
34
  end
44
35
  end # Or
45
36
 
46
37
  class Not < EventType
47
- def initialize(type)
38
+ def initialize type
48
39
  type = EventType.new type unless type.kind_of? EventType
49
40
  @type = type
50
41
  end
51
42
 
52
- private
53
- def _match(types)
43
+ def match types
54
44
  ! @type.match types
55
45
  end
56
46
  end # Not
@@ -1,4 +1,4 @@
1
1
  module Seh
2
2
  # @private
3
- VERSION = "0.1.0"
3
+ VERSION = "0.3.0"
4
4
  end
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
8
8
  s.authors = ["Ryan Berckmans"]
9
9
  s.email = ["ryan.berckmans@gmail.com"]
10
10
  s.homepage = "https://github.com/ryanberckmans/seh"
11
- s.summary = "Structured event handler. Pure ruby event handling similar to w3c dom events; alpha wip."
12
- s.description = "#{s.summary} Lots of bells and whistles to support complex event handling as required by stuff like video games. Event handling in a synchronous specific order. Events 'bubble', and event targets can have multiple parents and common ancestors. Staged event callbacks: event.before { 'the united states of' }; event.after { 'america' }. Staged callbacks allow an ancestor to influence affect of event on a descendant: ancestor.before { |event| event.damage *= 2 }; descendant.after { |event| player.health -= event.damage }. Events use 'tag-style' types: event.type :hostile ; event.type :spell. Handle only events which pass a filter: player.bind( Seh::and :hostile, Seh::not( :spell ) ) { |event| 'Hostile non-spell!!' }. Optional event failure: event.fail; event.success { 'yay!' }; event.failure { 'oops!' }. Event inherits from OpenStruct for dynamic properties: event.omgs = 'omgs a dynamic attribute'"
11
+ s.summary = "Structured event handler. Pure ruby event handling similar to w3c dom events."
12
+ s.description = "#{s.summary} Synchronous, local event handling for complex emergent behaviors as required by something like a game engine. v0.3.0 improves a lot on v0.1.0."
13
13
 
14
14
  s.rubyforge_project = "seh"
15
15
 
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+ require 'seh/event_bind_disconnector'
3
+
4
+ module Seh
5
+ describe EventBindDisconnector do
6
+ before :each do @proc = ->{ "nice!" } end
7
+ subject { EventBindDisconnector.new @proc }
8
+
9
+ it "calls proc on disconnect" do
10
+ @proc.should_receive(:call).once
11
+ subject.disconnect
12
+ end
13
+
14
+ its(:disconnect) { should be_nil }
15
+
16
+ it "sets the proc reference to nil on disconnect, so the proc is only ever called once" do
17
+ @proc.should_receive(:call).once
18
+ subject.disconnect
19
+ subject.disconnect
20
+ subject.disconnect
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+ require 'seh/event_bind'
3
+ require 'seh/event_type'
4
+
5
+ module Seh
6
+ describe EventBind do
7
+ before :each do
8
+ @event_type = EventType.new :some_type
9
+ @block = ->{ callback! }
10
+ end
11
+ subject { EventBind.new @event_type, &@block }
12
+ its(:block) { should eq(@block) }
13
+ its(:event_type) { should eq(@event_type) }
14
+
15
+ it "wraps a non-EventType in an EventType" do
16
+ type = :not_an_EventType
17
+ EventType.should_receive(:new).with(type).once.and_call_original
18
+ bind = EventBind.new type, &@block
19
+ bind.event_type.should be_a(EventType)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,209 @@
1
+ require 'spec_helper'
2
+
3
+ require 'seh/event'
4
+ require 'seh/event_target'
5
+
6
+ module Seh
7
+ class ObservableEventTarget
8
+ include EventTarget
9
+ def initialize
10
+ @observers = []
11
+ end
12
+ attr_accessor :observers
13
+ end
14
+
15
+ describe Event do
16
+ it "adds a few targets" do
17
+ target1 = EventTarget::Default.new
18
+ target2 = EventTarget::Default.new
19
+ expect { subject.target target1, target2 }.to change{ subject.send :collect_targets }.from(Set.new).to(Set.new << target1 << target2)
20
+ end
21
+
22
+ it "dispatch runs target and then stage callbacks" do
23
+ subject.should_receive(:run_target_callbacks).once.ordered
24
+ subject.should_receive(:run_stage_callbacks).once.ordered
25
+ subject.dispatch
26
+ end
27
+
28
+ it "dispatch may not be run twice" do
29
+ subject.dispatch
30
+ expect { subject.dispatch }.to raise_error
31
+ end
32
+
33
+ context "testing abort" do
34
+ it "does nothing when #abort is called before #dispatch" do
35
+ subject.abort
36
+ subject.should_receive(:run_target_callbacks).never
37
+ subject.should_receive(:run_stage_callbacks).never
38
+ subject.dispatch
39
+ end
40
+
41
+ it "visits all targets when #abort is called during a target callback" do
42
+ type = :foo
43
+
44
+ target1 = ObservableEventTarget.new
45
+ target2 = EventTarget::Default.new
46
+ target3 = EventTarget::Default.new
47
+ target1.observers << target3
48
+
49
+ target1.bind type do |event| event.abort end
50
+
51
+ @result = 0
52
+
53
+ target3.bind type do |event| @result += 2 end
54
+ target2.bind type do |event| @result += 1 end
55
+
56
+ subject.type type
57
+ subject.target target1, target2
58
+ subject.dispatch
59
+ @result.should eq(3)
60
+ end
61
+
62
+ it "finishes the start stage and then stops when #abort is called during a start callback" do
63
+ @result = 0
64
+ subject.start do subject.abort end
65
+ subject.start do @result += 1 end
66
+ subject.start do @result += 2 end
67
+ subject.add_stage :never_happens
68
+ subject.bind :never_happens do @result += 5 end
69
+ subject.dispatch
70
+ @result.should eq(3)
71
+ end
72
+
73
+ it "finishes the current stage and then stops when #abort is called during a stage callback" do
74
+ @result = 0
75
+ subject.add_stage :does_happens
76
+ subject.add_stage :never_happens
77
+ subject.bind :does_happens do @result += 1; subject.abort end
78
+ subject.bind :does_happens do @result += 2 end
79
+ subject.bind :never_happens do @result += 5 end
80
+ subject.dispatch
81
+ @result.should eq(3)
82
+ end
83
+
84
+ it "doesn't run finish callbacks when #abort is called during a stage callback" do
85
+ @result = 0
86
+ subject.add_stage :does_happens
87
+ subject.bind :does_happens do @result += 1; subject.abort end
88
+ subject.finish do @result += 5 end
89
+ subject.dispatch
90
+ @result.should eq(1)
91
+ end
92
+ end
93
+
94
+ context "testing callbacks" do
95
+ before :each do
96
+ @counter = 0
97
+ @callback = ->event{
98
+ @counter += 1
99
+ }
100
+ end
101
+
102
+ context "testing stage callbacks" do
103
+ it "runs a start callback" do
104
+ expect { subject.start &@callback}.to change{ subject.send :run_stage_callbacks; @counter }.from(0).to(1)
105
+ end
106
+ it "runs a finish callback" do
107
+ expect { subject.finish &@callback}.to change{ subject.send :run_stage_callbacks; @counter }.from(0).to(1)
108
+ end
109
+ it "runs a stage callback" do
110
+ stage = :foo
111
+ subject.add_stage stage
112
+ expect { subject.bind stage, &@callback}.to change{ subject.send :run_stage_callbacks; @counter }.from(0).to(1)
113
+ end
114
+ it "runs a stage whose stage_decision_block evaluates to true" do
115
+ @result = 0
116
+ stage = :should_be_run
117
+ subject.add_stage stage do true end
118
+ subject.bind stage do @result += 1 end
119
+ subject.bind stage do @result += 2 end
120
+ subject.dispatch
121
+ @result.should eq(3)
122
+ end
123
+ it "skips a stage whose stage_decision_block evaluates to false" do
124
+ @result = 0
125
+ stage = :should_not_be_run
126
+ subject.add_stage stage do false end
127
+ subject.bind stage do @result += 1 end
128
+ subject.bind stage do @result += 2 end
129
+ subject.dispatch
130
+ @result.should eq(0)
131
+ end
132
+ it "runs start before a stage" do
133
+ result = []
134
+ subject.add_stage :stage2
135
+ subject.bind :stage2 do result << :second end
136
+ subject.start { result << :first }
137
+ subject.send :run_stage_callbacks
138
+ result.should eq([:first,:second])
139
+ end
140
+ it "runs finish after a stage" do
141
+ result = []
142
+ subject.add_stage :stage2
143
+ subject.bind :stage2 do result << :first end
144
+ subject.finish { result << :second }
145
+ subject.send :run_stage_callbacks
146
+ result.should eq([:first,:second])
147
+ end
148
+ it "runs stages the order they were added" do
149
+ result = []
150
+ subject.add_stage :stage2
151
+ subject.add_stage :stage3
152
+ subject.add_stage :stage4
153
+ subject.bind :stage4 do result << :third end
154
+ subject.bind :stage3 do result << :second end
155
+ subject.bind :stage2 do result << :first end
156
+ subject.send :run_stage_callbacks
157
+ result.should eq([:first,:second,:third])
158
+ end
159
+ end
160
+
161
+ context "testing invoking of target callbacks" do
162
+ before :each do
163
+ @target = EventTarget::Default.new
164
+ @type = :some_type
165
+ subject.target @target
166
+ subject.type @type
167
+ end
168
+
169
+ it "calls a target callback" do
170
+ expect { @target.bind @type, &@callback }.to change{ subject.send :run_target_callbacks; @counter}.from(0).to(1)
171
+ end
172
+
173
+ it "passes self to a target callback" do
174
+ @result = nil
175
+ @target.bind @type do |event| @result = event end
176
+ subject.send:run_target_callbacks
177
+ @result.should eq(subject)
178
+ end
179
+ end
180
+ end # testing callbacks
181
+
182
+
183
+ context "with a target containing a DAG of observers" do
184
+ before :each do
185
+ @target1 = ObservableEventTarget.new
186
+ @observer1 = EventTarget::Default.new
187
+ @observer2 = EventTarget::Default.new
188
+ @observer3 = ObservableEventTarget.new
189
+ @observer4 = ObservableEventTarget.new
190
+ @observer5 = EventTarget::Default.new
191
+
192
+ @observer3.observers << @observer2
193
+ @observer3.observers << @observer4
194
+ @observer4.observers << @observer5 << @observer1
195
+
196
+ @target2 = ObservableEventTarget.new
197
+ @observer6 = EventTarget::Default.new
198
+
199
+ @target2.observers << @observer6
200
+
201
+ @target1.observers << @observer1 << @observer2 << @observer3
202
+ end
203
+
204
+ it "collects_targets in a Set which traverses the observer graph" do
205
+ expect { subject.target @target1, @observer5, @target2 }.to change { subject.send :collect_targets}.from(Set.new).to(Set.new << @target1 << @target2 << @observer1 << @observer2 << @observer3 << @observer4 << @observer5 << @observer6 )
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+ require 'seh/event_target'
3
+ require 'seh/event_bind_disconnector'
4
+
5
+ module Seh
6
+ module EventTarget
7
+ describe Default do
8
+ it { subject.should be_a(EventTarget) }
9
+
10
+ its(:observers) { should eq([]) }
11
+
12
+ it { subject.each_matching_callback([]).should be_nil }
13
+
14
+ it "adds a bind" do
15
+ event_type = :some_type
16
+ block = -> { callback! }
17
+ EventBind.should_receive(:new).with(event_type,&block).once.and_call_original
18
+ bind_disconnector = subject.bind event_type, &block
19
+ bind_disconnector.should be_a(EventBindDisconnector)
20
+ end
21
+
22
+ context "with added binds" do
23
+ before :each do
24
+ @event_type1 = :another_type
25
+ @block1 = -> { callback! }
26
+ @bind_disconnector1 = subject.bind @event_type1, &@block1
27
+
28
+ @event_type2 = :second_type
29
+ @block2 = -> { twocallback! }
30
+ @bind_disconnector2 = subject.bind @event_type2, &@block2
31
+
32
+ @event_type3 = :third_type
33
+ @block3 = ->event{ "do nothing" }
34
+ @bind_disconnector3 = subject.bind_once @event_type3, &@block3
35
+ end
36
+
37
+ it "yields the block for a matching bind in each_matching_callback" do
38
+ expected_blocks = []
39
+ subject.each_matching_callback([@event_type1]) { |block| expected_blocks << block }
40
+ expected_blocks.should include(@block1)
41
+ expected_blocks.size.should eq(1)
42
+ end
43
+
44
+ it "yields both blocks for the matching binds in each_matching_callback" do
45
+ expected_blocks = []
46
+ subject.each_matching_callback([@event_type1,@event_type2]) { |block| expected_blocks << block }
47
+ expected_blocks.should include(@block1)
48
+ expected_blocks.should include(@block2)
49
+ expected_blocks.size.should eq(2)
50
+ end
51
+
52
+ it "yields nothing when no binds match the event_types in each_matching_callback" do
53
+ expected_blocks = []
54
+ subject.each_matching_callback([:unknown_type]) { |block| expected_blocks << block }
55
+ expected_blocks.size.should eq(0)
56
+ end
57
+
58
+ it "disconnects bind3 after a single block invocation, since bind3 was connected using bind_once" do
59
+ expected_blocks = []
60
+ subject.each_matching_callback([@event_type3]) { |block| expected_blocks << block }
61
+ expected_blocks.should_not include(@block3) # @block3 would have been wrapped in another block during bind_once, so we don't expect to find it
62
+ expected_blocks.size.should eq(1) # the single block in the array is the new block wrapping @block3
63
+ expected_blocks[0].call
64
+
65
+ # After @block3 was called, we shouldn't expect to find it anymore
66
+ expected_blocks.clear
67
+ expected_blocks.size.should eq(0) # verify clear
68
+ subject.each_matching_callback([@event_type3]) { |block| expected_blocks << block }
69
+ expected_blocks.size.should eq(0)
70
+ end
71
+
72
+ context "with bind1 disconnected" do
73
+ before :each do
74
+ @bind_disconnector1.disconnect
75
+ end
76
+
77
+ it "yields only the second block in each_matching_callback, since the first was disconnected" do
78
+ expected_blocks = []
79
+ subject.each_matching_callback([@event_type1,@event_type2]) { |block| expected_blocks << block }
80
+ expected_blocks.should include(@block2)
81
+ expected_blocks.size.should eq(1)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end