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