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
@@ -1,52 +1,127 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'finite_machine/undefined_transition'
4
+
3
5
  module FiniteMachine
4
- # A class responsible for storing chain of events
6
+ # A class responsible for storing chain of events.
7
+ #
8
+ # Used internally by {StateMachine}.
9
+ #
10
+ # @api private
5
11
  class EventsChain
6
12
  include Threadable
7
13
  extend Forwardable
8
14
 
9
- # The current state machine
10
- attr_threadsafe :machine
11
-
12
15
  # The chain of events
16
+ #
17
+ # @api private
13
18
  attr_threadsafe :chain
14
19
 
15
- def_delegators :@chain, :[], :empty?
20
+ def_delegators :@chain, :empty?, :size
16
21
 
17
22
  # Initialize a EventsChain
18
23
  #
19
- # @param [StateMachine] machine
20
- # the state machine
24
+ # @api private
25
+ def initialize
26
+ @chain = {}
27
+ end
28
+
29
+ # Check if event is present
30
+ #
31
+ # @example
32
+ # events_chain.exists?(:go) # => true
33
+ #
34
+ # @param [Symbol] name
35
+ # the event name
36
+ #
37
+ # @return [Boolean]
38
+ # true if event is present, false otherwise
21
39
  #
22
40
  # @api public
23
- def initialize(machine)
24
- @machine = machine
25
- @chain = {}
41
+ def exists?(name)
42
+ !chain[name].nil?
26
43
  end
27
44
 
28
- # Insert transition under given event name
45
+ # Add transition under name
29
46
  #
30
- # @param [Symbol] name
31
- # the event name
47
+ # @param [Symbol] the event name
32
48
  #
33
- # @param [Transition]
49
+ # @param [Transition] transition
50
+ # the transition to add under event name
34
51
  #
35
52
  # @return [nil]
36
53
  #
37
54
  # @api public
38
- def insert(name, transition)
39
- return false unless chain[name]
40
- chain[name] << transition
55
+ def add(name, transition)
56
+ if exists?(name)
57
+ chain[name] << transition
58
+ else
59
+ chain[name] = [transition]
60
+ end
61
+ self
41
62
  end
42
63
 
43
- # Add event under name
64
+ # Finds transitions for the event name
44
65
  #
45
- # @return [nil]
66
+ # @param [Symbol] name
67
+ #
68
+ # @example
69
+ # events_chain[:start] # => []
70
+ #
71
+ # @return [Array[Transition]]
72
+ # the transitions matching event name
73
+ #
74
+ # @api public
75
+ def find(name)
76
+ chain.fetch(name) { [] }
77
+ end
78
+ alias_method :[], :find
79
+
80
+ # Retrieve all event names
81
+ #
82
+ # @example
83
+ # events_chain.events # => [:init, :start, :stop]
84
+ #
85
+ # @return [Array[Symbol]]
86
+ # All event names
87
+ #
88
+ # @api public
89
+ def events
90
+ chain.keys
91
+ end
92
+
93
+ # Retreive all unique states
94
+ #
95
+ # @example
96
+ # events_chain.states # => [:yellow, :green, :red]
97
+ #
98
+ # @return [Array[Symbol]]
99
+ # the array of all unique states
100
+ #
101
+ # @api public
102
+ def states
103
+ chain.values.flatten.map(&:states).map(&:to_a).flatten.uniq
104
+ end
105
+
106
+ # Retrieves all state transitions
107
+ #
108
+ # @return [Array[Hash]]
109
+ #
110
+ # @api public
111
+ def state_transitions
112
+ chain.values.flatten.map(&:states)
113
+ end
114
+
115
+ # Retrieve from states for the event name
116
+ #
117
+ # @param [Symbol] event_name
118
+ #
119
+ # @example
120
+ # events_chain.states_for(:start) # => [:yellow, :green]
46
121
  #
47
122
  # @api public
48
- def add(name, event)
49
- chain[name] = event
123
+ def states_for(name)
124
+ find(name).map(&:states).flat_map(&:keys)
50
125
  end
51
126
 
52
127
  # Check if event is valid and transition can be performed
@@ -54,23 +129,47 @@ module FiniteMachine
54
129
  # @return [Boolean]
55
130
  #
