state_machines 0.101.0 → 0.200.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b9c5ce3b64a05bfea23f266688b2dd1623124d030e4d4eaaa31d0c8b9c5f28c
4
- data.tar.gz: 3126dccbc693deb8b3255408202400360fdffb14e439eda3923f4c3a320f452f
3
+ metadata.gz: 10897c2b3fdaea1f4728eb063c09f7996c6164f938d814dc486c746c4e91c081
4
+ data.tar.gz: e7e89af7bfa089d2f1a7c7324f028223e530231e808e34398a2665c11992c168
5
5
  SHA512:
6
- metadata.gz: d6982ddb9e837d4e7d7763b69045733f2751a74a867fa38789f58e7232a96583f7b2336ed8af7d5ebd0196ae58562aa8f19fb7d2f0f79425a1c5c22e6ddda966
7
- data.tar.gz: 315a781c1daa8f85a6b9f20790245191f5540014fb35b1e2a2a41827828ab7748b33de43e016b088da3054f9ef60b541ba390d492d7aad23f95f546b9b9e933d
6
+ metadata.gz: cb901d7e14ae688dbfec57c312a6edc338156dc0b239a9f56f6898443bbb3ed33e1aad85a260b5ab6c6aa21f3513fdac1e19f24b09418d8994be6f6f80a50a41
7
+ data.tar.gz: ca11e59b75a28e0c3a9f27fae6a9ac194082a9a1a034d1160050f21c8645f69111e6179e64c5d0048143f5d095f789207372c9d2ef3b3d2a0a07e10cd154438d
@@ -31,18 +31,7 @@ module StateMachines
31
31
  # Create async tasks for each transition
32
32
  tasks = map do |transition|
33
33
  Async do
34
- if use_event_attributes? && !block_given?
35
- transition.transient = true
36
- transition.machine.write_safely(object, :event_transition, transition)
37
- run_actions
38
- transition
39
- else
40
- within_transaction do
41
- catch(:halt) { run_callbacks(&block) }
42
- rollback unless success?
43
- end
44
- transition
45
- end
34
+ execute_transition(transition, &block)
46
35
  end
47
36
  end
48
37
 
@@ -80,18 +69,7 @@ module StateMachines
80
69
  each do |transition|
81
70
  threads << Thread.new do
82
71
  begin
83
- result = if use_event_attributes? && !block_given?
84
- transition.transient = true
85
- transition.machine.write_safely(object, :event_transition, transition)
86
- run_actions
87
- transition
88
- else
89
- within_transaction do
90
- catch(:halt) { run_callbacks(&block) }
91
- rollback unless success?
92
- end
93
- transition
94
- end
72
+ result = execute_transition(transition, &block)
95
73
 
96
74
  results_mutex.with_write_lock { results << result }
97
75
  rescue StandardError => e
@@ -112,6 +90,23 @@ module StateMachines
112
90
 
113
91
  private
114
92
 
93
+ # Runs a single transition either via event attributes or the standard
94
+ # callback/transaction flow, returning the transition on completion
95
+ def execute_transition(transition, &block)
96
+ if use_event_attributes? && !block_given?
97
+ transition.transient = true
98
+ transition.machine.write_safely(object, :event_transition, transition)
99
+ run_actions
100
+ else
101
+ within_transaction do
102
+ catch(:halt) { run_callbacks(&block) }
103
+ rollback unless success?
104
+ end
105
+ end
106
+
107
+ transition
108
+ end
109
+
115
110
  # Override run_actions to be thread-safe when needed
116
111
  def run_actions(&block)
117
112
  catch_exceptions do
@@ -163,41 +163,25 @@ module StateMachines
163
163
  # Symbol methods currently don't support event arguments
164
164
  # This maintains backward compatibility
165
165
  evaluate_method(object, method)
166
- in Proc => proc
167
- arity = proc.arity
166
+ in Proc | Method => callable
167
+ arity = callable.arity
168
168
 
169
169
  # Arity-based decision for backward compatibility using pattern matching
170
170
  case arity
171
171
  in 0
172
- proc.call
172
+ callable.call
173
173
  in 1
174
- proc.call(object)
174
+ callable.call(object)
175
175
  in -1
176
176
  # Splat parameters: object + all event args
177
- proc.call(object, *event_args)
177
+ callable.call(object, *event_args)
178
178
  in arity if arity > 1
179
179
  # Explicit parameters: object + limited event args
180
180
  args_needed = arity - 1 # Subtract 1 for the object parameter
181
- proc.call(object, *event_args[0, args_needed])
181
+ callable.call(object, *event_args[0, args_needed])
182
182
  else
