state_machines 0.100.4 → 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.
@@ -5,6 +5,230 @@ module StateMachines
5
5
  module EventMethods
6
6
  # Defines one or more events for the machine and the transitions that can
7
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
8
232
  def event(*names, &)
9
233
  options = names.last.is_a?(Hash) ? names.pop : {}
10
234
  StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
@@ -38,6 +262,93 @@ module StateMachines
38
262
 
39
263
  # Creates a new transition that determines what to change the current state
40
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.
41
352
  def transition(options)
42
353
  raise ArgumentError, 'Must specify :on event' unless options[:on]
43
354
 
@@ -48,9 +359,75 @@ module StateMachines
48
359
  branches.length == 1 ? branches.first : branches
49
360
  end
50
361
 
51
- # Gets the list of all possible transition paths from the current state to
52
- # the given target state. If multiple target states are provided, then
53
- # this will return all possible paths to those states.
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]
54
431
  def paths_for(object, requirements = {})
55
432
  PathCollection.new(object, self, requirements)
56
433
  end
@@ -58,6 +58,28 @@ module StateMachines
58
58
  state.matches?(owner_class_attribute_default)
59
59
  end
60
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
+
61
83
  private
62
84
 
63
85
  # Gets the default messages that can be used in the machine for invalid