finite_machine 0.6.1 → 0.7.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.
@@ -0,0 +1,65 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+ # A class responsible for event notification
5
+ class HookEvent
6
+ include Threadable
7
+
8
+ MESSAGE = :trigger
9
+
10
+ # HookEvent state
11
+ attr_threadsafe :state
12
+
13
+ # HookEvent type
14
+ attr_threadsafe :type
15
+
16
+ # Data associated with the event
17
+ attr_threadsafe :data
18
+
19
+ # Transition associated with the event
20
+ attr_threadsafe :transition
21
+
22
+ def initialize(state, transition, *data, &block)
23
+ @state = state
24
+ @transition = transition
25
+ @data = *data
26
+ @type = self.class
27
+ end
28
+
29
+ def notify(subscriber, *args, &block)
30
+ if subscriber.respond_to? MESSAGE
31
+ subscriber.public_send(MESSAGE, self, *args, &block)
32
+ end
33
+ end
34
+
35
+ class Anystate < HookEvent; end
36
+
37
+ class Enter < Anystate; end
38
+
39
+ class Transition < Anystate; end
40
+
41
+ class Exit < Anystate; end
42
+
43
+ class Anyaction < HookEvent; end
44
+
45
+ class Before < Anyaction; end
46
+
47
+ class After < Anyaction; end
48
+
49
+ EVENTS = Anystate, Enter, Transition, Exit, Anyaction, Before, After
50
+
51
+ def self.event_name
52
+ name.split('::').last.downcase.to_sym
53
+ end
54
+
55
+ def self.to_s
56
+ event_name
57
+ end
58
+
59
+ EVENTS.each do |event|
60
+ (class << self; self; end).class_eval do
61
+ define_method(event.event_name) { event }
62
+ end
63
+ end
64
+ end # HookEvent
65
+ end # FiniteMachine
@@ -13,7 +13,7 @@ module FiniteMachine
13
13
  # Hoosk.new(machine)
14
14
  #
15
15
  # @api public
16
- def initialize(machine)
16
+ def initialize
17
17
  @collection = Hash.new do |events_hash, event_type|
18
18
  events_hash[event_type] = Hash.new do |state_hash, name|
19
19
  state_hash[name] = []
@@ -28,13 +28,13 @@ module FiniteMachine
28
28
  # @param [Proc] callback
29
29
  #
30
30
  # @example
31
- # hooks.register :enterstate, :green do ... end
31
+ # hooks.register HookEvent::Enter, :green do ... end
32
32
  #
33
33
  # @return [Hash]
34
34
  #
35
35
  # @api public
36
36
  def register(event_type, name, callback)
37
- @collection[event_type][name] << callback
37
+ collection[event_type][name] << callback
38
38
  end
39
39
 
40
40
  # Unregister callback
@@ -44,13 +44,14 @@ module FiniteMachine
44
44
  # @param [Proc] callback
45
45
  #
46
46
  # @example
47
- # hooks.unregister :enterstate, :green do ... end
47
+ # hooks.unregister HookEvent::Enter, :green do ... end
48
48
  #
49
49
  # @return [Hash]
50
50
  #
51
51
  # @api public
52
52
  def unregister(event_type, name, callback)
53
- @collection[event_type][name].shift
53
+ callbacks = collection[event_type][name]
54
+ callbacks.delete(callback)
54
55
  end
55
56
 
56
57
  # Return all hooks matching event and state
@@ -65,10 +66,37 @@ module FiniteMachine
65
66
  # @return [Hash]
66
67
  #
67
68
  # @api public
68
- def call(event_type, event_state, event)
69
- @collection[event_type][event_state].each do |hook|
69
+ def call(event_type, event_state)
70
+ collection[event_type][event_state].each do |hook|
70
71
  yield hook
71
72
  end
72
73
  end
74
+
75
+ # Check if collection has any elements
76
+ #
77
+ # @return [Boolean]
78
+ #
79
+ # @api public
80
+ def empty?
81
+ collection.empty?
82
+ end
83
+
84
+ # String representation
85
+ #
86
+ # @return [String]
87
+ #
88
+ # @api public
89
+ def to_s
90
+ self.inspect
91
+ end
92
+
93
+ # String representation
94
+ #
95
+ # @return [String]
96
+ #
97
+ # @api public
98
+ def inspect
99
+ "<##{self.class}:0x#{object_id.to_s(16)} @collection=#{collection.inspect}>"
100
+ end
73
101
  end # Hooks
74
102
  end # FiniteMachine