183
183
  # Negative arity other than -1 (unlikely but handle gracefully)
184
- proc.call(object, *event_args)
185
- end
186
- in Method => meth
187
- arity = meth.arity
188
-
189
- case arity
190
- in 0
191
- meth.call
192
- in 1
193
- meth.call(object)
194
- in -1
195
- meth.call(object, *event_args)
196
- in arity if arity > 1
197
- args_needed = arity - 1
198
- meth.call(object, *event_args[0, args_needed])
199
- else
200
- meth.call(object, *event_args)
184
+ callable.call(object, *event_args)
201
185
  end
202
186
  in String
203
187
  # String evaluation doesn't support event arguments for security
@@ -5,46 +5,320 @@ module StateMachines
5
5
  module Callbacks
6
6
  # Creates a callback that will be invoked *before* a transition is
7
7
  # performed so long as the given requirements match the transition.
8
+ #
9
+ # == The callback
10
+ #
11
+ # Callbacks must be defined as either an argument, in the :do option, or
12
+ # as a block. For example,
13
+ #
14
+ # class Vehicle
15
+ # state_machine do
16
+ # before_transition :set_alarm
17
+ # before_transition :set_alarm, all => :parked
18
+ # before_transition all => :parked, :do => :set_alarm
19
+ # before_transition all => :parked do |vehicle, transition|
20
+ # vehicle.set_alarm
21
+ # end
22
+ # ...
23
+ # end
24
+ # end
25
+ #
26
+ # Notice that the first three callbacks are the same in terms of how the
27
+ # methods to invoke are defined. However, using the <tt>:do</tt> can
28
+ # provide for a more fluid DSL.
29
+ #
30
+ # In addition, multiple callbacks can be defined like so:
31
+ #
32
+ # class Vehicle
33
+ # state_machine do
34
+ # before_transition :set_alarm, :lock_doors, all => :parked
35
+ # before_transition all => :parked, :do => [:set_alarm, :lock_doors]
36
+ # before_transition :set_alarm do |vehicle, transition|
37
+ # vehicle.lock_doors
38
+ # end
39
+ # end
40
+ # end
41
+ #
42
+ # Notice that the different ways of configuring methods can be mixed.
43
+ #
44
+ # == State requirements
45
+ #
46
+ # Callbacks can require that the machine be transitioning from and to
47
+ # specific states. These requirements use a Hash syntax to map beginning
48
+ # states to ending states. For example,
49
+ #
50
+ # before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
51
+ #
52
+ # In this case, the +set_alarm+ callback will only be called if the machine
53
+ # is transitioning from +parked+ to +idling+ or from +idling+ to +parked+.
54
+ #
55
+ # To help define state requirements, a set of helpers are available for
56
+ # slightly more complex matching:
57
+ # * <tt>all</tt> - Matches every state/event in the machine
58
+ # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state/event except those specified
59
+ # * <tt>any</tt> - An alias for +all+ (matches every state/event in the machine)
60
+ # * <tt>same</tt> - Matches the same state being transitioned from
61
+ #
62
+ # See StateMachines::MatcherHelpers for more information.
63
+ #
64
+ # Examples:
65
+ #
66
+ # before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
67
+ # before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
68
+ # before_transition all => :parked, :do => ... # Matches all states to parked
69
+ # before_transition any => same, :do => ... # Matches every loopback
70
+ #
71
+ # == Event requirements
72
+ #
73
+ # In addition to state requirements, an event requirement can be defined so
74
+ # that the callback is only invoked on specific events using the +on+
75
+ # option. This can also use the same matcher helpers as the state
76
+ # requirements.
77
+ #
78
+ # Examples:
79
+ #
80
+ # before_transition :on => :ignite, :do => ... # Matches only on ignite
81
+ # before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
82
+ # before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
83
+ #
84
+ # == Verbose Requirements
85
+ #
86
+ # Requirements can also be defined using verbose options rather than the
87
+ # implicit Hash syntax and helper methods described above.
88
+ #
89
+ # Configuration options:
90
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
91
+ # are specified, then all states will match.
92
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
93
+ # specified, then all states will match.
94
+ # * <tt>:on</tt> - One or more events that fired the transition. If none
95
+ # are specified, then all events will match.
96
+ # * <tt>:except_from</tt> - One or more states *not* being transitioned from
97
+ # * <tt>:except_to</tt> - One more states *not* being transitioned to
98
+ # * <tt>:except_on</tt> - One or more events that *did not* fire the transition
99
+ #
100
+ # Examples:
101
+ #
102
+ # before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
103
+ # before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
104
+ #
105
+ # == Conditions
106
+ #
107
+ # In addition to the state/event requirements, a condition can also be
108
+ # defined to help determine whether the callback should be invoked.
109
+ #
110
+ # Configuration options:
111
+ # * <tt>:if</tt> - A method, proc or string to call to determine if the
112
+ # callback should occur (e.g. :if => :allow_callbacks, or
113
+ # :if => lambda {|user| user.signup_step > 2}). The method, proc or string
114
+ # should return or evaluate to a true or false value.
115
+ # * <tt>:unless</tt> - A method, proc or string to call to determine if the
116
+ # callback should not occur (e.g. :unless => :skip_callbacks, or
117
+ # :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
118
+ # string should return or evaluate to a true or false value.
119
+ #
120
+ # Examples:
121
+ #
122
+ # before_transition :parked => :idling, :if => :moving?, :do => ...
123
+ # before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
124
+ #
125
+ # == Accessing the transition
126
+ #
127
+ # In addition to passing the object being transitioned, the actual
128
+ # transition describing the context (e.g. event, from, to) can be accessed
129
+ # as well. This additional argument is only passed if the callback allows
130
+ # for it.
131
+ #
132
+ # For example,
133
+ #
134
+ # class Vehicle
135
+ # # Only specifies one parameter (the object being transitioned)
136
+ # before_transition all => :parked do |vehicle|
137
+ # vehicle.set_alarm
138
+ # end
139
+ #
140
+ # # Specifies 2 parameters (object being transitioned and actual transition)
141
+ # before_transition all => :parked do |vehicle, transition|
142
+ # vehicle.set_alarm(transition)
143
+ # end
144
+ # end
145
+ #
146
+ # *Note* that the object in the callback will only be passed in as an
147
+ # argument if callbacks are configured to *not* be bound to the object
148
+ # involved. This is the default and may change on a per-integration basis.
149
+ #
150
+ # See StateMachines::Transition for more information about the
151
+ # attributes available on the transition.
152
+ #
153
+ # == Usage with delegates
154
+ #
155
+ # As noted above, state_machine uses the callback method's argument list
156
+ # arity to determine whether to include the transition in the method call.
157
+ # If you're using delegates, such as those defined in ActiveSupport or
158
+ # Forwardable, the actual arity of the delegated method gets masked. This
159
+ # means that callbacks which reference delegates will always get passed the
160
+ # transition as an argument. For example:
161
+ #
162
+ # class Vehicle
163
+ # extend Forwardable
164
+ # delegate :refresh => :dashboard
165
+ #
166
+ # state_machine do
167
+ # before_transition :refresh
168
+ # ...
169
+ # end
170
+ #
171
+ # def dashboard
172
+ # @dashboard ||= Dashboard.new
173
+ # end
174
+ # end
175
+ #
176
+ # class Dashboard
177
+ # def refresh(transition)
178
+ # # ...
179
+ # end
180
+ # end
181
+ #
182
+ # In the above example, <tt>Dashboard#refresh</tt> *must* defined a
183
+ # +transition+ argument. Otherwise, an +ArgumentError+ exception will get
184
+ # raised. The only way around this is to avoid the use of delegates and
185
+ # manually define the delegate method so that the correct arity is used.
186
+ #
187
+ # == Examples
188
+ #
189
+ # Below is an example of a class with one state machine and various types
190
+ # of +before+ transitions defined for it:
191
+ #
192
+ # class Vehicle
193
+ # state_machine do
194
+ # # Before all transitions
195
+ # before_transition :update_dashboard
196
+ #
197
+ # # Before specific transition:
198
+ # before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
199
+ #
200
+ # # With conditional callback:
201
+ # before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
202
+ #
203
+ # # Using helpers:
204
+ # before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
205
+ # ...
206
+ # end
207
+ # end
208
+ #
209
+ # As can be seen, any number of transitions can be created using various
210
+ # combinations of configuration options.
8
211
  def before_transition(*args, **options, &)