56
131
  # @api public
57
- def valid_event?(event, *args, &block)
58
- chain[event].next_transition.valid?(*args, &block)
132
+ def can_perform?(name, from_state, *conditions)
133
+ !match_transition_with(name, from_state, *conditions).nil?
59
134
  end
60
135
 
61
- # Select transition that passes constraints condition
136
+ # Check if event has branching choice transitions or not
137
+ #
138
+ # @example
139
+ # events_chain.choice_transition?(:go, :green) # => true
62
140
  #
63
141
  # @param [Symbol] name
64
142
  # the event name
65
143
  #
66
- # @return [Transition]
144
+ # @param [Symbol] from_state
145
+ # the transition from state
146
+ #
147
+ # @return [Boolean]
148
+ # true if transition has any branches, false otherwise
67
149
  #
68
150
  # @api public
69
- def select_transition(name, *args)
70
- chain[name].find_transition(*args)
151
+ def choice_transition?(name, from_state)
152
+ find(name).select { |trans| trans.matches?(from_state) }.size > 1
153
+ end
154
+
155
+ # Find transition without checking conditions
156
+ #
157
+ # @param [Symbol] name
158
+ # the event name
159
+ #
160
+ # @param [Symbol] from_state
161
+ # the transition from state
162
+ #
163
+ # @return [Transition, nil]
164
+ # returns transition, nil otherwise
165
+ #
166
+ # @api private
167
+ def match_transition(name, from_state)
168
+ find(name).find { |trans| trans.matches?(from_state) }
71
169
  end
72
170
 
73
- # Examine choice transitions to find one matching condition
171
+ # Examine transitions for event name that start in from state
172
+ # and find one matching condition.
74
173
  #
75
174
  # @param [Symbol] name
76
175
  # the event name
@@ -82,24 +181,71 @@ module FiniteMachine
82
181
  # The choice transition that matches
83
182
  #
84
183
  # @api public
85
- def select_choice_transition(name, from_state, *args, &block)
86
- chain[name].state_transitions.find do |trans|
87
- [from_state, ANY_STATE].include?(trans.from_state) &&
88
- trans.check_conditions(*args, &block)
184
+ def match_transition_with(name, from_state, *conditions)
185
+ find(name).find do |trans|
186
+ trans.matches?(from_state) &&
187
+ trans.check_conditions(*conditions)
89
188
  end
90
189
  end
91
190
 
92
- # Check if any of the transition constraints passes
191
+ # Select transition that matches conditions
93
192
  #
94
193
  # @param [Symbol] name
95
194
  # the event name
195
+ # @param [Symbol] from_state
196
+ # the transition from state
197
+ # @param [Array[Object]] conditions
198
+ # the conditional data
96
199
  #
97
- # @return [Boolean]
200
+ # @return [Transition]
201
+ #
202
+ # @api public
203
+ def select_transition(name, from_state, *conditions)
204
+ if choice_transition?(name, from_state)
205
+ match_transition_with(name, from_state, *conditions)
206
+ else
207
+ match_transition(name, from_state)
208
+ end
209
+ end
210
+
211
+ # Find state that this machine can move to
212
+ #
213
+ # @example
214
+ # evenst_chain.move_to(:go, :green) # => :red
215
+ #
216
+ # @param [Symbol] name
217
+ # the event name
218
+ #
219
+ # @param [Symbol] from_state
220
+ # the transition from state
221
+ #
222
+ # @param [Array] conditions
223
+ # the data associated with this transition
224
+ #
225
+ # @return [Symbol]
226
+ # the transition `to` state
227
+ #
228
+ # @api public
229
+ def move_to(name, from_state, *conditions)
230
+ transition = select_transition(name, from_state, *conditions)
231
+ transition ||= UndefinedTransition.new(name)
232
+
233
+ transition.to_state(from_state)
234
+ end
235
+
236
+ # Set cancelled status for all transitions matching event name
237
+ #
238
+ # @param [Symbol] name
239
+ # the event name
240
+ # @param [Symbol] status
241
+ # true to cancel, false otherwise
242
+ #
243
+ # @return [nil]
98
244
  #
99
245
  # @api public