@@ -6,6 +6,7 @@ module FiniteMachine
6
6
  # A class responsible for observing state changes
7
7
  class Observer
8
8
  include Threadable
9
+ include Safety
9
10
 
10
11
  # The current state machine
11
12
  attr_threadsafe :machine
@@ -19,7 +20,7 @@ module FiniteMachine
19
20
  def initialize(machine)
20
21
  @machine = machine
21
22
  @machine.subscribe(self)
22
- @hooks = FiniteMachine::Hooks.new(machine)
23
+ @hooks = FiniteMachine::Hooks.new
23
24
  end
24
25
 
25
26
  # Evaluate in current context
@@ -31,19 +32,20 @@ module FiniteMachine
31
32
 
32
33
  # Register callback for a given event type
33
34
  #
34
- # @param [Symbol, FiniteMachine::Event] event_type
35
+ # @param [Symbol, FiniteMachine::HookEvent] event_type
35
36
  # @param [Array] args
36
37
  # @param [Proc] callback
37
38
  #
38
39
  # @api public
39
- def on(event_type = Event, *args, &callback)
40
+ # TODO: throw error if event type isn't handled
41
+ def on(event_type = HookEvent, *args, &callback)
40
42
  sync_exclusive do
41
43
  name, async, _ = args
42
44
  name = ANY_EVENT if name.nil?
43
45
  async = false if async.nil?
44
- ensure_valid_callback_name!(name)
46
+ ensure_valid_callback_name!(event_type, name)
45
47
  callback.extend(Async) if async == :async
46
- hooks.register event_type.to_s, name, callback
48
+ hooks.register event_type, name, callback
47
49
  end
48
50
  end
49
51
 
@@ -60,39 +62,44 @@ module FiniteMachine
60
62
 
61
63
  module Async; end
62
64
 
63
- def listen_on(type, *args, &callback)
64
- name = args.first
65
- events = []
66
- case name
67
- when *state_names then events << :"#{type}state"
68
- when *event_names then events << :"#{type}action"
69
- else events << :"#{type}state" << :"#{type}action"
70
- end
71
- events.each { |event| on(Event.send(event), *args, &callback) }
72
- end
73
-
74
65
  def on_enter(*args, &callback)
75
- listen_on :enter, *args, &callback
66
+ on HookEvent::Enter, *args, &callback
76
67
  end
77
68
 
78
69
  def on_transition(*args, &callback)
79
- listen_on :transition, *args, &callback
70
+ on HookEvent::Transition, *args, &callback
80
71
  end
81
72
 
82
73
  def on_exit(*args, &callback)
83
- listen_on :exit, *args, &callback
74
+ on HookEvent::Exit, *args, &callback
84
75
  end
85
76
 
86
77
  def once_on_enter(*args, &callback)
87
- listen_on :enter, *args, &callback.extend(Once)
78
+ on HookEvent::Enter, *args, &callback.extend(Once)
88
79
  end
89
80
 
90
81
  def once_on_transition(*args, &callback)
91
- listen_on :transition, *args, &callback.extend(Once)
82
+ on HookEvent::Transition, *args, &callback.extend(Once)
92
83
  end
93
84
 
94
85
  def once_on_exit(*args, &callback)
95
- listen_on :exit, *args, &callback.extend(Once)
86
+ on HookEvent::Exit, *args, &callback.extend(Once)
87
+ end
88
+
89
+ def on_before(*args, &callback)
90
+ on HookEvent::Before, *args, &callback
91
+ end
92
+
93
+ def on_after(*args, &callback)
94
+ on HookEvent::After, *args, &callback
95
+ end
96
+
97
+ def once_on_before(*args, &callback)
98
+ on HookEvent::Before, *args, &callback.extend(Once)
99
+ end
100
+
101
+ def once_on_after(*args, &callback)
102
+ on HookEvent::After, *args, &callback.extend(Once)
96
103
  end
97
104
 
98
105
  # Trigger all listeners
@@ -101,9 +108,8 @@ module FiniteMachine
101
108
  def trigger(event, *args, &block)
102
109
  sync_exclusive do
103
110
  [event.type, ANY_EVENT].each do |event_type|
104
- [event.state, ANY_STATE,
105
- ANY_STATE_HOOK, ANY_EVENT_HOOK].each do |event_state|
106
- hooks.call(event_type, event_state, event) do |hook|
111
+ [event.state, ANY_STATE].each do |event_state|
112
+ hooks.call(event_type, event_state) do |hook|
107
113
  handle_callback(hook, event)
108
114
  off(event_type, event_state, &hook) if hook.is_a?(Once)
