finite_machine 0.13.0 → 0.14.1

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.
@@ -33,7 +33,7 @@ module FiniteMachine
33
33
  if block_given?
34
34
  options[:with] = block
35
35
  else
36
- raise ArgumentError, 'Need to provide error handler.'
36
+ raise ArgumentError, "Need to provide error handler."
37
37
  end
38
38
  end
39
39
  evaluate_exceptions(exceptions, options)
@@ -71,7 +71,7 @@ module FiniteMachine
71
71
  #
72
72
  # @api private
73
73
  def extract_const(class_name)
74
- class_name.split('::').reduce(FiniteMachine) do |constant, part|
74
+ class_name.split("::").reduce(FiniteMachine) do |constant, part|
75
75
  constant.const_get(part)
76
76
  end
77
77
  end
@@ -85,9 +85,9 @@ module FiniteMachine
85
85
  target.method(handler)
86
86
  when Proc
87
87
  if handler.arity.zero?
88
- proc { instance_exec(&handler) }
88
+ -> { instance_exec(&handler) }
89
89
  else
90
- proc { |_exception| instance_exec(_exception, &handler) }
90
+ ->(exception) { instance_exec(exception, &handler) }
91
91
  end
92
92
  end
93
93
  end
@@ -101,16 +101,24 @@ module FiniteMachine
101
101
  # @api private
102
102
  def evaluate_exceptions(exceptions, options)
103
103
  exceptions.each do |exception|
104
- key = if exception.is_a?(Class) && exception <= Exception
105
- exception.name
106
- elsif exception.is_a?(String)
107
- exception
108
- else
109
- raise ArgumentError, "#{exception} isn't an Exception"
110
- end
111
-
104
+ key = extract_exception_name(exception)
112
105
  error_handlers << [key, options[:with]]
113
106
  end
114
107
  end
108
+
109
+ # Extract exception name
110
+ #
111
+ # @param [Class,Exception,String] exception
112
+ #
113
+ # @api private
114
+ def extract_exception_name(exception)
115
+ if exception.is_a?(Class) && exception <= Exception
116
+ exception.name
117
+ elsif exception.is_a?(String)
118
+ exception
119
+ else
120
+ raise ArgumentError, "#{exception} isn't an Exception"
121
+ end
122
+ end
115
123
  end # Catchable
116
124
  end # FiniteMachine
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'transition_builder'
3
+ require_relative "transition_builder"
4
4
 
5
5
  module FiniteMachine
6
6
  # A class responsible for merging choice options
@@ -39,6 +39,6 @@ module FiniteMachine
39
39
  @transitions.merge(conditions))
40
40
  transition_builder.call(@transitions[:from] => to)
41
41
  end
42
- alias_method :default, :choice
42
+ alias default choice
43
43
  end # ChoiceMerger
44
44
  end # FiniteMachine
@@ -1,30 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FiniteMachine
4
- # A class responsible for defining standalone state machine
4
+ # Responsible for defining a standalone state machine
5
+ #
6
+ # @api public
5
7
  class Definition
6
- # The machine deferreds
8
+ # The any event constant
7
9
  #
8
- # @return [Array[Proc]]
10
+ # @example
11
+ # on_before(any_event) { ... }
9
12
  #
10
- # @api private
11
- def self.deferreds
12
- @deferreds ||= []
13
+ # @return [FiniteMachine::Const]
14
+ #
15
+ # @api public
16
+ def self.any_event
17
+ ANY_EVENT
13
18
  end
14
19
 
15
- # Add deferred
20
+ # The any state constant
16
21
  #
17
- # @param [Proc] deferred
18
- # the deferred execution
22
+ # @example
23
+ # event :go, any_state => :green
19
24
  #
20
- # @return [Array[Proc]]
25
+ # @example
26
+ # on_enter(any_state) { ... }
21
27
  #
22
- # @api private
23
- def self.add_deferred(deferred)
24
- deferreds << deferred
28
+ # @return [FiniteMachine::Const]
29
+ #
30
+ # @api public
31
+ def self.any_state
32
+ ANY_STATE
25
33
  end
26
34
 
27
- # Instantiate a new Definition
35
+ # Initialize a StateMachine
28
36
  #
29
37
  # @example
30
38
  # class Engine < FiniteMachine::Definition
