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
@@ -11,9 +11,8 @@ module FiniteMachine
|
|
11
11
|
# Initialize a subscribers collection
|
12
12
|
#
|
13
13
|
# @api public
|
14
|
-
def initialize
|
15
|
-
super
|
16
|
-
@machine = machine
|
14
|
+
def initialize
|
15
|
+
super
|
17
16
|
@subscribers = []
|
18
17
|
end
|
19
18
|
|
@@ -55,13 +54,25 @@ module FiniteMachine
|
|
55
54
|
|
56
55
|
# Visit subscribers and notify
|
57
56
|
#
|
58
|
-
# @param [
|
57
|
+
# @param [HookEvent] hook_event
|
58
|
+
# the callback event to notify about
|
59
59
|
#
|
60
60
|
# @return [undefined]
|
61
61
|
#
|
62
62
|
# @api public
|
63
|
-
def visit(
|
64
|
-
each { |subscriber|
|
63
|
+
def visit(hook_event, *data)
|
64
|
+
each { |subscriber|
|
65
|
+
synchronize { hook_event.notify(subscriber, *data) }
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Number of subscribed listeners
|
70
|
+
#
|
71
|
+
# @return [Integer]
|
72
|
+
#
|
73
|
+
# @api public
|
74
|
+
def size
|
75
|
+
synchronize { @subscribers.size }
|
65
76
|
end
|
66
77
|
|
67
78
|
# Reset subscribers
|
@@ -5,94 +5,57 @@ module FiniteMachine
|
|
5
5
|
class Transition
|
6
6
|
include Threadable
|
7
7
|
|
8
|
+
# The event name
|
8
9
|
attr_threadsafe :name
|
9
10
|
|
10
|
-
# State transitioning from
|
11
|
-
attr_threadsafe :from_states
|
12
|
-
|
13
|
-
# State transitioning to
|
14
|
-
attr_threadsafe :to_states
|
15
|
-
|
16
11
|
# Predicates before transitioning
|
17
12
|
attr_threadsafe :conditions
|
18
13
|
|
19
14
|
# The current state machine
|
20
15
|
attr_threadsafe :machine
|
21
16
|
|
22
|
-
# The original from state
|
23
|
-
attr_threadsafe :from_state
|
24
|
-
|
25
17
|
# Check if transition should be cancelled
|
26
18
|
attr_threadsafe :cancelled
|
27
19
|
|
28
20
|
# All states for this transition event
|
29
21
|
attr_threadsafe :states
|
30
22
|
|
31
|
-
# Silence callbacks
|
32
|
-
attr_threadsafe :silent
|
33
|
-
|
34
23
|
# Initialize a Transition
|
35
24
|
#
|
25
|
+
# @example
|
26
|
+
# attributes = {parsed_states: {green: :yellow}}
|
27
|
+
# Transition.new(machine, attributes)
|
28
|
+
#
|
36
29
|
# @param [StateMachine] machine
|
30
|
+
#
|
37
31
|
# @param [Hash] attrs
|
38
32
|
#
|
33
|
+
# @return [Transition]
|
34
|
+
#
|
39
35
|
# @api public
|
40
36
|
def initialize(machine, attrs = {})
|
41
37
|
@machine = machine
|
42
|
-
@name = attrs
|
43
|
-
@states = attrs.fetch(:
|
44
|
-
@silent = attrs.fetch(:silent, false)
|
45
|
-
@from_states = @states.keys
|
46
|
-
@to_states = @states.values
|
47
|
-
@from_state = @from_states.first
|
38
|
+
@name = attrs[:name]
|
39
|
+
@states = attrs.fetch(:states, {})
|
48
40
|
@if = Array(attrs.fetch(:if, []))
|
49
41
|
@unless = Array(attrs.fetch(:unless, []))
|
50
42
|
@conditions = make_conditions
|
51
|
-
@cancelled = false
|
43
|
+
@cancelled = attrs.fetch(:cancelled, false)
|
52
44
|
end
|
53
45
|
|
54
|
-
#
|
46
|
+
# Check if this transition is cancelled or not
|
55
47
|
#
|
56
|
-
# @
|
57
|
-
#
|
58
|
-
# @param [Hash] attrs
|
59
|
-
#
|
60
|
-
# @example
|
61
|
-
# attributes = {parsed_states: {green: :yellow}, silent: true}
|
62
|
-
# Transition.create(machine, attrbiutes)
|
63
|
-
#
|
64
|
-
# @return [Transition]
|
65
|
-
#
|
66
|
-
# @api public
|
67
|
-
def self.create(machine, attrs = {})
|
68
|
-
transition = new(machine, attrs)
|
69
|
-
transition.update_transitions
|
70
|
-
transition.define_state_query_methods
|
71
|
-
transition
|
72
|
-
end
|
73
|
-
|
74
|
-
# Decide :to state from available transitions for this event
|
75
|
-
#
|
76
|
-
# @return [Symbol]
|
48
|
+
# @return [Boolean]
|
77
49
|
#
|
78
50
|
# @api public
|
79
|
-
def
|
80
|
-
|
81
|
-
found_trans = machine.select_choice_transition(name, from_state, *args)
|
82
|
-
|
83
|
-
if found_trans.nil? # no choice found
|
84
|
-
from_state
|
85
|
-
else
|
86
|
-
found_trans.states[from_state] || found_trans.states[ANY_STATE]
|
87
|
-
end
|
88
|
-
else
|
89
|
-
available_trans = machine.transitions[name]
|
90
|
-
available_trans[from_state] || available_trans[ANY_STATE]
|
91
|
-
end
|
51
|
+
def cancelled?
|
52
|
+
@cancelled
|
92
53
|
end
|
93
54
|
|
94
55
|
# Reduce conditions
|
95
56
|
#
|
57
|
+
# @return [Array[Callable]]
|
58
|
+
#
|
96
59
|
# @api private
|
97
60
|
def make_conditions
|
98
61
|
@if.map { |c| Callable.new(c) } +
|
@@ -101,153 +64,62 @@ module FiniteMachine
|
|
101
64
|
|
102
65
|
# Verify conditions returning true if all match, false otherwise
|
103
66
|
#
|
67
|
+
# @param [Array[Object]] args
|
68
|
+
# the arguments for the condition
|
69
|
+
#
|
104
70
|
# @return [Boolean]
|
105
71
|
#
|
106
72
|
# @api private
|
107
|
-
def check_conditions(*args
|
73
|
+
def check_conditions(*args)
|
108
74
|
conditions.all? do |condition|
|
109
|
-
condition.call(machine.target, *args
|
75
|
+
condition.call(machine.target, *args)
|
110
76
|
end
|
111
77
|
end
|
112
78
|
|
113
|
-
# Check if
|
79
|
+
# Check if this transition matches from state
|
114
80
|
#
|
115
|
-
# @param [Symbol]
|
116
|
-
# the
|
117
|
-
#
|
118
|
-
# @return [Boolean]
|
119
|
-
#
|
120
|
-
# @api public
|
121
|
-
def same?(state)
|
122
|
-
states[state] == state || (states[ANY_STATE] == state && from_state == state)
|
123
|
-
end
|
124
|
-
|
125
|
-
# Check if from matches current state
|
81
|
+
# @param [Symbol] from
|
82
|
+
# the from state to match against
|
126
83
|
#
|
127
84
|
# @example
|
128
|
-
# transition.
|
85
|
+
# transition = Transition.new(machine, states: {:green => :red})
|
86
|
+
# transition.matches?(:green) # => true
|
129
87
|
#
|
130
88
|
# @return [Boolean]
|
131
89
|
# Return true if match is found, false otherwise.
|
132
90
|
#
|
133
91
|
# @api public
|
134
|
-
def
|
135
|
-
|
136
|
-
end
|
137
|
-
|
138
|
-
# Check if this transition has branching choice or not
|
139
|
-
#
|
140
|
-
# @return [Boolean]
|
141
|
-
#
|
142
|
-
# @api public
|
143
|
-
def transition_choice?
|
144
|
-
matching = machine.transitions[name]
|
145
|
-
[matching[from_state], matching[ANY_STATE]].any? do |match|
|
146
|
-
match.is_a?(Array)
|
147
|
-
end
|
92
|
+
def matches?(from)
|
93
|
+
states.keys.any? { |state| [ANY_STATE, from].include?(state) }
|
148
94
|
end
|
149
95
|
|
150
|
-
#
|
96
|
+
# Find to state for this transition given the from state
|
151
97
|
#
|
152
|
-
# @param [
|
98
|
+
# @param [Symbol] from
|
99
|
+
# the from state to check
|
153
100
|
#
|
154
|
-
# @
|
101
|
+
# @example
|
102
|
+
# transition = Transition.new(machine, states: {:green => :red})
|
103
|
+
# transition.to_state(:green) # => :red
|
155
104
|
#
|
156
|
-
# @return [
|
105
|
+
# @return [Symbol]
|
106
|
+
# the to state
|
157
107
|
#
|
158
108
|
# @api public
|
159
|
-
def
|
160
|
-
if
|
161
|
-
|
162
|
-
else
|
163
|
-
check_conditions(*args, &block)
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
# Add transition to the machine
|
168
|
-
#
|
169
|
-
# @return [Transition]
|
170
|
-
#
|
171
|
-
# @api private
|
172
|
-
def update_transitions
|
173
|
-
from_states.each do |from|
|
174
|
-
if (value = machine.transitions[name][from])
|
175
|
-
machine.transitions[name][from] = [value, states[from]].flatten
|
176
|
-
else
|
177
|
-
machine.transitions[name][from] = states[from] || ANY_STATE
|
178
|
-
end
|
179
|
-
end
|
180
|
-
self
|
181
|
-
end
|
182
|
-
|
183
|
-
# Define helper state mehods for the transition states
|
184
|
-
#
|
185
|
-
# @return [Transition]
|
186
|
-
#
|
187
|
-
# @api private
|
188
|
-
def define_state_query_methods
|
189
|
-
from_states.concat(to_states).each do |state|
|
190
|
-
define_state_query_method(state)
|
191
|
-
end
|
192
|
-
self
|
193
|
-
end
|
194
|
-
|
195
|
-
# Define state helper method
|
196
|
-
#
|
197
|
-
# @param [Symbol] state
|
198
|
-
#
|
199
|
-
# @api private
|
200
|
-
def define_state_query_method(state)
|
201
|
-
return if machine.respond_to?("#{state}?")
|
202
|
-
machine.send(:define_singleton_method, "#{state}?") do
|
203
|
-
machine.is?(state.to_sym)
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
# Set state on the machine
|
208
|
-
#
|
209
|
-
# @api private
|
210
|
-
def update_state(*args)
|
211
|
-
if transition_choice?
|
212
|
-
found_trans = machine.select_transition(name, *args)
|
213
|
-
machine.state = found_trans.to_states.first
|
109
|
+
def to_state(from)
|
110
|
+
if cancelled?
|
111
|
+
from
|
214
112
|
else
|
215
|
-
|
216
|
-
machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
# Find latest from state
|
221
|
-
#
|
222
|
-
# Note that for the exit hook the call hasn't happened yet so
|
223
|
-
# we need to find previous to state when the from is :any.
|
224
|
-
#
|
225
|
-
# @return [Object] from_state
|
226
|
-
#
|
227
|
-
# @api private
|
228
|
-
def latest_from_state
|
229
|
-
sync_shared do
|
230
|
-
from_state == ANY_STATE ? machine.previous_state : from_state
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
# Execute current transition
|
235
|
-
#
|
236
|
-
# @return [nil]
|
237
|
-
#
|
238
|
-
# @api private
|
239
|
-
def execute(*args)
|
240
|
-
sync_exclusive do
|
241
|
-
return if cancelled
|
242
|
-
self.from_state = machine.state
|
243
|
-
update_state(*args)
|
244
|
-
machine.previous_state = machine.state
|
245
|
-
machine.initial_state = machine.state if from_state == DEFAULT_STATE
|
113
|
+
states[from] || states[ANY_STATE]
|
246
114
|
end
|
247
115
|
end
|
248
116
|
|
249
117
|
# Return transition name
|
250
118
|
#
|
119
|
+
# @example
|
120
|
+
# transition = Transition.new(machine, name: :go)
|
121
|
+
# transition.to_s # => 'go'
|
122
|
+
#
|
251
123
|
# @return [String]
|
252
124
|
#
|
253
125
|
# @api public
|
@@ -1,8 +1,14 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
require 'finite_machine/state_parser'
|
4
|
+
require 'finite_machine/event_definition'
|
5
|
+
require 'finite_machine/state_definition'
|
6
|
+
|
3
7
|
module FiniteMachine
|
4
8
|
# A class reponsible for building transition out of parsed states
|
5
9
|
#
|
10
|
+
# Used internally by {DSL} to
|
11
|
+
#
|
6
12
|
# @api private
|
7
13
|
class TransitionBuilder
|
8
14
|
include Threadable
|
@@ -14,6 +20,8 @@ module FiniteMachine
|
|
14
20
|
|
15
21
|
attr_threadsafe :event_definition
|
16
22
|
|
23
|
+
attr_threadsafe :state_definition
|
24
|
+
|
17
25
|
# Initialize a TransitionBuilder
|
18
26
|
#
|
19
27
|
# @example
|
@@ -24,25 +32,35 @@ module FiniteMachine
|
|
24
32
|
@machine = machine
|
25
33
|
@attributes = attributes
|
26
34
|
@event_definition = EventDefinition.new(machine)
|
35
|
+
@state_definition = StateDefinition.new(machine)
|
27
36
|
end
|
28
37
|
|
29
38
|
# Creates transitions for the states
|
30
39
|
#
|
31
40
|
# @example
|
32
|
-
#
|
41
|
+
# transition_builder.call([:green, :yellow] => :red)
|
33
42
|
#
|
34
43
|
# @param [Hash[Symbol]] states
|
35
44
|
# The states to extract
|
36
45
|
#
|
37
|
-
# @return [
|
46
|
+
# @return [self]
|
38
47
|
#
|
39
48
|
# @api public
|
40
49
|
def call(states)
|
41
|
-
|
42
|
-
attributes.merge!(
|
43
|
-
transition = Transition.
|
44
|
-
|
50
|
+
StateParser.new(states).parse do |from, to|
|
51
|
+
attributes.merge!(states: { from => to })
|
52
|
+
transition = Transition.new(machine, attributes)
|
53
|
+
name = attributes[:name]
|
54
|
+
silent = attributes.fetch(:silent, false)
|
55
|
+
|
56
|
+
machine.events_chain.add(name, transition)
|
57
|
+
|
58
|
+
unless machine.respond_to?(name)
|
59
|
+
event_definition.apply(name, silent)
|
60
|
+
end
|
61
|
+
state_definition.apply({ from => to })
|
45
62
|
end
|
63
|
+
self
|
46
64
|
end
|
47
65
|
end # TransitionBuilder
|
48
66
|
end # FiniteMachine
|
@@ -35,10 +35,11 @@ module FiniteMachine
|
|
35
35
|
# @return [self]
|
36
36
|
#
|
37
37
|
# @api private
|
38
|
-
def initialize(transition, *data)
|
39
|
-
|
40
|
-
@
|
41
|
-
@
|
38
|
+
# def initialize(transition, *data)
|
39
|
+
def initialize(hook_event, to)
|
40
|
+
@name = hook_event.event_name
|
41
|
+
@from = hook_event.from
|
42
|
+
@to = to
|
42
43
|
freeze
|
43
44
|
end
|
44
45
|
end # TransitionEvent
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
# Stand in for lack of matching transition.
|
5
|
+
#
|
6
|
+
# Used internally by {EventsChain}
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class UndefinedTransition
|
10
|
+
include Threadable
|
11
|
+
|
12
|
+
# Initialize an undefined transition
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
def initialize(name)
|
16
|
+
self.name = name
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_state(from)
|
20
|
+
from
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
other.is_a?(UndefinedTransition) && name == other.name
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
attr_threadsafe :name
|
30
|
+
|
31
|
+
end # UndefinedTransition
|
32
|
+
end # FiniteMachine
|