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
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
# A class responsible for defining state query methods on state machine
|
5
|
+
#
|
6
|
+
# Used by {TranstionBuilder} to add state query definition
|
7
|
+
# to the {StateMachine} instance.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class StateDefinition
|
11
|
+
include Threadable
|
12
|
+
|
13
|
+
# Initialize a StateDefinition
|
14
|
+
#
|
15
|
+
# @param [StateMachine] machine
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
def initialize(machine)
|
19
|
+
self.machine = machine
|
20
|
+
end
|
21
|
+
|
22
|
+
# Define query methods for states
|
23
|
+
#
|
24
|
+
# @param [Hash] states
|
25
|
+
# the states that require query helpers
|
26
|
+
#
|
27
|
+
# @return [nil]
|
28
|
+
#
|
29
|
+
# @api public
|
30
|
+
def apply(states)
|
31
|
+
define_state_query_methods(states)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# The current state machine
|
37
|
+
attr_threadsafe :machine
|
38
|
+
|
39
|
+
# Define helper state mehods for the transition states
|
40
|
+
#
|
41
|
+
# @param [Hash] states
|
42
|
+
# the states to define helpers for
|
43
|
+
#
|
44
|
+
# @return [nil]
|
45
|
+
#
|
46
|
+
# @api private
|
47
|
+
def define_state_query_methods(states)
|
48
|
+
states.to_a.flatten.each do |state|
|
49
|
+
define_state_query_method(state)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Define state helper method
|
54
|
+
#
|
55
|
+
# @param [Symbol] state
|
56
|
+
# the state to define helper for
|
57
|
+
#
|
58
|
+
# @api private
|
59
|
+
def define_state_query_method(state)
|
60
|
+
return if machine.respond_to?("#{state}?")
|
61
|
+
machine.send(:define_singleton_method, "#{state}?") do
|
62
|
+
machine.is?(state.to_sym)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end # StateDefinition
|
66
|
+
end # FiniteMachine
|
@@ -14,92 +14,87 @@ module FiniteMachine
|
|
14
14
|
# Final state, defaults to :none
|
15
15
|
attr_threadsafe :final_state
|
16
16
|
|
17
|
-
#
|
18
|
-
attr_threadsafe :
|
17
|
+
# The prefix used to name events.
|
18
|
+
attr_threadsafe :namespace
|
19
19
|
|
20
|
-
#
|
21
|
-
attr_threadsafe :
|
20
|
+
# The state machine environment
|
21
|
+
attr_threadsafe :env
|
22
22
|
|
23
|
-
#
|
24
|
-
attr_threadsafe :
|
23
|
+
# The state machine event definitions
|
24
|
+
attr_threadsafe :events_chain
|
25
25
|
|
26
|
-
#
|
27
|
-
|
26
|
+
# Errors DSL
|
27
|
+
#
|
28
|
+
# @return [ErrorsDSL]
|
29
|
+
#
|
30
|
+
# @api private
|
31
|
+
attr_threadsafe :errors_dsl
|
28
32
|
|
29
|
-
#
|
30
|
-
|
33
|
+
# Events DSL
|
34
|
+
#
|
35
|
+
# @return [EventsDSL]
|
36
|
+
#
|
37
|
+
# @api private
|
38
|
+
attr_threadsafe :events_dsl
|
31
39
|
|
32
40
|
# The state machine observer
|
41
|
+
#
|
42
|
+
# @return [Observer]
|
43
|
+
#
|
44
|
+
# @api private
|
33
45
|
attr_threadsafe :observer
|
34
46
|
|
47
|
+
# Current state
|
48
|
+
#
|
49
|
+
# @return [Symbol]
|
50
|
+
#
|
51
|
+
# @api private
|
52
|
+
attr_threadsafe :state
|
53
|
+
|
35
54
|
# The state machine subscribers
|
55
|
+
#
|
56
|
+
# @return [Subscribers]
|
57
|
+
#
|
58
|
+
# @api private
|
36
59
|
attr_threadsafe :subscribers
|
37
60
|
|
38
|
-
# The state machine environment
|
39
|
-
attr_threadsafe :env
|
40
|
-
|
41
|
-
# The previous state before transition
|
42
|
-
attr_threadsafe :previous_state
|
43
|
-
|
44
|
-
# The state machine event definitions
|
45
|
-
attr_threadsafe :events_chain
|
46
|
-
|
47
61
|
# Allow or not logging of transitions
|
48
62
|
attr_threadsafe :log_transitions
|
49
63
|
|
50
64
|
def_delegators :@dsl, :initial, :terminal, :target, :trigger_init,
|
51
65
|
:alias_target
|
52
66
|
|
53
|
-
def_delegator
|
54
|
-
|
55
|
-
def_delegators :@events_chain, :check_choice_conditions, :select_transition,
|
56
|
-
:select_choice_transition
|
67
|
+
def_delegator :events_dsl, :event
|
57
68
|
|
58
69
|
# Initialize state machine
|
59
70
|
#
|
60
71
|
# @api private
|
61
72
|
def initialize(*args, &block)
|
62
73
|
attributes = args.last.is_a?(Hash) ? args.pop : {}
|
74
|
+
self.event_queue = EventQueue.new
|
75
|
+
|
63
76
|
@initial_state = DEFAULT_STATE
|
64
|
-
@
|
77
|
+
@async_proxy = AsyncProxy.new(self)
|
78
|
+
@subscribers = Subscribers.new
|
65
79
|
@observer = Observer.new(self)
|
66
|
-
@
|
67
|
-
@events_chain = EventsChain.new(self)
|
80
|
+
@events_chain = EventsChain.new
|
68
81
|
@env = Env.new(self, [])
|
69
82
|
@events_dsl = EventsDSL.new(self)
|
70
|
-
@
|
83
|
+
@errors_dsl = ErrorsDSL.new(self)
|
71
84
|
@dsl = DSL.new(self, attributes)
|
72
85
|
|
73
86
|
@dsl.call(&block) if block_given?
|
74
87
|
trigger_init
|
75
88
|
end
|
76
89
|
|
90
|
+
# Subscribe observer for event notifications
|
91
|
+
#
|
77
92
|
# @example
|
78
93
|
# machine.subscribe(Observer.new(machine))
|
79
94
|
#
|
80
95
|
# @api public
|
81
96
|
def subscribe(*observers)
|
82
|
-
|
83
|
-
end
|
84
|
-
|
85
|
-
# TODO: use trigger to actually fire state machine events!
|
86
|
-
# Notify about event all the subscribers
|
87
|
-
#
|
88
|
-
# @param [FiniteMachine::HookEvent] :event_type
|
89
|
-
# The hook event type.
|
90
|
-
# @param [FiniteMachine::Transition] :event_transition
|
91
|
-
# The event transition.
|
92
|
-
# @param [Array[Object]] :data
|
93
|
-
# The data associated with the hook event.
|
94
|
-
#
|
95
|
-
# @return [nil]
|
96
|
-
#
|
97
|
-
# @api public
|
98
|
-
def notify(event_type, event_transition, *data)
|
99
|
-
sync_shared do
|
100
|
-
hook_event = event_type.build(state, event_transition, *data)
|
101
|
-
subscribers.visit(hook_event)
|
102
|
-
end
|
97
|
+
sync_exclusive { subscribers.subscribe(*observers) }
|
103
98
|
end
|
104
99
|
|
105
100
|
# Help to mark the event as synchronous
|
@@ -118,7 +113,6 @@ module FiniteMachine
|
|
118
113
|
#
|
119
114
|
# @api public
|
120
115
|
def async(method_name = nil, *args, &block)
|
121
|
-
@async_proxy = AsyncProxy.new(self)
|
122
116
|
if method_name
|
123
117
|
@async_proxy.method_missing method_name, *args, &block
|
124
118
|
else
|
@@ -162,18 +156,19 @@ module FiniteMachine
|
|
162
156
|
#
|
163
157
|
# @api public
|
164
158
|
def states
|
165
|
-
sync_shared
|
166
|
-
event_names.map { |event| transitions[event].to_a }.flatten.uniq
|
167
|
-
end
|
159
|
+
sync_shared { events_chain.states }
|
168
160
|
end
|
169
161
|
|
170
162
|
# Retireve all event names
|
171
163
|
#
|
164
|
+
# @example
|
165
|
+
# fsm.event_names # => [:init, :start, :stop]
|
166
|
+
#
|
172
167
|
# @return [Array[Symbol]]
|
173
168
|
#
|
174
169
|
# @api public
|
175
170
|
def event_names
|
176
|
-
sync_shared {
|
171
|
+
sync_shared { events_chain.events }
|
177
172
|
end
|
178
173
|
|
179
174
|
# Checks if event can be triggered
|
@@ -189,11 +184,9 @@ module FiniteMachine
|
|
189
184
|
# @return [Boolean]
|
190
185
|
#
|
191
186
|
# @api public
|
192
|
-
def can?(*args
|
193
|
-
|
194
|
-
|
195
|
-
valid_state ||= transitions[event].key?(ANY_STATE)
|
196
|
-
valid_state && events_chain.valid_event?(event, *args, &block)
|
187
|
+
def can?(*args)
|
188
|
+
event_name = args.shift
|
189
|
+
events_chain.can_perform?(event_name, current, *args)
|
197
190
|
end
|
198
191
|
|
199
192
|
# Checks if event cannot be triggered
|
@@ -230,82 +223,167 @@ module FiniteMachine
|
|
230
223
|
sync_exclusive { self.state = state }
|
231
224
|
end
|
232
225
|
|
233
|
-
#
|
226
|
+
# Check if state is reachable
|
234
227
|
#
|
235
|
-
# @
|
228
|
+
# @param [Symbol] event_name
|
229
|
+
# the event name for all transitions
|
236
230
|
#
|
237
|
-
# @
|
238
|
-
|
231
|
+
# @return [Boolean]
|
232
|
+
#
|
233
|
+
# @api private
|
234
|
+
def valid_state?(event_name)
|
235
|
+
current_states = events_chain.states_for(event_name)
|
236
|
+
current_states.any? { |state| state == current || state == ANY_STATE }
|
237
|
+
end
|
238
|
+
|
239
|
+
# Notify about event all the subscribers
|
240
|
+
#
|
241
|
+
# @param [HookEvent] :hook_event_type
|
242
|
+
# The hook event type.
|
243
|
+
# @param [FiniteMachine::Transition] :event_transition
|
244
|
+
# The event transition.
|
245
|
+
# @param [Array[Object]] :data
|
246
|
+
# The data associated with the hook event.
|
247
|
+
#
|
248
|
+
# @return [nil]
|
249
|
+
#
|
250
|
+
# @api private
|
251
|
+
def notify(hook_event_type, event_name, from, *data)
|
239
252
|
sync_shared do
|
240
|
-
|
241
|
-
|
253
|
+
hook_event = hook_event_type.build(current, event_name, from)
|
254
|
+
subscribers.visit(hook_event, *data)
|
242
255
|
end
|
243
256
|
end
|
244
257
|
|
245
|
-
|
246
|
-
|
247
|
-
# Check if state is reachable
|
248
|
-
#
|
249
|
-
# @param [FiniteMachine::Transition]
|
258
|
+
# Attempt performing event trigger for valid state
|
250
259
|
#
|
251
260
|
# @return [Boolean]
|
261
|
+
# true is trigger successful, false otherwise
|
252
262
|
#
|
253
263
|
# @api private
|
254
|
-
def
|
255
|
-
|
256
|
-
|
264
|
+
def try_trigger(event_name)
|
265
|
+
if valid_state?(event_name)
|
266
|
+
yield
|
267
|
+
else
|
257
268
|
exception = InvalidStateError
|
258
269
|
catch_error(exception) ||
|
259
|
-
fail(exception, "inappropriate current state '#{
|
260
|
-
|
270
|
+
fail(exception, "inappropriate current state '#{current}'")
|
271
|
+
|
272
|
+
false
|
261
273
|
end
|
262
|
-
return true
|
263
274
|
end
|
264
275
|
|
265
|
-
#
|
276
|
+
# Trigger transition event with data
|
266
277
|
#
|
267
|
-
# @param [
|
268
|
-
#
|
278
|
+
# @param [Symbol] event_name
|
279
|
+
# the event name
|
280
|
+
# @param [Array] data
|
269
281
|
#
|
270
|
-
# @return [
|
271
|
-
#
|
282
|
+
# @return [Boolean]
|
283
|
+
# true when transition is successful, false otherwise
|
272
284
|
#
|
273
|
-
# @api
|
274
|
-
def
|
285
|
+
# @api public
|
286
|
+
def trigger!(event_name, *data, &block)
|
275
287
|
sync_exclusive do
|
276
|
-
|
288
|
+
from = current # Save away current state
|
289
|
+
|
290
|
+
notify HookEvent::Before, event_name, from, *data
|
277
291
|
|
278
|
-
|
279
|
-
|
292
|
+
status = try_trigger(event_name) do
|
293
|
+
if can?(event_name, *data)
|
294
|
+
notify HookEvent::Exit, event_name, from, *data
|
280
295
|
|
281
|
-
|
282
|
-
event_transition.execute(*args)
|
283
|
-
Logger.report_transition(event_transition, *args) if log_transitions
|
296
|
+
stat = transition!(event_name, *data, &block)
|
284
297
|
|
285
|
-
notify HookEvent::Transition,
|
286
|
-
|
287
|
-
|
298
|
+
notify HookEvent::Transition, event_name, from, *data
|
299
|
+
notify HookEvent::Enter, event_name, from, *data
|
300
|
+
else
|
301
|
+
stat = false
|
288
302
|
end
|
303
|
+
stat
|
304
|
+
end
|
305
|
+
|
306
|
+
notify HookEvent::After, event_name, from, *data
|
307
|
+
|
308
|
+
status
|
309
|
+
end
|
310
|
+
end
|
289
311
|
|
290
|
-
|
312
|
+
# Trigger transition event without raising any errors
|
313
|
+
#
|
314
|
+
# @param [Symbol] event_name
|
315
|
+
#
|
316
|
+
# @return [Boolean]
|
317
|
+
# true on successful transition, false otherwise
|
318
|
+
#
|
319
|
+
# @api public
|
320
|
+
def trigger(event_name, *data, &block)
|
321
|
+
trigger!(event_name, *data, &block)
|
322
|
+
rescue InvalidStateError, TransitionError
|
323
|
+
false
|
324
|
+
end
|
291
325
|
|
292
|
-
|
326
|
+
# Find available state to transition to and transition
|
327
|
+
#
|
328
|
+
# @param [Symbol] event_name
|
329
|
+
#
|
330
|
+
# @api private
|
331
|
+
def transition!(event_name, *data, &block)
|
332
|
+
from_state = current
|
333
|
+
to_state = events_chain.move_to(event_name, from_state, *data)
|
293
334
|
|
294
|
-
|
295
|
-
else
|
296
|
-
notify HookEvent::After, event_transition, *args
|
335
|
+
block.call(from_state, to_state) if block
|
297
336
|
|
298
|
-
|
299
|
-
|
337
|
+
if log_transitions
|
338
|
+
Logger.report_transition(event_name, from_state, to_state, *data)
|
300
339
|
end
|
340
|
+
|
341
|
+
try_trigger(event_name) { transition_to!(to_state) }
|
342
|
+
end
|
343
|
+
|
344
|
+
def transition(event_name, *data, &block)
|
345
|
+
transition!(event_name, *data, &block)
|
346
|
+
rescue InvalidStateError, TransitionError
|
347
|
+
false
|
301
348
|
end
|
302
349
|
|
350
|
+
# Update this state machine state to new one
|
351
|
+
#
|
352
|
+
# @param [Symbol] new_state
|
353
|
+
#
|
354
|
+
# @raise [TransitionError]
|
355
|
+
#
|
356
|
+
# @api private
|
357
|
+
def transition_to!(new_state)
|
358
|
+
from_state = current
|
359
|
+
self.state = new_state
|
360
|
+
self.initial_state = new_state if from_state == DEFAULT_STATE
|
361
|
+
true
|
362
|
+
rescue Exception => e
|
363
|
+
catch_error(e) || raise_transition_error(e)
|
364
|
+
end
|
365
|
+
|
366
|
+
# String representation of this machine
|
367
|
+
#
|
368
|
+
# @return [String]
|
369
|
+
#
|
370
|
+
# @api public
|
371
|
+
def inspect
|
372
|
+
sync_shared do
|
373
|
+
"<##{self.class}:0x#{object_id.to_s(16)} @states=#{states}, " \
|
374
|
+
"@events=#{event_names}, " \
|
375
|
+
"@transitions=#{events_chain.state_transitions}>"
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
private
|
380
|
+
|
303
381
|
# Raise when failed to transition between states
|
304
382
|
#
|
305
383
|
# @param [Exception] error
|
306
384
|
# the error to describe
|
307
385
|
#
|
308
|
-
# @raise [
|
386
|
+
# @raise [TransitionError]
|
309
387
|
#
|
310
388
|
# @api private
|
311
389
|
def raise_transition_error(error)
|