finite_machine 0.10.2 → 0.11.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 +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
|