motion-state-machine 0.8.1
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.
- 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
|