109
115
  end
@@ -150,39 +156,14 @@ module FiniteMachine
150
156
  event.transition.cancelled = (result == CANCELLED)
151
157
  end
152
158
 
153
- # Set of all state names
159
+ # Callback names including all states and events
154
160
  #
155
- # @return [Set]
161
+ # @return [Array[Symbol]]
162
+ # valid callback names
156
163
  #
157
164
  # @api private
158
- def state_names
159
- @names = Set.new
160
- @names.merge machine.states
161
- @names.merge [ANY_STATE, ANY_STATE_HOOK]
162
- end
163
-
164
- # Set of all event names
165
- #
166
- # @return [Set]
167
- #
168
- # @api private
169
- def event_names
170
- @names = Set.new
171
- @names.merge machine.event_names
172
- @names.merge [ANY_EVENT, ANY_EVENT_HOOK]
173
- end
174
-
175
165
  def callback_names
176
- state_names + event_names
177
- end
178
-
179
- def ensure_valid_callback_name!(name)
180
- unless callback_names.include?(name)
181
- exception = InvalidCallbackNameError
182
- machine.catch_error(exception) ||
183
- raise(exception, "#{name} is not a valid callback name." +
184
- " Valid callback names are #{callback_names.to_a.inspect}")
185
- end
166
+ machine.states + machine.event_names + [ANY_EVENT]
186
167
  end
187
168
 
188
169
  # Forward the message to observer
@@ -196,7 +177,7 @@ module FiniteMachine
196
177
  # @api private
197
178
  def method_missing(method_name, *args, &block)
198
179
  _, event_name, callback_name = *method_name.to_s.match(/^(\w*?on_\w+?)_(\w+)$/)
199
- if callback_names.include?(callback_name.to_sym)
180
+ if callback_name && callback_names.include?(callback_name.to_sym)
200
181
  public_send(event_name, :"#{callback_name}", *args, &block)
201
182
  else
202
183
  super
@@ -214,7 +195,7 @@ module FiniteMachine
214
195
  # @api private
215
196
  def respond_to_missing?(method_name, include_private = false)
216
197
  *_, callback_name = *method_name.to_s.match(/^(\w*?on_\w+?)_(\w+)$/)
217
- callback_names.include?(:"#{callback_name}")
198
+ callback_name && callback_names.include?(:"#{callback_name}")
218
199
  end
219
200
  end # Observer
220
201
  end # FiniteMachine