@@ -43,20 +51,49 @@ module FiniteMachine
43
51
  end
44
52
  end
45
53
 
46
- # Set deferrerd methods on the subclass
54
+ # Add deferred methods to the subclass
55
+ #
56
+ # @param [Class] subclass
57
+ # the inheriting subclass
58
+ #
59
+ # @return [void]
47
60
  #
48
61
  # @api private
49
62
  def self.inherited(subclass)
50
63
  super
51
64
 
52
- self.deferreds.each { |d| subclass.add_deferred(d) }
65
+ deferreds.each { |d| subclass.add_deferred(d) }
66
+ end
67
+
68
+ # The state machine deferreds
69
+ #
70
+ # @return [Array<Proc>]
71
+ #
72
+ # @api private
73
+ def self.deferreds
74
+ @deferreds ||= []
75
+ end
76
+
77
+ # Add deferred
78
+ #
79
+ # @param [Proc] deferred
80
+ # the deferred execution
81
+ #
82
+ # @return [Array<Proc>]
83
+ #
84
+ # @api private
85
+ def self.add_deferred(deferred)
86
+ deferreds << deferred
53
87
  end
54
88
 
55
89
  # Delay lookup of DSL method
56
90
  #
57
91
  # @param [Symbol] method_name
92
+ # the method name
93
+ # @param [Array] arguments
94
+ # the method arguments
58
95
  #
59
- # @return [nil]
96
+ # @return [void]
60
97
  #
61
98
  # @api private
62
99
  def self.method_missing(method_name, *arguments, &block)
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'choice_merger'
4
- require_relative 'safety'
5
- require_relative 'transition_builder'
3
+ require_relative "choice_merger"
4
+ require_relative "safety"
5
+ require_relative "transition_builder"
6
6
 
7
7
  module FiniteMachine
8
8
  # A generic DSL for describing the state machine
@@ -38,6 +38,13 @@ module FiniteMachine
38
38
  end
39
39
  end
40
40
 
41
+ # Check if message can be handled by this DSL
42
+ #
43
+ # @api private
44
+ def respond_to_missing?(method_name, include_private = false)
45
+ @machine.respond_to?(method_name) || super
46
+ end
47
+
41
48
  # Configure state machine properties
42
49
  #
43
50
  # @api private
@@ -65,6 +72,29 @@ module FiniteMachine
65
72
  log_transitions(@attrs.fetch(:log_transitions, false))
66
73
  end
67
74
 
75
+ # Add aliases for the target object
76
+ #
77
+ # @example
78
+ # FiniteMachine.define do
79
+ # target_alias :engine
80
+ #
81
+ # on_transition do |event|
82
+ # engine.state = event.to
83
+ # end
84
+ # end
85
+ #
86
+ # @param [Array<Symbol>] aliases
87
+ # the names for target alias
88
+ #
89
+ # @api public
90
+ def alias_target(*aliases)
91
+ aliases.each do |alias_name|
92
+ next if env.aliases.include?(alias_name)
93
+
94
+ env.aliases << alias_name
95
+ end
96
+ end
97
+
68
98
  # Define initial state
69
99
  #
70
100
  # @param [Symbol] value
@@ -197,8 +227,8 @@ module FiniteMachine
197
227
  #
198
228
  # @api private
199
229
  def raise_missing_state
200
- fail MissingInitialStateError,
201
- 'Provide state to transition :to for the initial event'
230
+ raise MissingInitialStateError,
231
+ "Provide state to transition :to for the initial event"
202
232
  end
203
233
  end # DSL
204
234
  end # FiniteMachine
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'threadable'
3
+ require_relative "threadable"
4
4
 
5
5
  module FiniteMachine
6
6
  # Holds references to targets and aliases
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent/map'
4
- require 'forwardable'
3
+ require "concurrent/map"
4
+ require "forwardable"
5
5
 
6
- require_relative 'threadable'
7
- require_relative 'undefined_transition'
6
+ require_relative "threadable"
7
+ require_relative "undefined_transition"
8
8
 
9
9
  module FiniteMachine
10
10
  # A class responsible for storing mappings between event namess and
@@ -182,8 +182,7 @@ module FiniteMachine
182
182
  # @api public
183
183
  def match_transition_with(name, from_state, *conditions)