100
- def check_choice_conditions(name, *args, &block)
101
- chain[name].state_transitions.any? do |trans|
102
- trans.current? && trans.check_conditions(*args, &block)
246
+ def cancel_transitions(name, status)
247
+ chain[name].each do |trans|
248
+ trans.cancelled = status
103
249
  end
104
250
  end
105
251
 
@@ -6,86 +6,91 @@ module FiniteMachine
6
6
  include Threadable
7
7
  include Comparable
8
8
 
9
- MESSAGE = :trigger
9
+ class Anystate < HookEvent; end
10
+
11
+ class Enter < Anystate; end
12
+
13
+ class Transition < Anystate; end
10
14
 
11
- # HookEvent name
15
+ class Exit < Anystate; end
16
+
17
+ class Anyaction < HookEvent; end
18
+
19
+ class Before < Anyaction; end
20
+
21
+ class After < Anyaction; end
22
+
23
+ EVENTS = Anystate, Enter, Transition, Exit, Anyaction, Before, After
24
+
25
+ MESSAGE = :emit
26
+
27
+ # HookEvent state or action
12
28
  attr_threadsafe :name
13
29
 
14
30
  # HookEvent type
15
31
  attr_threadsafe :type
16
32
 
17
- # Data associated with the event
18
- attr_threadsafe :data
33
+ # The from state this hook has been fired
34
+ attr_threadsafe :from
19
35
 
20
- # Transition associated with the event
21
- attr_threadsafe :transition
36
+ # The event name triggering this hook event
37
+ attr_threadsafe :event_name
22
38
 
23
39
  # Instantiate a new HookEvent object
24
40
  #
25
41
  # @param [Symbol] name
26
42
  # The action or state name
27
- # @param [FiniteMachine::Transition]
28
- # The transition associated with this event.
29
- # @param [Array[Object]] data
43
+ #
44
+ # @param [Symbol] event_name
45
+ # The event name associated with this hook event.
30
46
  #
31
47
  # @example
32
- # HookEvent.new(:green, ...)
48
+ # HookEvent.new(:green, :move, :green)
33
49
  #
34
- # @return [Object]
50
+ # @return [self]
35
51
  #
36
52
  # @api public
37
- def initialize(name, transition, *data)
53
+ def initialize(name, event_name, from)
38
54
  @name = name
39
- @transition = transition
40
- @data = *data
41
55
  @type = self.class
56
+ @event_name = event_name
57
+ @from = from
42
58
  freeze
43
59
  end
44
60
 
45
61
  # Build event hook
46
62
  #
47
63
  # @param [Symbol] :state
48
- # The state name.
49
- # @param [FiniteMachine::Transition] :event_transition
50
- # The transition associted with this hook.
51
- # @param [Array[Object]] :data
52
- # The data associated with this hook
64
+ # The state or action name.
65
+ #
66
+ # @param [Symbol] :event_name
67
+ # The event name associted with this hook.
53
68
  #
54
69
  # @return [self]
55
70
  #
56
71
  # @api public
57
- def self.build(state, event_transition, *data)
58
- state_or_action = self < Anystate ? state : event_transition.name
59
- new(state_or_action, event_transition, *data)
72
+ def self.build(state, event_name, from)
73
+ state_or_action = self < Anystate ? state : event_name
74
+ new(state_or_action, event_name, from)
60
75
  end
61
76
 
62
77
  # Notify subscriber about this event
63
78
  #
79
+ # @param [Observer] subscriber
80
+ # the object subscribed to be notified about this event
81
+ #
82
+ # @param [Array] data
83
+ # the data associated with the triggered event
84
+ #
64
85
  # @return [nil]
65
86
  #
66
87
  # @api public
67
- def notify(subscriber, *args, &block)
68
- if subscriber.respond_to? MESSAGE
69
- subscriber.public_send(MESSAGE, self, *args, &block)
88
+ def notify(subscriber, *data)
89
+ if subscriber.respond_to?(MESSAGE)
90
+ subscriber.public_send(MESSAGE, self, *data)
70
91
  end
71
92
  end
72
93
 
