finite_machine 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,6 +5,7 @@ module FiniteMachine
5
5
  # Base class for state machine
6
6
  class StateMachine
7
7
  include Threadable
8
+ include Catchable
8
9
 
9
10
  # Initial state, defaults to :none
10
11
  attr_threadsafe :initial_state
@@ -18,6 +19,9 @@ module FiniteMachine
18
19
  # Events DSL
19
20
  attr_threadsafe :events
20
21
 
22
+ # Errors DSL
23
+ attr_threadsafe :errors
24
+
21
25
  # The prefix used to name events.
22
26
  attr_threadsafe :namespace
23
27
 
@@ -39,6 +43,7 @@ module FiniteMachine
39
43
  def initialize(*args, &block)
40
44
  @subscribers = Subscribers.new(self)
41
45
  @events = EventsDSL.new(self)
46
+ @errors = ErrorsDSL.new(self)
42
47
  @observer = Observer.new(self)
43
48
  @transitions = Hash.new { |hash, name| hash[name] = Hash.new }
44
49
  @env = Environment.new(target: self)
@@ -57,10 +62,12 @@ module FiniteMachine
57
62
  #
58
63
  # @api public
59
64
  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)
65
+ sync_shared do
66
+ event_class = Event.const_get(event_type.capitalize.to_s)
67
+ state_or_action = event_class < Event::Anystate ? state : _transition.name
68
+ _event = event_class.new(state_or_action, _transition, *data)
69
+ subscribers.visit(_event)
70
+ end
64
71
  end
65
72
 
66
73
  # Get current state
@@ -136,21 +143,18 @@ module FiniteMachine
136
143
  is?(final_state)
137
144
  end
138
145
 
139
- #
140
- #
141
- # @api public
142
- def errors
143
- end
144
-
145
146
  private
146
147
 
147
148
  # Check if state is reachable
148
149
  #
149
150
  # @api private
150
- def validate_state(_transition)
151
+ def valid_state?(_transition)
151
152
  current_states = transitions[_transition.name].keys
152
- if !current_states.include?(state) && !current_states.include?(ANY_STATE)
153
- raise TransitionError, "inappropriate current state '#{state}'"
153
+ if !current_states.include?(state) && !current_states.include?(ANY_STATE)
154
+ exception = InvalidStateError
155
+ catch_error(exception) ||
156
+ raise(exception, "inappropriate current state '#{state}'")
157
+ true
154
158
  end
155
159
  end
156
160
 
@@ -158,9 +162,11 @@ module FiniteMachine
158
162
  #
159
163
  # @api private
160
164
  def transition(_transition, *args, &block)
161
- validate_state(_transition)
165
+ return CANCELLED if valid_state?(_transition)
162
166
 
163
- return CANCELLED unless _transition.conditions.all? { |c| c.call(env) }
167
+ return CANCELLED unless _transition.conditions.all? do |condition|
168
+ condition.call(env.target)
169
+ end
164
170
  return NOTRANSITION if state == _transition.to
165
171
 
166
172
  sync_exclusive do
@@ -172,9 +178,10 @@ module FiniteMachine
172
178
 
173
179
  notify :transitionstate, _transition, *args
174
180
  notify :transitionaction, _transition, *args
175
- rescue StandardError => e
176
- raise TransitionError, "#(#{e.class}): #{e.message}\n" +
177
- "occured at #{e.backtrace.join("\n")}"
181
+ rescue Exception => e
182
+ catch_error(e) ||
183
+ raise(TransitionError, "#(#{e.class}): #{e.message}\n" +
184
+ "occured at #{e.backtrace.join("\n")}")
178
185
  end
179
186
 
180
187
  notify :enterstate, _transition, *args
@@ -184,5 +191,17 @@ module FiniteMachine
184
191
  SUCCEEDED
185
192
  end
186
193
 