184
184
  find(name).find do |trans|
185
- trans.matches?(from_state) &&
186
- trans.check_conditions(*conditions)
185
+ trans.matches?(from_state) && trans.check_conditions(*conditions)
187
186
  end
188
187
  end
189
188
 
@@ -29,7 +29,7 @@ module FiniteMachine
29
29
  #
30
30
  # @api public
31
31
  def self.event_name
32
- name.split('::').last.downcase.to_sym
32
+ name.split("::").last.downcase.to_sym
33
33
  end
34
34
 
35
35
  # String representation
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent/map'
3
+ require "concurrent/array"
4
+ require "concurrent/map"
4
5
 
5
- require_relative 'hook_event'
6
+ require_relative "hook_event"
6
7
 
7
8
  module FiniteMachine
8
9
  # A class reponsible for registering callbacks
@@ -12,13 +13,17 @@ module FiniteMachine
12
13
  # Initialize a hooks_map of hooks
13
14
  #
14
15
  # @example
15
- # Hoosk.new(machine)
16
+ # Hooks.new
16
17
  #
17
18
  # @api public
18
19
  def initialize
19
20
  @hooks_map = Concurrent::Map.new do |events_hash, hook_event|
20
- events_hash[hook_event] = Concurrent::Map.new do |state_hash, name|
21
- state_hash[name] = []
21
+ events_hash.compute_if_absent(hook_event) do
22
+ Concurrent::Map.new do |state_hash, name|
23
+ state_hash.compute_if_absent(name) do
24
+ Concurrent::Array.new
25
+ end
26
+ end
22
27
  end
23
28
  end
24
29
  end
@@ -1,20 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'listener'
4
- require 'thread'
3
+ require_relative "listener"
4
+ require "thread"
5
5
 
6
6
  module FiniteMachine
7
- # Allows for storage of asynchronous messages such as events
7
+ # Responsible for storage of asynchronous messages such as events
8
8
  # and callbacks.
9
9
  #
10
- # Used internally by {Observer} and {StateMachine}
10
+ # Used internally by {Observer}
11
11
  #
12
12
  # @api private
13
13
  class MessageQueue
14
- # Initialize an event queue in separate thread
14
+ # Initialize a MessageQueue
15
15
  #
16
16
  # @example
17
- # MessageQueue.new
17
+ # message_queue = FiniteMachine::MessageQueue.new
18
18
  #
19
19
  # @api public
20
20
  def initialize
@@ -28,13 +28,21 @@ module FiniteMachine
28
28
 
29
29
  # Start a new thread with a queue of callback events to run
30
30
  #
31
+ # @example
32
+ # message_queue.start
33
+ #
34
+ # @return [Thread, nil]
35
+ #
31
36
  # @api private
32
37
  def start
33
38
  return if running?
39
+
34
40
  @mutex.synchronize { spawn_thread }
35
41
  end
36
42
 
37
- # Spawn new background thread
43
+ # Spawn a new background thread
44
+ #
45
+ # @return [Thread]
38
46
  #
39
47
  # @api private
40
48
  def spawn_thread
@@ -44,18 +52,27 @@ module FiniteMachine
44
52
  end
45
53
  end
46
54
 
55
+ # Check whether or not the message queue is running
56
+ #
57
+ # @example
58
+ # message_queue.running?
59
+ #
60
+ # @return [Boolean]
61
+ #
62
+ # @api public
47
63
  def running?
48
64
  !@thread.nil? && alive?
49
65
  end
50
66
 
51
- # Add asynchronous event to the event queue to process
67
+ # Add an asynchronous event to the message queue to process
52
68
  #
53
69
  # @example
54
- # event_queue << AsyncCall.build(...)
70
+ # message_queue << AsyncCall.build(...)
55
71
  #
56
- # @param [AsyncCall] event
72
+ # @param [FiniteMachine::AsyncCall] event
73
+ # the event to add
57
74
  #
58
- # @return [nil]
75
+ # @return [void]
59
76
  #
60
77
  # @api public
61
78
  def <<(event)
@@ -69,7 +86,12 @@ module FiniteMachine
69
86
  end
70
87
  end
71
88
 