73
- class Anystate < HookEvent; end
74
-
75
- class Enter < Anystate; end
76
-
77
- class Transition < Anystate; end
78
-
79
- class Exit < Anystate; end
80
-
81
- class Anyaction < HookEvent; end
82
-
83
- class Before < Anyaction; end
84
-
85
- class After < Anyaction; end
86
-
87
- EVENTS = Anystate, Enter, Transition, Exit, Anyaction, Before, After
88
-
89
94
  # Extract event name
90
95
  #
91
96
  # @return [String] the event name
@@ -111,7 +116,7 @@ module FiniteMachine
111
116
  # @api public
112
117
  def <=>(other)
113
118
  other.is_a?(type) &&
114
- [name, transition, data] <=> [other.name, other.transition, other.data]
119
+ [name, from, event_name] <=> [other.name, other.from, other.event_name]
115
120
  end
116
121
  alias_method :eql?, :==
117
122
 
@@ -29,13 +29,12 @@ module FiniteMachine
29
29
  end
30
30
  end
31
31
 
32
- def report_transition(event_transition, *args)
33
- message = "Transition: @event=#{event_transition.name} "
32
+ def report_transition(name, from, to, *args)
33
+ message = "Transition: @event=#{name} "
34
34
  unless args.empty?
35
35
  message << "@with=[#{args.join(',')}] "
36
36
  end
37
- message << "#{event_transition.from_state} -> "
38
- message << "#{event_transition.machine.current}"
37
+ message << "#{from} -> #{to}"
39
38
  info(message)
40
39
  end
41
40
  end # Logger
@@ -1,6 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'set'
3
+ require 'finite_machine/hooks'
4
4
 
5
5
  module FiniteMachine
6
6
  # A class responsible for observing state changes
@@ -104,15 +104,22 @@ module FiniteMachine
104
104
  on HookEvent::After, *args, &callback.extend(Once)
105
105
  end
106
106
 
107
- # Trigger all listeners
107
+ # Execute each of the hooks in order with supplied data
108
+ #
109
+ # @param [HookEvent] event
110
+ # the hook event
111
+ #
112
+ # @param [Array[Object]] data
113
+ #
114
+ # @return [nil]
108
115
  #
109
116
  # @api public
110
- def trigger(event, *args, &block)
117
+ def emit(event, *data)
111
118
  sync_exclusive do
112
119
  [event.type].each do |event_type|
113
120
  [event.name, ANY_STATE, ANY_EVENT].each do |event_name|
114
121
  hooks.call(event_type, event_name) do |hook|
115
- handle_callback(hook, event)
122
+ handle_callback(hook, event, *data)
116
123
  off(event_type, event_name, &hook) if hook.is_a?(Once)
117
124
  end
118
125
  end
@@ -134,18 +141,26 @@ module FiniteMachine
134
141
  #
135
142
  # @api private
136
143
  def create_callable(hook)
137
- deferred_hook = proc do |_trans_event, *_data|
138
- machine.instance_exec(_trans_event, *_data, &hook)
144
+ callback = proc do |trans_event, *data|
145
+ machine.instance_exec(trans_event, *data, &hook)
139
146
  end
140
- Callable.new(deferred_hook)
147
+ Callable.new(callback)
141
148
  end
142
149
 
143
150
  # Handle callback and decide if run synchronously or asynchronously
144
151
  #
152
+ # @param [Proc] :hook
153
+ # The hook to evaluate
154
+ #
155
+ # @param [HookEvent] :event
156
+ # The event for which the hook is called
157
+ #
158
+ # @param [Array[Object]] :data
159
+ #
145
160
  # @api private
146
- def handle_callback(hook, event)
147
- data = event.data
148
- trans_event = TransitionEvent.new(event.transition, *data)
161
+ def handle_callback(hook, event, *data)
162
+ to = machine.events_chain.move_to(event.event_name, event.from, *data)
163
+ trans_event = TransitionEvent.new(event, to)
149
164
  callable = create_callable(hook)
150
165
 
151
166
  if hook.is_a?(Async)
@@ -155,7 +170,8 @@ module FiniteMachine
155
170
  result = callable.call(trans_event, *data)
156
171
  end
157
172
 
158
- event.transition.cancelled = (result == CANCELLED)
173
+ machine.events_chain.cancel_transitions(event.event_name,
174
+ (result == CANCELLED))
159
175
  end
160
176
 
161
177
  # Callback names including all states and events