9
- # Extract legacy positional arguments and merge with keyword options
10
- parsed_options = parse_callback_arguments(args, options)
11
-
12
- # Only validate callback-specific options, not state transition requirements
13
- callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
14
- StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
15
-
16
- add_callback(:before, parsed_options, &)
212
+ add_transition_callback(:before, args, options, &)
17
213
  end
18
214
 
19
215
  # Creates a callback that will be invoked *after* a transition is
20
216
  # performed so long as the given requirements match the transition.
217
+ #
218
+ # See +before_transition+ for a description of the possible configurations
219
+ # for defining callbacks.
21
220
  def after_transition(*args, **options, &)
22
- # Extract legacy positional arguments and merge with keyword options
23
- parsed_options = parse_callback_arguments(args, options)
24
-
25
- # Only validate callback-specific options, not state transition requirements
26
- callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
27
- StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
28
-
29
- add_callback(:after, parsed_options, &)
221
+ add_transition_callback(:after, args, options, &)
30
222
  end
31
223
 
32
- # Creates a callback that will be invoked *around* a transition so long
33
- # as the given requirements match the transition.
224
+ # Creates a callback that will be invoked *around* a transition so long as
225
+ # the given requirements match the transition.
226
+ #
227
+ # == The callback
228
+ #
229
+ # Around callbacks wrap transitions, executing code both before and after.
230
+ # These callbacks are defined in the exact same manner as before / after
231
+ # callbacks with the exception that the transition must be yielded to in
232
+ # order to finish running it.
233
+ #
234
+ # If defining +around+ callbacks using blocks, you must yield within the
235
+ # transition by directly calling the block (since yielding is not allowed
236
+ # within blocks).
237
+ #
238
+ # For example,
239
+ #
240
+ # class Vehicle
241
+ # state_machine do
242
+ # around_transition do |block|
243
+ # Benchmark.measure { block.call }
244
+ # end
245
+ #
246
+ # around_transition do |vehicle, block|
247
+ # logger.info "vehicle was #{state}..."
248
+ # block.call
249
+ # logger.info "...and is now #{state}"
250
+ # end
251
+ #
252
+ # around_transition do |vehicle, transition, block|
253
+ # logger.info "before #{transition.event}: #{vehicle.state}"
254
+ # block.call
255
+ # logger.info "after #{transition.event}: #{vehicle.state}"
256
+ # end
257
+ # end
258
+ # end
259
+ #
260
+ # Notice that referencing the block is similar to doing so within an
261
+ # actual method definition in that it is always the last argument.
262
+ #
263
+ # On the other hand, if you're defining +around+ callbacks using method
264
+ # references, you can yield like normal:
265
+ #
266
+ # class Vehicle
267
+ # state_machine do
268
+ # around_transition :benchmark
269
+ # ...
270
+ # end
271
+ #
272
+ # def benchmark
273
+ # Benchmark.measure { yield }
274
+ # end
275
+ # end
276
+ #
277
+ # See +before_transition+ for a description of the possible configurations
278
+ # for defining callbacks.
34
279
  def around_transition(*args, **options, &)