72
- # Add listener to the queue to receive messages
89
+ # Add a listener for the message queue to receive notifications
90
+ #
91
+ # @example
92
+ # message_queue.subscribe { |event| ... }
93
+ #
94
+ # @return [void]
73
95
  #
74
96
  # @api public
75
97
  def subscribe(*args, &block)
@@ -80,20 +102,20 @@ module FiniteMachine
80
102
  end
81
103
  end
82
104
 
83
- # Check if there are any events to handle
105
+ # Check whether or not there are any messages to handle
84
106
  #
85
107
  # @example
86
- # event_queue.empty?
108
+ # message_queue.empty?
87
109
  #
88
110
  # @api public
89
111
  def empty?
90
112
  @mutex.synchronize { @queue.empty? }
91
113
  end
92
114
 
93
- # Check if the event queue is alive
115
+ # Check whether or not the message queue is alive
94
116
  #
95
117
  # @example
96
- # event_queue.alive?
118
+ # message_queue.alive?
97
119
  #
98
120
  # @return [Boolean]
99
121
  #
@@ -102,31 +124,35 @@ module FiniteMachine
102
124
  @mutex.synchronize { !@dead }
103
125
  end
104
126
 
105
- # Join the event queue from current thread
127
+ # Join the message queue from the current thread
106
128
  #
107
129
  # @param [Fixnum] timeout
130
+ # the time limit
108
131
  #
109
132
  # @example
110
- # event_queue.join
133
+ # message_queue.join
111
134
  #
112
- # @return [nil, Thread]
135
+ # @return [Thread, nil]
113
136
  #
114
137
  # @api public
115
138
  def join(timeout = nil)
116
139
  return unless @thread
140
+
117
141
  timeout.nil? ? @thread.join : @thread.join(timeout)
118
142
  end
119
143
 
120
- # Shut down this event queue and clean it up
144
+ # Shut down this message queue and clean it up
121
145
  #
122
146
  # @example
123
- # event_queue.shutdown
147
+ # message_queue.shutdown
148
+ #
149
+ # @raise [FiniteMachine::MessageQueueDeadError]
124
150
  #
125
151
  # @return [Boolean]
126
152
  #
127
153
  # @api public
128
154
  def shutdown
129
- fail EventQueueDeadError, 'event queue already dead' if @dead
155
+ raise MessageQueueDeadError, "message queue already dead" if @dead
130
156
 
131
157
  queue = []
132
158
  @mutex.synchronize do
@@ -142,10 +168,10 @@ module FiniteMachine
142
168
  true
143
169
  end
144
170
 
145
- # Get number of events waiting for processing
171
+ # The number of messages waiting for processing
146
172
  #
147
173
  # @example
148
- # event_queue.size
174
+ # message_queue.size
149
175
  #
150
176
  # @return [Integer]
151
177
  #
@@ -154,6 +180,14 @@ module FiniteMachine
154
180
  @mutex.synchronize { @queue.size }
155
181
  end
156
182
 
183
+ # Inspect this message queue
184
+ #
185
+ # @example
186
+ # message_queue.inspect
187
+ #
188
+ # @return [String]
189
+ #
190
+ # @api public
157
191
  def inspect
158
192
  @mutex.synchronize do
159
193
  "#<#{self.class}:#{object_id.to_s(16)} @size=#{size}, @dead=#{@dead}>"
@@ -162,9 +196,12 @@ module FiniteMachine
162
196
 
163
197
  private
164
198
 
165
- # Notify consumers about process event
199
+ # Notify listeners about the event
200
+ #
201
+ # @param [FiniteMachine::AsyncCall] event
202
+ # the event to notify listeners about
166
203
  #
167
- # @param [AsyncCall] event
204
+ # @return [void]
168
205
  #
169
206
  # @api private
170
207
  def notify_listeners(event)
@@ -184,6 +221,7 @@ module FiniteMachine
184
221
  end
185
222
  event = @queue.pop
186
223
  break unless event
224
+
187
225
  notify_listeners(event)
188
226
  event.dispatch
189
227
  end
@@ -192,6 +230,14 @@ module FiniteMachine
192
230
  Logger.error "Error while running event: #{Logger.format_error(ex)}"
193
231
  end
194
232
 
233
+ # Log discarded message
234
+ #
235
+ # @param [FiniteMachine::AsyncCall] message
236
+ # the message to discard
237
+ #
238
+ # @return [void]
239
+ #
240
+ # @api private
195
241
  def discard_message(message)
