seh 0.1.0 → 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.
@@ -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