finite_machine 0.0.1 → 0.1.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,188 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+
5
+ # Base class for state machine
6
+ class StateMachine
7
+ include Threadable
8
+
9
+ # Initial state, defaults to :none
10
+ attr_threadsafe :initial_state
11
+
12
+ # Final state, defaults to :none
13
+ attr_threadsafe :final_state
14
+
15
+ # Current state
16
+ attr_threadsafe :state
17
+
18
+ # Events DSL
19
+ attr_threadsafe :events
20
+
21
+ # The prefix used to name events.
22
+ attr_threadsafe :namespace
23
+
24
+ # The events and their transitions.
25
+ attr_threadsafe :transitions
26
+
27
+ # The state machine observer
28
+ attr_threadsafe :observer
29
+
30
+ # The state machine subscribers
31
+ attr_threadsafe :subscribers
32
+
33
+ # The state machine environment
34
+ attr_threadsafe :env
35
+
36
+ # Initialize state machine
37
+ #
38
+ # @api private
39
+ def initialize(*args, &block)
40
+ @subscribers = Subscribers.new(self)
41
+ @events = EventsDSL.new(self)
42
+ @observer = Observer.new(self)
43
+ @transitions = Hash.new { |hash, name| hash[name] = Hash.new }
44
+ @env = Environment.new(target: self)
45
+
46
+ @dsl = DSL.new self
47
+ @dsl.call(&block) if block_given?
48
+ send(:"#{@dsl.initial_event}") unless @dsl.defer
49
+ end
50
+
51
+ def subscribe(*observers)
52
+ @subscribers.subscribe(*observers)
53
+ end
54
+
55
+ # TODO: use trigger to actually fire state machine events!
56
+ # Notify about event
57
+ #
58
+ # @api public
59
+ def notify(event_type, _transition, *data)
60
+ event_class = Event.const_get(event_type.capitalize.to_s)
61
+ state_or_action = event_class < Event::Anystate ? state : _transition.name
62
+ _event = event_class.new(state_or_action, _transition, *data)
63
+ subscribers.visit(_event)
64
+ end
65
+
66
+ # Get current state
67
+ #
68
+ # @return [String]
69
+ #
70
+ # @api public
71
+ def current
72
+ state
73
+ end
74
+
75
+ # Check if current state machtes provided state
76
+ #
77
+ # @param [String, Array[String]] state
78
+ #
79
+ # @return [Boolean]
80
+ #
81
+ # @api public
82
+ def is?(state)
83
+ if state.is_a?(Array)
84
+ state.include? current
85
+ else
86
+ state == current
87
+ end
88
+ end
89
+
90
+ # Retrieve all states
91
+ #
92
+ # @return [Array[Symbol]]
93
+ #
94
+ # @api public
95
+ def states
96
+ event_names.map { |event| transitions[event].to_a }.flatten.uniq
97
+ end
98
+
99
+ # Retireve all event names
100
+ #
101
+ # @return [Array[Symbol]]
102
+ #
103
+ # @api public
104
+ def event_names
105
+ transitions.keys
106
+ end
107
+
108
+ # Checks if event can be triggered
109
+ #
110
+ # @param [String] event
111
+ #
112
+ # @return [Boolean]
113
+ #
114
+ # @api public
115
+ def can?(event)
116
+ transitions[event].key?(current) || transitions[event].key?(ANY_STATE)
117
+ end
118
+
119
+ # Checks if event cannot be triggered
120
+ #
121
+ # @param [String] event
122
+ #
123
+ # @return [Boolean]
124
+ #
125
+ # @api public
126
+ def cannot?(event)
127
+ !can?(event)
128
+ end
129
+
130
+ # Checks if terminal state has been reached
131
+ #
132
+ # @return [Boolean]
133
+ #
134
+ # @api public
135
+ def finished?
136
+ is?(final_state)
137
+ end
138
+
139
+ #
140
+ #
141
+ # @api public
142
+ def errors
143
+ end
144
+
145
+ private
146
+
147
+ # Check if state is reachable
148
+ #
149
+ # @api private
150
+ def validate_state(_transition)
151
+ current_states = transitions[_transition.name].keys
152
+ if !current_states.include?(state) && !current_states.include?(ANY_STATE)
153
+ raise TransitionError, "inappropriate current state '#{state}'"
154
+ end
155
+ end
156
+
157
+ # Performs transition
158
+ #
159
+ # @api private
160
+ def transition(_transition, *args, &block)
161
+ validate_state(_transition)
162
+
163
+ return CANCELLED unless _transition.conditions.all? { |c| c.call(env) }
164
+ return NOTRANSITION if state == _transition.to
165
+
166
+ sync_exclusive do
167
+ notify :exitstate, _transition, *args
168
+ notify :enteraction, _transition, *args
169
+
170
+ begin
171
+ _transition.call
172
+
173
+ notify :transitionstate, _transition, *args
174
+ notify :transitionaction, _transition, *args
175
+ rescue StandardError => e
176
+ raise TransitionError, "#(#{e.class}): #{e.message}\n" +
177
+ "occured at #{e.backtrace.join("\n")}"
178
+ end
179
+
180
+ notify :enterstate, _transition, *args
181
+ notify :exitaction, _transition, *args
182
+ end
183
+
184
+ SUCCEEDED
185
+ end
186
+
187
+ end # StateMachine
188
+ end # FiniteMachine
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+
5
+ # A class responsibile for storage of event subscribers
6
+ class Subscribers
7
+ include Enumerable
8
+
9
+ def initialize(machine)
10
+ @machine = machine
11
+ @subscribers = []
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ def each(&block)
16
+ @subscribers.each(&block)
17
+ end
18
+
19
+ def index(subscriber)
20
+ @subscribers.index(subscriber)
21
+ end
22
+
23
+ def empty?
24
+ @subscribers.empty?
25
+ end
26
+
27
+ def subscribe(*observers)
28
+ observers.each { |observer| @subscribers << observer }
29
+ end
30
+
31
+ def visit(event)
32
+ each { |subscriber| @mutex.synchronize { event.notify subscriber } }
33
+ end
34
+
35
+ def reset
36
+ @subscribers.clear
37
+ self
38
+ end
39
+
40
+ end # Subscribers
41
+ end # FiniteMachine
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+ module Threadable
5
+ module InstanceMethods
6
+ @@sync = Sync.new
7
+
8
+ def sync_exclusive(&block)
9
+ @@sync.synchronize(:EX, &block)
10
+ end
11
+
12
+ def sync_shared(&block)
13
+ @@sync.synchronize(:SH, &block)
14
+ end
15
+ end
16
+
17
+ def self.included(base)
18
+ base.extend ClassMethods
19
+ base.module_eval do
20
+ include InstanceMethods
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ include InstanceMethods
26
+
27
+ def attr_threadsafe(*attrs)
28
+ attrs.flatten.each do |attr|
29
+ class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
30
+ def #{attr}
31
+ sync_shared { @#{attr} }
32
+ end
33
+ alias_method '#{attr}?', '#{attr}'
34
+
35
+ def #{attr}=(value)
36
+ sync_exclusive { @#{attr} = value }
37
+ end
38
+ RUBY_EVAL
39
+ end
40
+ end
41
+ end
42
+
43
+ end # Threadable
44
+ end # FiniteMachine
@@ -0,0 +1,69 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+
5
+ # Class describing a transition associated with a given event
6
+ class Transition
7
+ include Threadable
8
+
9
+ attr_threadsafe :name
10
+
11
+ # State transitioning from
12
+ attr_threadsafe :from
13
+
14
+ # State transitioning to
15
+ attr_threadsafe :to
16
+
17
+ # Predicates before transitioning
18
+ attr_threadsafe :conditions
19
+
20
+ # The current state machine
21
+ attr_threadsafe :machine
22
+
23
+ # Initialize a Transition
24
+ #
25
+ # @api public
26
+ def initialize(machine, attrs = {})
27
+ @machine = machine
28
+ @name = attrs.fetch(:name, DEFAULT_STATE)
29
+ @from, @to = *parse_states(attrs)
30
+ @if = Array(attrs.fetch(:if, []))
31
+ @unless = Array(attrs.fetch(:unless, []))
32
+ @conditions = make_conditions
33
+ end
34
+
35
+ def make_conditions
36
+ @if.map { |c| Callable.new(c) } +
37
+ @unless.map { |c| Callable.new(c).invert }
38
+ end
39
+
40
+ # Extract states from attributes
41
+ #
42
+ # @api private
43
+ def parse_states(attrs)
44
+ _attrs = attrs.dup
45
+ [:name, :if, :unless].each { |key| _attrs.delete(key) }
46
+
47
+ if [:from, :to].any? { |key| attrs.keys.include?(key) }
48
+ [Array(_attrs[:from] || ANY_STATE), _attrs[:to]]
49
+ else
50
+ [(keys = _attrs.keys).flatten, _attrs[keys.first]]
51
+ end
52
+ end
53
+
54
+ # Execute current transition
55
+ #
56
+ # @api private
57
+ def call
58
+ sync_exclusive do
59
+ transitions = machine.transitions[name]
60
+ machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
61
+ end
62
+ end
63
+
64
+ def inspect
65
+ [@name, @from, @to, @conditions].inspect
66
+ end
67
+
68
+ end # Transition
69
+ end # FiniteMachine
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module FiniteMachine
2
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
3
5
  end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ require 'finite_machine'
4
+
5
+ RSpec.configure do |config|
6
+ config.treat_symbols_as_metadata_keys_with_true_values = true
7
+ config.run_all_when_everything_filtered = true
8
+ config.filter_run :focus
9
+
10
+ # Run specs in random order to surface order dependencies. If you find an
11
+ # order dependency and want to debug it, you can fix the order by providing
12
+ # the seed, which is printed after each run.
13
+ # --seed 1234
14
+ config.order = 'random'
15
+ end
@@ -0,0 +1,391 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe FiniteMachine, 'callbacks' do
6
+
7
+ it "triggers default init event" do
8
+ called = []
9
+ fsm = FiniteMachine.define do
10
+ initial :green
11
+
12
+ events {
13
+ event :slow, :green => :yellow
14
+ event :stop, :yellow => :red
15
+ event :ready, :red => :yellow
16
+ event :go, :yellow => :green
17
+ }
18
+
19
+ callbacks {
20
+ # generic callbacks
21
+ on_enter do |event| called << 'on_enter' end
22
+ on_transition do |event| called << 'on_transition' end
23
+ on_exit do |event| called << 'on_exit' end
24
+
25
+ # state callbacks
26
+ on_enter :none do |event| called << 'on_enter_none' end
27
+ on_enter :green do |event| called << 'on_enter_green' end
28
+
29
+ on_transition :none do |event| called << 'on_transition_none' end
30
+ on_transition :green do |event| called << 'on_transition_green' end
31
+
32
+ on_exit :none do |event| called << 'on_exit_none' end
33
+ on_exit :green do |event| called << 'on_exit_green' end
34
+
35
+ # event callbacks
36
+ on_enter :init do |event| called << 'on_enter_init' end
37
+ on_transition :init do |event| called << 'on_transition_init' end
38
+ on_exit :init do |event| called << 'on_exit_init' end
39
+ }
40
+ end
41
+
42
+ expect(called).to eql([
43
+ 'on_exit_none',
44
+ 'on_exit',
45
+ 'on_enter_init',
46
+ 'on_enter',
47
+ 'on_transition_green',
48
+ 'on_transition',
49
+ 'on_transition_init',
50
+ 'on_transition',
51
+ 'on_enter_green',
52
+ 'on_enter',
53
+ 'on_exit_init',
54
+ 'on_exit'
55
+ ])
56
+ end
57
+
58
+ it "executes callbacks in order" do
59
+ called = []
60
+ fsm = FiniteMachine.define do
61
+ initial :green
62
+
63
+ events {
64
+ event :slow, :green => :yellow
65
+ event :stop, :yellow => :red
66
+ event :ready, :red => :yellow
67
+ event :go, :yellow => :green
68
+ }
69
+
70
+ callbacks {
71
+ # generic callbacks
72
+ on_enter do |event| called << 'on_enter' end
73
+ on_transition do |event| called << 'on_transition' end
74
+ on_exit do |event| called << 'on_exit' end
75
+
76
+ # state callbacks
77
+ on_enter :green do |event| called << 'on_enter_green' end
78
+ on_enter :yellow do |event| called << "on_enter_yellow" end
79
+ on_enter :red do |event| called << "on_enter_red" end
80
+
81
+ on_transition :green do |event| called << 'on_transition_green' end
82
+ on_transition :yellow do |event| called << "on_transition_yellow" end
83
+ on_transition :red do |event| called << "on_transition_red" end
84
+
85
+ on_exit :green do |event| called << 'on_exit_green' end
86
+ on_exit :yellow do |event| called << "on_exit_yellow" end
87
+ on_exit :red do |event| called << "on_exit_red" end
88
+
89
+ # event callbacks
90
+ on_enter :slow do |event| called << 'on_enter_slow' end
91
+ on_enter :stop do |event| called << "on_enter_stop" end
92
+ on_enter :ready do |event| called << "on_enter_ready" end
93
+ on_enter :go do |event| called << "on_enter_go" end
94
+
95
+ on_transition :slow do |event| called << 'on_transition_slow' end
96
+ on_transition :stop do |event| called << "on_transition_stop" end
97
+ on_transition :ready do |event| called << "on_transition_ready" end
98
+ on_transition :go do |event| called << "on_transition_go" end
99
+
100
+ on_exit :slow do |event| called << 'on_exit_slow' end
101
+ on_exit :stop do |event| called << "on_exit_stop" end
102
+ on_exit :ready do |event| called << "on_exit_ready" end
103
+ on_exit :go do |event| called << "on_exit_go" end
104
+ }
105
+ end
106
+
107
+ called = []
108
+ fsm.slow
109
+ expect(called).to eql([
110
+ 'on_exit_green',
111
+ 'on_exit',
112
+ 'on_enter_slow',
113
+ 'on_enter',
114
+ 'on_transition_yellow',
115
+ 'on_transition',
116
+ 'on_transition_slow',
117
+ 'on_transition',
118
+ 'on_enter_yellow',
119
+ 'on_enter',
120
+ 'on_exit_slow',
121
+ 'on_exit'
122
+ ])
123
+
124
+ called = []
125
+ fsm.stop
126
+ expect(called).to eql([
127
+ 'on_exit_yellow',
128
+ 'on_exit',
129
+ 'on_enter_stop',
130
+ 'on_enter',
131
+ 'on_transition_red',
132
+ 'on_transition',
133
+ 'on_transition_stop',
134
+ 'on_transition',
135
+ 'on_enter_red',
136
+ 'on_enter',
137
+ 'on_exit_stop',
138
+ 'on_exit'
139
+ ])
140
+
141
+ called = []
142
+ fsm.ready
143
+ expect(called).to eql([
144
+ 'on_exit_red',
145
+ 'on_exit',
146
+ 'on_enter_ready',
147
+ 'on_enter',
148
+ 'on_transition_yellow',
149
+ 'on_transition',
150
+ 'on_transition_ready',
151
+ 'on_transition',
152
+ 'on_enter_yellow',
153
+ 'on_enter',
154
+ 'on_exit_ready',
155
+ 'on_exit'
156
+ ])
157
+
158
+ called = []
159
+ fsm.go
160
+ expect(called).to eql([
161
+ 'on_exit_yellow',
162
+ 'on_exit',
163
+ 'on_enter_go',
164
+ 'on_enter',
165
+ 'on_transition_green',
166
+ 'on_transition',
167
+ 'on_transition_go',
168
+ 'on_transition',
169
+ 'on_enter_green',
170
+ 'on_enter',
171
+ 'on_exit_go',
172
+ 'on_exit'
173
+ ])
174
+ end
175
+
176
+ it "allows multiple callbacks for the same state" do
177
+ called = []
178
+ fsm = FiniteMachine.define do
179
+ initial :green
180
+
181
+ events {
182
+ event :slow, :green => :yellow
183
+ event :stop, :yellow => :red
184
+ event :ready, :red => :yellow
185
+ event :go, :yellow => :green
186
+ }
187
+
188
+ callbacks {
189
+ # generic callbacks
190
+ on_enter do |event| called << 'on_enter' end
191
+ on_transition do |event| called << 'on_transition' end
192
+ on_exit do |event| called << 'on_exit' end
193
+
194
+ # state callbacks
195
+ on_exit :green do |event| called << 'on_exit_green_1' end
196
+ on_exit :green do |event| called << 'on_exit_green_2' end
197
+ on_enter :yellow do |event| called << 'on_enter_yellow_1' end
198
+ on_enter :yellow do |event| called << 'on_enter_yellow_2' end
199
+ on_transition :yellow do |event| called << 'on_transition_yellow_1' end
200
+ on_transition :yellow do |event| called << 'on_transition_yellow_2' end
201
+
202
+ # event callbacks
203
+ on_enter :slow do |event| called << 'on_enter_slow_1' end
204
+ on_enter :slow do |event| called << 'on_enter_slow_2' end
205
+ on_transition :slow do |event| called << 'on_transition_slow_1' end
206
+ on_transition :slow do |event| called << 'on_transition_slow_2' end
207
+ on_exit :slow do |event| called << 'on_exit_slow_1' end
208
+ on_exit :slow do |event| called << 'on_exit_slow_2' end
209
+ }
210
+ end
211
+
212
+ called = []
213
+ fsm.slow
214
+ expect(fsm.current).to eql(:yellow)
215
+ expect(called).to eql([
216
+ 'on_exit_green_1',
217
+ 'on_exit_green_2',
218
+ 'on_exit',
219
+ 'on_enter_slow_1',
220
+ 'on_enter_slow_2',
221
+ 'on_enter',
222
+ 'on_transition_yellow_1',
223
+ 'on_transition_yellow_2',
224
+ 'on_transition',
225
+ 'on_transition_slow_1',
226
+ 'on_transition_slow_2',
227
+ 'on_transition',
228
+ 'on_enter_yellow_1',
229
+ 'on_enter_yellow_2',
230
+ 'on_enter',
231
+ 'on_exit_slow_1',
232
+ 'on_exit_slow_2',
233
+ 'on_exit'
234
+ ])
235
+ end
236
+
237
+ it "allows for fluid callback definition" do
238
+ called = []
239
+ fsm = FiniteMachine.define do
240
+ initial :green
241
+
242
+ events {
243
+ event :slow, :green => :yellow
244
+ event :stop, :yellow => :red
245
+ event :ready, :red => :yellow
246
+ event :go, :yellow => :green
247
+ }
248
+
249
+ callbacks {
250
+ # state callbacks
251
+ on_exit_green do |event| called << 'on_exit_green' end
252
+ on_enter_yellow do |event| called << 'on_enter_yellow' end
253
+ on_transition_yellow do |event| called << 'on_transition_yellow' end
254
+
255
+ # event callbacks
256
+ on_enter_slow do |event| called << 'on_enter_slow' end
257
+ on_transition_slow do |event| called << 'on_transition_slow' end
258
+ on_exit_slow do |event| called << 'on_exit_slow' end
259
+ }
260
+ end
261
+
262
+ called = []
263
+ fsm.slow
264
+ expect(fsm.current).to eql(:yellow)
265
+ expect(called).to eql([
266
+ 'on_exit_green',
267
+ 'on_enter_slow',
268
+ 'on_transition_yellow',
269
+ 'on_transition_slow',
270
+ 'on_enter_yellow',
271
+ 'on_exit_slow'
272
+ ])
273
+ end
274
+
275
+ it "passes event object to callback" do
276
+ evt = nil
277
+
278
+ fsm = FiniteMachine.define do
279
+ initial :green
280
+
281
+ events {
282
+ event :slow, :green => :yellow
283
+ }
284
+
285
+ callbacks {
286
+ on_enter(:yellow) { |e| evt = e }
287
+ }
288
+ end
289
+
290
+ expect(fsm.current).to eql(:green)
291
+ fsm.slow
292
+ expect(fsm.current).to eql(:yellow)
293
+
294
+ expect(evt.from).to eql(:green)
295
+ expect(evt.to).to eql(:yellow)
296
+ expect(evt.name).to eql(:slow)
297
+ end
298
+
299
+ it "passes extra parameters to callbacks" do
300
+ expected = {name: :init, from: :none, to: :green, a: nil, b: nil, c: nil }
301
+
302
+ callback = Proc.new { |event, a, b, c|
303
+ expect(event.from).to eql(expected[:from])
304
+ expect(event.to).to eql(expected[:to])
305
+ expect(event.name).to eql(expected[:name])
306
+ expect(a).to eql(expected[:a])
307
+ expect(b).to eql(expected[:b])
308
+ expect(c).to eql(expected[:c])
309
+ }
310
+
311
+ fsm = FiniteMachine.define do
312
+ initial :green
313
+
314
+ events {
315
+ event :slow, :green => :yellow
316
+ event :stop, :yellow => :red
317
+ event :ready, :red => :yellow
318
+ event :go, :yellow => :green
319
+ }
320
+
321
+ callbacks {
322
+ # generic callbacks
323
+ on_enter &callback
324
+ on_transition &callback
325
+ on_exit &callback
326
+
327
+ # state callbacks
328
+ on_enter :green, &callback
329
+ on_enter :yellow, &callback
330
+ on_enter :red, &callback
331
+
332
+ on_transition :green , &callback
333
+ on_transition :yellow, &callback
334
+ on_transition :red , &callback
335
+
336
+ on_exit :green , &callback
337
+ on_exit :yellow, &callback
338
+ on_exit :red , &callback
339
+
340
+ # event callbacks
341
+ on_enter :slow , &callback
342
+ on_enter :stop , &callback
343
+ on_enter :ready, &callback
344
+ on_enter :go , &callback
345
+
346
+ on_transition :slow , &callback
347
+ on_transition :stop , &callback
348
+ on_transition :ready, &callback
349
+ on_transition :go , &callback
350
+
351
+ on_exit :slow , &callback
352
+ on_exit :stop , &callback
353
+ on_exit :ready, &callback
354
+ on_exit :go , &callback
355
+ }
356
+ end
357
+
358
+
359
+ expected = {name: :slow, from: :green, to: :yellow, a: 1, b: 2, c: 3}
360
+ fsm.slow(1, 2, 3)
361
+
362
+ expected = {name: :stop, from: :yellow, to: :red, a: 'foo', b: 'bar'}
363
+ fsm.stop('foo', 'bar')
364
+
365
+ expected = {name: :ready, from: :red, to: :yellow, a: :foo, b: :bar}
366
+ fsm.ready(:foo, :bar)
367
+
368
+ expected = {name: :go, from: :yellow, to: :green, a: nil, b: nil}
369
+ fsm.go(nil, nil)
370
+ end
371
+
372
+ it "raises an error with invalid callback name" do
373
+ expect {
374
+ FiniteMachine.define do
375
+ initial :green
376
+
377
+ events {
378
+ event :slow, :green => :yellow
379
+ }
380
+
381
+ callbacks {
382
+ on_enter(:magic) { |event| called << 'on_enter'}
383
+ }
384
+ end
385
+ }.to raise_error(FiniteMachine::InvalidCallbackNameError, /magic is not a valid callback name/)
386
+ end
387
+
388
+ xit "propagates exceptions raised inside callback"
389
+
390
+ xit "executes callbacks with multiple 'from' transitions"
391
+ end