event_state 0.0.1

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/README.rdoc ADDED
@@ -0,0 +1,137 @@
1
+ = event_state
2
+
3
+ * http://github.com/jdleesmiller/event_state
4
+
5
+ == SYNOPSIS
6
+
7
+ A small embedded DSL for implementing stateful protocols in EventMachine using
8
+ finite state machines. The protocol is specified in terms of _states_ and
9
+ _messages_. The processing happens in the states, and the messages that can be
10
+ sent or received from each state are declared by name using the DSL.
11
+
12
+ Here's everyone's favorite example: an echo server. It starts in the
13
+ +:listening+ state, in which it can receive a +Noise+ message. It then
14
+ transitions to the +:speaking+ state. After a short delay (+EM.add_timer+), it
15
+ sends the noise back to the client, which causes it to transition back to the
16
+ listening state.
17
+
18
+ class MessageEchoServer < EventState::ObjectMachine
19
+ Noise = Struct.new(:content)
20
+
21
+ protocol do
22
+ state :listening do
23
+ on_recv Noise, :speaking
24
+ end
25
+
26
+ state :speaking do
27
+ on_send Noise, :listening
28
+
29
+ on_enter do |noise|
30
+ EM.add_timer 0.5 do
31
+ send_message Noise.new(noise.content)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ In a picture (generated from the code above using
39
+ {EventState::Machine.print_state_machine_dot}):
40
+
41
+ http://github.com/jdleesmiller/event_state/raw/master/assets/echo.png
42
+
43
+ Here the start state is indicated by a double circle, a blue arrow is a message
44
+ that can be received, and a red arrow is a message that can be sent.
45
+
46
+ The {EventState::ObjectMachine} base class extends {EventState::Machine}, which
47
+ in turn extends <tt>EventMachine::Connection</tt>. +ObjectMachine+ handles
48
+ serializing and deserializing the ruby objects using
49
+ <tt>EventMachine::ObjectProtocol</tt>, and (by default) it uses the class of the
50
+ message object as the message name. In this example, the message name is
51
+ +Noise+. +Machine+ provides the state machine DSL and the primitives for
52
+ handling arbitrary kinds of messages.
53
+
54
+ Here is the corresponding client and a demo showing how to run it:
55
+
56
+ class MessageEchoClient < EventState::ObjectMachine
57
+ Noise = MessageEchoServer::Noise
58
+
59
+ def initialize noises
60
+ super
61
+ @noises = noises
62
+ end
63
+
64
+ protocol do
65
+ state :speaking do
66
+ on_send Noise, :listening
67
+
68
+ on_enter do
69
+ if @noises.empty?
70
+ EM.stop
71
+ else
72
+ send_message MessageEchoServer::Noise.new(@noises.shift)
73
+ end
74
+ end
75
+ end
76
+
77
+ state :listening do
78
+ on_recv Noise, :speaking
79
+
80
+ on_enter do |noise|
81
+ puts "heard: #{noise.content}"
82
+ end
83
+ end
84
+ end
85
+
86
+ def self.demo
87
+ EM.run do
88
+ EM.start_server('localhost', 14159, MessageEchoServer)
89
+ EM.connect('localhost', 14159, MessageEchoClient, %w(foo bar baz))
90
+ end
91
+ end
92
+ end
93
+
94
+ Output:
95
+ heard: foo
96
+ heard: bar
97
+ heard: baz
98
+
99
+ == INSTALLATION
100
+
101
+ Not yet released; will eventually be installable with:
102
+
103
+ gem install event_state
104
+
105
+ == RELATED PROJECTS
106
+
107
+ This library was inspired by http://slagyr.github.com/statemachine which
108
+ provides a nice DSL for defining state machines but doesn't integrate directly
109
+ with EventMachine. See also the http://www.complang.org/ragel state machine
110
+ compiler and Zed Shaw's http://zedshaw.com/essays/ragel_state_charts.html blog
111
+ post about it.
112
+
113
+ == LICENSE
114
+
115
+ (The MIT License)
116
+
117
+ Copyright (c) 2011 John Lees-Miller
118
+
119
+ Permission is hereby granted, free of charge, to any person obtaining
120
+ a copy of this software and associated documentation files (the
121
+ 'Software'), to deal in the Software without restriction, including
122
+ without limitation the rights to use, copy, modify, merge, publish,
123
+ distribute, sublicense, and/or sell copies of the Software, and to
124
+ permit persons to whom the Software is furnished to do so, subject to
125
+ the following conditions:
126
+
127
+ The above copyright notice and this permission notice shall be
128
+ included in all copies or substantial portions of the Software.
129
+
130
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
131
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
132
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
133
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
134
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
135
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
136
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
137
+
@@ -0,0 +1,467 @@
1
+ module EventState
2
+ #
3
+ # Base class for state machines driven by +EventMachine+.
4
+ #
5
+ # This class provides a domain-specific language (DSL) for declaring the
6
+ # structure of the machine. See the {file:README} for examples and the general
7
+ # idea of how this works.
8
+ #
9
+ # If you are sending ruby objects as messages, see {ObjectMachine}; it handles
10
+ # serialization (using EventMachine's +ObjectProtocol+) and names messages
11
+ # according to their classes (but you can override this).
12
+ #
13
+ # If you have some other kind of messages, then you should subclass this class
14
+ # directly. Two methods are required:
15
+ # 1. Override EventMachine's +receive_data+ method to call
16
+ # {#transition_on_recv} with the received message.
17
+ # 2. Override EventMachine's +send_data+ method to call {#transition_on_send}
18
+ # with the message to be sent.
19
+ # Note that {#transition_on_recv} and {#transition_on_send} take a message
20
+ # _name_ as well as a message. You have to define the mapping from messages
21
+ # to message names so that the message names correspond with the transitions
22
+ # declared using the DSL ({on_send} and {on_recv} in particular).
23
+ #
24
+ class Machine < EventMachine::Connection
25
+ class << self
26
+ #
27
+ # Declare the protocol; pass a block to declare the {state}s.
28
+ #
29
+ # When the block terminates, this method declares any 'implicit' states
30
+ # that have been referenced by {on_send} or {on_recv} but that have not
31
+ # been declared with {state}. It also does some basic sanity checking.
32
+ #
33
+ # There can be multiple protocol blocks declared for one class; it is
34
+ # equivalent to moving all of the definitions to the same block.
35
+ #
36
+ # @yield [] declare the {state}s in the protocol
37
+ #
38
+ # @return [nil]
39
+ #
40
+ def protocol
41
+ raise "cannot nest protocol blocks" if defined?(@protocol) && @protocol
42
+
43
+ @start_state = nil unless defined?(@start_state)
44
+ @states = {} unless defined?(@states)
45
+
46
+ @protocol = true
47
+ @state = nil
48
+ yield
49
+ @protocol = false
50
+
51
+ # add implicitly defined states to @states to avoid having to check for
52
+ # nil states while the machine is running
53
+ explicit_states = Set[*@states.keys]
54
+ all_states = Set[*@states.values.map {|state|
55
+ state.on_sends.values + state.on_recvs.values}.flatten]
56
+ implicit_states = all_states - explicit_states
57
+ implicit_states.each do |state_name|
58
+ @states[state_name] = State.new(state_name)
59
+ end
60
+ end
61
+
62
+ #
63
+ # Exchange sends for receives and receives for sends in the +base+
64
+ # protocol, and clear all of the {on_enter} and {on_exit} handlers. It
65
+ # often happens that a server and a client follow protocols with the same
66
+ # states, but with sends and receives interchanged. This method is
67
+ # intended to help with this case. You can, for example, reverse a server
68
+ # and use the passed block to define new {on_enter} and {on_exit} handlers
69
+ # appropriate for the client.
70
+ #
71
+ # The start state is determined by the first state declared in the given
72
+ # block (not by the protocol being reversed).
73
+ #
74
+ # @param [Class] base
75
+ #
76
+ # @yield [] define {state}s new {on_enter} and {on_exit} handlers
77
+ #
78
+ # @return [nil]
79
+ #
80
+ def reverse_protocol base, &block
81
+ raise "cannot mirror if already have a protocol" if defined?(@protocol)
82
+
83
+ @states = Hash[base.states.map {|state_name, state|
84
+ new_state = EventState::State.new(state_name)
85
+ new_state.on_recvs = state.on_sends.dup
86
+ new_state.on_sends = state.on_recvs.dup
87
+ [state_name, new_state]
88
+ }]
89
+
90
+ protocol(&block)
91
+ end
92
+
93
+ #
94
+ # Declare a state; pass a block to configure the state using {on_enter},
95
+ # {on_send} and so on.
96
+ #
97
+ # By default, the machine's start state is the first state declared using
98
+ # this method.
99
+ #
100
+ # @yield [] configure the state
101
+ #
102
+ # @return [nil]
103
+ #
104
+ def state state_name
105
+ raise "must be called from within a protocol block" unless @protocol
106
+ raise "cannot nest calls to state" if @state
107
+
108
+ # create new state or edit exiting state
109
+ @state = @states[state_name] || State.new(state_name)
110
+
111
+ # configure this state
112
+ yield
113
+
114
+ # need to know the start state
115
+ @start_state = @state unless @start_state
116
+
117
+ # index by name for easy lookup
118
+ @states[@state.name] = @state
119
+
120
+ # ensure that on_enter etc. aren't callable outside of a state block
121
+ @state = nil
122
+ end
123
+
124
+ #
125
+ # Register a block to be called after the machine enters the current
126
+ # {state}.
127
+ #
128
+ # The machine changes state in response to a message being sent or
129
+ # received, and you can register an {on_enter} handler that is specific
130
+ # to the message that caused the change. Or you can render a catch-all
131
+ # block that will be called if no more specific handler was found (see
132
+ # example below).
133
+ #
134
+ # If a catch-all +on_enter+ block is given for the {start_state}, it is
135
+ # called from EventMachine's +post_init+ method. In this case (and only
136
+ # this case), the message passed to the block is +nil+.
137
+ #
138
+ # @example
139
+ # state :foo do
140
+ # on_enter :my_message do |message|
141
+ # # got here due to a :my_message message
142
+ # end
143
+ #
144
+ # on_enter do
145
+ # # got here some other way
146
+ # end
147
+ # end
148
+ #
149
+ # @param [Array<Symbol>] message_names zero or more
150
+ #
151
+ # @yield [message]
152
+ #
153
+ # @yieldparam [Message, nil] message nil iff this is the start state and
154
+ # the machine has just started up (called from +post_init+)
155
+ #
156
+ # @return [nil]
157
+ #
158
+ def on_enter *message_names, &block
159
+ raise "on_enter must be called from a state block" unless @state
160
+ if message_names == []
161
+ raise "on_enter block already given" if @state.default_on_enter
162
+ @state.default_on_enter = block
163
+ else
164
+ save_state_handlers('on_enter', @state.on_enters, message_names,block)
165
+ end
166
+ nil
167
+ end
168
+
169
+ #
170
+ # Register a block to be called after the machine exits the current
171
+ # {state}. See {on_enter} for more information.
172
+ #
173
+ # @param [Array<Symbol>] message_names zero or more
174
+ #
175
+ # @yield [message]
176
+ #
177
+ # @yieldparam [Message] message
178
+ #
179
+ # @return [nil]
180
+ #
181
+ def on_exit *message_names, &block
182
+ raise "on_exit must be called from a state block" unless @state
183
+ if message_names == []
184
+ raise "on_exit block already given" if @state.default_on_exit
185
+ @state.default_on_exit = block
186
+ else
187
+ save_state_handlers('on_exit', @state.on_exits, message_names, block)
188
+ end
189
+ nil
190
+ end
191
+
192
+ #
193
+ # Declare which state the machine transitions to when one of the given
194
+ # messages is sent in this {state}.
195
+ #
196
+ # @example
197
+ # state :foo do
198
+ # on_enter do
199
+ # EM.defer proc { sleep 3 },
200
+ # proc { send_message DoneMessage.new(42) }
201
+ # end
202
+ # on_send DoneMessage, :bar
203
+ # end
204
+ #
205
+ # @param [Array<Symbol>] message_names one or more
206
+ #
207
+ # @param [Symbol] next_state_name
208
+ #
209
+ # @return [nil]
210
+ #
211
+ def on_send *message_names, next_state_name
212
+ raise "on_send must be called from a state block" unless @state
213
+ message_names.flatten.each do |name|
214
+ @state.on_sends[name] = next_state_name
215
+ end
216
+ end
217
+
218
+ #
219
+ # Called when EventMachine calls +unbind+ on the connection while it is in
220
+ # the current state. This may indicate that the connection has been closed
221
+ # or timed out. The default is to take no action.
222
+ #
223
+ # @yield [] called upon +unbind+
224
+ #
225
+ # @return [nil]
226
+ #
227
+ def on_unbind &block
228
+ raise "on_unbind must be called from a state block" unless @state
229
+ @state.on_unbind = block
230
+ nil
231
+ end
232
+
233
+ #
234
+ # Declare which state the machine transitions to when one of the given
235
+ # messages is received in this {state}. See {on_send} for more
236
+ # information.
237
+ #
238
+ # @param [Array<Symbol>] message_names one or more
239
+ #
240
+ # @param [Symbol] next_state_name
241
+ #
242
+ # @return [nil]
243
+ #
244
+ def on_recv *message_names, next_state_name
245
+ raise "on_recv must be called from a state block" unless @state
246
+ message_names.flatten.each do |name|
247
+ @state.on_recvs[name] = next_state_name
248
+ end
249
+ end
250
+
251
+ #
252
+ # @return [Hash<Symbol, State>] map from state names (ruby symbols) to
253
+ # {State}s
254
+ #
255
+ attr_reader :states
256
+
257
+ #
258
+ # @return [State] the machine enters this state when a new connection is
259
+ # established
260
+ #
261
+ attr_reader :start_state
262
+
263
+ #
264
+ # The complete list of transitions declared in the state machine (an edge
265
+ # list).
266
+ #
267
+ # @return [Array] each entry is of the form <tt>[state_name, [:send |
268
+ # :recv, message_name], next_state_name]</tt>
269
+ #
270
+ def transitions
271
+ states.values.map{|state|
272
+ [[state.on_sends, :send], [state.on_recvs, :recv]].map {|pairs, kind|
273
+ pairs.map{|message, next_state_name|
274
+ [state.name, [kind, message], next_state_name]}}}.flatten(2)
275
+ end
276
+
277
+ #
278
+ # Print the state machine in dot (graphviz) format.
279
+ #
280
+ # By default, the 'send' edges are red and 'receive' edges are blue, and
281
+ # the start state is indicated by a double border.
282
+ #
283
+ # @param [Hash] opts extra options
284
+ #
285
+ # @option opts [IO] :io (StringIO.new) to print to
286
+ #
287
+ # @option opts [String] :graph_options ('') dot graph options
288
+ #
289
+ # @option opts [Proc] :message_name_transform transform message names
290
+ #
291
+ # @option opts [Proc] :state_name_form transform state names
292
+ #
293
+ # @option opts [String] :recv_edge_style ('color=blue')
294
+ #
295
+ # @option opts [String] :send_edge_style ('color=red')
296
+ #
297
+ # @return [IO] the +:io+ option
298
+ #
299
+ def print_state_machine_dot opts={}
300
+ io = opts[:io] || StringIO.new
301
+ graph_options = opts[:graph_options] || ''
302
+ message_name_transform = opts[:message_name_transform] || proc {|x| x}
303
+ state_name_transform = opts[:state_name_transform] || proc {|x| x}
304
+ recv_edge_style = opts[:recv_edge_style] || 'color=blue'
305
+ send_edge_style = opts[:send_edge_style] || 'color=red'
306
+
307
+ io.puts "digraph #{self.name.inspect} {\n #{graph_options}"
308
+
309
+ io.puts " #{start_state.name} [peripheries=2];" # double border
310
+
311
+ transitions.each do |state_name, (kind, message_name), next_state_name|
312
+ s0 = state_name_transform.call(state_name)
313
+ s1 = state_name_transform.call(next_state_name)
314
+ label = message_name_transform.call(message_name)
315
+
316
+ style = case kind
317
+ when :recv then
318
+ "#{recv_edge_style},label=\"#{label}\""
319
+ when :send then
320
+ "#{send_edge_style},label=\"#{label}\""
321
+ else
322
+ raise "unknown kind #{kind}"
323
+ end
324
+ io.puts " #{s0} -> #{s1} [#{style}];"
325
+ end
326
+ io.puts "}"
327
+
328
+ io
329
+ end
330
+
331
+ private
332
+
333
+ def save_state_handlers handler_type, handlers, message_names, block
334
+ message_names.flatten.each do |name|
335
+ raise "#{handler_type} already defined for #{name}" if handlers[name]
336
+ handlers[name] = block
337
+ end
338
+ end
339
+ end
340
+
341
+ #
342
+ # Called by +EventMachine+ when a new connection has been established. This
343
+ # calls the +on_enter+ handler for the machine's start state with a +nil+
344
+ # message.
345
+ #
346
+ # Be sure to call +super+ if you override this method, or the +on_enter+
347
+ # handler for the start state will not be called.
348
+ #
349
+ # @return [nil]
350
+ #
351
+ def post_init
352
+ @state = self.class.start_state
353
+ @state.call_on_enter self, nil, nil
354
+ nil
355
+ end
356
+
357
+ #
358
+ # Called by +EventMachine+ when a connection is closed. This calls the
359
+ # {on_unbind} handler for the current state.
360
+ #
361
+ # @return [nil]
362
+ #
363
+ def unbind
364
+ #puts "#{self.class} UNBIND"
365
+ handler = @state.on_unbind
366
+ self.instance_exec(&handler) if handler
367
+ nil
368
+ end
369
+
370
+ #
371
+ # Move the state machine from its current state to the successor state that
372
+ # it should be in after receiving the given +message+, according to the
373
+ # protocol defined using the DSL.
374
+ #
375
+ # If the received message is not valid according to the protocol, then the
376
+ # protocol error handler is called (see {ProtocolError}).
377
+ #
378
+ # The precise order of events is:
379
+ # 1. the +on_exit+ handler of the current state is called with +message+
380
+ # 2. the current state is set to the successor state
381
+ # 3. the +on_enter+ handler of the new current state is called with
382
+ # +message+
383
+ #
384
+ # @param [Object] message_name the name for +message+; this is what relates
385
+ # the message data to the transitions defined with {on_send}; must be
386
+ # hashable and comparable by value; for example, a symbol, string,
387
+ # number or class makes a good message name
388
+ #
389
+ # @param [Object] message received
390
+ #
391
+ # @return [nil]
392
+ #
393
+ def transition_on_recv message_name, message
394
+ #puts "#{self.class}: RECV #{message_name}"
395
+ # look up successor state
396
+ next_state_name = @state.on_recvs[message_name]
397
+
398
+ # if there is no registered successor state, it's a protocol error
399
+ if next_state_name.nil?
400
+ raise RecvProtocolError.new(self, @state.name, message_name, message)
401
+ else
402
+ transition message_name, message, next_state_name
403
+ end
404
+
405
+ nil
406
+ end
407
+
408
+ #
409
+ # Move the state machine from its current state to the successor state that
410
+ # it should be in after sending the given +message+, according to the
411
+ # protocol defined using the DSL.
412
+ #
413
+ # If the message to be sent is not valid according to the protocol, then the
414
+ # protocol error handler is called (see {ProtocolError}). If
415
+ # the message is valid, then the precise order of events is:
416
+ # 1. this method yields the message to the supplied block (if any); the
417
+ # intention is that the block is used to actually send the message
418
+ # 2. the +on_exit+ handler of the current state is called with +message+
419
+ # 3. the current state is set to the successor state
420
+ # 4. the +on_enter+ handler of the new current state is called with
421
+ # +message+
422
+ #
423
+ # @param [Object] message received
424
+ #
425
+ # @param [Object] message_name the name for +message+; this is what relates
426
+ # the message data to the transitions defined with {on_send}; must be
427
+ # hashable and comparable by value; for example, a symbol, string,
428
+ # number or class makes a good message name
429
+ #
430
+ # @yield [message] should actually send the message, typically using
431
+ # EventMachine's +send_data+ method
432
+ #
433
+ # @yieldparam [Object] message the same message passed to this method
434
+ #
435
+ # @return [nil]
436
+ #
437
+ def transition_on_send message_name, message
438
+ #puts "#{self.class}: SEND #{message_name}"
439
+ # look up successor state
440
+ next_state_name = @state.on_sends[message_name]
441
+
442
+ # if there is no registered successor state, it's a protocol error
443
+ if next_state_name.nil?
444
+ raise SendProtocolError.new(self, @state.name, message_name, message)
445
+ else
446
+ # let the caller send the message before we transition
447
+ yield message if block_given?
448
+
449
+ transition message_name, message, next_state_name
450
+ end
451
+
452
+ nil
453
+ end
454
+
455
+ private
456
+
457
+ #
458
+ # Update @state and call appropriate on_exit and on_enter handlers.
459
+ #
460
+ def transition message_name, message, next_state_name
461
+ @state.call_on_exit self, message_name, message
462
+ @state = self.class.states[next_state_name]
463
+ @state.call_on_enter self, message_name, message
464
+ end
465
+ end
466
+ end
467
+
@@ -0,0 +1,54 @@
1
+ module EventState
2
+ #
3
+ # Base class for a machine in which the messages are ruby objects. See the
4
+ # {file:README} for examples.
5
+ #
6
+ class ObjectMachine < Machine
7
+ include EventMachine::P::ObjectProtocol
8
+
9
+ #
10
+ # Return the class of the message as the message name. You can override this
11
+ # method to provide your own mapping from messages to names.
12
+ #
13
+ # @param [Object] message
14
+ #
15
+ # @return [Object] must be hashable and comparable by value; for example, a
16
+ # symbol, string, number or class makes a good message name
17
+ #
18
+ def message_name message
19
+ message.class
20
+ end
21
+
22
+ #
23
+ # Called by +EventMachine+ (actually, the +ObjectProtocol+) when a message
24
+ # is received; makes the appropriate transition.
25
+ #
26
+ # Note: if you want to receive non-messages as well, you should override
27
+ # this method in your subclass, and call +super+ only when a message is
28
+ # received.
29
+ #
30
+ # @param [Object] message received
31
+ #
32
+ # @return [nil]
33
+ #
34
+ def receive_object message
35
+ transition_on_recv message_name(message), message
36
+ end
37
+
38
+ #
39
+ # Call <tt>ObjectProtocol</tt>'s +send_object+ on the message and make the
40
+ # transition.
41
+ #
42
+ # @param [Object] message to be sent
43
+ #
44
+ # @return [nil]
45
+ #
46
+ def send_message message
47
+ raise "not on the reactor thread" unless EM.reactor_thread?
48
+ transition_on_send message_name(message), message do |msg|
49
+ send_object msg
50
+ end
51
+ end
52
+ end
53
+ end
54
+
@@ -0,0 +1,68 @@
1
+ module EventState
2
+ #
3
+ # A state in a state machine. This class is for internal use; you will not
4
+ # usually need to use it directly.
5
+ #
6
+ # Note that the <tt>Proc</tt>s stored here are executed in the context of an
7
+ # instance of a subclass of {Machine}, rather than in the context in which
8
+ # they were defined.
9
+ #
10
+ # @attr [Symbol] name state name
11
+ #
12
+ # @attr [Hash<Symbol, Proc>] on_enters map from message names to handlers
13
+ #
14
+ # @attr [Proc, nil] default_on_enter called when the state is entered via a
15
+ # message that does not have an associated handler in +on_enters+
16
+ #
17
+ # @attr [Hash<Symbol, Proc>] on_exits map from message names to handlers
18
+ #
19
+ # @attr [Proc, nil] default_on_exit called when the state is exited via a
20
+ # message that does not have an associated handler in +on_exits+
21
+ #
22
+ # @attr [Hash<Symbol, Symbol>] on_sends map from message names to successor
23
+ # state names
24
+ #
25
+ # @attr [Hash<Symbol, Symbol>] on_recvs map from message names to successor
26
+ # state names
27
+ #
28
+ State = Struct.new(:name,
29
+ :on_enters, :default_on_enter,
30
+ :on_exits, :default_on_exit,
31
+ :on_unbind, :on_sends, :on_recvs) do
32
+ def initialize(*)
33
+ super
34
+ self.on_enters ||= {}
35
+ self.on_exits ||= {}
36
+ self.on_sends ||= {}
37
+ self.on_recvs ||= {}
38
+ end
39
+
40
+ #
41
+ # @private
42
+ #
43
+ def call_on_enter context, message_name, message
44
+ call_state_handler context, on_enters, default_on_enter,
45
+ message_name, message
46
+ end
47
+
48
+ #
49
+ # @private
50
+ #
51
+ def call_on_exit context, message_name, message
52
+ call_state_handler context, on_exits, default_on_exit,
53
+ message_name, message
54
+ end
55
+
56
+ private
57
+
58
+ def call_state_handler context, handlers, default_handler,
59
+ message_name, message
60
+
61
+ # use message-specific handler if it exists; otherwise use the default
62
+ handler = handlers[message_name] || default_handler
63
+
64
+ # evaluate the block in the right context, namely the machine instance
65
+ context.instance_exec(message, &handler) if handler
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,10 @@
1
+ module EventState
2
+ # Major version number.
3
+ VERSION_MAJOR = 0
4
+ # Minor version number.
5
+ VERSION_MINOR = 0
6
+ # Patch number.
7
+ VERSION_PATCH = 1
8
+ # Version string.
9
+ VERSION = [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH].join('.')
10
+ end
@@ -0,0 +1,78 @@
1
+ require 'eventmachine'
2
+
3
+ require 'event_state/version'
4
+ require 'event_state/state'
5
+ require 'event_state/machine'
6
+ require 'event_state/object_machine'
7
+
8
+ #
9
+ # See the {file:README} for details.
10
+ #
11
+ module EventState
12
+
13
+ #
14
+ # Base class for {SendProtocolError} and {RecvProtocolError}.
15
+ #
16
+ # You can catch these errors by registering a block with
17
+ # <tt>EventMachine.error_handler</tt>.
18
+ #
19
+ class ProtocolError < RuntimeError
20
+ def initialize machine, state_name, action, message_name, data
21
+ @machine, @state_name, @action, @message_name, @data =
22
+ machine, state_name, action, message_name, data
23
+ end
24
+
25
+ #
26
+ # @return [Machine] the machine that raised the error
27
+ #
28
+ attr_reader :machine
29
+
30
+ #
31
+ # @return [Symbol] the name of the state in which the error occurred
32
+ #
33
+ attr_reader :state_name
34
+
35
+ #
36
+ # @return [:recv, :send] whether the error occurred on a send or a receive
37
+ #
38
+ attr_reader :action
39
+
40
+ #
41
+ # @return [Object] the name of the message sent or received
42
+ #
43
+ attr_reader :message_name
44
+
45
+ #
46
+ # @return [Object] the message / data sent or received
47
+ #
48
+ attr_reader :data
49
+
50
+ #
51
+ # @return [String]
52
+ #
53
+ def inspect
54
+ "#<#{self.class}: for #{machine.class} in"\
55
+ " #{state_name.inspect} state: #{action} #{message_name}>"
56
+ end
57
+ end
58
+
59
+ #
60
+ # Error raised by {Machine} when a message that is invalid according to the
61
+ # machine's protocol is sent.
62
+ #
63
+ class SendProtocolError < ProtocolError
64
+ def initialize machine, state_name, message_name, data
65
+ super machine, state_name, :send, message_name, data
66
+ end
67
+ end
68
+
69
+ #
70
+ # Error raised by {Machine} when a message that is invalid according to the
71
+ # machine's protocol is received.
72
+ #
73
+ class RecvProtocolError < ProtocolError
74
+ def initialize machine, state_name, message_name, data
75
+ super machine, state_name, :recv, message_name, data
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,359 @@
1
+ require 'event_state'
2
+ require 'test/unit'
3
+
4
+ # load example machines
5
+ require 'event_state/ex_echo'
6
+ require 'event_state/ex_readme'
7
+ require 'event_state/ex_secret'
8
+
9
+ # give more helpful errors
10
+ Thread.abort_on_exception = true
11
+
12
+ class TestEventState < Test::Unit::TestCase
13
+ include EventState
14
+
15
+ DEFAULT_HOST = 'localhost'
16
+ DEFAULT_PORT = 14159
17
+
18
+ def run_server_and_client server_class, client_class, opts={}, &block
19
+ host = opts[:host] || DEFAULT_HOST
20
+ port = opts[:port] || DEFAULT_PORT
21
+ server_args = opts[:server_args] || []
22
+ client_args = opts[:client_args] || []
23
+
24
+ client = nil
25
+ EM.run do
26
+ EM.error_handler do |e|
27
+ puts "EM ERROR: #{e.inspect}"
28
+ puts e.backtrace
29
+ end
30
+ EventMachine.start_server(host, port, server_class, *server_args)
31
+ client = EventMachine.connect(host, port, client_class,
32
+ *client_args, &block)
33
+ end
34
+ client
35
+ end
36
+
37
+ def run_echo_test client_class
38
+ server_log = []
39
+ recorder = run_server_and_client(LoggingEchoServer, client_class,
40
+ server_args: [server_log],
41
+ client_args: [%w(foo bar baz), []]).recorder
42
+
43
+ assert_equal [
44
+ "entering listening state", # on_enter called on the start state
45
+ "exiting listening state", # when a message is received
46
+ "speaking foo", # the first noise
47
+ "exiting speaking state", # sent echo to client
48
+ "entering listening state", # now listening for next noise
49
+ "exiting listening state", # ...
50
+ "speaking bar",
51
+ "exiting speaking state",
52
+ "entering listening state",
53
+ "exiting listening state",
54
+ "speaking baz",
55
+ "exiting speaking state",
56
+ "entering listening state"], server_log
57
+ end
58
+
59
+ def test_echo_basic
60
+ assert_equal %w(foo bar baz),
61
+ run_server_and_client(EchoServer, EchoClient,
62
+ client_args: [%w(foo bar baz), []]).recorder
63
+ end
64
+
65
+ def test_delayed_echo
66
+ assert_equal %w(foo bar baz),
67
+ run_server_and_client(DelayedEchoServer, EchoClient,
68
+ server_args: [0.5],
69
+ client_args: [%w(foo bar baz), []]).recorder
70
+ end
71
+
72
+ def test_echo_with_object_protocol_client
73
+ run_echo_test ObjectProtocolEchoClient
74
+ end
75
+
76
+ def test_echo_with_event_state_client
77
+ run_echo_test EchoClient
78
+ end
79
+
80
+ def test_secret_server
81
+ run_server_and_client(TopSecretServer, TopSecretClient)
82
+ end
83
+
84
+ def test_print_state_machine_dot
85
+ dot = EchoClient.print_state_machine_dot(:graph_options => 'rankdir=LR;')
86
+ assert_equal <<DOT, dot.string
87
+ digraph "EventState::EchoClient" {
88
+ rankdir=LR;
89
+ speaking [peripheries=2];
90
+ speaking -> listening [color=red,label="String"];
91
+ listening -> speaking [color=blue,label="String"];
92
+ }
93
+ DOT
94
+ end
95
+
96
+ class TestDSLBasic < EventState::Machine; end
97
+
98
+ def test_dsl_basic
99
+ #
100
+ # check that we get the transitions right for this simple DSL
101
+ #
102
+ trans = nil
103
+ TestDSLBasic.class_eval do
104
+ protocol do
105
+ state :foo do
106
+ on_recv :hello, :bar
107
+ end
108
+ state :bar do
109
+ on_recv :good_bye, :foo
110
+ end
111
+ end
112
+ trans = transitions
113
+ end
114
+
115
+ assert_equal [
116
+ [:foo, [:recv, :hello], :bar],
117
+ [:bar, [:recv, :good_bye], :foo]], trans
118
+ end
119
+
120
+ class TestDSLNoNestedProtocols < EventState::Machine; end
121
+
122
+ def test_dsl_no_nested_states
123
+ #
124
+ # nested protocol blocks are illegal
125
+ #
126
+ assert_raises(RuntimeError) {
127
+ TestDSLNoNestedProtocols.class_eval do
128
+ protocol do
129
+ protocol do
130
+ end
131
+ end
132
+ end
133
+ }
134
+ end
135
+
136
+ class TestDSLNoNestedStates < EventState::Machine; end
137
+
138
+ def test_dsl_no_nested_states
139
+ #
140
+ # nested state blocks are illegal
141
+ #
142
+ assert_raises(RuntimeError) {
143
+ TestDSLNoNestedStates.class_eval do
144
+ protocol do
145
+ state :foo do
146
+ state :bar do
147
+ end
148
+ end
149
+ end
150
+ end
151
+ }
152
+ end
153
+
154
+ class TestDSLImplicitState < EventState::Machine; end
155
+
156
+ def test_dsl_implicit_state
157
+ #
158
+ # if a state is referenced in an on_send or on_recv but is not declared with
159
+ # state, the protocol method should add it to @states when it terminates
160
+ #
161
+ inner_states = nil
162
+ outer_states = nil
163
+ TestDSLImplicitState.class_eval do
164
+ protocol do
165
+ state :foo do
166
+ on_send :bar, :baz
167
+ end
168
+ inner_states = states.dup
169
+ end
170
+ outer_states = states.dup
171
+ end
172
+ assert_nil inner_states[:baz]
173
+ assert_kind_of EventState::State, outer_states[:baz]
174
+ end
175
+
176
+ class TestDelayClient < EventState::ObjectMachine
177
+ def initialize log, delays
178
+ super
179
+ @log = log
180
+ @delays = delays
181
+ end
182
+
183
+ protocol do
184
+ state :foo do
185
+ on_send String, :bar
186
+ on_enter do
187
+ EM.defer proc {
188
+ @log << "sleeping in foo"
189
+ sleep @delays.shift
190
+ @log << "finished sleep in foo"
191
+ }, proc {
192
+ send_message 'awake!'
193
+ }
194
+ end
195
+ on_unbind do
196
+ @log << "unbound in foo"
197
+ end
198
+ end
199
+
200
+ state :bar do
201
+ on_enter do
202
+ EM.defer proc {
203
+ @log << "sleeping in bar"
204
+ sleep @delays.shift
205
+ @log << "finished sleep in bar"
206
+ }, proc {
207
+ close_connection
208
+ }
209
+ end
210
+ on_unbind do
211
+ @log << "unbound in bar"
212
+ end
213
+ end
214
+ end
215
+ end
216
+
217
+ class TestUnbindServer < EventState::ObjectMachine
218
+ def initialize log, timeout
219
+ super
220
+ @log = log
221
+ @timeout = timeout
222
+ end
223
+ protocol do
224
+ state :foo do
225
+ on_enter do
226
+ @log << "entered foo"
227
+ self.comm_inactivity_timeout = @timeout
228
+ end
229
+ on_unbind do
230
+ @log << "unbound in foo"
231
+ EM.stop
232
+ end
233
+
234
+ on_recv String, :bar
235
+ end
236
+ state :bar do
237
+ on_enter do
238
+ @log << "entered bar"
239
+ end
240
+ on_unbind do
241
+ @log << "unbound in bar"
242
+ EM.stop
243
+ end
244
+ end
245
+ end
246
+ end
247
+
248
+ def test_unbind_on_timeout
249
+ #
250
+ # first, set delays so that we time out in the first server state (foo)
251
+ #
252
+ server_log = []
253
+ client_log = []
254
+ run_server_and_client(TestUnbindServer, TestDelayClient,
255
+ server_args: [server_log, 1],
256
+ client_args: [client_log, [2,2]])
257
+ assert_equal [
258
+ "entered foo",
259
+ "unbound in foo"], server_log
260
+
261
+ # the client log isn't entirely deterministic; depends on threading
262
+ assert_equal "sleeping in foo", client_log.first
263
+
264
+ #
265
+ # next, set delays so that we time out in the second server state (bar)
266
+ #
267
+ server_log = []
268
+ client_log = []
269
+ run_server_and_client(TestUnbindServer, TestDelayClient,
270
+ server_args: [server_log, 1],
271
+ client_args: [client_log, [0.5,2]])
272
+ assert_equal [
273
+ "entered foo",
274
+ "entered bar",
275
+ "unbound in bar"], server_log
276
+ assert_equal [
277
+ "sleeping in foo",
278
+ "finished sleep in foo",
279
+ "sleeping in bar",
280
+ "unbound in bar"], client_log
281
+ end
282
+
283
+ def test_readme_example
284
+ MessageEchoClient.demo
285
+ end
286
+
287
+ class TestProtocolErrorSend < EventState::ObjectMachine
288
+ protocol do
289
+ state :foo do
290
+ on_send String, :bar
291
+ on_enter do
292
+ send_message 42
293
+ end
294
+ end
295
+ end
296
+ end
297
+
298
+ def test_protocol_error_on_send
299
+ #
300
+ # TestProtocolErrorSend sends an integer instead of a string; this should
301
+ # cause an error on the server side; the server doesn't shut down; it just
302
+ # calls the EM error_handler
303
+ #
304
+ error = nil
305
+ EM.run do
306
+ EM.error_handler do |e|
307
+ # we get a ConnectionNotBound error from the client, too
308
+ error = e if !error
309
+ EM.stop
310
+ end
311
+ EM.start_server DEFAULT_HOST, DEFAULT_PORT, TestProtocolErrorSend
312
+ EM.connect DEFAULT_HOST, DEFAULT_PORT do
313
+ # just want to force the server to init
314
+ end
315
+ end
316
+
317
+ assert_kind_of SendProtocolError, error
318
+ assert_kind_of TestProtocolErrorSend, error.machine
319
+ assert_equal :foo, error.state_name
320
+ assert_equal :send, error.action
321
+ assert_equal Fixnum, error.message_name
322
+ assert_equal 42, error.data
323
+ end
324
+
325
+ class TestProtocolErrorClient < EventState::ObjectMachine
326
+ protocol do
327
+ state :foo do
328
+ on_send Fixnum, :bar
329
+ on_enter do
330
+ send_message 42
331
+ end
332
+ end
333
+ end
334
+ end
335
+
336
+ def test_protocol_error_on_recv
337
+ #
338
+ # the EchoServer expects a String, but TestProtocolErrorClient gives it an
339
+ # integer; this causes a protocol error
340
+ #
341
+ error = nil
342
+ EM.run do
343
+ EM.error_handler do |e|
344
+ error = e
345
+ EM.stop
346
+ end
347
+ EM.start_server DEFAULT_HOST, DEFAULT_PORT, EchoServer
348
+ EM.connect DEFAULT_HOST, DEFAULT_PORT, TestProtocolErrorClient
349
+ end
350
+
351
+ assert_kind_of RecvProtocolError, error
352
+ assert_kind_of EchoServer, error.machine
353
+ assert_equal :listening, error.state_name
354
+ assert_equal :recv, error.action
355
+ assert_equal Fixnum, error.message_name
356
+ assert_equal 42, error.data
357
+ end
358
+ end
359
+
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: event_state
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - John Lees-Miller
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-30 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: &80005190 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.12.10
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *80005190
25
+ - !ruby/object:Gem::Dependency
26
+ name: gemma
27
+ requirement: &80004940 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *80004940
36
+ description: A small embedded DSL for implementing stateful protocols in EventMachine
37
+ using finite state machines.
38
+ email:
39
+ - jdleesmiller@gmail.com
40
+ executables: []
41
+ extensions: []
42
+ extra_rdoc_files:
43
+ - README.rdoc
44
+ files:
45
+ - lib/event_state/version.rb
46
+ - lib/event_state/object_machine.rb
47
+ - lib/event_state/state.rb
48
+ - lib/event_state/machine.rb
49
+ - lib/event_state.rb
50
+ - README.rdoc
51
+ - test/event_state/event_state_test.rb
52
+ homepage: http://github.com/jdleesmiller/event_state
53
+ licenses: []
54
+ post_install_message:
55
+ rdoc_options:
56
+ - --main
57
+ - README.rdoc
58
+ - --title
59
+ - event_state-0.0.1 Documentation
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ segments:
69
+ - 0
70
+ hash: 304276877
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ segments:
78
+ - 0
79
+ hash: 304276877
80
+ requirements: []
81
+ rubyforge_project: event_state
82
+ rubygems_version: 1.8.10
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: StateMachines for EventMachines.
86
+ test_files:
87
+ - test/event_state/event_state_test.rb