ultra-smart-sys 0.0.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.
- checksums.yaml +7 -0
- data/state_machines-0.201.0/LICENSE.txt +23 -0
- data/state_machines-0.201.0/README.md +1024 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_event_extensions.rb +49 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_events.rb +282 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_machine.rb +60 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_transition_collection.rb +136 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/thread_safe_state.rb +47 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode.rb +64 -0
- data/state_machines-0.201.0/lib/state_machines/branch.rb +246 -0
- data/state_machines-0.201.0/lib/state_machines/callback.rb +223 -0
- data/state_machines-0.201.0/lib/state_machines/core.rb +43 -0
- data/state_machines-0.201.0/lib/state_machines/core_ext/class/state_machine.rb +5 -0
- data/state_machines-0.201.0/lib/state_machines/core_ext.rb +4 -0
- data/state_machines-0.201.0/lib/state_machines/error.rb +115 -0
- data/state_machines-0.201.0/lib/state_machines/eval_helpers.rb +227 -0
- data/state_machines-0.201.0/lib/state_machines/event.rb +247 -0
- data/state_machines-0.201.0/lib/state_machines/event_collection.rb +149 -0
- data/state_machines-0.201.0/lib/state_machines/extensions.rb +150 -0
- data/state_machines-0.201.0/lib/state_machines/helper_module.rb +19 -0
- data/state_machines-0.201.0/lib/state_machines/integrations/base.rb +49 -0
- data/state_machines-0.201.0/lib/state_machines/integrations.rb +111 -0
- data/state_machines-0.201.0/lib/state_machines/machine/action_hooks.rb +53 -0
- data/state_machines-0.201.0/lib/state_machines/machine/async_extensions.rb +88 -0
- data/state_machines-0.201.0/lib/state_machines/machine/callbacks.rb +333 -0
- data/state_machines-0.201.0/lib/state_machines/machine/class_methods.rb +95 -0
- data/state_machines-0.201.0/lib/state_machines/machine/configuration.rb +128 -0
- data/state_machines-0.201.0/lib/state_machines/machine/event_methods.rb +436 -0
- data/state_machines-0.201.0/lib/state_machines/machine/helper_generators.rb +125 -0
- data/state_machines-0.201.0/lib/state_machines/machine/integration.rb +92 -0
- data/state_machines-0.201.0/lib/state_machines/machine/parsing.rb +77 -0
- data/state_machines-0.201.0/lib/state_machines/machine/rendering.rb +17 -0
- data/state_machines-0.201.0/lib/state_machines/machine/scoping.rb +44 -0
- data/state_machines-0.201.0/lib/state_machines/machine/state_methods.rb +398 -0
- data/state_machines-0.201.0/lib/state_machines/machine/utilities.rb +86 -0
- data/state_machines-0.201.0/lib/state_machines/machine/validation.rb +39 -0
- data/state_machines-0.201.0/lib/state_machines/machine.rb +615 -0
- data/state_machines-0.201.0/lib/state_machines/machine_collection.rb +105 -0
- data/state_machines-0.201.0/lib/state_machines/macro_methods.rb +522 -0
- data/state_machines-0.201.0/lib/state_machines/matcher.rb +124 -0
- data/state_machines-0.201.0/lib/state_machines/matcher_helpers.rb +56 -0
- data/state_machines-0.201.0/lib/state_machines/node_collection.rb +226 -0
- data/state_machines-0.201.0/lib/state_machines/options_validator.rb +72 -0
- data/state_machines-0.201.0/lib/state_machines/path.rb +123 -0
- data/state_machines-0.201.0/lib/state_machines/path_collection.rb +91 -0
- data/state_machines-0.201.0/lib/state_machines/state.rb +314 -0
- data/state_machines-0.201.0/lib/state_machines/state_collection.rb +113 -0
- data/state_machines-0.201.0/lib/state_machines/state_context.rb +134 -0
- data/state_machines-0.201.0/lib/state_machines/stdio_renderer.rb +74 -0
- data/state_machines-0.201.0/lib/state_machines/syntax_validator.rb +54 -0
- data/state_machines-0.201.0/lib/state_machines/test_helper.rb +775 -0
- data/state_machines-0.201.0/lib/state_machines/transition.rb +593 -0
- data/state_machines-0.201.0/lib/state_machines/transition_collection.rb +310 -0
- data/state_machines-0.201.0/lib/state_machines/version.rb +5 -0
- data/state_machines-0.201.0/lib/state_machines.rb +6 -0
- data/ultra-smart-sys.gemspec +12 -0
- metadata +96 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StateMachines
|
|
4
|
+
class Machine
|
|
5
|
+
module Callbacks
|
|
6
|
+
# Creates a callback that will be invoked *before* a transition is
|
|
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.
|
|
211
|
+
def before_transition(*args, **options, &)
|
|
212
|
+
add_transition_callback(:before, args, options, &)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Creates a callback that will be invoked *after* a transition is
|
|
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.
|
|
220
|
+
def after_transition(*args, **options, &)
|
|
221
|
+
add_transition_callback(:after, args, options, &)
|
|
222
|
+
end
|
|
223
|
+
|
|
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.
|
|
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, &)
|
|
312
|
+
# Extract legacy positional arguments and merge with keyword options
|
|
313
|
+
parsed_options = parse_callback_arguments(args, options)
|
|
314
|
+
StateMachines::OptionsValidator.assert_valid_keys!(parsed_options, :on, :do, :if, :unless)
|
|
315
|
+
|
|
316
|
+
add_callback(:failure, parsed_options, &)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
private
|
|
320
|
+
|
|
321
|
+
def add_transition_callback(type, args, options, &)
|
|
322
|
+
# Extract legacy positional arguments and merge with keyword options
|
|
323
|
+
parsed_options = parse_callback_arguments(args, options)
|
|
324
|
+
|
|
325
|
+
# Only validate callback-specific options, not state transition requirements
|
|
326
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
|
327
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
|
328
|
+
|
|
329
|
+
add_callback(type, parsed_options, &)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StateMachines
|
|
4
|
+
class Machine
|
|
5
|
+
module ClassMethods
|
|
6
|
+
# Attempts to find or create a state machine for the given class. For
|
|
7
|
+
# example,
|
|
8
|
+
#
|
|
9
|
+
# StateMachines::Machine.find_or_create(Vehicle)
|
|
10
|
+
# StateMachines::Machine.find_or_create(Vehicle, :initial => :parked)
|
|
11
|
+
# StateMachines::Machine.find_or_create(Vehicle, :status)
|
|
12
|
+
# StateMachines::Machine.find_or_create(Vehicle, :status, :initial => :parked)
|
|
13
|
+
#
|
|
14
|
+
# If a machine of the given name already exists in one of the class's
|
|
15
|
+
# superclasses, then a copy of that machine will be created and stored
|
|
16
|
+
# in the new owner class (the original will remain unchanged).
|
|
17
|
+
def find_or_create(owner_class, *args, &)
|
|
18
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
19
|
+
name = args.first || :state
|
|
20
|
+
|
|
21
|
+
# Find an existing machine
|
|
22
|
+
machine = (owner_class.respond_to?(:state_machines) &&
|
|
23
|
+
((args.first && owner_class.state_machines[name]) || (!args.first &&
|
|
24
|
+
owner_class.state_machines.values.first))) || nil
|
|
25
|
+
|
|
26
|
+
if machine
|
|
27
|
+
# Only create a new copy if changes are being made to the machine in
|
|
28
|
+
# a subclass
|
|
29
|
+
if machine.owner_class != owner_class && (options.any? || block_given?)
|
|
30
|
+
machine = machine.clone
|
|
31
|
+
machine.initial_state = options[:initial] if options.include?(:initial)
|
|
32
|
+
machine.owner_class = owner_class
|
|
33
|
+
# Configure async mode if requested in options
|
|
34
|
+
machine.configure_async_mode!(options[:async]) if options.include?(:async)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Evaluate DSL
|
|
38
|
+
machine.instance_eval(&) if block_given?
|
|
39
|
+
else
|
|
40
|
+
# No existing machine: create a new one
|
|
41
|
+
machine = new(owner_class, name, options, &)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
machine
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def draw(*)
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Default messages to use for validation errors in ORM integrations
|
|
52
|
+
# Thread-safe access via atomic operations on simple values
|
|
53
|
+
attr_accessor :ignore_method_conflicts
|
|
54
|
+
|
|
55
|
+
def default_messages
|
|
56
|
+
@default_messages ||= {
|
|
57
|
+
invalid: 'is invalid',
|
|
58
|
+
invalid_event: 'cannot transition when %s',
|
|
59
|
+
invalid_transition: 'cannot transition via "%1$s"'
|
|
60
|
+
}.freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def default_messages=(messages)
|
|
64
|
+
# Atomic replacement with frozen object
|
|
65
|
+
@default_messages = deep_freeze_hash(messages)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def replace_messages(message_hash)
|
|
69
|
+
# Atomic replacement: read current messages, merge with new ones, replace atomically
|
|
70
|
+
current_messages = @default_messages || {}
|
|
71
|
+
merged_messages = current_messages.merge(message_hash)
|
|
72
|
+
@default_messages = deep_freeze_hash(merged_messages)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
attr_writer :renderer
|
|
76
|
+
|
|
77
|
+
def renderer
|
|
78
|
+
return @renderer if @renderer
|
|
79
|
+
|
|
80
|
+
STDIORenderer
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Deep freezes a hash and all its string values for thread safety
|
|
86
|
+
def deep_freeze_hash(hash)
|
|
87
|
+
hash.each_with_object({}) do |(key, value), frozen_hash|
|
|
88
|
+
frozen_key = key.respond_to?(:freeze) ? key.freeze : key
|
|
89
|
+
frozen_value = value.respond_to?(:freeze) ? value.freeze : value
|
|
90
|
+
frozen_hash[frozen_key] = frozen_value
|
|
91
|
+
end.freeze
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StateMachines
|
|
4
|
+
class Machine
|
|
5
|
+
module Configuration
|
|
6
|
+
# Initializes a new state machine with the given configuration.
|
|
7
|
+
def initialize(owner_class, *args, &)
|
|
8
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
9
|
+
|
|
10
|
+
# Find an integration that matches this machine's owner class
|
|
11
|
+
@integration = if options.include?(:integration)
|
|
12
|
+
options[:integration] && StateMachines::Integrations.find_by_name(options[:integration])
|
|
13
|
+
else
|
|
14
|
+
StateMachines::Integrations.match(owner_class)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Validate options including integration-specific options
|
|
18
|
+
valid_keys = [:attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions, :async]
|
|
19
|
+
valid_keys += @integration.integration_options if @integration
|
|
20
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, valid_keys)
|
|
21
|
+
|
|
22
|
+
if @integration
|
|
23
|
+
extend @integration
|
|
24
|
+
options = (@integration.defaults || {}).merge(options)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Add machine-wide defaults
|
|
28
|
+
options = { use_transactions: true, initialize: true }.merge(options)
|
|
29
|
+
|
|
30
|
+
# Set machine configuration
|
|
31
|
+
@name = args.first || :state
|
|
32
|
+
@attribute = options[:attribute] || @name
|
|
33
|
+
@events = EventCollection.new(self)
|
|
34
|
+
@states = StateCollection.new(self)
|
|
35
|
+
@callbacks = { before: [], after: [], failure: [] }
|
|
36
|
+
@namespace = options[:namespace]
|
|
37
|
+
@messages = options[:messages] || {}
|
|
38
|
+
@action = options[:action]
|
|
39
|
+
@use_transactions = options[:use_transactions]
|
|
40
|
+
@initialize_state = options[:initialize]
|
|
41
|
+
@action_hook_defined = false
|
|
42
|
+
@async_requested = options[:async]
|
|
43
|
+
|
|
44
|
+
self.owner_class = owner_class
|
|
45
|
+
|
|
46
|
+
# Merge with sibling machine configurations
|
|
47
|
+
add_sibling_machine_configs
|
|
48
|
+
|
|
49
|
+
# Define class integration
|
|
50
|
+
define_helpers
|
|
51
|
+
define_scopes(options[:plural])
|
|
52
|
+
after_initialize
|
|
53
|
+
|
|
54
|
+
# Evaluate DSL
|
|
55
|
+
instance_eval(&) if block_given?
|
|
56
|
+
|
|
57
|
+
# Configure async mode if requested, after owner_class is set and DSL is evaluated
|
|
58
|
+
configure_async_mode!(true) if @async_requested
|
|
59
|
+
|
|
60
|
+
self.initial_state = options[:initial] unless sibling_machines.any?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Creates a copy of this machine in addition to copies of each associated
|
|
64
|
+
# event/states/callback, so that the modifications to those collections do
|
|
65
|
+
# not affect the original machine.
|
|
66
|
+
def initialize_copy(orig) # :nodoc:
|
|
67
|
+
super
|
|
68
|
+
|
|
69
|
+
@events = @events.dup
|
|
70
|
+
@events.machine = self
|
|
71
|
+
@states = @states.dup
|
|
72
|
+
@states.machine = self
|
|
73
|
+
@callbacks = { before: @callbacks[:before].dup, after: @callbacks[:after].dup, failure: @callbacks[:failure].dup }
|
|
74
|
+
@async_requested = orig.instance_variable_get(:@async_requested)
|
|
75
|
+
@async_mode_enabled = orig.instance_variable_get(:@async_mode_enabled)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Sets the class which is the owner of this state machine. Any methods
|
|
79
|
+
# generated by states, events, or other parts of the machine will be defined
|
|
80
|
+
# on the given owner class.
|
|
81
|
+
def owner_class=(klass)
|
|
82
|
+
@owner_class = klass
|
|
83
|
+
|
|
84
|
+
# Create modules for extending the class with state/event-specific methods
|
|
85
|
+
@helper_modules = helper_modules = { instance: HelperModule.new(self, :instance), class: HelperModule.new(self, :class) }
|
|
86
|
+
owner_class.class_eval do
|
|
87
|
+
extend helper_modules[:class]
|
|
88
|
+
include helper_modules[:instance]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Add class-/instance-level methods to the owner class for state initialization
|
|
92
|
+
unless owner_class < StateMachines::InstanceMethods
|
|
93
|
+
owner_class.class_eval do
|
|
94
|
+
extend StateMachines::ClassMethods
|
|
95
|
+
include StateMachines::InstanceMethods
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
define_state_initializer if @initialize_state
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Record this machine as matched to the name in the current owner class.
|
|
102
|
+
# This will override any machines mapped to the same name in any superclasses.
|
|
103
|
+
owner_class.state_machines[name] = self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Sets the initial state of the machine. This can be either the static name
|
|
107
|
+
# of a state or a lambda block which determines the initial state at
|
|
108
|
+
# creation time.
|
|
109
|
+
def initial_state=(new_initial_state)
|
|
110
|
+
@initial_state = new_initial_state
|
|
111
|
+
add_states([@initial_state]) unless dynamic_initial_state?
|
|
112
|
+
|
|
113
|
+
# Update all states to reflect the new initial state
|
|
114
|
+
states.each { |state| state.initial = (state.name == @initial_state) }
|
|
115
|
+
|
|
116
|
+
# Warn if the owner class and the machine have conflicting defaults for
|
|
117
|
+
# the machine's attribute. May be deferred by integrations (e.g.
|
|
118
|
+
# ActiveRecord) to avoid touching the DB at class load time.
|
|
119
|
+
schedule_conflicting_attribute_default_check
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Gets the attribute name for the given machine scope.
|
|
123
|
+
def attribute(name = :state)
|
|
124
|
+
name == :state ? @attribute : :"#{self.name}_#{name}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|