@@ -0,0 +1,113 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+ # Module for responsible for safety checks against known methods
5
+ module Safety
6
+
7
+ EVENT_CONFLICT_MESSAGE = \
8
+ "You tried to define an event named \"%{name}\", however this would " \
9
+ "generate \"%{type}\" method \"%{method}\", which is already defined " \
10
+ "by %{source}"
11
+
12
+ STATE_CALLBACK_CONFLICT_MESSAGE = \
13
+ "\"%{type}\" callback is a state listener and cannot be used " \
14
+ "with \"%{name}\" event name. Please use on_before or on_after instead."
15
+
16
+ EVENT_CALLBACK_CONFLICT_MESSAGE = \
17
+ "\"%{type}\" callback is an event listener and cannot be used " \
18
+ "with \"%{name}\" state name. Please use on_enter, on_transition or " \
19
+ "on_exit instead."
20
+
21
+ CALLBACK_INVALID_MESSAGE = \
22
+ "\"%{name}\" is not a valid callback name. " \
23
+ "Valid callback names are \"%{callbacks}"
24
+
25
+ # Raise error when the method is already defined
26
+ #
27
+ # @example
28
+ # detect_event_conflict!(:test, "test=")
29
+ #
30
+ # @raise [FiniteMachine::AlreadyDefinedError]
31
+ #
32
+ # @return [nil]
33
+ #
34
+ # @api public
35
+ def detect_event_conflict!(event_name, method_name = event_name)
36
+ if method_already_implemented?(method_name)
37
+ fail FiniteMachine::AlreadyDefinedError, EVENT_CONFLICT_MESSAGE % {
38
+ name: event_name,
39
+ type: :instance,
40
+ method: method_name,
41
+ source: 'FiniteMachine'
42
+ }
43
+ end
44
+ end
45
+
46
+ # Raise error when the callback name is not valid
47
+ #
48
+ # @example
49
+ # ensure_valid_callback_name!(HookEvent::Enter, ":state_name")
50
+ #
51
+ # @raise [FiniteMachine::InvalidCallbackNameError]
52
+ #
53
+ # @return [nil]
54
+ #
55
+ # @api public
56
+ def ensure_valid_callback_name!(event_type, name)
57
+ message = if machine.states.include?(name) &&
58
+ event_type < HookEvent::Anyaction
59
+ EVENT_CALLBACK_CONFLICT_MESSAGE % {
60
+ type: "on_#{event_type.to_s}",
61
+ name: name
62
+ }
63
+ elsif machine.event_names.include?(name) &&
64
+ event_type < HookEvent::Anystate
65
+ STATE_CALLBACK_CONFLICT_MESSAGE % {
66
+ type: "on_#{event_type.to_s}",
67
+ name: name
68
+ }
69
+ elsif !callback_names.include?(name)
70
+ CALLBACK_INVALID_MESSAGE % {
71
+ name: name,
72
+ callbacks: callback_names.to_a.inspect
73
+ }
74
+ else
75
+ nil
76
+ end
77
+ message && fail_invalid_callback_error(message)
78
+ end
79
+
80
+ private
81
+
82
+ def fail_invalid_callback_error(message)
83
+ exception = InvalidCallbackNameError
84
+ machine.catch_error(exception) || fail(exception, message)
85
+ end
86
+
87
+ # Check if method is already implemented inside StateMachine
88
+ #
89
+ # @param [String] name
90
+ # the method name
91
+ #
92
+ # @return [Boolean]
93
+ #
94
+ # @api private
95
+ def method_already_implemented?(name)
96
+ method_defined_within?(name, FiniteMachine::StateMachine)
97
+ end
98
+
99
+ # Check if method is defined within a given class
100
+ #
101
+ # @param [String] name
102
+ # the method name
103
+ #
104
+ # @param [Object] klass
105
+ #
106
+ # @return [Boolean]
107
+ #
108
+ # @api private
109
+ def method_defined_within?(name, klass)
110
+ klass.method_defined?(name) || klass.private_method_defined?(name)
111
+ end
112
+ end # Safety
113
+ end # FiniteMachine
@@ -20,7 +20,7 @@ module FiniteMachine
20
20
  attr_threadsafe :state
21
21
 
22
22
  # Events DSL
23
- attr_threadsafe :events
23
+ attr_threadsafe :events_dsl
24
24
 
25
25
  # Errors DSL
26
26
  attr_threadsafe :errors
@@ -40,23 +40,27 @@ module FiniteMachine
40
40
  # The state machine environment
41
41
  attr_threadsafe :env
42
42
 
43
+ # The state machine event definitions
44
+ attr_threadsafe :events_chain
45
+
43
46
  def_delegators :@dsl, :initial, :terminal, :target
44
47
 
45
- def_delegator :@events, :event
48
+ def_delegator :@events_dsl, :event
46
49
 
47
50
  # Initialize state machine
48
51
  #
49
52
  # @api private
50
53
  def initialize(*args, &block)
51
- attributes = args.last.is_a?(Hash) ? args.pop : {}
54
+ attributes = args.last.is_a?(Hash) ? args.pop : {}
52
55
  @initial_state = DEFAULT_STATE
53
- @subscribers = Subscribers.new(self)
54
- @events = EventsDSL.new(self)
55
- @errors = ErrorsDSL.new(self)
56
- @observer = Observer.new(self)
57
- @transitions = Hash.new { |hash, name| hash[name] = Hash.new }
58
- @env = Environment.new(target: self)
59
- @dsl = DSL.new(self, attributes)
56
+ @subscribers = Subscribers.new(self)
57
+ @events_dsl = EventsDSL.new(self)
58
+ @errors = ErrorsDSL.new(self)
59
+ @observer = Observer.new(self)
60
+ @transitions = Hash.new { |hash, name| hash[name] = Hash.new }
61
+ @events_chain = {}
62
+ @env = Environment.new(target: self)
63
+ @dsl = DSL.new(self, attributes)
60
64
 
61
65
  @dsl.call(&block) if block_given?
62
66
  end
@@ -75,9 +79,8 @@ module FiniteMachine
75
79
  # @api public
76
80
  def notify(event_type, _transition, *data)
77
81
  sync_shared do
78
- event_class = Event.const_get(event_type.capitalize.to_s)
79
- state_or_action = event_class < Event::Anystate ? state : _transition.name
80
- _event = event_class.new(state_or_action, _transition, *data)
82
+ state_or_action = event_type < HookEvent::Anystate ? state : _transition.name
83
+ _event = event_type.new(state_or_action, _transition, *data)
81
84
  subscribers.visit(_event)