194
+ def method_missing(method_name, *args, &block)
195
+ if env.target.respond_to?(method_name.to_sym)
196
+ env.target.send(method_name.to_sym, *args, &block)
197
+ else
198
+ super
199
+ end
200
+ end
201
+
202
+ def respond_to_missing?(method_name, include_private = false)
203
+ env.target.respond_to?(method_name.to_sym)
204
+ end
205
+
187
206
  end # StateMachine
188
207
  end # FiniteMachine
@@ -29,7 +29,7 @@ module FiniteMachine
29
29
  end
30
30
 
31
31
  def visit(event)
32
- each { |subscriber| @mutex.synchronize { event.notify subscriber } }
32
+ each { |subscriber| event.notify subscriber }
33
33
  end
34
34
 
35
35
  def reset
@@ -1,6 +1,8 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
+
5
+ # A mixin to allow instance methods to be synchronized
4
6
  module Threadable
5
7
  module InstanceMethods
6
8
  @@sync = Sync.new
@@ -27,8 +29,12 @@ module FiniteMachine
27
29
  def attr_threadsafe(*attrs)
28
30
  attrs.flatten.each do |attr|
29
31
  class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
30
- def #{attr}
31
- sync_shared { @#{attr} }
32
+ def #{attr}(*args)
33
+ if args.empty?
34
+ sync_shared { @#{attr} }
35
+ else
36
+ self.#{attr} = args.shift
37
+ end
32
38
  end
33
39
  alias_method '#{attr}?', '#{attr}'
34
40
 
@@ -20,6 +20,9 @@ module FiniteMachine
20
20
  # The current state machine
21
21
  attr_threadsafe :machine
22
22
 
23
+ # The original from state
24
+ attr_threadsafe :from_state
25
+
23
26
  # Initialize a Transition
24
27
  #
25
28
  # @api public
@@ -27,6 +30,7 @@ module FiniteMachine
27
30
  @machine = machine
28
31
  @name = attrs.fetch(:name, DEFAULT_STATE)
29
32
  @from, @to = *parse_states(attrs)
33
+ @from_state = @from.first
30
34
  @if = Array(attrs.fetch(:if, []))
31
35
  @unless = Array(attrs.fetch(:unless, []))
32
36
  @conditions = make_conditions
@@ -43,6 +47,7 @@ module FiniteMachine
43
47
  def parse_states(attrs)
44
48
  _attrs = attrs.dup
45
49
  [:name, :if, :unless].each { |key| _attrs.delete(key) }
50
+ raise_not_enough_transitions(attrs) unless _attrs.any?
46
51
 
47
52
  if [:from, :to].any? { |key| attrs.keys.include?(key) }
48
53
  [Array(_attrs[:from] || ANY_STATE), _attrs[:to]]
@@ -51,18 +56,57 @@ module FiniteMachine
51
56
  end
52
57
  end
53
58
 
59
+ # Add transition to the machine
60
+ #
61
+ # @return [Transition]
62
+ #
63
+ # @api private
64
+ def define
65
+ from.each do |from|
66
+ machine.transitions[name][from] = to || from
67
+ end
68
+ end
69
+
70
+ # Define event on the machine
71
+ #
72
+ # @api private
73
+ def define_event
74
+ _transition = self
75
+ # TODO check if event is already defined and raise error
76
+ machine.class.__send__(:define_method, name) do |*args, &block|
77
+ transition(_transition, *args, &block)
78
+ end
79
+ end
80
+
54
81
  # Execute current transition
55
82
  #
56
83
  # @api private
57
84
  def call
58
85
  sync_exclusive do
59
86
  transitions = machine.transitions[name]
87
+ self.from_state = machine.state
60
88
  machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
61
89
  end
62
90
  end
63
91
 
92
+ # Return transition name
93
+ #
94
+ # @api public
95
+ def to_s
96
+ @name
97
+ end
98
+
64
99
  def inspect
65
- [@name, @from, @to, @conditions].inspect
100
+ "<#{self.class} name: #{@name}, transitions: #{@from} => #{@to}, when: #{@conditions}>"
101
+ end
102
+
103
+ private
104
+
105
+ # Raise error when not enough transitions are provided
106
+ #
107
+ # @api private
108
+ def raise_not_enough_transitions(attrs)
109
+ raise NotEnoughTransitionsError, "please provide state transitions for '#{attrs.inspect}'"
66
110
  end
67
111
 
68
112
  end # Transition
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -12,4 +12,13 @@ RSpec.configure do |config|
12
12
  # the seed, which is printed after each run.
13
13
  # --seed 1234
14
14
  config.order = 'random'
15
+
16
+ # Remove defined constants
17
+ config.before :each do
18
+ [:Car, :Logger].each do |class_name|
19
+ if Object.const_defined?(class_name)
20
+ Object.send(:remove_const, class_name)
21
+ end
22
+ end
23
+ end
15
24
  end
@@ -0,0 +1,91 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe FiniteMachine::Callable, '#call' do
6
+
7
+ before(:each) {
8
+ Car = Class.new do
9
+ attr_reader :result
10
+
11
+ def turn_engine_on
12
+ @result = 'turn_engine_on'
13
+ @engine_on = true
14
+ end
15
+
16
+ def set_engine(value)
17
+ @result = "set_engine(#{value})"
18
+ @engine = value.to_sym == :on
19
+ end
20
+
21
+ def turn_engine_off
22
+ @result = 'turn_engine_off'
23
+ @engine_on = false
24
+ end
25
+
26
+ def engine_on?
27
+ @result = 'engine_on'
28
+ !!@engine_on
29
+ end
30
+ end
31
+ }
32
+
33
+ let(:called) { [] }
34
+
35
+ let(:target) { Car.new }
36
+
37
+ let(:instance) { described_class.new(object) }
38
+
39
+ context 'when string' do
40
+ let(:object) { 'engine_on?' }
41
+
42
+ it 'executes method on target' do
43
+ instance.call(target)
44
+ expect(target.result).to eql('engine_on')
45
+ end
46
+ end
47
+
48
+ context 'when string' do
49
+ let(:object) { 'set_engine(:on)' }
50
+
51
+ it 'executes method with arguments' do
52
+ instance.call(target)
53
+ expect(target.result).to eql('set_engine(on)')
54
+ end
55
+ end
56
+
57
+ context 'when symbol' do
58
+ let(:object) { :engine_on? }
59
+
60
+ it 'executes method on target' do
61
+ instance.call(target)
62
+ expect(target.result).to eql('engine_on')
63
+ end
64
+ end
65
+
66
+ context 'when proc without args' do
67
+ let(:object) { proc { |a| called << "block_with(#{a})" } }
68
+
69
+ it 'passes arguments' do
70
+ instance.call(target)
71
+ expect(called).to eql(["block_with(#{target})"])
72
+ end
73
+ end
74
+
75
+ context 'when proc with args' do
76
+ let(:object) { proc { |a,b| called << "block_with(#{a},#{b})" } }
77
+
78
+ it 'passes arguments' do
79
+ instance.call(target, :red)
80
+ expect(called).to eql(["block_with(#{target},red)"])
81
+ end
82
+ end
83
+
84
+ context 'when unknown' do
85
+ let(:object) { Object.new }
86
+
87
+ it 'raises error' do
88
+ expect { instance.call(target) }.to raise_error(ArgumentError)
89
+ end
90
+ end
91
+ end
@@ -18,9 +18,17 @@ describe FiniteMachine, 'callbacks' do
18
18
 
19
19
  callbacks {
20
20
  # generic callbacks
21
- on_enter do |event| called << 'on_enter' end
22
- on_transition do |event| called << 'on_transition' end
21
+ on_enter do |event| called << 'on_enter' end
22
+ on_enter_state do |event| called << 'on_enter_state' end
23
+ on_enter_event do |event| called << 'on_enter_event' end
24
+
25
+ on_transition do |event| called << 'on_transition' end
26
+ on_transition_state do |event| called << 'on_transition_state' end
27
+ on_transition_event do |event| called << 'on_transition_event' end
28
+
23
29
  on_exit do |event| called << 'on_exit' end
30
+ on_exit_state do |event| called << 'on_exit_state' end
31
+ on_exit_event do |event| called << 'on_exit_event' end
24
32
 
25
33
  # state callbacks
26
34
  on_enter :none do |event| called << 'on_enter_none' end
@@ -42,16 +50,22 @@ describe FiniteMachine, 'callbacks' do
42
50
  expect(called).to eql([
43
51
  'on_exit_none',
44
52
  'on_exit',
53
+ 'on_exit_state',
45
54
  'on_enter_init',
46
55
  'on_enter',
56
+ 'on_enter_event',
47
57
  'on_transition_green',
48
58
  'on_transition',
59
+ 'on_transition_state',
49
60
  'on_transition_init',
50
61
  'on_transition',
62
+ 'on_transition_event',
51
63
  'on_enter_green',
52
64
  'on_enter',
65
+ 'on_enter_state',
53
66
  'on_exit_init',
54
- 'on_exit'
67
+ 'on_exit',
68
+ 'on_exit_event'
55
69
  ])
56
70
  end
57
71
 
@@ -69,9 +83,17 @@ describe FiniteMachine, 'callbacks' do
69
83
 
70
84
  callbacks {
71
85
  # generic callbacks
72
- on_enter do |event| called << 'on_enter' end
73
- on_transition do |event| called << 'on_transition' end
86
+ on_enter do |event| called << 'on_enter' end
87
+ on_enter_state do |event| called << 'on_enter_state' end
88
+ on_enter_event do |event| called << 'on_enter_event' end
89
+
90
+ on_transition do |event| called << 'on_transition' end
91
+ on_transition_state do |event| called << 'on_transition_state' end
92
+ on_transition_event do |event| called << 'on_transition_event' end
93
+
74
94
  on_exit do |event| called << 'on_exit' end
95
+ on_exit_state do |event| called << 'on_exit_state' end
96
+ on_exit_event do |event| called << 'on_exit_event' end
75
97
 
76
98
  # state callbacks
77
99
  on_enter :green do |event| called << 'on_enter_green' end
@@ -109,16 +131,22 @@ describe FiniteMachine, 'callbacks' do
109
131
  expect(called).to eql([
110
132
  'on_exit_green',
111
133
  'on_exit',
134
+ 'on_exit_state',
112
135
  'on_enter_slow',
113
136
  'on_enter',
137
+ 'on_enter_event',
114
138
  'on_transition_yellow',
115
139
  'on_transition',
140
+ 'on_transition_state',
116
141
  'on_transition_slow',
117
142
  'on_transition',
143
+ 'on_transition_event',
118
144
  'on_enter_yellow',
119
145
  'on_enter',
146
+ 'on_enter_state',
120
147
  'on_exit_slow',
121
- 'on_exit'
148
+ 'on_exit',
149
+ 'on_exit_event'
122
150
  ])
123
151
 
124
152
  called = []
@@ -126,16 +154,22 @@ describe FiniteMachine, 'callbacks' do
126
154
  expect(called).to eql([
127
155
  'on_exit_yellow',
128
156
  'on_exit',
157
+ 'on_exit_state',
129
158
  'on_enter_stop',
130
159
  'on_enter',
160
+ 'on_enter_event',
131
161
  'on_transition_red',
132
162
  'on_transition',
163
+ 'on_transition_state',
133
164
  'on_transition_stop',
134
165
  'on_transition',
166
+ 'on_transition_event',
135
167
  'on_enter_red',
136
168
  'on_enter',
169
+ 'on_enter_state',
137
170
  'on_exit_stop',
138
- 'on_exit'
171
+ 'on_exit',
172
+ 'on_exit_event'
139
173
  ])
140
174
 
141
175
  called = []
@@ -143,16 +177,22 @@ describe FiniteMachine, 'callbacks' do
143
177
  expect(called).to eql([
144
178
  'on_exit_red',
145
179
  'on_exit',
180
+ 'on_exit_state',
146
181
  'on_enter_ready',
147
182
  'on_enter',
183
+ 'on_enter_event',
148
184
  'on_transition_yellow',
149
185
  'on_transition',
186
+ 'on_transition_state',
150
187
  'on_transition_ready',
151
188
  'on_transition',
189
+ 'on_transition_event',
152
190
  'on_enter_yellow',
153
191
  'on_enter',
192
+ 'on_enter_state',
154
193
  'on_exit_ready',
155
- 'on_exit'
194
+ 'on_exit',
195
+ 'on_exit_event'
156
196
  ])
157
197
 
158
198
  called = []
@@ -160,16 +200,22 @@ describe FiniteMachine, 'callbacks' do
160
200
  expect(called).to eql([
161
201
  'on_exit_yellow',
162
202
  'on_exit',
203
+ 'on_exit_state',
163
204
  'on_enter_go',
164
205
  'on_enter',
206
+ 'on_enter_event',
165
207
  'on_transition_green',
166
208
  'on_transition',
209
+ 'on_transition_state',
167
210
  'on_transition_go',
168
211
  'on_transition',
212
+ 'on_transition_event',
169
213
  'on_enter_green',
170
214
  'on_enter',
215
+ 'on_enter_state',
171
216
  'on_exit_go',
172
- 'on_exit'
217
+ 'on_exit',
218
+ 'on_exit_event'
173
219
  ])
174
220
  end
175
221
 
@@ -279,7 +325,32 @@ describe FiniteMachine, 'callbacks' do
279
325
  initial :green
280
326
 
281
327
  events {
282
- event :slow, :green => :yellow
328
+ event :slow, :green => :yellow
329
+ }
330
+
331
+ callbacks {
332
+ on_enter(:yellow) { |e| evt = e }
333
+ }
334
+ end
335
+
336
+ expect(fsm.current).to eql(:green)
337
+ fsm.slow
338
+ expect(fsm.current).to eql(:yellow)
339
+
340
+ expect(evt.from).to eql(:green)
341
+ expect(evt.to).to eql(:yellow)
342
+ expect(evt.name).to eql(:slow)
343
+ end
344
+
345
+ it "identifies the from state for callback event parameter" do
346
+ evt = nil
347
+
348
+ fsm = FiniteMachine.define do
349
+ initial :green
350
+
351
+ events {
352
+ event :slow, [:red, :blue, :green] => :yellow
353
+ event :fast, :red => :purple
283
354
  }
284
355
 
285
356
  callbacks {
@@ -307,10 +378,13 @@ describe FiniteMachine, 'callbacks' do
307
378
  expect(b).to eql(expected[:b])
308
379
  expect(c).to eql(expected[:c])
309
380
  }
381
+ context = self
310
382
 
311
383
  fsm = FiniteMachine.define do
312
384
  initial :green
313
385
 
386
+ target context
387
+
314
388
  events {
315
389
  event :slow, :green => :yellow
316
390
  event :stop, :yellow => :red
@@ -355,7 +429,6 @@ describe FiniteMachine, 'callbacks' do
355
429
  }
356
430
  end
357
431
 
358
-
359
432
  expected = {name: :slow, from: :green, to: :yellow, a: 1, b: 2, c: 3}
360
433
  fsm.slow(1, 2, 3)
361
434
 
@@ -385,7 +458,18 @@ describe FiniteMachine, 'callbacks' do
385
458
  }.to raise_error(FiniteMachine::InvalidCallbackNameError, /magic is not a valid callback name/)
386
459
  end
387
460
 
388
- xit "propagates exceptions raised inside callback"
461
+ it "propagates exceptions raised inside callback" do
462
+ fsm = FiniteMachine.define do
463
+ initial :green
464
+
465
+ events { event :slow, :green => :yellow }
466
+
467
+ callbacks { on_enter(:yellow) { raise RuntimeError } }
468
+ end
469
+
470
+ expect(fsm.current).to eql(:green)
471
+ expect { fsm.slow }.to raise_error(RuntimeError)
472
+ end
389
473
 
390
474
  xit "executes callbacks with multiple 'from' transitions"
391
475
  end