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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/Gemfile +1 -1
  4. data/README.md +73 -35
  5. data/assets/finite_machine_logo.png +0 -0
  6. data/lib/finite_machine.rb +0 -7
  7. data/lib/finite_machine/async_proxy.rb +1 -2
  8. data/lib/finite_machine/dsl.rb +13 -14
  9. data/lib/finite_machine/event_definition.rb +32 -35
  10. data/lib/finite_machine/events_chain.rb +183 -37
  11. data/lib/finite_machine/hook_event.rb +47 -42
  12. data/lib/finite_machine/logger.rb +3 -4
  13. data/lib/finite_machine/observer.rb +27 -11
  14. data/lib/finite_machine/state_definition.rb +66 -0
  15. data/lib/finite_machine/state_machine.rb +177 -99
  16. data/lib/finite_machine/subscribers.rb +17 -6
  17. data/lib/finite_machine/thread_context.rb +1 -1
  18. data/lib/finite_machine/transition.rb +45 -173
  19. data/lib/finite_machine/transition_builder.rb +24 -6
  20. data/lib/finite_machine/transition_event.rb +5 -4
  21. data/lib/finite_machine/undefined_transition.rb +32 -0
  22. data/lib/finite_machine/version.rb +1 -1
  23. data/spec/spec_helper.rb +1 -0
  24. data/spec/unit/async_events_spec.rb +24 -18
  25. data/spec/unit/callbacks_spec.rb +0 -19
  26. data/spec/unit/event_names_spec.rb +19 -0
  27. data/spec/unit/events_chain/add_spec.rb +25 -0
  28. data/spec/unit/events_chain/cancel_transitions_spec.rb +22 -0
  29. data/spec/unit/events_chain/choice_transition_spec.rb +28 -0
  30. data/spec/unit/events_chain/clear_spec.rb +7 -18
  31. data/spec/unit/events_chain/events_spec.rb +18 -0
  32. data/spec/unit/events_chain/inspect_spec.rb +14 -17
  33. data/spec/unit/events_chain/match_transition_spec.rb +37 -0
  34. data/spec/unit/events_chain/move_to_spec.rb +48 -0
  35. data/spec/unit/events_chain/states_for_spec.rb +17 -0
  36. data/spec/unit/events_spec.rb +119 -27
  37. data/spec/unit/hook_event/build_spec.rb +15 -0
  38. data/spec/unit/hook_event/eql_spec.rb +3 -4
  39. data/spec/unit/hook_event/initialize_spec.rb +14 -11
  40. data/spec/unit/hook_event/notify_spec.rb +14 -0
  41. data/spec/unit/{initialize_spec.rb → initial_spec.rb} +1 -1
  42. data/spec/unit/inspect_spec.rb +1 -1
  43. data/spec/unit/logger_spec.rb +4 -5
  44. data/spec/unit/subscribers_spec.rb +20 -9
  45. data/spec/unit/transition/check_conditions_spec.rb +54 -0
  46. data/spec/unit/transition/inspect_spec.rb +2 -2
  47. data/spec/unit/transition/matches_spec.rb +23 -0
  48. data/spec/unit/transition/states_spec.rb +31 -0
  49. data/spec/unit/transition/to_state_spec.rb +27 -0
  50. data/spec/unit/trigger_spec.rb +22 -0
  51. data/spec/unit/undefined_transition/eql_spec.rb +17 -0
  52. data/tasks/console.rake +1 -0
  53. metadata +39 -23
  54. data/lib/finite_machine/event.rb +0 -146
  55. data/spec/unit/event/add_spec.rb +0 -16
  56. data/spec/unit/event/eql_spec.rb +0 -37
  57. data/spec/unit/event/initialize_spec.rb +0 -38
  58. data/spec/unit/event/inspect_spec.rb +0 -21
  59. data/spec/unit/event/next_transition_spec.rb +0 -35
  60. data/spec/unit/events_chain/check_choice_conditions_spec.rb +0 -20
  61. data/spec/unit/events_chain/insert_spec.rb +0 -26
  62. data/spec/unit/events_chain/select_transition_spec.rb +0 -23
  63. 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