82
85
  end
83
86
  end
@@ -142,7 +145,9 @@ module FiniteMachine
142
145
  #
143
146
  # @api public
144
147
  def states
145
- event_names.map { |event| transitions[event].to_a }.flatten.uniq
148
+ sync_shared do
149
+ event_names.map { |event| transitions[event].to_a }.flatten.uniq
150
+ end
146
151
  end
147
152
 
148
153
  # Retireve all event names
@@ -151,7 +156,7 @@ module FiniteMachine
151
156
  #
152
157
  # @api public
153
158
  def event_names
154
- transitions.keys
159
+ sync_shared { transitions.keys }
155
160
  end
156
161
 
157
162
  # Checks if event can be triggered
@@ -191,6 +196,18 @@ module FiniteMachine
191
196
  is?(final_state)
192
197
  end
193
198
 
199
+ # String representation of this machine
200
+ #
201
+ # @return [String]
202
+ #
203
+ # @api public
204
+ def inspect
205
+ sync_shared do
206
+ "<##{self.class}:0x#{object_id.to_s(16)} @states=#{states}, " \
207
+ "@events=#{event_names}, @transitions=#{transitions.inspect}>"
208
+ end
209
+ end
210
+
194
211
  private
195
212
 
196
213
  # Check if state is reachable
@@ -216,32 +233,40 @@ module FiniteMachine
216
233
  #
217
234
  # @api private
218
235
  def transition(_transition, *args, &block)
219
- return CANCELLED if valid_state?(_transition)
236
+ sync_exclusive do
237
+ notify HookEvent::Before, _transition, *args
220
238
 
221
- return CANCELLED unless _transition.conditions.all? do |condition|
222
- condition.call(env.target, *args)
223
- end
224
- return NOTRANSITION if _transition.different?(state)
239
+ return CANCELLED if valid_state?(_transition)
240
+ return CANCELLED unless _transition.valid?(*args, &block)
225
241
 
226
- sync_exclusive do
227
- notify :exitstate, _transition, *args
242
+ notify HookEvent::Exit, _transition, *args
228
243
 
229
244
  begin
230
245
  _transition.call
231
- notify :enteraction, _transition, *args
232
- notify :transitionstate, _transition, *args
233
- notify :transitionaction, _transition, *args
246
+
247
+ notify HookEvent::Transition, _transition, *args
234
248
  rescue Exception => e
235
- catch_error(e) ||
236
- raise(TransitionError, "#(#{e.class}): #{e.message}\n" +
237
- "occured at #{e.backtrace.join("\n")}")
249
+ catch_error(e) || raise_transition_error(e)
238
250
  end
239
251
 
240
- notify :enterstate, _transition, *args
241
- notify :exitaction, _transition, *args
252
+ notify HookEvent::Enter, _transition, *args
253
+ notify HookEvent::After, _transition, *args
254
+
255
+ _transition.same?(state) ? NOTRANSITION : SUCCEEDED
242
256
  end
257
+ end
243
258
 
244
- SUCCEEDED
259
+ # Raise when failed to transition between states
260
+ #
261
+ # @param [Exception] error
262
+ # the error to describe
263
+ #
264
+ # @raise [FiniteMachine::TransitionError]
265
+ #
266
+ # @api private
267
+ def raise_transition_error(error)
268
+ fail(TransitionError, "#(#{error.class}): #{error.message}\n" \
269
+ "occured at #{error.backtrace.join("\n")}")
245
270
  end
246
271
 
247
272
  # Forward the message to target, observer or self
@@ -254,8 +279,8 @@ module FiniteMachine
254
279
  #
255
280
  # @api private
256
281
  def method_missing(method_name, *args, &block)
257
- if env.target.respond_to?(method_name.to_sym)
258
- env.target.public_send(method_name.to_sym, *args, &block)
282
+ if target.respond_to?(method_name.to_sym)
283
+ target.public_send(method_name.to_sym, *args, &block)
259
284
  elsif observer.respond_to?(method_name.to_sym)
260
285
  observer.public_send(method_name.to_sym, *args, &block)
261
286
  else
@@ -273,7 +298,9 @@ module FiniteMachine
273
298
  #
274
299
  # @api private
275
300
  def respond_to_missing?(method_name, include_private = false)
276
- env.target.respond_to?(method_name.to_sym)
301
+ env.target.respond_to?(method_name.to_sym) ||
302
+ observer.respond_to?(method_name.to_sym) ||
303
+ super
277
304
  end
278
305
  end # StateMachine
279
306
  end # FiniteMachine