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
@@ -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
|