280
+ add_transition_callback(:around, args, options, &)
281
+ end
282
+
283
+ # Creates a callback that will be invoked *after* a transition failures to
284
+ # be performed so long as the given requirements match the transition.
285
+ #
286
+ # See +before_transition+ for a description of the possible configurations
287
+ # for defining callbacks. *Note* however that you cannot define the state
288
+ # requirements in these callbacks. You may only define event requirements.
289
+ #
290
+ # = The callback
291
+ #
292
+ # Failure callbacks get invoked whenever an event fails to execute. This
293
+ # can happen when no transition is available, a +before+ callback halts
294
+ # execution, or the action associated with this machine fails to succeed.
295
+ # In any of these cases, any failure callback that matches the attempted
296
+ # transition will be run.
297
+ #
298
+ # For example,
299
+ #
300
+ # class Vehicle
301
+ # state_machine do
302
+ # after_failure do |vehicle, transition|
303
+ # logger.error "vehicle #{vehicle} failed to transition on #{transition.event}"
304
+ # end
305
+ #
306
+ # after_failure :on => :ignite, :do => :log_ignition_failure
307
+ #
308
+ # ...
309
+ # end
310
+ # end
311
+ def after_failure(*args, **options, &)
35
312
  # Extract legacy positional arguments and merge with keyword options
36
313
  parsed_options = parse_callback_arguments(args, options)
314
+ StateMachines::OptionsValidator.assert_valid_keys!(parsed_options, :on, :do, :if, :unless)
37
315
 
38
- # Only validate callback-specific options, not state transition requirements
39
- callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
40
- StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
41
-
42
- add_callback(:around, parsed_options, &)
316
+ add_callback(:failure, parsed_options, &)
43
317
  end
44
318
 
45
- # Creates a callback that will be invoked after a transition has failed
46
- # to be performed.
47
- def after_failure(*args, **options, &)
319
+ private
320
+
321
+ def add_transition_callback(type, args, options, &)
48
322
  # Extract legacy positional arguments and merge with keyword options
49
323
  parsed_options = parse_callback_arguments(args, options)
50
324
 
@@ -52,7 +326,7 @@ module StateMachines
52
326
  callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
53
327
  StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
54
328
 
55
- add_callback(:failure, parsed_options, &)
329
+ add_callback(type, parsed_options, &)
56
330
  end
57
331
  end
58
332
  end