finite_machine 0.7.1 → 0.8.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.
- 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
|