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
@@ -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