event_state 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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