196
242
  Logger.debug "Discarded message: #{message}" if $DEBUG
197
243
  end
@@ -1,32 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'async_call'
4
- require_relative 'callable'
5
- require_relative 'hook_event'
6
- require_relative 'hooks'
7
- require_relative 'message_queue'
8
- require_relative 'safety'
9
- require_relative 'transition_event'
3
+ require "securerandom"
4
+
5
+ require_relative "async_call"
6
+ require_relative "callable"
7
+ require_relative "hook_event"
8
+ require_relative "hooks"
9
+ require_relative "message_queue"
10
+ require_relative "safety"
11
+ require_relative "transition_event"
10
12
 
11
13
  module FiniteMachine
12
14
  # A class responsible for observing state changes
13
15
  class Observer < GenericDSL
14
16
  include Safety
15
17
 
16
- # Clean up callback queue
17
- #
18
- # @api private
19
- def self.cleanup_callback_queue
20
- proc do
21
- begin
22
- if callback_queue.alive?
23
- callback_queue.shutdown
24
- end
25
- rescue MessageQueueDeadError
26
- end
27
- end
28
- end
29
-
30
18
  # The current state machine
31
19
  attr_reader :machine
32
20
 
@@ -44,11 +32,6 @@ module FiniteMachine
44
32
  @hooks = Hooks.new
45
33
 
46
34
  @machine.subscribe(self)
47
- ObjectSpace.define_finalizer(self, self.class.cleanup_callback_queue)
48
- end
49
-
50
- def callback_queue
51
- @callback_queue ||= MessageQueue.new
52
35
  end
53
36
 
54
37
  # Evaluate in current context
@@ -204,6 +187,35 @@ module FiniteMachine
204
187
  callback_queue << async_call
205
188
  end
206
189
 
190
+ # Get an existing callback queue or create a new one
191
+ #
192
+ # @return [FiniteMachine::MessageQueue]
193
+ #
194
+ # @api private
195
+ def callback_queue
196
+ @callback_queue ||= MessageQueue.new.tap do
197
+ @queue_id = SecureRandom.uuid
198
+ ObjectSpace.define_finalizer(@queue_id, proc do
199
+ cleanup_callback_queue
200
+ end)
201
+ end
202
+ end
203
+
204
+ # Clean up the callback queue
205
+ #
206
+ # @return [Boolean, nil]
207
+ #
208
+ # @api private
209
+ def cleanup_callback_queue
210
+ ObjectSpace.undefine_finalizer(@queue_id) if @queue_id
211
+ return unless @callback_queue && callback_queue.alive?
212
+
213
+ begin
214
+ callback_queue.shutdown
215
+ rescue MessageQueueDeadError
216
+ end
217
+ end
218
+
207
219
  # Create callable instance
208
220
  #
209
221
  # @api private
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'hook_event'
3
+ require_relative "hook_event"
4
4
 
5
5
  module FiniteMachine
6
6
  # Module responsible for safety checks against known methods
@@ -39,7 +39,7 @@ module FiniteMachine
39
39
  name: event_name,
40
40
  type: :instance,
41
41
  method: method_name,
42
- source: 'FiniteMachine'
42
+ source: "FiniteMachine"
43
43
  }
44
44
  end
45
45
  end
@@ -89,8 +89,8 @@ module FiniteMachine
89
89
  # @api private
90
90
  def wrong_event_name?(name, event_type)
91
91
  machine.states.include?(name) &&
92
- !machine.events.include?(name) &&
93
- event_type < HookEvent::Anyaction
92
+ !machine.events.include?(name) &&
93
+ event_type < HookEvent::Anyaction
94
94
  end
95
95
 
96
96
  # Check if state name exists
@@ -104,8 +104,8 @@ module FiniteMachine
104
104
  # @api private
105
105
  def wrong_state_name?(name, event_type)
106
106
  machine.events.include?(name) &&
107
- !machine.states.include?(name) &&
108
- event_type < HookEvent::Anystate
107
+ !machine.states.include?(name) &&
108
+ event_type < HookEvent::Anystate
109
109
  end
110
110
 
111
111
  def raise_invalid_callback_error(message)