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,436 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StateMachines
|
|
4
|
+
class Machine
|
|
5
|
+
module EventMethods
|
|
6
|
+
# Defines one or more events for the machine and the transitions that can
|
|
7
|
+
# be performed when those events are run.
|
|
8
|
+
#
|
|
9
|
+
# This method is also aliased as +on+ for improved compatibility with
|
|
10
|
+
# using a domain-specific language.
|
|
11
|
+
#
|
|
12
|
+
# Configuration options:
|
|
13
|
+
# * <tt>:human_name</tt> - The human-readable version of this event's name.
|
|
14
|
+
# By default, this is either defined by the integration or stringifies the
|
|
15
|
+
# name and converts underscores to spaces.
|
|
16
|
+
#
|
|
17
|
+
# == Instance methods
|
|
18
|
+
#
|
|
19
|
+
# The following instance methods are generated when a new event is defined
|
|
20
|
+
# (the "park" event is used as an example):
|
|
21
|
+
# * <tt>park(..., run_action = true)</tt> - Fires the "park" event,
|
|
22
|
+
# transitioning from the current state to the next valid state. If the
|
|
23
|
+
# last argument is a boolean, it will control whether the machine's action
|
|
24
|
+
# gets run.
|
|
25
|
+
# * <tt>park!(..., run_action = true)</tt> - Fires the "park" event,
|
|
26
|
+
# transitioning from the current state to the next valid state. If the
|
|
27
|
+
# transition fails, then a StateMachines::InvalidTransition error will be
|
|
28
|
+
# raised. If the last argument is a boolean, it will control whether the
|
|
29
|
+
# machine's action gets run.
|
|
30
|
+
# * <tt>can_park?(requirements = {})</tt> - Checks whether the "park" event
|
|
31
|
+
# can be fired given the current state of the object. This will *not* run
|
|
32
|
+
# validations or callbacks in ORM integrations. It will only determine if
|
|
33
|
+
# the state machine defines a valid transition for the event. To check
|
|
34
|
+
# whether an event can fire *and* passes validations, use event attributes
|
|
35
|
+
# (e.g. state_event) as described in the "Events" documentation of each
|
|
36
|
+
# ORM integration.
|
|
37
|
+
# * <tt>park_transition(requirements = {})</tt> - Gets the next transition
|
|
38
|
+
# that would be performed if the "park" event were to be fired now on the
|
|
39
|
+
# object or nil if no transitions can be performed. Like <tt>can_park?</tt>
|
|
40
|
+
# this will also *not* run validations or callbacks. It will only
|
|
41
|
+
# determine if the state machine defines a valid transition for the event.
|
|
42
|
+
#
|
|
43
|
+
# With a namespace of "car", the above names map to the following methods:
|
|
44
|
+
# * <tt>can_park_car?</tt>
|
|
45
|
+
# * <tt>park_car_transition</tt>
|
|
46
|
+
# * <tt>park_car</tt>
|
|
47
|
+
# * <tt>park_car!</tt>
|
|
48
|
+
#
|
|
49
|
+
# The <tt>can_park?</tt> and <tt>park_transition</tt> helpers both take an
|
|
50
|
+
# optional set of requirements for determining what transitions are available
|
|
51
|
+
# for the current object. These requirements include:
|
|
52
|
+
# * <tt>:from</tt> - One or more states to transition from. If none are
|
|
53
|
+
# specified, then this will be the object's current state.
|
|
54
|
+
# * <tt>:to</tt> - One or more states to transition to. If none are
|
|
55
|
+
# specified, then this will match any to state.
|
|
56
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
|
57
|
+
# conditionals defined for each one. Default is true.
|
|
58
|
+
#
|
|
59
|
+
# == Defining transitions
|
|
60
|
+
#
|
|
61
|
+
# +event+ requires a block which allows you to define the possible
|
|
62
|
+
# transitions that can happen as a result of that event. For example,
|
|
63
|
+
#
|
|
64
|
+
# event :park, :stop do
|
|
65
|
+
# transition :idling => :parked
|
|
66
|
+
# end
|
|
67
|
+
#
|
|
68
|
+
# event :first_gear do
|
|
69
|
+
# transition :parked => :first_gear, :if => :seatbelt_on?
|
|
70
|
+
# transition :parked => same # Allow to loopback if seatbelt is off
|
|
71
|
+
# end
|
|
72
|
+
#
|
|
73
|
+
# See StateMachines::Event#transition for more information on
|
|
74
|
+
# the possible options that can be passed in.
|
|
75
|
+
#
|
|
76
|
+
# *Note* that this block is executed within the context of the actual event
|
|
77
|
+
# object. As a result, you will not be able to reference any class methods
|
|
78
|
+
# on the model without referencing the class itself. For example,
|
|
79
|
+
#
|
|
80
|
+
# class Vehicle
|
|
81
|
+
# def self.safe_states
|
|
82
|
+
# [:parked, :idling, :stalled]
|
|
83
|
+
# end
|
|
84
|
+
#
|
|
85
|
+
# state_machine do
|
|
86
|
+
# event :park do
|
|
87
|
+
# transition Vehicle.safe_states => :parked
|
|
88
|
+
# end
|
|
89
|
+
# end
|
|
90
|
+
# end
|
|
91
|
+
#
|
|
92
|
+
# == Overriding the event method
|
|
93
|
+
#
|
|
94
|
+
# By default, this will define an instance method (with the same name as the
|
|
95
|
+
# event) that will fire the next possible transition for that. Although the
|
|
96
|
+
# +before_transition+, +after_transition+, and +around_transition+ hooks
|
|
97
|
+
# allow you to define behavior that gets executed as a result of the event's
|
|
98
|
+
# transition, you can also override the event method in order to have a
|
|
99
|
+
# little more fine-grained control.
|
|
100
|
+
#
|
|
101
|
+
# For example:
|
|
102
|
+
#
|
|
103
|
+
# class Vehicle
|
|
104
|
+
# state_machine do
|
|
105
|
+
# event :park do
|
|
106
|
+
# ...
|
|
107
|
+
# end
|
|
108
|
+
# end
|
|
109
|
+
#
|
|
110
|
+
# def park(*)
|
|
111
|
+
# take_deep_breath # Executes before the transition (and before_transition hooks) even if no transition is possible
|
|
112
|
+
# if result = super # Runs the transition and all before/after/around hooks
|
|
113
|
+
# applaud # Executes after the transition (and after_transition hooks)
|
|
114
|
+
# end
|
|
115
|
+
# result
|
|
116
|
+
# end
|
|
117
|
+
# end
|
|
118
|
+
#
|
|
119
|
+
# There are a few important things to note here. First, the method
|
|
120
|
+
# signature is defined with an unlimited argument list in order to allow
|
|
121
|
+
# callers to continue passing arguments that are expected by state_machine.
|
|
122
|
+
# For example, it will still allow calls to +park+ with a single parameter
|
|
123
|
+
# for skipping the configured action.
|
|
124
|
+
#
|
|
125
|
+
# Second, the overridden event method must call +super+ in order to run the
|
|
126
|
+
# logic for running the next possible transition. In order to remain
|
|
127
|
+
# consistent with other events, the result of +super+ is returned.
|
|
128
|
+
#
|
|
129
|
+
# Third, any behavior defined in this method will *not* get executed if
|
|
130
|
+
# you're taking advantage of attribute-based event transitions. For example:
|
|
131
|
+
#
|
|
132
|
+
# vehicle = Vehicle.new
|
|
133
|
+
# vehicle.state_event = 'park'
|
|
134
|
+
# vehicle.save
|
|
135
|
+
#
|
|
136
|
+
# In this case, the +park+ event will run the before/after/around transition
|
|
137
|
+
# hooks and transition the state, but the behavior defined in the overriden
|
|
138
|
+
# +park+ method will *not* be executed.
|
|
139
|
+
#
|
|
140
|
+
# == Defining additional arguments
|
|
141
|
+
#
|
|
142
|
+
# Additional arguments can be passed into events and accessed by transition
|
|
143
|
+
# hooks like so:
|
|
144
|
+
#
|
|
145
|
+
# class Vehicle
|
|
146
|
+
# state_machine do
|
|
147
|
+
# after_transition :on => :park do |vehicle, transition|
|
|
148
|
+
# kind = *transition.args # :parallel
|
|
149
|
+
# ...
|
|
150
|
+
# end
|
|
151
|
+
# after_transition :on => :park, :do => :take_deep_breath
|
|
152
|
+
#
|
|
153
|
+
# event :park do
|
|
154
|
+
# ...
|
|
155
|
+
# end
|
|
156
|
+
#
|
|
157
|
+
# def take_deep_breath(transition)
|
|
158
|
+
# kind = *transition.args # :parallel
|
|
159
|
+
# ...
|
|
160
|
+
# end
|
|
161
|
+
# end
|
|
162
|
+
# end
|
|
163
|
+
#
|
|
164
|
+
# vehicle = Vehicle.new
|
|
165
|
+
# vehicle.park(:parallel)
|
|
166
|
+
#
|
|
167
|
+
# *Remember* that if the last argument is a boolean, it will be used as the
|
|
168
|
+
# +run_action+ parameter to the event action. Using the +park+ action
|
|
169
|
+
# example from above, you can might call it like so:
|
|
170
|
+
#
|
|
171
|
+
# vehicle.park # => Uses default args and runs machine action
|
|
172
|
+
# vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action
|
|
173
|
+
# vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action
|
|
174
|
+
#
|
|
175
|
+
# If you decide to override the +park+ event method *and* define additional
|
|
176
|
+
# arguments, you can do so as shown below:
|
|
177
|
+
#
|
|
178
|
+
# class Vehicle
|
|
179
|
+
# state_machine do
|
|
180
|
+
# event :park do
|
|
181
|
+
# ...
|
|
182
|
+
# end
|
|
183
|
+
# end
|
|
184
|
+
#
|
|
185
|
+
# def park(kind = :parallel, *args)
|
|
186
|
+
# take_deep_breath if kind == :parallel
|
|
187
|
+
# super
|
|
188
|
+
# end
|
|
189
|
+
# end
|
|
190
|
+
#
|
|
191
|
+
# Note that +super+ is called instead of <tt>super(*args)</tt>. This allow
|
|
192
|
+
# the entire arguments list to be accessed by transition callbacks through
|
|
193
|
+
# StateMachines::Transition#args.
|
|
194
|
+
#
|
|
195
|
+
# === Using matchers
|
|
196
|
+
#
|
|
197
|
+
# The +all+ / +any+ matchers can be used to easily execute blocks for a
|
|
198
|
+
# group of events. Note, however, that you cannot use these matchers to
|
|
199
|
+
# set configurations for events. Blocks using these matchers can be
|
|
200
|
+
# defined at any point in the state machine and will always get applied to
|
|
201
|
+
# the proper events.
|
|
202
|
+
#
|
|
203
|
+
# For example:
|
|
204
|
+
#
|
|
205
|
+
# state_machine :initial => :parked do
|
|
206
|
+
# ...
|
|
207
|
+
#
|
|
208
|
+
# event all - [:crash] do
|
|
209
|
+
# transition :stalled => :parked
|
|
210
|
+
# end
|
|
211
|
+
# end
|
|
212
|
+
#
|
|
213
|
+
# == Example
|
|
214
|
+
#
|
|
215
|
+
# class Vehicle
|
|
216
|
+
# state_machine do
|
|
217
|
+
# # The park, stop, and halt events will all share the given transitions
|
|
218
|
+
# event :park, :stop, :halt do
|
|
219
|
+
# transition [:idling, :backing_up] => :parked
|
|
220
|
+
# end
|
|
221
|
+
#
|
|
222
|
+
# event :stop do
|
|
223
|
+
# transition :first_gear => :idling
|
|
224
|
+
# end
|
|
225
|
+
#
|
|
226
|
+
# event :ignite do
|
|
227
|
+
# transition :parked => :idling
|
|
228
|
+
# transition :idling => same # Allow ignite while still idling
|
|
229
|
+
# end
|
|
230
|
+
# end
|
|
231
|
+
# end
|
|
232
|
+
def event(*names, &)
|
|
233
|
+
options = names.last.is_a?(Hash) ? names.pop : {}
|
|
234
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
|
|
235
|
+
|
|
236
|
+
# Store the context so that it can be used for / matched against any event
|
|
237
|
+
# that gets added
|
|
238
|
+
@events.context(names, &) if block_given?
|
|
239
|
+
|
|
240
|
+
if names.first.is_a?(Matcher)
|
|
241
|
+
# Add any events referenced in the matcher. When matchers are used,
|
|
242
|
+
# events are not allowed to be configured.
|
|
243
|
+
raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
|
|
244
|
+
|
|
245
|
+
events = add_events(names.first.values)
|
|
246
|
+
else
|
|
247
|
+
events = add_events(names)
|
|
248
|
+
|
|
249
|
+
# Update the configuration for the event(s)
|
|
250
|
+
events.each do |event|
|
|
251
|
+
event.human_name = options[:human_name] if options.include?(:human_name)
|
|
252
|
+
|
|
253
|
+
# Add any states that may have been referenced within the event
|
|
254
|
+
add_states(event.known_states)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
events.length == 1 ? events.first : events
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
alias on event
|
|
262
|
+
|
|
263
|
+
# Creates a new transition that determines what to change the current state
|
|
264
|
+
# to when an event fires.
|
|
265
|
+
#
|
|
266
|
+
# == Defining transitions
|
|
267
|
+
#
|
|
268
|
+
# The options for a new transition uses the Hash syntax to map beginning
|
|
269
|
+
# states to ending states. For example,
|
|
270
|
+
#
|
|
271
|
+
# transition :parked => :idling, :idling => :first_gear, :on => :ignite
|
|
272
|
+
#
|
|
273
|
+
# In this case, when the +ignite+ event is fired, this transition will cause
|
|
274
|
+
# the state to be +idling+ if it's current state is +parked+ or +first_gear+
|
|
275
|
+
# if it's current state is +idling+.
|
|
276
|
+
#
|
|
277
|
+
# To help define these implicit transitions, a set of helpers are available
|
|
278
|
+
# for slightly more complex matching:
|
|
279
|
+
# * <tt>all</tt> - Matches every state in the machine
|
|
280
|
+
# * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
|
|
281
|
+
# * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
|
|
282
|
+
# * <tt>same</tt> - Matches the same state being transitioned from
|
|
283
|
+
#
|
|
284
|
+
# See StateMachines::MatcherHelpers for more information.
|
|
285
|
+
#
|
|
286
|
+
# Examples:
|
|
287
|
+
#
|
|
288
|
+
# transition all => nil, :on => :ignite # Transitions to nil regardless of the current state
|
|
289
|
+
# transition all => :idling, :on => :ignite # Transitions to :idling regardless of the current state
|
|
290
|
+
# transition all - [:idling, :first_gear] => :idling, :on => :ignite # Transitions every state but :idling and :first_gear to :idling
|
|
291
|
+
# transition nil => :idling, :on => :ignite # Transitions to :idling from the nil state
|
|
292
|
+
# transition :parked => :idling, :on => :ignite # Transitions to :idling if :parked
|
|
293
|
+
# transition [:parked, :stalled] => :idling, :on => :ignite # Transitions to :idling if :parked or :stalled
|
|
294
|
+
#
|
|
295
|
+
# transition :parked => same, :on => :park # Loops :parked back to :parked
|
|
296
|
+
# transition [:parked, :stalled] => same, :on => [:park, :stall] # Loops either :parked or :stalled back to the same state on the park and stall events
|
|
297
|
+
# transition all - :parked => same, :on => :noop # Loops every state but :parked back to the same state
|
|
298
|
+
#
|
|
299
|
+
# # Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear
|
|
300
|
+
# transition :parked => :idling, :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up
|
|
301
|
+
#
|
|
302
|
+
# == Verbose transitions
|
|
303
|
+
#
|
|
304
|
+
# Transitions can also be defined use an explicit set of configuration
|
|
305
|
+
# options:
|
|
306
|
+
# * <tt>:from</tt> - A state or array of states that can be transitioned from.
|
|
307
|
+
# If not specified, then the transition can occur for *any* state.
|
|
308
|
+
# * <tt>:to</tt> - The state that's being transitioned to. If not specified,
|
|
309
|
+
# then the transition will simply loop back (i.e. the state will not change).
|
|
310
|
+
# * <tt>:except_from</tt> - A state or array of states that *cannot* be
|
|
311
|
+
# transitioned from.
|
|
312
|
+
#
|
|
313
|
+
# These options must be used when defining transitions within the context
|
|
314
|
+
# of a state.
|
|
315
|
+
#
|
|
316
|
+
# Examples:
|
|
317
|
+
#
|
|
318
|
+
# transition :to => nil, :on => :park
|
|
319
|
+
# transition :to => :idling, :on => :ignite
|
|
320
|
+
# transition :except_from => [:idling, :first_gear], :to => :idling, :on => :ignite
|
|
321
|
+
# transition :from => nil, :to => :idling, :on => :ignite
|
|
322
|
+
# transition :from => [:parked, :stalled], :to => :idling, :on => :ignite
|
|
323
|
+
#
|
|
324
|
+
# == Conditions
|
|
325
|
+
#
|
|
326
|
+
# In addition to the state requirements for each transition, a condition
|
|
327
|
+
# can also be defined to help determine whether that transition is
|
|
328
|
+
# available. These options will work on both the normal and verbose syntax.
|
|
329
|
+
#
|
|
330
|
+
# Configuration options:
|
|
331
|
+
# * <tt>:if</tt> - A method, proc or string to call to determine if the
|
|
332
|
+
# transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
|
|
333
|
+
# The condition should return or evaluate to true or false.
|
|
334
|
+
# * <tt>:unless</tt> - A method, proc or string to call to determine if the
|
|
335
|
+
# transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
|
|
336
|
+
# The condition should return or evaluate to true or false.
|
|
337
|
+
#
|
|
338
|
+
# Examples:
|
|
339
|
+
#
|
|
340
|
+
# transition :parked => :idling, :on => :ignite, :if => :moving?
|
|
341
|
+
# transition :parked => :idling, :on => :ignite, :unless => :stopped?
|
|
342
|
+
# transition :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up, :if => :seatbelt_on?
|
|
343
|
+
#
|
|
344
|
+
# transition :from => :parked, :to => :idling, :on => ignite, :if => :moving?
|
|
345
|
+
# transition :from => :parked, :to => :idling, :on => ignite, :unless => :stopped?
|
|
346
|
+
#
|
|
347
|
+
# == Order of operations
|
|
348
|
+
#
|
|
349
|
+
# Transitions are evaluated in the order in which they're defined. As a
|
|
350
|
+
# result, if more than one transition applies to a given object, then the
|
|
351
|
+
# first transition that matches will be performed.
|
|
352
|
+
def transition(options)
|
|
353
|
+
raise ArgumentError, 'Must specify :on event' unless options[:on]
|
|
354
|
+
|
|
355
|
+
branches = []
|
|
356
|
+
options = options.dup
|
|
357
|
+
event(*Array(options.delete(:on))) { branches << transition(options) }
|
|
358
|
+
|
|
359
|
+
branches.length == 1 ? branches.first : branches
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Generates a list of the possible transition sequences that can be run on
|
|
363
|
+
# the given object. These paths can reveal all of the possible states and
|
|
364
|
+
# events that can be encountered in the object's state machine based on the
|
|
365
|
+
# object's current state.
|
|
366
|
+
#
|
|
367
|
+
# Configuration options:
|
|
368
|
+
# * +from+ - The initial state to start all paths from. By default, this
|
|
369
|
+
# is the object's current state.
|
|
370
|
+
# * +to+ - The target state to end all paths on. By default, paths will
|
|
371
|
+
# end when they loop back to the first transition on the path.
|
|
372
|
+
# * +deep+ - Whether to allow the target state to be crossed more than once
|
|
373
|
+
# in a path. By default, paths will immediately stop when the target
|
|
374
|
+
# state (if specified) is reached. If this is enabled, then paths can
|
|
375
|
+
# continue even after reaching the target state; they will stop when
|
|
376
|
+
# reaching the target state a second time.
|
|
377
|
+
#
|
|
378
|
+
# *Note* that the object is never modified when the list of paths is
|
|
379
|
+
# generated.
|
|
380
|
+
#
|
|
381
|
+
# == Examples
|
|
382
|
+
#
|
|
383
|
+
# class Vehicle
|
|
384
|
+
# state_machine :initial => :parked do
|
|
385
|
+
# event :ignite do
|
|
386
|
+
# transition :parked => :idling
|
|
387
|
+
# end
|
|
388
|
+
#
|
|
389
|
+
# event :shift_up do
|
|
390
|
+
# transition :idling => :first_gear, :first_gear => :second_gear
|
|
391
|
+
# end
|
|
392
|
+
#
|
|
393
|
+
# event :shift_down do
|
|
394
|
+
# transition :second_gear => :first_gear, :first_gear => :idling
|
|
395
|
+
# end
|
|
396
|
+
# end
|
|
397
|
+
# end
|
|
398
|
+
#
|
|
399
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
|
|
400
|
+
# vehicle.state # => "parked"
|
|
401
|
+
#
|
|
402
|
+
# vehicle.state_paths
|
|
403
|
+
# # => [
|
|
404
|
+
# # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
|
|
405
|
+
# # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
|
|
406
|
+
# # #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>,
|
|
407
|
+
# # #<StateMachines::Transition attribute=:state event=:shift_down from="second_gear" from_name=:second_gear to="first_gear" to_name=:first_gear>,
|
|
408
|
+
# # #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>],
|
|
409
|
+
# #
|
|
410
|
+
# # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
|
|
411
|
+
# # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
|
|
412
|
+
# # #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>]
|
|
413
|
+
# # ]
|
|
414
|
+
#
|
|
415
|
+
# vehicle.state_paths(:from => :parked, :to => :second_gear)
|
|
416
|
+
# # => [
|
|
417
|
+
# # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
|
|
418
|
+
# # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
|
|
419
|
+
# # #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>]
|
|
420
|
+
# # ]
|
|
421
|
+
#
|
|
422
|
+
# In addition to getting the possible paths that can be accessed, you can
|
|
423
|
+
# also get summary information about the states / events that can be
|
|
424
|
+
# accessed at some point along one of the paths. For example:
|
|
425
|
+
#
|
|
426
|
+
# # Get the list of states that can be accessed from the current state
|
|
427
|
+
# vehicle.state_paths.to_states # => [:idling, :first_gear, :second_gear]
|
|
428
|
+
#
|
|
429
|
+
# # Get the list of events that can be accessed from the current state
|
|
430
|
+
# vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down]
|
|
431
|
+
def paths_for(object, requirements = {})
|
|
432
|
+
PathCollection.new(object, self, requirements)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StateMachines
|
|
4
|
+
class Machine
|
|
5
|
+
module HelperGenerators
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
# Adds helper methods for interacting with the state machine, including
|
|
9
|
+
# for states, events, and transitions
|
|
10
|
+
def define_helpers
|
|
11
|
+
define_state_accessor
|
|
12
|
+
define_state_predicate
|
|
13
|
+
define_event_helpers
|
|
14
|
+
define_path_helpers
|
|
15
|
+
define_action_helpers if define_action_helpers?
|
|
16
|
+
define_name_helpers
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Defines the initial values for state machine attributes. Static values
|
|
20
|
+
# are set prior to the original initialize method and dynamic values are
|
|
21
|
+
# set *after* the initialize method in case it is dependent on it.
|
|
22
|
+
def define_state_initializer
|
|
23
|
+
define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
|
|
24
|
+
def initialize(*)
|
|
25
|
+
self.class.state_machines.initialize_states(self) { super }
|
|
26
|
+
end
|
|
27
|
+
END_EVAL
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Adds reader/writer methods for accessing the state attribute
|
|
31
|
+
def define_state_accessor
|
|
32
|
+
attribute = self.attribute
|
|
33
|
+
|
|
34
|
+
@helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute)
|
|
35
|
+
@helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Adds predicate method to the owner class for determining the name of the
|
|
39
|
+
# current state
|
|
40
|
+
def define_state_predicate
|
|
41
|
+
call_super = owner_class_ancestor_has_method?(:instance, "#{name}?") ? true : false
|
|
42
|
+
define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
|
|
43
|
+
def #{name}?(*args)
|
|
44
|
+
args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
|
|
45
|
+
end
|
|
46
|
+
END_EVAL
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Adds helper methods for getting information about this state machine's
|
|
50
|
+
# events
|
|
51
|
+
def define_event_helpers
|
|
52
|
+
# Gets the events that are allowed to fire on the current object
|
|
53
|
+
define_helper(:instance, attribute(:events)) do |machine, object, *args|
|
|
54
|
+
machine.events.valid_for(object, *args).map(&:name)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Gets the next possible transitions that can be run on the current
|
|
58
|
+
# object
|
|
59
|
+
define_helper(:instance, attribute(:transitions)) do |machine, object, *args|
|
|
60
|
+
machine.events.transitions_for(object, *args)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Fire an arbitrary event for this machine
|
|
64
|
+
define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args|
|
|
65
|
+
machine.events.fetch(event).fire(object, *args)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Add helpers for tracking the event / transition to invoke when the
|
|
69
|
+
# action is called
|
|
70
|
+
return unless action
|
|
71
|
+
|
|
72
|
+
event_attribute = attribute(:event)
|
|
73
|
+
define_helper(:instance, event_attribute) do |machine, object|
|
|
74
|
+
# Interpret non-blank events as present
|
|
75
|
+
event = machine.read(object, :event, true)
|
|
76
|
+
event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A roundabout way of writing the attribute is used here so that
|
|
80
|
+
# integrations can hook into this modification
|
|
81
|
+
define_helper(:instance, "#{event_attribute}=") do |machine, object, value|
|
|
82
|
+
machine.write(object, :event, value, true)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
event_transition_attribute = attribute(:event_transition)
|
|
86
|
+
define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
|
|
87
|
+
protected; attr_accessor #{event_transition_attribute.inspect}
|
|
88
|
+
END_EVAL
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Adds helper methods for getting information about this state machine's
|
|
92
|
+
# available transition paths
|
|
93
|
+
def define_path_helpers
|
|
94
|
+
# Gets the paths of transitions available to the current object
|
|
95
|
+
define_helper(:instance, attribute(:paths)) do |machine, object, *args|
|
|
96
|
+
machine.paths_for(object, *args)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Adds helper methods for accessing naming information about states and
|
|
101
|
+
# events on the owner class
|
|
102
|
+
def define_name_helpers
|
|
103
|
+
# Gets the humanized version of a state
|
|
104
|
+
define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state|
|
|
105
|
+
machine.states.fetch(state).human_name(klass)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Gets the humanized version of an event
|
|
109
|
+
define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event|
|
|
110
|
+
machine.events.fetch(event).human_name(klass)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Gets the state name for the current value
|
|
114
|
+
define_helper(:instance, attribute(:name)) do |machine, object|
|
|
115
|
+
machine.states.match!(object).name
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Gets the human state name for the current value
|
|
119
|
+
define_helper(:instance, "human_#{attribute(:name)}") do |machine, object|
|
|
120
|
+
machine.states.match!(object).human_name(object.class)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StateMachines
|
|
4
|
+
class Machine
|
|
5
|
+
module Integration
|
|
6
|
+
# Marks the given object as invalid with the given message.
|
|
7
|
+
#
|
|
8
|
+
# By default, this is a no-op.
|
|
9
|
+
def invalidate(_object, _attribute, _message, _values = []); end
|
|
10
|
+
|
|
11
|
+
# Gets a description of the errors for the given object. This is used to
|
|
12
|
+
# provide more detailed information when an InvalidTransition exception is
|
|
13
|
+
# raised.
|
|
14
|
+
def errors_for(_object)
|
|
15
|
+
''
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Resets any errors previously added when invalidating the given object.
|
|
19
|
+
#
|
|
20
|
+
# By default, this is a no-op.
|
|
21
|
+
def reset(_object); end
|
|
22
|
+
|
|
23
|
+
# Generates a user-friendly name for the given message.
|
|
24
|
+
def generate_message(name, values = [])
|
|
25
|
+
format(@messages[name] || @messages[:invalid_transition] || default_messages[name] || default_messages[:invalid_transition], state: values.first)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Runs a transaction, yielding the given block.
|
|
29
|
+
#
|
|
30
|
+
# By default, this is a no-op.
|
|
31
|
+
def within_transaction(object, &)
|
|
32
|
+
if use_transactions && respond_to?(:transaction, true)
|
|
33
|
+
transaction(object, &)
|
|
34
|
+
else
|
|
35
|
+
yield
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
protected
|
|
40
|
+
|
|
41
|
+
# Runs additional initialization hooks. By default, this is a no-op.
|
|
42
|
+
def after_initialize; end
|
|
43
|
+
|
|
44
|
+
# Always yields
|
|
45
|
+
def transaction(_object)
|
|
46
|
+
yield
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Gets the initial attribute value defined by the owner class (outside of
|
|
50
|
+
# the machine's definition). By default, this is always nil.
|
|
51
|
+
def owner_class_attribute_default
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Checks whether the given state matches the attribute default specified
|
|
56
|
+
# by the owner class
|
|
57
|
+
def owner_class_attribute_default_matches?(state)
|
|
58
|
+
state.matches?(owner_class_attribute_default)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Warns if the owner class and the machine have defined conflicting
|
|
62
|
+
# defaults for the machine's attribute.
|
|
63
|
+
def check_conflicting_attribute_default
|
|
64
|
+
initial_state = states.detect(&:initial)
|
|
65
|
+
has_owner_default = !owner_class_attribute_default.nil?
|
|
66
|
+
has_conflicting_default = dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state)
|
|
67
|
+
return unless has_owner_default && has_conflicting_default
|
|
68
|
+
|
|
69
|
+
warn(
|
|
70
|
+
"Both #{owner_class.name} and its #{name.inspect} machine have defined " \
|
|
71
|
+
"a different default for \"#{attribute}\". Use only one or the other for " \
|
|
72
|
+
'defining defaults to avoid unexpected behaviors.'
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Schedules or immediately runs the conflicting attribute default check.
|
|
77
|
+
# Override in integrations to defer the check (e.g. until after the DB
|
|
78
|
+
# is ready) to avoid triggering a database connection at class load time.
|
|
79
|
+
def schedule_conflicting_attribute_default_check
|
|
80
|
+
check_conflicting_attribute_default
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Gets the default messages that can be used in the machine for invalid
|
|
86
|
+
# transitions.
|
|
87
|
+
def default_messages
|
|
88
|
+
{ invalid_transition: '%<state>s cannot transition via "%<event>s"' }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|