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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/state_machines-0.201.0/LICENSE.txt +23 -0
  3. data/state_machines-0.201.0/README.md +1024 -0
  4. data/state_machines-0.201.0/lib/state_machines/async_mode/async_event_extensions.rb +49 -0
  5. data/state_machines-0.201.0/lib/state_machines/async_mode/async_events.rb +282 -0
  6. data/state_machines-0.201.0/lib/state_machines/async_mode/async_machine.rb +60 -0
  7. data/state_machines-0.201.0/lib/state_machines/async_mode/async_transition_collection.rb +136 -0
  8. data/state_machines-0.201.0/lib/state_machines/async_mode/thread_safe_state.rb +47 -0
  9. data/state_machines-0.201.0/lib/state_machines/async_mode.rb +64 -0
  10. data/state_machines-0.201.0/lib/state_machines/branch.rb +246 -0
  11. data/state_machines-0.201.0/lib/state_machines/callback.rb +223 -0
  12. data/state_machines-0.201.0/lib/state_machines/core.rb +43 -0
  13. data/state_machines-0.201.0/lib/state_machines/core_ext/class/state_machine.rb +5 -0
  14. data/state_machines-0.201.0/lib/state_machines/core_ext.rb +4 -0
  15. data/state_machines-0.201.0/lib/state_machines/error.rb +115 -0
  16. data/state_machines-0.201.0/lib/state_machines/eval_helpers.rb +227 -0
  17. data/state_machines-0.201.0/lib/state_machines/event.rb +247 -0
  18. data/state_machines-0.201.0/lib/state_machines/event_collection.rb +149 -0
  19. data/state_machines-0.201.0/lib/state_machines/extensions.rb +150 -0
  20. data/state_machines-0.201.0/lib/state_machines/helper_module.rb +19 -0
  21. data/state_machines-0.201.0/lib/state_machines/integrations/base.rb +49 -0
  22. data/state_machines-0.201.0/lib/state_machines/integrations.rb +111 -0
  23. data/state_machines-0.201.0/lib/state_machines/machine/action_hooks.rb +53 -0
  24. data/state_machines-0.201.0/lib/state_machines/machine/async_extensions.rb +88 -0
  25. data/state_machines-0.201.0/lib/state_machines/machine/callbacks.rb +333 -0
  26. data/state_machines-0.201.0/lib/state_machines/machine/class_methods.rb +95 -0
  27. data/state_machines-0.201.0/lib/state_machines/machine/configuration.rb +128 -0
  28. data/state_machines-0.201.0/lib/state_machines/machine/event_methods.rb +436 -0
  29. data/state_machines-0.201.0/lib/state_machines/machine/helper_generators.rb +125 -0
  30. data/state_machines-0.201.0/lib/state_machines/machine/integration.rb +92 -0
  31. data/state_machines-0.201.0/lib/state_machines/machine/parsing.rb +77 -0
  32. data/state_machines-0.201.0/lib/state_machines/machine/rendering.rb +17 -0
  33. data/state_machines-0.201.0/lib/state_machines/machine/scoping.rb +44 -0
  34. data/state_machines-0.201.0/lib/state_machines/machine/state_methods.rb +398 -0
  35. data/state_machines-0.201.0/lib/state_machines/machine/utilities.rb +86 -0
  36. data/state_machines-0.201.0/lib/state_machines/machine/validation.rb +39 -0
  37. data/state_machines-0.201.0/lib/state_machines/machine.rb +615 -0
  38. data/state_machines-0.201.0/lib/state_machines/machine_collection.rb +105 -0
  39. data/state_machines-0.201.0/lib/state_machines/macro_methods.rb +522 -0
  40. data/state_machines-0.201.0/lib/state_machines/matcher.rb +124 -0
  41. data/state_machines-0.201.0/lib/state_machines/matcher_helpers.rb +56 -0
  42. data/state_machines-0.201.0/lib/state_machines/node_collection.rb +226 -0
  43. data/state_machines-0.201.0/lib/state_machines/options_validator.rb +72 -0
  44. data/state_machines-0.201.0/lib/state_machines/path.rb +123 -0
  45. data/state_machines-0.201.0/lib/state_machines/path_collection.rb +91 -0
  46. data/state_machines-0.201.0/lib/state_machines/state.rb +314 -0
  47. data/state_machines-0.201.0/lib/state_machines/state_collection.rb +113 -0
  48. data/state_machines-0.201.0/lib/state_machines/state_context.rb +134 -0
  49. data/state_machines-0.201.0/lib/state_machines/stdio_renderer.rb +74 -0
  50. data/state_machines-0.201.0/lib/state_machines/syntax_validator.rb +54 -0
  51. data/state_machines-0.201.0/lib/state_machines/test_helper.rb +775 -0
  52. data/state_machines-0.201.0/lib/state_machines/transition.rb +593 -0
  53. data/state_machines-0.201.0/lib/state_machines/transition_collection.rb +310 -0
  54. data/state_machines-0.201.0/lib/state_machines/version.rb +5 -0
  55. data/state_machines-0.201.0/lib/state_machines.rb +6 -0
  56. data/ultra-smart-sys.gemspec +12 -0
  57. 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