finite_machine 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +154 -46
- data/lib/finite_machine.rb +1 -0
- data/lib/finite_machine/choice_merger.rb +44 -0
- data/lib/finite_machine/dsl.rb +17 -10
- data/lib/finite_machine/event.rb +36 -5
- data/lib/finite_machine/observer.rb +1 -1
- data/lib/finite_machine/state_machine.rb +22 -5
- data/lib/finite_machine/state_parser.rb +3 -1
- data/lib/finite_machine/transition.rb +43 -10
- data/lib/finite_machine/transition_event.rb +2 -2
- data/lib/finite_machine/version.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/unit/async_events_spec.rb +1 -1
- data/spec/unit/callbacks_spec.rb +29 -8
- data/spec/unit/can_spec.rb +49 -1
- data/spec/unit/choice_spec.rb +137 -0
- data/spec/unit/if_unless_spec.rb +47 -6
- data/spec/unit/initialize_spec.rb +34 -4
- data/spec/unit/states_spec.rb +15 -1
- metadata +5 -2
data/lib/finite_machine/dsl.rb
CHANGED
@@ -67,9 +67,9 @@ module FiniteMachine
|
|
67
67
|
#
|
68
68
|
# @api public
|
69
69
|
def initial(value)
|
70
|
-
state, name, self.defer = parse(value)
|
71
|
-
self.initial_event
|
72
|
-
machine.event(name,
|
70
|
+
state, name, self.defer, silent = parse(value)
|
71
|
+
self.initial_event = name
|
72
|
+
machine.event(name, FiniteMachine::DEFAULT_STATE => state, silent: silent)
|
73
73
|
end
|
74
74
|
|
75
75
|
# Trigger initial event
|
@@ -158,12 +158,13 @@ module FiniteMachine
|
|
158
158
|
#
|
159
159
|
# @api private
|
160
160
|
def parse(value)
|
161
|
-
|
162
|
-
[value, FiniteMachine::DEFAULT_EVENT_NAME, false]
|
163
|
-
else
|
161
|
+
if value.is_a?(Hash)
|
164
162
|
[value.fetch(:state) { raise_missing_state },
|
165
163
|
value.fetch(:event) { FiniteMachine::DEFAULT_EVENT_NAME },
|
166
|
-
value.fetch(:defer) { false }
|
164
|
+
value.fetch(:defer) { false },
|
165
|
+
value.fetch(:silent) { true }]
|
166
|
+
else
|
167
|
+
[value, FiniteMachine::DEFAULT_EVENT_NAME, false, true]
|
167
168
|
end
|
168
169
|
end
|
169
170
|
|
@@ -176,8 +177,8 @@ module FiniteMachine
|
|
176
177
|
end
|
177
178
|
end # DSL
|
178
179
|
|
180
|
+
# A DSL for describing events
|
179
181
|
class EventsDSL < GenericDSL
|
180
|
-
|
181
182
|
# Create event and associate transition
|
182
183
|
#
|
183
184
|
# @example
|
@@ -191,13 +192,19 @@ module FiniteMachine
|
|
191
192
|
sync_exclusive do
|
192
193
|
attributes = attrs.merge!(name: name)
|
193
194
|
FiniteMachine::StateParser.new(attrs).parse_states do |from, to|
|
194
|
-
|
195
|
-
|
195
|
+
if block_given?
|
196
|
+
merger = ChoiceMerger.new(self, attributes)
|
197
|
+
merger.instance_eval(&block)
|
198
|
+
else
|
199
|
+
attributes.merge!(parsed_states: { from => to })
|
200
|
+
Transition.create(machine, attributes)
|
201
|
+
end
|
196
202
|
end
|
197
203
|
end
|
198
204
|
end
|
199
205
|
end # EventsDSL
|
200
206
|
|
207
|
+
# A DSL for describing error conditions
|
201
208
|
class ErrorsDSL < GenericDSL
|
202
209
|
# Add error handler
|
203
210
|
#
|
data/lib/finite_machine/event.rb
CHANGED
@@ -14,10 +14,16 @@ module FiniteMachine
|
|
14
14
|
# The reference to the state machine for this event
|
15
15
|
attr_threadsafe :machine
|
16
16
|
|
17
|
+
# The silent option for this transition
|
18
|
+
attr_threadsafe :silent
|
19
|
+
|
20
|
+
# Initialize an Event
|
21
|
+
#
|
17
22
|
# @api private
|
18
23
|
def initialize(machine, attrs = {})
|
19
24
|
@machine = machine
|
20
25
|
@name = attrs.fetch(:name, DEFAULT_STATE)
|
26
|
+
@silent = attrs.fetch(:silent, false)
|
21
27
|
@state_transitions = []
|
22
28
|
# TODO: add event conditions
|
23
29
|
end
|
@@ -45,21 +51,46 @@ module FiniteMachine
|
|
45
51
|
#
|
46
52
|
# @api private
|
47
53
|
def next_transition
|
48
|
-
|
49
|
-
|
50
|
-
transition.from_state ==
|
51
|
-
|
54
|
+
sync_shared do
|
55
|
+
state_transitions.find do |transition|
|
56
|
+
transition.from_state == machine.current ||
|
57
|
+
transition.from_state == ANY_STATE
|
58
|
+
end || state_transitions.first
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Find transition matching conditions
|
63
|
+
#
|
64
|
+
# @param [Array[Object]] args
|
65
|
+
#
|
66
|
+
# return FiniteMachine::TransitionChoice
|
67
|
+
#
|
68
|
+
# @api private
|
69
|
+
def find_transition(*args)
|
70
|
+
sync_shared do
|
71
|
+
state_transitions.find { |trans| trans.check_conditions(*args) }
|
72
|
+
end
|
52
73
|
end
|
53
74
|
|
54
75
|
# Trigger this event
|
55
76
|
#
|
77
|
+
# If silent option is passed the event will not fire any callbacks
|
78
|
+
#
|
79
|
+
# @example
|
80
|
+
# transition = Transition.create(machine, {})
|
81
|
+
# transition.call
|
82
|
+
#
|
56
83
|
# @return [nil]
|
57
84
|
#
|
58
85
|
# @api public
|
59
86
|
def call(*args, &block)
|
60
87
|
sync_exclusive do
|
61
88
|
_transition = next_transition
|
62
|
-
|
89
|
+
if silent
|
90
|
+
_transition.call(*args, &block)
|
91
|
+
else
|
92
|
+
machine.send(:transition, _transition, *args, &block)
|
93
|
+
end
|
63
94
|
end
|
64
95
|
end
|
65
96
|
|
@@ -142,8 +142,8 @@ module FiniteMachine
|
|
142
142
|
#
|
143
143
|
# @api private
|
144
144
|
def handle_callback(hook, event)
|
145
|
-
trans_event = TransitionEvent.build(event.transition)
|
146
145
|
data = event.data
|
146
|
+
trans_event = TransitionEvent.build(event.transition, *data)
|
147
147
|
callable = create_callable(hook)
|
148
148
|
|
149
149
|
if hook.is_a?(Async)
|
@@ -165,13 +165,19 @@ module FiniteMachine
|
|
165
165
|
# @example
|
166
166
|
# fsm.can?(:go) # => true
|
167
167
|
#
|
168
|
+
# @example
|
169
|
+
# fsm.can?(:go, 'Piotr') # checks condition with parameter 'Piotr'
|
170
|
+
#
|
168
171
|
# @param [String] event
|
169
172
|
#
|
170
173
|
# @return [Boolean]
|
171
174
|
#
|
172
175
|
# @api public
|
173
|
-
def can?(
|
174
|
-
|
176
|
+
def can?(*args, &block)
|
177
|
+
event = args.shift
|
178
|
+
valid_state = transitions[event].key?(current)
|
179
|
+
valid_state ||= transitions[event].key?(ANY_STATE)
|
180
|
+
valid_state &&= events_chain[event].next_transition.valid?(*args, &block)
|
175
181
|
end
|
176
182
|
|
177
183
|
# Checks if event cannot be triggered
|
@@ -184,8 +190,8 @@ module FiniteMachine
|
|
184
190
|
# @return [Boolean]
|
185
191
|
#
|
186
192
|
# @api public
|
187
|
-
def cannot?(
|
188
|
-
!can?(
|
193
|
+
def cannot?(*args, &block)
|
194
|
+
!can?(*args, &block)
|
189
195
|
end
|
190
196
|
|
191
197
|
# Checks if terminal state has been reached
|
@@ -197,6 +203,17 @@ module FiniteMachine
|
|
197
203
|
is?(final_state)
|
198
204
|
end
|
199
205
|
|
206
|
+
# Restore this machine to a known state
|
207
|
+
#
|
208
|
+
# @param [Symbol] state
|
209
|
+
#
|
210
|
+
# @return nil
|
211
|
+
#
|
212
|
+
# @api public
|
213
|
+
def restore!(state)
|
214
|
+
sync_exclusive { self.state = state }
|
215
|
+
end
|
216
|
+
|
200
217
|
# String representation of this machine
|
201
218
|
#
|
202
219
|
# @return [String]
|
@@ -243,7 +260,7 @@ module FiniteMachine
|
|
243
260
|
notify HookEvent::Exit, _transition, *args
|
244
261
|
|
245
262
|
begin
|
246
|
-
_transition.call
|
263
|
+
_transition.call(*args)
|
247
264
|
|
248
265
|
notify HookEvent::Transition, _transition, *args
|
249
266
|
rescue Exception => e
|
@@ -7,6 +7,8 @@ module FiniteMachine
|
|
7
7
|
|
8
8
|
attr_threadsafe :attrs
|
9
9
|
|
10
|
+
BLACKLIST = [:name, :if, :unless, :silent].freeze
|
11
|
+
|
10
12
|
# Initialize a StateParser
|
11
13
|
#
|
12
14
|
# @example
|
@@ -83,7 +85,7 @@ module FiniteMachine
|
|
83
85
|
# @api private
|
84
86
|
def ensure_only_states!(attrs)
|
85
87
|
_attrs = attrs.dup
|
86
|
-
|
88
|
+
BLACKLIST.each { |key| _attrs.delete(key) }
|
87
89
|
raise_not_enough_transitions unless _attrs.any?
|
88
90
|
_attrs
|
89
91
|
end
|
@@ -29,6 +29,9 @@ module FiniteMachine
|
|
29
29
|
# All states for this transition event
|
30
30
|
attr_threadsafe :map
|
31
31
|
|
32
|
+
# Silence callbacks
|
33
|
+
attr_threadsafe :silent
|
34
|
+
|
32
35
|
# Initialize a Transition
|
33
36
|
#
|
34
37
|
# @param [StateMachine] machine
|
@@ -39,6 +42,7 @@ module FiniteMachine
|
|
39
42
|
@machine = machine
|
40
43
|
@name = attrs.fetch(:name, DEFAULT_STATE)
|
41
44
|
@map = attrs.fetch(:parsed_states, {})
|
45
|
+
@silent = attrs.fetch(:silent, false)
|
42
46
|
@from_states = @map.keys
|
43
47
|
@to_states = @map.values
|
44
48
|
@from_state = @from_states.first
|
@@ -60,7 +64,7 @@ module FiniteMachine
|
|
60
64
|
#
|
61
65
|
# @api public
|
62
66
|
def self.create(machine, attrs = {})
|
63
|
-
_transition =
|
67
|
+
_transition = new(machine, attrs)
|
64
68
|
_transition.update_transitions
|
65
69
|
_transition.define_state_methods
|
66
70
|
_transition.define_event
|
@@ -72,8 +76,13 @@ module FiniteMachine
|
|
72
76
|
# @return [Symbol]
|
73
77
|
#
|
74
78
|
# @api public
|
75
|
-
def to_state
|
76
|
-
machine.transitions[name][from_state]
|
79
|
+
def to_state(*args)
|
80
|
+
if machine.transitions[name][from_state].is_a? Array
|
81
|
+
found_trans = machine.events_chain[name].find_transition(*args)
|
82
|
+
found_trans.map[from_state]
|
83
|
+
else
|
84
|
+
machine.transitions[name][from_state]
|
85
|
+
end
|
77
86
|
end
|
78
87
|
|
79
88
|
# Reduce conditions
|
@@ -84,6 +93,17 @@ module FiniteMachine
|
|
84
93
|
@unless.map { |c| Callable.new(c).invert }
|
85
94
|
end
|
86
95
|
|
96
|
+
# Verify conditions returning true if all match, false otherwise
|
97
|
+
#
|
98
|
+
# @return [Boolean]
|
99
|
+
#
|
100
|
+
# @api private
|
101
|
+
def check_conditions(*args, &block)
|
102
|
+
conditions.all? do |condition|
|
103
|
+
condition.call(machine.target, *args, &block)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
87
107
|
# Check if moved to different state or not
|
88
108
|
#
|
89
109
|
# @param [Symbol] state
|
@@ -106,19 +126,27 @@ module FiniteMachine
|
|
106
126
|
#
|
107
127
|
# @api public
|
108
128
|
def valid?(*args, &block)
|
109
|
-
|
110
|
-
|
129
|
+
if machine.transitions[name][from_state].is_a? Array
|
130
|
+
machine.events_chain[name].state_transitions.any? do |trans|
|
131
|
+
trans.check_conditions(*args, &block)
|
132
|
+
end
|
133
|
+
else
|
134
|
+
check_conditions(*args, &block)
|
111
135
|
end
|
112
136
|
end
|
113
137
|
|
114
138
|
# Add transition to the machine
|
115
139
|
#
|
116
|
-
# @return [Transition]
|
140
|
+
# @return [FiniteMachine::Transition]
|
117
141
|
#
|
118
142
|
# @api private
|
119
143
|
def update_transitions
|
120
144
|
from_states.each do |from|
|
121
|
-
machine.transitions[name][from]
|
145
|
+
if value = machine.transitions[name][from]
|
146
|
+
machine.transitions[name][from] = [value, map[from]].flatten
|
147
|
+
else
|
148
|
+
machine.transitions[name][from] = map[from] || ANY_STATE
|
149
|
+
end
|
122
150
|
end
|
123
151
|
end
|
124
152
|
|
@@ -158,7 +186,7 @@ module FiniteMachine
|
|
158
186
|
#
|
159
187
|
# @api private
|
160
188
|
def define_event_transition(name)
|
161
|
-
_event = FiniteMachine::Event.new(machine, name: name)
|
189
|
+
_event = FiniteMachine::Event.new(machine, name: name, silent: silent)
|
162
190
|
_event << self
|
163
191
|
machine.events_chain[name] = _event
|
164
192
|
|
@@ -182,12 +210,17 @@ module FiniteMachine
|
|
182
210
|
# @return [nil]
|
183
211
|
#
|
184
212
|
# @api private
|
185
|
-
def call
|
213
|
+
def call(*args)
|
186
214
|
sync_exclusive do
|
187
215
|
return if cancelled
|
188
216
|
transitions = machine.transitions[name]
|
189
217
|
self.from_state = machine.state
|
190
|
-
machine.
|
218
|
+
if machine.transitions[name][from_state].is_a? Array
|
219
|
+
found_trans = machine.events_chain[name].find_transition(*args)
|
220
|
+
machine.state = found_trans.to_states.first
|
221
|
+
else
|
222
|
+
machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
|
223
|
+
end
|
191
224
|
machine.initial_state = machine.state if from_state == DEFAULT_STATE
|
192
225
|
end
|
193
226
|
end
|
@@ -17,11 +17,11 @@ module FiniteMachine
|
|
17
17
|
# @return [self]
|
18
18
|
#
|
19
19
|
# @api private
|
20
|
-
def self.build(transition)
|
20
|
+
def self.build(transition, *data)
|
21
21
|
instance = new
|
22
22
|
instance.name = transition.name
|
23
23
|
instance.from = transition.from_state
|
24
|
-
instance.to = transition.to_state
|
24
|
+
instance.to = transition.to_state(*data)
|
25
25
|
instance
|
26
26
|
end
|
27
27
|
end # TransitionEvent
|
data/spec/spec_helper.rb
CHANGED
@@ -30,7 +30,7 @@ RSpec.configure do |config|
|
|
30
30
|
|
31
31
|
# Remove defined constants
|
32
32
|
config.before :each do
|
33
|
-
[:Car, :Logger].each do |class_name|
|
33
|
+
[:Car, :Logger, :Bug, :User].each do |class_name|
|
34
34
|
if Object.const_defined?(class_name)
|
35
35
|
Object.send(:remove_const, class_name)
|
36
36
|
end
|
data/spec/unit/callbacks_spec.rb
CHANGED
@@ -7,7 +7,7 @@ describe FiniteMachine, 'callbacks' do
|
|
7
7
|
it "triggers default init event" do
|
8
8
|
called = []
|
9
9
|
fsm = FiniteMachine.define do
|
10
|
-
initial state: :green, defer: true
|
10
|
+
initial state: :green, defer: true, silent: false
|
11
11
|
|
12
12
|
callbacks {
|
13
13
|
# generic state callbacks
|
@@ -54,7 +54,7 @@ describe FiniteMachine, 'callbacks' do
|
|
54
54
|
it "executes callbacks in order" do
|
55
55
|
called = []
|
56
56
|
fsm = FiniteMachine.define do
|
57
|
-
initial :green
|
57
|
+
initial state: :green, silent: false
|
58
58
|
|
59
59
|
events {
|
60
60
|
event :slow, :green => :yellow
|
@@ -173,7 +173,7 @@ describe FiniteMachine, 'callbacks' do
|
|
173
173
|
it "maintains transition execution sequence from UML statechart" do
|
174
174
|
called = []
|
175
175
|
fsm = FiniteMachine.define do
|
176
|
-
initial :previous
|
176
|
+
initial state: :previous, silent: false
|
177
177
|
|
178
178
|
events {
|
179
179
|
event :go, :previous => :next, if: -> { called << 'guard'; true}
|
@@ -207,7 +207,7 @@ describe FiniteMachine, 'callbacks' do
|
|
207
207
|
it "allows multiple callbacks for the same state" do
|
208
208
|
called = []
|
209
209
|
fsm = FiniteMachine.define do
|
210
|
-
initial :green
|
210
|
+
initial state: :green, silent: false
|
211
211
|
|
212
212
|
events {
|
213
213
|
event :slow, :green => :yellow
|
@@ -549,7 +549,7 @@ describe FiniteMachine, 'callbacks' do
|
|
549
549
|
it "triggers callbacks only once" do
|
550
550
|
called = []
|
551
551
|
fsm = FiniteMachine.define do
|
552
|
-
initial :green
|
552
|
+
initial state: :green, silent: false
|
553
553
|
|
554
554
|
events {
|
555
555
|
event :slow, :green => :yellow
|
@@ -646,7 +646,7 @@ describe FiniteMachine, 'callbacks' do
|
|
646
646
|
it "groups states from separate events with the same name" do
|
647
647
|
callbacks = []
|
648
648
|
fsm = FiniteMachine.define do
|
649
|
-
initial :initial
|
649
|
+
initial state: :initial, silent: false
|
650
650
|
|
651
651
|
events {
|
652
652
|
event :bump, :initial => :low
|
@@ -720,7 +720,7 @@ describe FiniteMachine, 'callbacks' do
|
|
720
720
|
it "groups states under event name" do
|
721
721
|
callbacks = []
|
722
722
|
fsm = FiniteMachine.define do
|
723
|
-
initial :initial
|
723
|
+
initial state: :initial, silent: false
|
724
724
|
|
725
725
|
events {
|
726
726
|
event :bump, :initial => :low,
|
@@ -770,7 +770,7 @@ describe FiniteMachine, 'callbacks' do
|
|
770
770
|
it "permits state and event with the same name" do
|
771
771
|
called = []
|
772
772
|
fsm = FiniteMachine.define do
|
773
|
-
initial :on_hook
|
773
|
+
initial state: :on_hook, silent: false
|
774
774
|
|
775
775
|
events {
|
776
776
|
event :off_hook, :on_hook => :off_hook
|
@@ -795,4 +795,25 @@ describe FiniteMachine, 'callbacks' do
|
|
795
795
|
'on_enter_on_hook'
|
796
796
|
]);
|
797
797
|
end
|
798
|
+
|
799
|
+
it "allows to selectively silence events" do
|
800
|
+
called = []
|
801
|
+
fsm = FiniteMachine.define do
|
802
|
+
initial :yellow
|
803
|
+
|
804
|
+
events {
|
805
|
+
event :go, :yellow => :green, silent: true
|
806
|
+
event :stop, :green => :red
|
807
|
+
}
|
808
|
+
|
809
|
+
callbacks {
|
810
|
+
on_enter :green do |event| called << 'on_enter_yellow' end
|
811
|
+
on_enter :red do |event| called << 'on_enter_red' end
|
812
|
+
}
|
813
|
+
end
|
814
|
+
expect(fsm.current).to eq(:yellow)
|
815
|
+
fsm.go
|
816
|
+
fsm.stop
|
817
|
+
expect(called).to eq(['on_enter_red'])
|
818
|
+
end
|
798
819
|
end
|