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.
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)