- # Current state
18
- attr_threadsafe :state
17
+ # The prefix used to name events.
18
+ attr_threadsafe :namespace
19
19
 
20
- # Events DSL
21
- attr_threadsafe :events_dsl
20
+ # The state machine environment
21
+ attr_threadsafe :env
22
22
 
23
- # Errors DSL
24
- attr_threadsafe :errors
23
+ # The state machine event definitions
24
+ attr_threadsafe :events_chain
25
25
 
26
- # The prefix used to name events.
27
- attr_threadsafe :namespace
26
+ # Errors DSL
27
+ #
28
+ # @return [ErrorsDSL]
29
+ #
30
+ # @api private
31
+ attr_threadsafe :errors_dsl
28
32
 
29
- # The events and their transitions.
30
- attr_threadsafe :transitions
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 :@events_dsl, :event
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
- @subscribers = Subscribers.new(self)
77
+ @async_proxy = AsyncProxy.new(self)
78
+ @subscribers = Subscribers.new
65
79
  @observer = Observer.new(self)
66
- @transitions = Hash.new { |hash, name| hash[name] = Hash.new }
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
- @errors = ErrorsDSL.new(self)
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
- @subscribers.subscribe(*observers)
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 do
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 { transitions.keys }
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, &block)
193
- event = args.shift
194
- valid_state = transitions[event].key?(current)
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
- # String representation of this machine
226
+ # Check if state is reachable
234
227
  #
235
- # @return [String]
228
+ # @param [Symbol] event_name
229
+ # the event name for all transitions
236
230
  #
237
- # @api public
238
- def inspect
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
- "<##{self.class}:0x#{object_id.to_s(16)} @states=#{states}, " \
241
- "@events=#{event_names}, @transitions=#{transitions.inspect}>"
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
- private
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 valid_state?(event_transition)
255
- current_states = transitions[event_transition.name].keys
256
- if !current_states.include?(state) && !current_states.include?(ANY_STATE)
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 '#{state}'")
260
- return false
270
+ fail(exception, "inappropriate current state '#{current}'")
271
+
272
+ false
261
273
  end
262
- return true
263
274
  end
264
275
 
265
- # Performs transition
276
+ # Trigger transition event with data
266
277
  #
267
- # @param [Transition] event_transition
268
- # @param [Array] args
278
+ # @param [Symbol] event_name
279
+ # the event name
280
+ # @param [Array] data
269
281
  #
270
- # @return [Integer]
271
- # the status code for the transition
282
+ # @return [Boolean]
283
+ # true when transition is successful, false otherwise
272
284
  #
273
- # @api private
274
- def transition(event_transition, *args, &block)
285
+ # @api public
286
+ def trigger!(event_name, *data, &block)
275
287
  sync_exclusive do
276
- notify HookEvent::Before, event_transition, *args
288
+ from = current # Save away current state
289
+
290
+ notify HookEvent::Before, event_name, from, *data
277
291
 
278
- if valid_state?(event_transition) && event_transition.valid?(*args, &block)
279
- notify HookEvent::Exit, event_transition, *args
292
+ status = try_trigger(event_name) do
293
+ if can?(event_name, *data)
294
+ notify HookEvent::Exit, event_name, from, *data
280
295
 
281
- begin
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, event_transition, *args
286
- rescue Exception => e
287
- catch_error(e) || raise_transition_error(e)
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
- notify HookEvent::Enter, event_transition, *args
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
- notify HookEvent::After, event_transition, *args
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
- event_transition.same?(state) ? NOTRANSITION : SUCCEEDED
295
- else
296
- notify HookEvent::After, event_transition, *args
335
+ block.call(from_state, to_state) if block
297
336
 
298
- CANCELLED
299
- end
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 [FiniteMachine::TransitionError]
386
+ # @raise [TransitionError]
309
387
  #
310
388
  # @api private
311
389
  def raise_transition_error(error)