motion-state-machine 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +145 -0
- data/Rakefile +31 -0
- data/lib/motion-state-machine.rb +9 -0
- data/lib/motion-state-machine/base.rb +223 -0
- data/lib/motion-state-machine/spec_app_delegate.rb +7 -0
- data/lib/motion-state-machine/state.rb +379 -0
- data/lib/motion-state-machine/transition.rb +407 -0
- data/lib/motion-state-machine/version.rb +3 -0
- data/motion-state-machine.gemspec +19 -0
- data/spec/motion-state-machine/base_spec.rb +118 -0
- data/spec/motion-state-machine/benchmark_spec.rb +74 -0
- data/spec/motion-state-machine/notification_transition_spec.rb +57 -0
- data/spec/motion-state-machine/send_event_transition_spec.rb +53 -0
- data/spec/motion-state-machine/state_spec.rb +219 -0
- data/spec/motion-state-machine/timed_transition_spec.rb +48 -0
- data/spec/motion-state-machine/transition_spec.rb +157 -0
- metadata +90 -0
@@ -0,0 +1,379 @@
|
|
1
|
+
module StateMachine
|
2
|
+
class State
|
3
|
+
# @return [Symbol] The state's identifying symbol.
|
4
|
+
attr_reader :symbol
|
5
|
+
|
6
|
+
# @return [StateMachine::Base] the FSM that the state belongs to.
|
7
|
+
attr_reader :state_machine
|
8
|
+
|
9
|
+
# @return [String] the state's name. Only used in debug log output.
|
10
|
+
attr_accessor :name
|
11
|
+
|
12
|
+
# @return [Array] an array of +Proc+ objects called when entering
|
13
|
+
# the state.
|
14
|
+
attr_accessor :entry_actions
|
15
|
+
|
16
|
+
# @return [Array] an array of +Proc+ objects called when exiting
|
17
|
+
# the state.
|
18
|
+
attr_accessor :exit_actions
|
19
|
+
|
20
|
+
|
21
|
+
# @return [Hash] The state machine's internal transition map (event
|
22
|
+
# types -> event values -> possible transitions)
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# {
|
26
|
+
# :on => {
|
27
|
+
# :some_event => [transition1, transition2, ...],
|
28
|
+
# :other_event => [transition3, transition4, ...],
|
29
|
+
# },
|
30
|
+
# :after => {
|
31
|
+
# 5.0 => [transition5, transition6, ...]
|
32
|
+
# }
|
33
|
+
# }
|
34
|
+
|
35
|
+
attr_reader :transition_map
|
36
|
+
|
37
|
+
|
38
|
+
# Initializes a new State.
|
39
|
+
#
|
40
|
+
# @param [StateMachine] state_machine
|
41
|
+
# The state machine that the state belongs to.
|
42
|
+
#
|
43
|
+
# @param [Hash] options
|
44
|
+
# Configuration options for the state.
|
45
|
+
#
|
46
|
+
# @option options [Symbol] :symbol
|
47
|
+
# The state's identifier.
|
48
|
+
#
|
49
|
+
# @option options [String] :name (nil)
|
50
|
+
# The state's name. Only used in debug log output (optional).
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# StateMachine::State.new state_machine: my_fsm,
|
54
|
+
# :symbol => :doing_something,
|
55
|
+
# :name => "doing something very important"
|
56
|
+
#
|
57
|
+
# @return [StateMachine::State] a new State object
|
58
|
+
|
59
|
+
def initialize(state_machine, options)
|
60
|
+
@state_machine = state_machine
|
61
|
+
@symbol = options[:symbol]
|
62
|
+
@name = options[:name] || options[:symbol].to_s
|
63
|
+
if @symbol.nil? || @state_machine.nil?
|
64
|
+
raise ArgumentError, "Missing parameter"
|
65
|
+
end
|
66
|
+
|
67
|
+
@transition_map = {}
|
68
|
+
@entry_actions = []
|
69
|
+
@exit_actions = []
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
# @return [Boolean] indicates if the state is a termination state.
|
74
|
+
|
75
|
+
def terminating?
|
76
|
+
!!@terminating
|
77
|
+
end
|
78
|
+
|
79
|
+
def terminating=(value)
|
80
|
+
@terminating = !!value
|
81
|
+
end
|
82
|
+
|
83
|
+
# @api private
|
84
|
+
# Registers a transition in the transition map.
|
85
|
+
#
|
86
|
+
# @param [Transition] transition the transition to register.
|
87
|
+
|
88
|
+
def register(transition)
|
89
|
+
event_type = transition.class.instance_variable_get(:@event_type)
|
90
|
+
event_trigger_value = transition.event_trigger_value
|
91
|
+
|
92
|
+
transition_map[event_type] ||= {}
|
93
|
+
|
94
|
+
transitions =
|
95
|
+
(transition_map[event_type][event_trigger_value] ||= [])
|
96
|
+
transitions << transition
|
97
|
+
|
98
|
+
transition
|
99
|
+
end
|
100
|
+
|
101
|
+
class TransitionDefinitionDSL
|
102
|
+
|
103
|
+
# Initializes a new object that provides methods for configuring
|
104
|
+
# state transitions.
|
105
|
+
#
|
106
|
+
# See {Base#when} for an explanation how to use the DSL.
|
107
|
+
#
|
108
|
+
# @param [State] source_state
|
109
|
+
# The source state in which the transitions begin.
|
110
|
+
#
|
111
|
+
# @param [StateMachine] state_machine
|
112
|
+
# The state machine in which the transitions should be defined.
|
113
|
+
#
|
114
|
+
# @yieldparam [TransitionDefinitionDSL] state
|
115
|
+
# The DSL object. Call methods on this object to define
|
116
|
+
# transitions.
|
117
|
+
#
|
118
|
+
# @return [StateMachine::State::TransitionDefinitionDSL]
|
119
|
+
# the initialized object.
|
120
|
+
#
|
121
|
+
# @api private
|
122
|
+
# @see Base#when
|
123
|
+
|
124
|
+
def initialize(source_state, state_machine, &block)
|
125
|
+
@state_machine = state_machine
|
126
|
+
@state = source_state
|
127
|
+
yield(self)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Creates transitions to another state when defined events happen.
|
131
|
+
#
|
132
|
+
# If multiple trigger events are defined, any of them will create
|
133
|
+
# its own {Transition} object.
|
134
|
+
#
|
135
|
+
# You can specify guard blocks that can prevent a transition from
|
136
|
+
# happening.
|
137
|
+
#
|
138
|
+
# @param options [Hash]
|
139
|
+
# Configuration options for the transition.
|
140
|
+
#
|
141
|
+
# @option options [Symbol] :on
|
142
|
+
# Event symbol to trigger the transition via {Base#event}.
|
143
|
+
#
|
144
|
+
# @option options [String] :on_notification
|
145
|
+
# +NSNotification+ name that triggers the transition if posted
|
146
|
+
# via default +NSNotificationCenter+.
|
147
|
+
#
|
148
|
+
# @option options [Float] :after
|
149
|
+
# Defines a timeout after which the transition occurs if the
|
150
|
+
# state is not left before. Given in seconds.
|
151
|
+
#
|
152
|
+
# @option options [Proc] :if (nil)
|
153
|
+
# Block that should return a +Boolean+. Return +false+ in this
|
154
|
+
# block to prevent the transition from happening.
|
155
|
+
#
|
156
|
+
# @option options [Proc] :unless (nil)
|
157
|
+
# Block that should return a +Boolean+. Return +true+ in this
|
158
|
+
# block to prevent the transition from happening.
|
159
|
+
#
|
160
|
+
# @option options [Proc] :action (nil)
|
161
|
+
# Block that is executed after exiting the source state and
|
162
|
+
# before entering the destination state. Will be called with
|
163
|
+
# the state machine as first parameter.
|
164
|
+
#
|
165
|
+
# @option options [Boolean] :internal (false)
|
166
|
+
# For a transition from a state to itself: If +true+, the
|
167
|
+
# transition does not call entry/exit actions on the state
|
168
|
+
# when executed.
|
169
|
+
#
|
170
|
+
# @example
|
171
|
+
# fsm.when :loading do |state|
|
172
|
+
# state.transition_to :displaying_data,
|
173
|
+
# on: :data_loaded,
|
174
|
+
# if: proc { data.valid? },
|
175
|
+
# action: proc { dismiss_loading_indicator }
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
# @return [Array<StateMachine::Transition>] an array of all
|
179
|
+
# created transitions.
|
180
|
+
#
|
181
|
+
# @see Base#when
|
182
|
+
|
183
|
+
def transition_to(destination_state_symbol, options)
|
184
|
+
unless destination_state_symbol.is_a? Symbol
|
185
|
+
raise ArgumentError,
|
186
|
+
"No destination state given "\
|
187
|
+
"(got #{destination_state_symbol.inspect})"
|
188
|
+
end
|
189
|
+
|
190
|
+
options.merge! from: @state.symbol, to: destination_state_symbol
|
191
|
+
|
192
|
+
defined_event_types = Transition.types.select do |type|
|
193
|
+
!options[type].nil?
|
194
|
+
end
|
195
|
+
|
196
|
+
if defined_event_types.empty?
|
197
|
+
raise ArgumentError,
|
198
|
+
"No trigger event found in #{options}. "\
|
199
|
+
"Valid trigger event keys: #{Transition.types}."
|
200
|
+
end
|
201
|
+
|
202
|
+
transitions = []
|
203
|
+
|
204
|
+
defined_event_types.each do |type|
|
205
|
+
event_trigger_value = options[type]
|
206
|
+
if event_trigger_value
|
207
|
+
options.merge! state_machine: @state_machine,
|
208
|
+
type: type
|
209
|
+
transition = Transition.make options
|
210
|
+
@state.register(transition)
|
211
|
+
transitions << transition
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
transitions
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
# Defines a transition to a terminating state when a specified
|
220
|
+
# event happens. Works analog to {#transition_to}, but creates a
|
221
|
+
# terminating destination state automatically.
|
222
|
+
#
|
223
|
+
# @return [Array<StateMachine::Transition>]
|
224
|
+
# an array of all transitions that are defined in the option
|
225
|
+
# array (e.g. two transitions if you define an +:on+ and an
|
226
|
+
# +:after+ option, but no +:on_notification+ option).
|
227
|
+
#
|
228
|
+
# @see Base#when
|
229
|
+
|
230
|
+
def die(options)
|
231
|
+
termination_states = @state_machine.states.select(&:terminating?)
|
232
|
+
symbol = "terminated_#{termination_states.count}".to_sym
|
233
|
+
|
234
|
+
termination_state = @state_machine.state symbol
|
235
|
+
termination_state.terminating = true
|
236
|
+
|
237
|
+
transitions = transition_to(symbol, options)
|
238
|
+
event_texts = transitions.collect(&:event_log_text).join(" or ")
|
239
|
+
termination_state.name =
|
240
|
+
"terminated (internal state) #{event_texts}"
|
241
|
+
|
242
|
+
transitions
|
243
|
+
end
|
244
|
+
|
245
|
+
|
246
|
+
# Defines a block that will be called without parameters when the
|
247
|
+
# source state is entered.
|
248
|
+
#
|
249
|
+
# @see Base#when
|
250
|
+
|
251
|
+
def on_entry(&block)
|
252
|
+
@state.entry_actions << block
|
253
|
+
end
|
254
|
+
|
255
|
+
|
256
|
+
# Defines a block that will be called without parameters when the
|
257
|
+
# source state is exited.
|
258
|
+
#
|
259
|
+
# @see Base#when
|
260
|
+
|
261
|
+
def on_exit(&block)
|
262
|
+
@state.exit_actions << block
|
263
|
+
end
|
264
|
+
|
265
|
+
end
|
266
|
+
|
267
|
+
|
268
|
+
private
|
269
|
+
|
270
|
+
|
271
|
+
# Adds the outgoing transitions defined in the given block to the
|
272
|
+
# state.
|
273
|
+
|
274
|
+
def add_transition_map_defined_in(&block)
|
275
|
+
TransitionDefinitionDSL.new self, @state_machine, &block
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
# Sets the state machine's current_state to self, calls all entry
|
280
|
+
# actions and activates triggering mechanisms of all outgoing
|
281
|
+
# transitions.
|
282
|
+
|
283
|
+
def enter!
|
284
|
+
@state_machine.current_state = self
|
285
|
+
|
286
|
+
@entry_actions.each do |entry_action|
|
287
|
+
entry_action.call(@state_machine)
|
288
|
+
end
|
289
|
+
@transition_map.each do |type, events_to_transition_arrays|
|
290
|
+
events_to_transition_arrays.each do |event, transitions|
|
291
|
+
transitions.each(&:arm)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
|
297
|
+
# Sets the state machine's current_state to nil, calls all exit
|
298
|
+
# actions and deactivates triggering mechanisms of all outgoing
|
299
|
+
# transitions.
|
300
|
+
|
301
|
+
def exit!
|
302
|
+
map = @transition_map
|
303
|
+
map.each do |type, events_to_transition_arrays|
|
304
|
+
events_to_transition_arrays.each do |event, transitions|
|
305
|
+
transitions.each(&:unarm)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
@exit_actions.each do |exit_action|
|
310
|
+
exit_action.call(@state_machine)
|
311
|
+
end
|
312
|
+
@state_machine.current_state = nil
|
313
|
+
end
|
314
|
+
|
315
|
+
|
316
|
+
# Cleans up references to allow easier GC.
|
317
|
+
|
318
|
+
def cleanup
|
319
|
+
@transition_map.each do |type, events_to_transition_arrays|
|
320
|
+
events_to_transition_arrays.each do |event, transitions|
|
321
|
+
transitions.clear
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
@transition_map = nil
|
326
|
+
@state_machine = nil
|
327
|
+
@entry_actions = nil
|
328
|
+
@exit_actions = nil
|
329
|
+
end
|
330
|
+
|
331
|
+
|
332
|
+
# Executes the registered transition for the given event type and
|
333
|
+
# event trigger value, if such a transition exists and it is
|
334
|
+
# allowed.
|
335
|
+
#
|
336
|
+
# @raise [RuntimeError] if multiple transitions would be allowed at
|
337
|
+
# the same time.
|
338
|
+
|
339
|
+
def guarded_execute(event_type, event_trigger_value)
|
340
|
+
@state_machine.raise_outside_initial_queue
|
341
|
+
|
342
|
+
return if terminating?
|
343
|
+
|
344
|
+
if @transition_map[event_type].nil? ||
|
345
|
+
@transition_map[event_type][event_trigger_value].nil?
|
346
|
+
raise ArgumentError,
|
347
|
+
"No registered transition found "\
|
348
|
+
"for event #{event_type}:#{event_trigger_value}."
|
349
|
+
end
|
350
|
+
|
351
|
+
possible_transitions =
|
352
|
+
@transition_map[event_type][event_trigger_value]
|
353
|
+
|
354
|
+
unless possible_transitions.empty?
|
355
|
+
allowed_transitions = possible_transitions.select(&:allowed?)
|
356
|
+
|
357
|
+
if allowed_transitions.empty?
|
358
|
+
@state_machine.log "All transitions are disallowed for "\
|
359
|
+
"#{event_type}:#{event_trigger_value}."
|
360
|
+
elsif allowed_transitions.count > 1
|
361
|
+
list = allowed_transitions.collect do |t|
|
362
|
+
"-> #{t.options[:to]}"
|
363
|
+
end
|
364
|
+
raise RuntimeError,
|
365
|
+
"Not sure which transition to trigger "\
|
366
|
+
"when #{symbol} while #{self} (allowed: #{list}). "\
|
367
|
+
"Please make sure guard conditions exclude each other."
|
368
|
+
else
|
369
|
+
transition = allowed_transitions.first
|
370
|
+
unless transition.nil?
|
371
|
+
transition.send :unguarded_execute
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
end
|
379
|
+
end
|
@@ -0,0 +1,407 @@
|
|
1
|
+
module StateMachine
|
2
|
+
|
3
|
+
# See subclasses for various transition implementations.
|
4
|
+
# Sorry for putting multiple classes in one file —
|
5
|
+
# RubyMotion has no decentral dependency management yet...
|
6
|
+
|
7
|
+
# @abstract Subclass and override {#event_description}, {#arm} and {#unarm} to implement a custom Transition class.
|
8
|
+
|
9
|
+
class Transition
|
10
|
+
|
11
|
+
# @return [Hash] configuration options of the transition.
|
12
|
+
attr_reader :options
|
13
|
+
|
14
|
+
# @return [Base] the state machine that this transition belongs to.
|
15
|
+
attr_reader :state_machine
|
16
|
+
|
17
|
+
# @return [State] the state from which the transition starts.
|
18
|
+
attr_reader :source_state
|
19
|
+
|
20
|
+
# @return [State] the state that the transition leads to.
|
21
|
+
attr_reader :destination_state
|
22
|
+
|
23
|
+
# @return [Object] a more specific object that triggers
|
24
|
+
# the transition.
|
25
|
+
attr_reader :event_trigger_value
|
26
|
+
|
27
|
+
|
28
|
+
class << self
|
29
|
+
# @return [Symbol] Metaclass attribute, contains the key that
|
30
|
+
# is used for generating the specific transition via {#make}.
|
31
|
+
attr_accessor :event_type
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
@@types_to_subclasses = {}
|
36
|
+
|
37
|
+
# @return [Array<Class<Transition>>] a list of all registered transition subclasses
|
38
|
+
def self.types
|
39
|
+
@@types_to_subclasses.keys
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
# Creates a new {Transition} object with the given options.
|
44
|
+
# The returned object's subclass is determined by the
|
45
|
+
# +:type+ option.
|
46
|
+
#
|
47
|
+
# @param options [Hash] Configuration options for the transition.
|
48
|
+
# @option options [Symbol] :type Type identifier for the transition, ex. +:on+, +:after+, +:on_notification+.
|
49
|
+
#
|
50
|
+
# See {#initialize} for all possible options.
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# StateMachine::Transition.make type: :to, ... # => #<StateMachine::Transition:...>
|
54
|
+
# @return [Transition] a new object with the class that fits the given +:type+ option.
|
55
|
+
|
56
|
+
def self.make(options)
|
57
|
+
klass = @@types_to_subclasses[options[:type]]
|
58
|
+
klass.new options
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
# Initializes a new {Transition} between two given states.
|
63
|
+
#
|
64
|
+
# Additionally, you must also supply an event trigger value as
|
65
|
+
# option. Its key must be equal to the transition class's
|
66
|
+
# +event_type+., ex. if +event_type+ is +:on+, you have to supply
|
67
|
+
# the event value using the option key +:on+.
|
68
|
+
#
|
69
|
+
# @param options [Hash] Configuration options for the transition.
|
70
|
+
#
|
71
|
+
# @option options [StateMachine::Base] :state_machine
|
72
|
+
# State machine that the transition belongs to.
|
73
|
+
#
|
74
|
+
# @option options [Symbol] :from
|
75
|
+
# State where the transition begins.
|
76
|
+
#
|
77
|
+
# @option options [Symbol] :to
|
78
|
+
# State where the transition ends.
|
79
|
+
#
|
80
|
+
# @option options [Proc] :if (nil)
|
81
|
+
# Block that should return a +Boolean+. If the block returns
|
82
|
+
# +false+, the transition will be blocked and not executed
|
83
|
+
# (optional).
|
84
|
+
#
|
85
|
+
# @option options [Proc] :unless (nil)
|
86
|
+
# Block that should return a +Boolean+. If the block returns
|
87
|
+
# +true+, the transition will be blocked and not executed
|
88
|
+
# (optional).
|
89
|
+
#
|
90
|
+
# @option options [Boolean] :internal (false)
|
91
|
+
# If set to true, the transition will not leave it's source
|
92
|
+
# state: Entry and Exit actions will not be called in this case.
|
93
|
+
# For internal transitions, +:from+ and +:to+ must be the same
|
94
|
+
# (optional).
|
95
|
+
#
|
96
|
+
# @note This method should not be used directly. To create +Transition+ objects, use {#make} instead.
|
97
|
+
|
98
|
+
def initialize(options)
|
99
|
+
@options = options.dup
|
100
|
+
@state_machine = @options.delete :state_machine
|
101
|
+
@source_state = @state_machine.state options[:from]
|
102
|
+
@destination_state = @state_machine.state options[:to]
|
103
|
+
|
104
|
+
event_type = self.class.event_type
|
105
|
+
if event_type.nil?
|
106
|
+
raise RuntimeError, "#{self.class} has no defined event type."
|
107
|
+
end
|
108
|
+
|
109
|
+
[:from, :to].each do |symbol|
|
110
|
+
unless options[symbol].is_a?(Symbol)
|
111
|
+
raise ":#{symbol} option must be given as symbol."
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
@event_trigger_value = options[event_type]
|
116
|
+
if @event_trigger_value.nil?
|
117
|
+
raise ArgumentError, "You must supply an event trigger value."
|
118
|
+
end
|
119
|
+
|
120
|
+
if options[:internal] && options[:from] != options[:to]
|
121
|
+
raise ArgumentError,
|
122
|
+
"Internal states must have same source and destination state."
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
# @return [Boolean] Asks the guard blocks given for +:if+ and
|
128
|
+
# +:unless+ if the transition is allowed. Returns +true+ if the
|
129
|
+
# transition is allowed to be executed.
|
130
|
+
|
131
|
+
def allowed?
|
132
|
+
if_guard = options[:if]
|
133
|
+
unless if_guard.nil?
|
134
|
+
return false unless if_guard.call(@state_machine)
|
135
|
+
end
|
136
|
+
unless_guard = options[:unless]
|
137
|
+
unless unless_guard.nil?
|
138
|
+
return false if unless_guard.call(@state_machine)
|
139
|
+
end
|
140
|
+
true
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
# @return [String] a short description of the event.
|
145
|
+
# Used for debug output.
|
146
|
+
|
147
|
+
def event_description
|
148
|
+
# Implement this in a subclass.
|
149
|
+
"after unclassified event"
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
def inspect
|
154
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} "\
|
155
|
+
"#{event_description} @options=#{options.inspect}>"
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
protected
|
160
|
+
|
161
|
+
|
162
|
+
# Defines a +Hash+ key symbol that is unique to a {Transition}
|
163
|
+
# subclass. The key is used by {#make} to identify which
|
164
|
+
# {Transition} subclass should be created.
|
165
|
+
#
|
166
|
+
# @param [Symbol] type_symbol
|
167
|
+
# Unique symbol identifying your transition subclass.
|
168
|
+
|
169
|
+
def self.type(type_symbol)
|
170
|
+
unless type_symbol.is_a?(Symbol)
|
171
|
+
raise ArgumentError, "Type must be given as symbol."
|
172
|
+
end
|
173
|
+
if @@types_to_subclasses[type_symbol].nil?
|
174
|
+
@@types_to_subclasses[type_symbol] = self
|
175
|
+
else
|
176
|
+
other_class = @@types_to_subclasses[type_symbol]
|
177
|
+
raise ArgumentError,
|
178
|
+
"Can't register :#{type_symbol} twice, "
|
179
|
+
"already used by #{other_class}."
|
180
|
+
end
|
181
|
+
@event_type = type_symbol
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
# Delegates handling the transition to the source state, which
|
186
|
+
# makes sure that there are no ambiguous transitions for the
|
187
|
+
# same event.
|
188
|
+
|
189
|
+
def handle_in_source_state
|
190
|
+
if @state_machine.initial_queue.nil?
|
191
|
+
raise RuntimeError, "State machine not started yet."
|
192
|
+
end
|
193
|
+
|
194
|
+
if Dispatch::Queue.current.to_s != @state_machine.initial_queue.to_s
|
195
|
+
raise RuntimeError,
|
196
|
+
"#{self.class.event_type}:#{@event_trigger_value} must be "\
|
197
|
+
"called from the queue where the state machine was started."
|
198
|
+
end
|
199
|
+
|
200
|
+
@source_state.send :guarded_execute,
|
201
|
+
self.class.event_type,
|
202
|
+
@event_trigger_value
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
|
209
|
+
# Executed the transition directly, without checking the guard
|
210
|
+
# blocks. Calls {State#exit!} on the source state, executes
|
211
|
+
# the transition's +:action+ block and calls {State#enter!} on
|
212
|
+
# the destination state.
|
213
|
+
|
214
|
+
def unguarded_execute
|
215
|
+
@source_state.send :exit! unless @options[:internal] == true
|
216
|
+
# Could use @state_machine.instance_eval(&options[:action]) here,
|
217
|
+
# but this would be much slower
|
218
|
+
@options[:action] && @options[:action].call(@state_machine)
|
219
|
+
@destination_state.send :enter! unless @options[:internal] == true
|
220
|
+
|
221
|
+
@state_machine.log "#{event_log_text}"
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
# @return [String] Debug string that can be logged after the
|
226
|
+
# transition has been executed.
|
227
|
+
|
228
|
+
def event_log_text
|
229
|
+
if @options[:internal]
|
230
|
+
"#{@state_machine.name} still #{destination_state.name} "\
|
231
|
+
"#{event_description} (internal transition, not exiting state)."
|
232
|
+
else
|
233
|
+
"#{@state_machine.name} #{destination_state.name} "\
|
234
|
+
"#{event_description}."
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
# Called by source {State} when it is entered. Allows the
|
240
|
+
# transition to initialize a mechanism that catches its trigger
|
241
|
+
# event. Override this in a subclass.
|
242
|
+
|
243
|
+
def arm
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
# Called by source {State} when it is exited. Subclass
|
248
|
+
# implementations should deactivate their triggering
|
249
|
+
# mechanism in this method.
|
250
|
+
|
251
|
+
def unarm
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|
255
|
+
|
256
|
+
|
257
|
+
# This is kind of the 'standard' transition. It is created when
|
258
|
+
# supplying the +:on+ option in the state machine definition.
|
259
|
+
#
|
260
|
+
# If armed, the transition is triggered when sending an event to the
|
261
|
+
# state machine by calling {StateMachine::Base#event} with the
|
262
|
+
# event's symbol as parameter. Sending the same event symbol while
|
263
|
+
# not armed will just ignore the event.
|
264
|
+
#
|
265
|
+
# Note that you should call the {StateMachine::Base#event} method
|
266
|
+
# from the same queue / thread where the state machine was started.
|
267
|
+
#
|
268
|
+
# @example Create a {SendEventTransition}:
|
269
|
+
#
|
270
|
+
# state_machine.when :sleeping do |state|
|
271
|
+
# state.transition_to :awake, on: :foo
|
272
|
+
# end
|
273
|
+
#
|
274
|
+
# state_machine.event :foo
|
275
|
+
# # => state machine goes to :awake state
|
276
|
+
|
277
|
+
class SendEventTransition < Transition
|
278
|
+
type :on
|
279
|
+
|
280
|
+
def initialize(options)
|
281
|
+
super(options)
|
282
|
+
unarm
|
283
|
+
end
|
284
|
+
|
285
|
+
def event_description
|
286
|
+
"after #{event_trigger_value}"
|
287
|
+
end
|
288
|
+
|
289
|
+
def arm
|
290
|
+
state_machine.register_event_handler event_trigger_value, self
|
291
|
+
end
|
292
|
+
|
293
|
+
def unarm
|
294
|
+
state_machine.register_event_handler event_trigger_value, nil
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
|
299
|
+
# Transitions of this type are triggered on a given timeout (in
|
300
|
+
# seconds). Created when supplying an :after option in the transition
|
301
|
+
# definition.
|
302
|
+
#
|
303
|
+
# The timeout is canceled when the state is left.
|
304
|
+
#
|
305
|
+
# The transition uses Grand Central Dispatch's timer source
|
306
|
+
# mechanism: It adds a timer source to the state machine's initial
|
307
|
+
# GCD queue.
|
308
|
+
#
|
309
|
+
# The system tries to achieve an accuracy of 1 millisecond for the
|
310
|
+
# timeout. You can change this behavior to trade timing accuracy vs.
|
311
|
+
# system performance by using the +leeway+ option (given in seconds).
|
312
|
+
# See {http://developer.apple.com/mac/library/DOCUMENTATION/Darwin/Reference/ManPages/man3/dispatch_source_set_timer.3.html Apple's GCD documentation}
|
313
|
+
# for more information about this parameter.
|
314
|
+
#
|
315
|
+
# @example Create a transition that timeouts after 8 hours:
|
316
|
+
#
|
317
|
+
# state_machine.when :sleeping do |state|
|
318
|
+
# # Timeout after 28800 seconds
|
319
|
+
# state.transition_to :awake, after: 8 * 60 * 60
|
320
|
+
# end
|
321
|
+
|
322
|
+
class TimedTransition < Transition
|
323
|
+
type :after
|
324
|
+
|
325
|
+
def event_description
|
326
|
+
"after #{event_trigger_value} seconds of "\
|
327
|
+
"#{source_state.name} (timeout)"
|
328
|
+
end
|
329
|
+
|
330
|
+
#
|
331
|
+
def arm
|
332
|
+
@state_machine.log "Starting timeout -> #{options[:to]}, "\
|
333
|
+
"after #{options[:after]}"
|
334
|
+
delay = event_trigger_value
|
335
|
+
interval = 0
|
336
|
+
leeway = @options[:leeway] || 0.001
|
337
|
+
queue = @state_machine.initial_queue
|
338
|
+
@timer = Dispatch::Source.timer(delay, interval, leeway, queue) do
|
339
|
+
@state_machine.log "Timeout!"
|
340
|
+
self.handle_in_source_state
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
def unarm
|
345
|
+
@state_machine.log "Timer unarmed"
|
346
|
+
@timer.cancel!
|
347
|
+
end
|
348
|
+
|
349
|
+
end
|
350
|
+
|
351
|
+
|
352
|
+
# Triggered on a specified +NSNotification+ name. Created when
|
353
|
+
# supplying an +:on_notification+ option in the transition definition.
|
354
|
+
#
|
355
|
+
# On entering the source state, the transition registers itself as
|
356
|
+
# +NSNotification+ observer on the default +NSNotificationCenter+. It
|
357
|
+
# deregisters when the state is exited.
|
358
|
+
#
|
359
|
+
# @example
|
360
|
+
# state_machine.when :awake do |state|
|
361
|
+
# state.transition_to :sleeping,
|
362
|
+
# on_notificaiton: UIApplicationDidEnterBackgroundNotification
|
363
|
+
# end
|
364
|
+
|
365
|
+
class NotificationTransition < Transition
|
366
|
+
type :on_notification
|
367
|
+
|
368
|
+
def initialize(options)
|
369
|
+
super options
|
370
|
+
end
|
371
|
+
|
372
|
+
def event_description
|
373
|
+
"after getting a #{event_trigger_value}"
|
374
|
+
end
|
375
|
+
|
376
|
+
def arm
|
377
|
+
NSNotificationCenter.defaultCenter.addObserver self,
|
378
|
+
selector: :handle_in_initial_queue,
|
379
|
+
name:event_trigger_value,
|
380
|
+
object:nil
|
381
|
+
@state_machine.log "Registered notification #{event_trigger_value}"
|
382
|
+
end
|
383
|
+
|
384
|
+
def unarm
|
385
|
+
NSNotificationCenter.defaultCenter.removeObserver self
|
386
|
+
@state_machine.log "Removed as observer"
|
387
|
+
end
|
388
|
+
|
389
|
+
|
390
|
+
private
|
391
|
+
|
392
|
+
# This makes sure that state entry/exit and transition actions are
|
393
|
+
# called within the initial queue/thread.
|
394
|
+
|
395
|
+
def handle_in_initial_queue
|
396
|
+
if state_machine.initial_queue.to_s == Dispatch::Queue.main.to_s
|
397
|
+
handle_in_source_state
|
398
|
+
else
|
399
|
+
state_machine.initial_queue.async do
|
400
|
+
handle_in_source_state
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
end
|
406
|
+
|
407
|
+
end
|