finite_machine 0.10.2 → 0.11.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 +16 -0
- data/Gemfile +1 -1
- data/README.md +73 -35
- data/assets/finite_machine_logo.png +0 -0
- data/lib/finite_machine.rb +0 -7
- data/lib/finite_machine/async_proxy.rb +1 -2
- data/lib/finite_machine/dsl.rb +13 -14
- data/lib/finite_machine/event_definition.rb +32 -35
- data/lib/finite_machine/events_chain.rb +183 -37
- data/lib/finite_machine/hook_event.rb +47 -42
- data/lib/finite_machine/logger.rb +3 -4
- data/lib/finite_machine/observer.rb +27 -11
- data/lib/finite_machine/state_definition.rb +66 -0
- data/lib/finite_machine/state_machine.rb +177 -99
- data/lib/finite_machine/subscribers.rb +17 -6
- data/lib/finite_machine/thread_context.rb +1 -1
- data/lib/finite_machine/transition.rb +45 -173
- data/lib/finite_machine/transition_builder.rb +24 -6
- data/lib/finite_machine/transition_event.rb +5 -4
- data/lib/finite_machine/undefined_transition.rb +32 -0
- data/lib/finite_machine/version.rb +1 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/unit/async_events_spec.rb +24 -18
- data/spec/unit/callbacks_spec.rb +0 -19
- data/spec/unit/event_names_spec.rb +19 -0
- data/spec/unit/events_chain/add_spec.rb +25 -0
- data/spec/unit/events_chain/cancel_transitions_spec.rb +22 -0
- data/spec/unit/events_chain/choice_transition_spec.rb +28 -0
- data/spec/unit/events_chain/clear_spec.rb +7 -18
- data/spec/unit/events_chain/events_spec.rb +18 -0
- data/spec/unit/events_chain/inspect_spec.rb +14 -17
- data/spec/unit/events_chain/match_transition_spec.rb +37 -0
- data/spec/unit/events_chain/move_to_spec.rb +48 -0
- data/spec/unit/events_chain/states_for_spec.rb +17 -0
- data/spec/unit/events_spec.rb +119 -27
- data/spec/unit/hook_event/build_spec.rb +15 -0
- data/spec/unit/hook_event/eql_spec.rb +3 -4
- data/spec/unit/hook_event/initialize_spec.rb +14 -11
- data/spec/unit/hook_event/notify_spec.rb +14 -0
- data/spec/unit/{initialize_spec.rb → initial_spec.rb} +1 -1
- data/spec/unit/inspect_spec.rb +1 -1
- data/spec/unit/logger_spec.rb +4 -5
- data/spec/unit/subscribers_spec.rb +20 -9
- data/spec/unit/transition/check_conditions_spec.rb +54 -0
- data/spec/unit/transition/inspect_spec.rb +2 -2
- data/spec/unit/transition/matches_spec.rb +23 -0
- data/spec/unit/transition/states_spec.rb +31 -0
- data/spec/unit/transition/to_state_spec.rb +27 -0
- data/spec/unit/trigger_spec.rb +22 -0
- data/spec/unit/undefined_transition/eql_spec.rb +17 -0
- data/tasks/console.rake +1 -0
- metadata +39 -23
- data/lib/finite_machine/event.rb +0 -146
- data/spec/unit/event/add_spec.rb +0 -16
- data/spec/unit/event/eql_spec.rb +0 -37
- data/spec/unit/event/initialize_spec.rb +0 -38
- data/spec/unit/event/inspect_spec.rb +0 -21
- data/spec/unit/event/next_transition_spec.rb +0 -35
- data/spec/unit/events_chain/check_choice_conditions_spec.rb +0 -20
- data/spec/unit/events_chain/insert_spec.rb +0 -26
- data/spec/unit/events_chain/select_transition_spec.rb +0 -23
- data/spec/unit/transition/parse_states_spec.rb +0 -42
@@ -1,52 +1,127 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
require 'finite_machine/undefined_transition'
|
4
|
+
|
3
5
|
module FiniteMachine
|
4
|
-
# A class responsible for storing chain of events
|
6
|
+
# A class responsible for storing chain of events.
|
7
|
+
#
|
8
|
+
# Used internally by {StateMachine}.
|
9
|
+
#
|
10
|
+
# @api private
|
5
11
|
class EventsChain
|
6
12
|
include Threadable
|
7
13
|
extend Forwardable
|
8
14
|
|
9
|
-
# The current state machine
|
10
|
-
attr_threadsafe :machine
|
11
|
-
|
12
15
|
# The chain of events
|
16
|
+
#
|
17
|
+
# @api private
|
13
18
|
attr_threadsafe :chain
|
14
19
|
|
15
|
-
def_delegators :@chain, :
|
20
|
+
def_delegators :@chain, :empty?, :size
|
16
21
|
|
17
22
|
# Initialize a EventsChain
|
18
23
|
#
|
19
|
-
# @
|
20
|
-
|
24
|
+
# @api private
|
25
|
+
def initialize
|
26
|
+
@chain = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Check if event is present
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# events_chain.exists?(:go) # => true
|
33
|
+
#
|
34
|
+
# @param [Symbol] name
|
35
|
+
# the event name
|
36
|
+
#
|
37
|
+
# @return [Boolean]
|
38
|
+
# true if event is present, false otherwise
|
21
39
|
#
|
22
40
|
# @api public
|
23
|
-
def
|
24
|
-
|
25
|
-
@chain = {}
|
41
|
+
def exists?(name)
|
42
|
+
!chain[name].nil?
|
26
43
|
end
|
27
44
|
|
28
|
-
#
|
45
|
+
# Add transition under name
|
29
46
|
#
|
30
|
-
# @param [Symbol] name
|
31
|
-
# the event name
|
47
|
+
# @param [Symbol] the event name
|
32
48
|
#
|
33
|
-
# @param [Transition]
|
49
|
+
# @param [Transition] transition
|
50
|
+
# the transition to add under event name
|
34
51
|
#
|
35
52
|
# @return [nil]
|
36
53
|
#
|
37
54
|
# @api public
|
38
|
-
def
|
39
|
-
|
40
|
-
|
55
|
+
def add(name, transition)
|
56
|
+
if exists?(name)
|
57
|
+
chain[name] << transition
|
58
|
+
else
|
59
|
+
chain[name] = [transition]
|
60
|
+
end
|
61
|
+
self
|
41
62
|
end
|
42
63
|
|
43
|
-
#
|
64
|
+
# Finds transitions for the event name
|
44
65
|
#
|
45
|
-
# @
|
66
|
+
# @param [Symbol] name
|
67
|
+
#
|
68
|
+
# @example
|
69
|
+
# events_chain[:start] # => []
|
70
|
+
#
|
71
|
+
# @return [Array[Transition]]
|
72
|
+
# the transitions matching event name
|
73
|
+
#
|
74
|
+
# @api public
|
75
|
+
def find(name)
|
76
|
+
chain.fetch(name) { [] }
|
77
|
+
end
|
78
|
+
alias_method :[], :find
|
79
|
+
|
80
|
+
# Retrieve all event names
|
81
|
+
#
|
82
|
+
# @example
|
83
|
+
# events_chain.events # => [:init, :start, :stop]
|
84
|
+
#
|
85
|
+
# @return [Array[Symbol]]
|
86
|
+
# All event names
|
87
|
+
#
|
88
|
+
# @api public
|
89
|
+
def events
|
90
|
+
chain.keys
|
91
|
+
end
|
92
|
+
|
93
|
+
# Retreive all unique states
|
94
|
+
#
|
95
|
+
# @example
|
96
|
+
# events_chain.states # => [:yellow, :green, :red]
|
97
|
+
#
|
98
|
+
# @return [Array[Symbol]]
|
99
|
+
# the array of all unique states
|
100
|
+
#
|
101
|
+
# @api public
|
102
|
+
def states
|
103
|
+
chain.values.flatten.map(&:states).map(&:to_a).flatten.uniq
|
104
|
+
end
|
105
|
+
|
106
|
+
# Retrieves all state transitions
|
107
|
+
#
|
108
|
+
# @return [Array[Hash]]
|
109
|
+
#
|
110
|
+
# @api public
|
111
|
+
def state_transitions
|
112
|
+
chain.values.flatten.map(&:states)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Retrieve from states for the event name
|
116
|
+
#
|
117
|
+
# @param [Symbol] event_name
|
118
|
+
#
|
119
|
+
# @example
|
120
|
+
# events_chain.states_for(:start) # => [:yellow, :green]
|
46
121
|
#
|
47
122
|
# @api public
|
48
|
-
def
|
49
|
-
|
123
|
+
def states_for(name)
|
124
|
+
find(name).map(&:states).flat_map(&:keys)
|
50
125
|
end
|
51
126
|
|
52
127
|
# Check if event is valid and transition can be performed
|
@@ -54,23 +129,47 @@ module FiniteMachine
|
|
54
129
|
# @return [Boolean]
|
55
130
|
#
|
56
131
|
# @api public
|
57
|
-
def
|
58
|
-
|
132
|
+
def can_perform?(name, from_state, *conditions)
|
133
|
+
!match_transition_with(name, from_state, *conditions).nil?
|
59
134
|
end
|
60
135
|
|
61
|
-
#
|
136
|
+
# Check if event has branching choice transitions or not
|
137
|
+
#
|
138
|
+
# @example
|
139
|
+
# events_chain.choice_transition?(:go, :green) # => true
|
62
140
|
#
|
63
141
|
# @param [Symbol] name
|
64
142
|
# the event name
|
65
143
|
#
|
66
|
-
# @
|
144
|
+
# @param [Symbol] from_state
|
145
|
+
# the transition from state
|
146
|
+
#
|
147
|
+
# @return [Boolean]
|
148
|
+
# true if transition has any branches, false otherwise
|
67
149
|
#
|
68
150
|
# @api public
|
69
|
-
def
|
70
|
-
|
151
|
+
def choice_transition?(name, from_state)
|
152
|
+
find(name).select { |trans| trans.matches?(from_state) }.size > 1
|
153
|
+
end
|
154
|
+
|
155
|
+
# Find transition without checking conditions
|
156
|
+
#
|
157
|
+
# @param [Symbol] name
|
158
|
+
# the event name
|
159
|
+
#
|
160
|
+
# @param [Symbol] from_state
|
161
|
+
# the transition from state
|
162
|
+
#
|
163
|
+
# @return [Transition, nil]
|
164
|
+
# returns transition, nil otherwise
|
165
|
+
#
|
166
|
+
# @api private
|
167
|
+
def match_transition(name, from_state)
|
168
|
+
find(name).find { |trans| trans.matches?(from_state) }
|
71
169
|
end
|
72
170
|
|
73
|
-
# Examine
|
171
|
+
# Examine transitions for event name that start in from state
|
172
|
+
# and find one matching condition.
|
74
173
|
#
|
75
174
|
# @param [Symbol] name
|
76
175
|
# the event name
|
@@ -82,24 +181,71 @@ module FiniteMachine
|
|
82
181
|
# The choice transition that matches
|
83
182
|
#
|
84
183
|
# @api public
|
85
|
-
def
|
86
|
-
|
87
|
-
|
88
|
-
trans.check_conditions(*
|
184
|
+
def match_transition_with(name, from_state, *conditions)
|
185
|
+
find(name).find do |trans|
|
186
|
+
trans.matches?(from_state) &&
|
187
|
+
trans.check_conditions(*conditions)
|
89
188
|
end
|
90
189
|
end
|
91
190
|
|
92
|
-
#
|
191
|
+
# Select transition that matches conditions
|
93
192
|
#
|
94
193
|
# @param [Symbol] name
|
95
194
|
# the event name
|
195
|
+
# @param [Symbol] from_state
|
196
|
+
# the transition from state
|
197
|
+
# @param [Array[Object]] conditions
|
198
|
+
# the conditional data
|
96
199
|
#
|
97
|
-
# @return [
|
200
|
+
# @return [Transition]
|
201
|
+
#
|
202
|
+
# @api public
|
203
|
+
def select_transition(name, from_state, *conditions)
|
204
|
+
if choice_transition?(name, from_state)
|
205
|
+
match_transition_with(name, from_state, *conditions)
|
206
|
+
else
|
207
|
+
match_transition(name, from_state)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Find state that this machine can move to
|
212
|
+
#
|
213
|
+
# @example
|
214
|
+
# evenst_chain.move_to(:go, :green) # => :red
|
215
|
+
#
|
216
|
+
# @param [Symbol] name
|
217
|
+
# the event name
|
218
|
+
#
|
219
|
+
# @param [Symbol] from_state
|
220
|
+
# the transition from state
|
221
|
+
#
|
222
|
+
# @param [Array] conditions
|
223
|
+
# the data associated with this transition
|
224
|
+
#
|
225
|
+
# @return [Symbol]
|
226
|
+
# the transition `to` state
|
227
|
+
#
|
228
|
+
# @api public
|
229
|
+
def move_to(name, from_state, *conditions)
|
230
|
+
transition = select_transition(name, from_state, *conditions)
|
231
|
+
transition ||= UndefinedTransition.new(name)
|
232
|
+
|
233
|
+
transition.to_state(from_state)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Set cancelled status for all transitions matching event name
|
237
|
+
#
|
238
|
+
# @param [Symbol] name
|
239
|
+
# the event name
|
240
|
+
# @param [Symbol] status
|
241
|
+
# true to cancel, false otherwise
|
242
|
+
#
|
243
|
+
# @return [nil]
|
98
244
|
#
|
99
245
|
# @api public
|
100
|
-
def
|
101
|
-
chain[name].
|
102
|
-
trans.
|
246
|
+
def cancel_transitions(name, status)
|
247
|
+
chain[name].each do |trans|
|
248
|
+
trans.cancelled = status
|
103
249
|
end
|
104
250
|
end
|
105
251
|
|
@@ -6,86 +6,91 @@ module FiniteMachine
|
|
6
6
|
include Threadable
|
7
7
|
include Comparable
|
8
8
|
|
9
|
-
|
9
|
+
class Anystate < HookEvent; end
|
10
|
+
|
11
|
+
class Enter < Anystate; end
|
12
|
+
|
13
|
+
class Transition < Anystate; end
|
10
14
|
|
11
|
-
|
15
|
+
class Exit < Anystate; end
|
16
|
+
|
17
|
+
class Anyaction < HookEvent; end
|
18
|
+
|
19
|
+
class Before < Anyaction; end
|
20
|
+
|
21
|
+
class After < Anyaction; end
|
22
|
+
|
23
|
+
EVENTS = Anystate, Enter, Transition, Exit, Anyaction, Before, After
|
24
|
+
|
25
|
+
MESSAGE = :emit
|
26
|
+
|
27
|
+
# HookEvent state or action
|
12
28
|
attr_threadsafe :name
|
13
29
|
|
14
30
|
# HookEvent type
|
15
31
|
attr_threadsafe :type
|
16
32
|
|
17
|
-
#
|
18
|
-
attr_threadsafe :
|
33
|
+
# The from state this hook has been fired
|
34
|
+
attr_threadsafe :from
|
19
35
|
|
20
|
-
#
|
21
|
-
attr_threadsafe :
|
36
|
+
# The event name triggering this hook event
|
37
|
+
attr_threadsafe :event_name
|
22
38
|
|
23
39
|
# Instantiate a new HookEvent object
|
24
40
|
#
|
25
41
|
# @param [Symbol] name
|
26
42
|
# The action or state name
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
43
|
+
#
|
44
|
+
# @param [Symbol] event_name
|
45
|
+
# The event name associated with this hook event.
|
30
46
|
#
|
31
47
|
# @example
|
32
|
-
# HookEvent.new(:green,
|
48
|
+
# HookEvent.new(:green, :move, :green)
|
33
49
|
#
|
34
|
-
# @return [
|
50
|
+
# @return [self]
|
35
51
|
#
|
36
52
|
# @api public
|
37
|
-
def initialize(name,
|
53
|
+
def initialize(name, event_name, from)
|
38
54
|
@name = name
|
39
|
-
@transition = transition
|
40
|
-
@data = *data
|
41
55
|
@type = self.class
|
56
|
+
@event_name = event_name
|
57
|
+
@from = from
|
42
58
|
freeze
|
43
59
|
end
|
44
60
|
|
45
61
|
# Build event hook
|
46
62
|
#
|
47
63
|
# @param [Symbol] :state
|
48
|
-
# The state name.
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
# The data associated with this hook
|
64
|
+
# The state or action name.
|
65
|
+
#
|
66
|
+
# @param [Symbol] :event_name
|
67
|
+
# The event name associted with this hook.
|
53
68
|
#
|
54
69
|
# @return [self]
|
55
70
|
#
|
56
71
|
# @api public
|
57
|
-
def self.build(state,
|
58
|
-
state_or_action = self < Anystate ? state :
|
59
|
-
new(state_or_action,
|
72
|
+
def self.build(state, event_name, from)
|
73
|
+
state_or_action = self < Anystate ? state : event_name
|
74
|
+
new(state_or_action, event_name, from)
|
60
75
|
end
|
61
76
|
|
62
77
|
# Notify subscriber about this event
|
63
78
|
#
|
79
|
+
# @param [Observer] subscriber
|
80
|
+
# the object subscribed to be notified about this event
|
81
|
+
#
|
82
|
+
# @param [Array] data
|
83
|
+
# the data associated with the triggered event
|
84
|
+
#
|
64
85
|
# @return [nil]
|
65
86
|
#
|
66
87
|
# @api public
|
67
|
-
def notify(subscriber, *
|
68
|
-
if subscriber.respond_to?
|
69
|
-
subscriber.public_send(MESSAGE, self, *
|
88
|
+
def notify(subscriber, *data)
|
89
|
+
if subscriber.respond_to?(MESSAGE)
|
90
|
+
subscriber.public_send(MESSAGE, self, *data)
|
70
91
|
end
|
71
92
|
end
|
72
93
|
|
73
|
-
class Anystate < HookEvent; end
|
74
|
-
|
75
|
-
class Enter < Anystate; end
|
76
|
-
|
77
|
-
class Transition < Anystate; end
|
78
|
-
|
79
|
-
class Exit < Anystate; end
|
80
|
-
|
81
|
-
class Anyaction < HookEvent; end
|
82
|
-
|
83
|
-
class Before < Anyaction; end
|
84
|
-
|
85
|
-
class After < Anyaction; end
|
86
|
-
|
87
|
-
EVENTS = Anystate, Enter, Transition, Exit, Anyaction, Before, After
|
88
|
-
|
89
94
|
# Extract event name
|
90
95
|
#
|
91
96
|
# @return [String] the event name
|
@@ -111,7 +116,7 @@ module FiniteMachine
|
|
111
116
|
# @api public
|
112
117
|
def <=>(other)
|
113
118
|
other.is_a?(type) &&
|
114
|
-
[name,
|
119
|
+
[name, from, event_name] <=> [other.name, other.from, other.event_name]
|
115
120
|
end
|
116
121
|
alias_method :eql?, :==
|
117
122
|
|
@@ -29,13 +29,12 @@ module FiniteMachine
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
def report_transition(
|
33
|
-
message = "Transition: @event=#{
|
32
|
+
def report_transition(name, from, to, *args)
|
33
|
+
message = "Transition: @event=#{name} "
|
34
34
|
unless args.empty?
|
35
35
|
message << "@with=[#{args.join(',')}] "
|
36
36
|
end
|
37
|
-
message << "#{
|
38
|
-
message << "#{event_transition.machine.current}"
|
37
|
+
message << "#{from} -> #{to}"
|
39
38
|
info(message)
|
40
39
|
end
|
41
40
|
end # Logger
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'finite_machine/hooks'
|
4
4
|
|
5
5
|
module FiniteMachine
|
6
6
|
# A class responsible for observing state changes
|
@@ -104,15 +104,22 @@ module FiniteMachine
|
|
104
104
|
on HookEvent::After, *args, &callback.extend(Once)
|
105
105
|
end
|
106
106
|
|
107
|
-
#
|
107
|
+
# Execute each of the hooks in order with supplied data
|
108
|
+
#
|
109
|
+
# @param [HookEvent] event
|
110
|
+
# the hook event
|
111
|
+
#
|
112
|
+
# @param [Array[Object]] data
|
113
|
+
#
|
114
|
+
# @return [nil]
|
108
115
|
#
|
109
116
|
# @api public
|
110
|
-
def
|
117
|
+
def emit(event, *data)
|
111
118
|
sync_exclusive do
|
112
119
|
[event.type].each do |event_type|
|
113
120
|
[event.name, ANY_STATE, ANY_EVENT].each do |event_name|
|
114
121
|
hooks.call(event_type, event_name) do |hook|
|
115
|
-
handle_callback(hook, event)
|
122
|
+
handle_callback(hook, event, *data)
|
116
123
|
off(event_type, event_name, &hook) if hook.is_a?(Once)
|
117
124
|
end
|
118
125
|
end
|
@@ -134,18 +141,26 @@ module FiniteMachine
|
|
134
141
|
#
|
135
142
|
# @api private
|
136
143
|
def create_callable(hook)
|
137
|
-
|
138
|
-
machine.instance_exec(
|
144
|
+
callback = proc do |trans_event, *data|
|
145
|
+
machine.instance_exec(trans_event, *data, &hook)
|
139
146
|
end
|
140
|
-
Callable.new(
|
147
|
+
Callable.new(callback)
|
141
148
|
end
|
142
149
|
|
143
150
|
# Handle callback and decide if run synchronously or asynchronously
|
144
151
|
#
|
152
|
+
# @param [Proc] :hook
|
153
|
+
# The hook to evaluate
|
154
|
+
#
|
155
|
+
# @param [HookEvent] :event
|
156
|
+
# The event for which the hook is called
|
157
|
+
#
|
158
|
+
# @param [Array[Object]] :data
|
159
|
+
#
|
145
160
|
# @api private
|
146
|
-
def handle_callback(hook, event)
|
147
|
-
|
148
|
-
trans_event = TransitionEvent.new(event
|
161
|
+
def handle_callback(hook, event, *data)
|
162
|
+
to = machine.events_chain.move_to(event.event_name, event.from, *data)
|
163
|
+
trans_event = TransitionEvent.new(event, to)
|
149
164
|
callable = create_callable(hook)
|
150
165
|
|
151
166
|
if hook.is_a?(Async)
|
@@ -155,7 +170,8 @@ module FiniteMachine
|
|
155
170
|
result = callable.call(trans_event, *data)
|
156
171
|
end
|
157
172
|
|
158
|
-
|
173
|
+
machine.events_chain.cancel_transitions(event.event_name,
|
174
|
+
(result == CANCELLED))
|
159
175
|
end
|
160
176
|
|
161
177
|
# Callback names including all states and events
|