state_machines 0.6.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +93 -13
- data/lib/state_machines/branch.rb +8 -4
- data/lib/state_machines/callback.rb +2 -0
- data/lib/state_machines/core.rb +3 -2
- data/lib/state_machines/core_ext/class/state_machine.rb +2 -0
- data/lib/state_machines/core_ext.rb +2 -0
- data/lib/state_machines/error.rb +2 -0
- data/lib/state_machines/eval_helpers.rb +38 -9
- data/lib/state_machines/event.rb +22 -7
- data/lib/state_machines/event_collection.rb +2 -0
- data/lib/state_machines/extensions.rb +2 -0
- data/lib/state_machines/helper_module.rb +2 -0
- data/lib/state_machines/integrations/base.rb +2 -0
- data/lib/state_machines/integrations.rb +2 -0
- data/lib/state_machines/machine/class_methods.rb +79 -0
- data/lib/state_machines/machine.rb +21 -67
- data/lib/state_machines/machine_collection.rb +5 -1
- data/lib/state_machines/macro_methods.rb +2 -0
- data/lib/state_machines/matcher.rb +3 -0
- data/lib/state_machines/matcher_helpers.rb +2 -0
- data/lib/state_machines/node_collection.rb +5 -1
- data/lib/state_machines/options_validator.rb +72 -0
- data/lib/state_machines/path.rb +5 -1
- data/lib/state_machines/path_collection.rb +5 -1
- data/lib/state_machines/state.rb +71 -43
- data/lib/state_machines/state_collection.rb +2 -0
- data/lib/state_machines/state_context.rb +5 -1
- data/lib/state_machines/stdio_renderer.rb +74 -0
- data/lib/state_machines/test_helper.rb +305 -0
- data/lib/state_machines/transition.rb +2 -0
- data/lib/state_machines/transition_collection.rb +5 -1
- data/lib/state_machines/version.rb +3 -1
- data/lib/state_machines.rb +4 -1
- metadata +11 -9
- data/lib/state_machines/assertions.rb +0 -40
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5aa5d105c78cb53f15f42e8662dd7f8e0d0d5f8807b88dcbd8b4201f13718384
|
4
|
+
data.tar.gz: d761cedbe052c5c8829626e0875f54dce064f7156162119b4a040594b439068c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 37398c9ca5f2de7413dfb7a8a467984d0fc4d47f8367ea0747fec6d9c1eaca5a7c3439f37988c02dd8ce27622a9a9e54cf5cc0b2d84404f00f92a862c168bb23
|
7
|
+
data.tar.gz: 159022534eb3c308bc2f2c8e960f111053631d3e10adafc9ae8156c95a26165f48690c682229a577bdfecf918b335ebe5b6d57e82e095c924585ad8d11b9d1ee
|
data/README.md
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|

|
2
|
-
[](https://codeclimate.com/github/state-machines/state_machines)
|
3
2
|
# State Machines
|
4
3
|
|
5
4
|
State Machines adds support for creating state machines for attributes on any Ruby class.
|
@@ -255,6 +254,71 @@ vehicle.state_name # => :parked
|
|
255
254
|
# vehicle.state = :parked
|
256
255
|
```
|
257
256
|
|
257
|
+
## Testing
|
258
|
+
|
259
|
+
State Machines provides a `TestHelper` module with assertion methods to make testing state machines easier and more expressive.
|
260
|
+
|
261
|
+
### Setup
|
262
|
+
|
263
|
+
Include the test helper in your test class:
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
# For Minitest
|
267
|
+
class VehicleTest < Minitest::Test
|
268
|
+
include StateMachines::TestHelper
|
269
|
+
|
270
|
+
def test_initial_state
|
271
|
+
vehicle = Vehicle.new
|
272
|
+
assert_state vehicle, :state, :parked
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# For RSpec
|
277
|
+
RSpec.describe Vehicle do
|
278
|
+
include StateMachines::TestHelper
|
279
|
+
|
280
|
+
it "starts in parked state" do
|
281
|
+
vehicle = Vehicle.new
|
282
|
+
assert_state vehicle, :state, :parked
|
283
|
+
end
|
284
|
+
end
|
285
|
+
```
|
286
|
+
|
287
|
+
### Available Assertions
|
288
|
+
|
289
|
+
The TestHelper provides both basic assertions and comprehensive state machine-specific assertions with `sm_` prefixes:
|
290
|
+
|
291
|
+
#### Basic Assertions
|
292
|
+
|
293
|
+
```ruby
|
294
|
+
vehicle = Vehicle.new
|
295
|
+
assert_state vehicle, :state, :parked
|
296
|
+
assert_can_transition vehicle, :ignite
|
297
|
+
assert_cannot_transition vehicle, :shift_up
|
298
|
+
assert_transition vehicle, :ignite, :state, :idling
|
299
|
+
```
|
300
|
+
|
301
|
+
#### Extended State Machine Assertions
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
machine = Vehicle.state_machine(:state)
|
305
|
+
vehicle = Vehicle.new
|
306
|
+
|
307
|
+
# State configuration
|
308
|
+
assert_sm_states_list machine, [:parked, :idling, :stalled]
|
309
|
+
assert_sm_initial_state machine, :parked
|
310
|
+
|
311
|
+
# Event behavior
|
312
|
+
assert_sm_event_triggers vehicle, :ignite
|
313
|
+
refute_sm_event_triggers vehicle, :shift_up
|
314
|
+
assert_sm_event_raises_error vehicle, :invalid_event, StateMachines::InvalidTransition
|
315
|
+
|
316
|
+
# Persistence (with ActiveRecord integration)
|
317
|
+
assert_sm_state_persisted record, expected: :active
|
318
|
+
```
|
319
|
+
|
320
|
+
The test helper works with both Minitest and RSpec, automatically detecting your testing framework.
|
321
|
+
|
258
322
|
## Additional Topics
|
259
323
|
|
260
324
|
### Explicit vs. Implicit Event Transitions
|
@@ -428,7 +492,7 @@ easily migrate from a different library, you can do so as shown below:
|
|
428
492
|
```ruby
|
429
493
|
class Vehicle
|
430
494
|
state_machine initial: :parked do
|
431
|
-
...
|
495
|
+
# ...
|
432
496
|
|
433
497
|
state :parked do
|
434
498
|
transition to: :idling, :on => [:ignite, :shift_up], if: :seatbelt_on?
|
@@ -464,7 +528,7 @@ example below:
|
|
464
528
|
```ruby
|
465
529
|
class Vehicle
|
466
530
|
state_machine initial: :parked do
|
467
|
-
...
|
531
|
+
# ...
|
468
532
|
|
469
533
|
transition parked: :idling, :on => [:ignite, :shift_up]
|
470
534
|
transition first_gear: :second_gear, second_gear: :third_gear, on: :shift_up
|
@@ -496,12 +560,31 @@ class Vehicle
|
|
496
560
|
transition [:idling, :first_gear] => :parked
|
497
561
|
end
|
498
562
|
|
499
|
-
...
|
563
|
+
# ...
|
500
564
|
end
|
501
565
|
end
|
502
566
|
```
|
503
567
|
|
504
|
-
|
568
|
+
#### Draw state machines
|
569
|
+
|
570
|
+
State machines includes a default STDIORenderer for debugging state machines without external dependencies.
|
571
|
+
This renderer can be used to visualize the state machine in the console.
|
572
|
+
|
573
|
+
To use the renderer, simply call the `draw` method on the state machine:
|
574
|
+
|
575
|
+
```ruby
|
576
|
+
Vehicle.state_machine.draw # Outputs the state machine diagram to the console
|
577
|
+
```
|
578
|
+
|
579
|
+
You can customize the output by passing in options to the `draw` method, such as the output stream:
|
580
|
+
|
581
|
+
```ruby
|
582
|
+
Vehicle.state_machine.draw(io: $stderr) # Outputs the state machine diagram to stderr
|
583
|
+
```
|
584
|
+
|
585
|
+
#### Dynamic definitions
|
586
|
+
|
587
|
+
There may be cases where the definition of a state machine is **dynamic**.
|
505
588
|
This means that you don't know the possible states or events for a machine until
|
506
589
|
runtime. For example, you may allow users in your application to manage the
|
507
590
|
state machine of a project or task in your system. This means that the list of
|
@@ -580,22 +663,19 @@ transitions.
|
|
580
663
|
|
581
664
|
Ruby versions officially supported and tested:
|
582
665
|
|
583
|
-
* Ruby (MRI)
|
584
|
-
* JRuby
|
585
|
-
* Rubinius
|
666
|
+
* Ruby (MRI) 3.0.0+
|
586
667
|
|
587
668
|
For graphing state machine:
|
588
669
|
|
589
|
-
* [state_machines-graphviz](
|
670
|
+
* [state_machines-graphviz](https://github.com/state-machines/state_machines-graphviz)
|
590
671
|
|
591
672
|
For documenting state machines:
|
592
673
|
|
593
|
-
* [state_machines-yard](
|
594
|
-
|
674
|
+
* [state_machines-yard](https://github.com/state-machines/state_machines-yard)
|
595
675
|
|
596
|
-
|
676
|
+
For RSpec testing, use the custom RSpec matchers:
|
597
677
|
|
598
|
-
*
|
678
|
+
* [state_machines-rspec](https://github.com/state-machines/state_machines-rspec)
|
599
679
|
|
600
680
|
## Contributing
|
601
681
|
|
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
1
5
|
module StateMachines
|
2
6
|
# Represents a set of requirements that must be met in order for a transition
|
3
7
|
# or callback to occur. Branches verify that the event, from state, and to
|
@@ -112,15 +116,15 @@ module StateMachines
|
|
112
116
|
# branch.match(object, :on => :ignite) # => {:to => ..., :from => ..., :on => ...}
|
113
117
|
# branch.match(object, :on => :park) # => nil
|
114
118
|
def match(object, query = {})
|
115
|
-
|
119
|
+
StateMachines::OptionsValidator.assert_valid_keys!(query, :from, :to, :on, :guard)
|
116
120
|
|
117
121
|
if (match = match_query(query)) && matches_conditions?(object, query)
|
118
122
|
match
|
119
123
|
end
|
120
124
|
end
|
121
125
|
|
122
|
-
def draw(graph, event, valid_states)
|
123
|
-
|
126
|
+
def draw(graph, event, valid_states, io = $stdout)
|
127
|
+
machine.renderer.draw_branch(self, graph, event, valid_states, io)
|
124
128
|
end
|
125
129
|
|
126
130
|
protected
|
@@ -129,7 +133,7 @@ module StateMachines
|
|
129
133
|
# whitelist nor a blacklist option is specified, then an AllMatcher is
|
130
134
|
# built.
|
131
135
|
def build_matcher(options, whitelist_option, blacklist_option)
|
132
|
-
|
136
|
+
StateMachines::OptionsValidator.assert_exclusive_keys!(options, whitelist_option, blacklist_option)
|
133
137
|
|
134
138
|
if options.include?(whitelist_option)
|
135
139
|
value = options[whitelist_option]
|
data/lib/state_machines/core.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Load all of the core implementation required to use state_machine. This
|
2
4
|
# includes:
|
3
5
|
# * StateMachines::MacroMethods which adds the state_machine DSL to your class
|
4
6
|
# * A set of initializers for setting state_machine defaults based on the current
|
5
7
|
# running environment (such as within Rails)
|
6
|
-
require 'state_machines/assertions'
|
7
8
|
require 'state_machines/error'
|
8
9
|
|
9
10
|
require 'state_machines/extensions'
|
@@ -40,4 +41,4 @@ require 'state_machines/path_collection'
|
|
40
41
|
require 'state_machines/machine'
|
41
42
|
require 'state_machines/machine_collection'
|
42
43
|
|
43
|
-
require 'state_machines/macro_methods'
|
44
|
+
require 'state_machines/macro_methods'
|
data/lib/state_machines/error.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module StateMachines
|
2
4
|
# Provides a set of helper methods for evaluating methods within the context
|
3
5
|
# of an object.
|
@@ -50,19 +52,17 @@ module StateMachines
|
|
50
52
|
# evaluate_method(person, lambda {|person| person.name}, 21) # => "John Smith"
|
51
53
|
# evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21) # => "John Smith is 21"
|
52
54
|
# evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21, 'male') # => ArgumentError: wrong number of arguments (3 for 2)
|
53
|
-
def evaluate_method(object, method, *args, &block)
|
55
|
+
def evaluate_method(object, method, *args, **kwargs, &block)
|
54
56
|
case method
|
55
57
|
when Symbol
|
56
58
|
klass = (class << object; self; end)
|
57
59
|
args = [] if (klass.method_defined?(method) || klass.private_method_defined?(method)) && object.method(method).arity == 0
|
58
|
-
object.send(method, *args, &block)
|
59
|
-
when Proc
|
60
|
+
object.send(method, *args, **kwargs, &block)
|
61
|
+
when Proc
|
60
62
|
args.unshift(object)
|
61
63
|
arity = method.arity
|
62
|
-
|
63
|
-
|
64
|
-
# argument for consistency across versions of Ruby
|
65
|
-
if block_given? && Proc === method && arity != 0
|
64
|
+
# Handle blocks for Procs
|
65
|
+
if block_given? && arity != 0
|
66
66
|
if [1, 2].include?(arity)
|
67
67
|
# Force the block to be either the only argument or the 2nd one
|
68
68
|
# after the object (may mean additional arguments get discarded)
|
@@ -76,9 +76,38 @@ module StateMachines
|
|
76
76
|
args = args[0, arity] if [0, 1].include?(arity)
|
77
77
|
end
|
78
78
|
|
79
|
-
|
79
|
+
# Call the Proc with the arguments
|
80
|
+
method.call(*args, **kwargs)
|
81
|
+
|
82
|
+
when Method
|
83
|
+
args.unshift(object)
|
84
|
+
arity = method.arity
|
85
|
+
|
86
|
+
# Methods handle blocks via &block, not as arguments
|
87
|
+
# Only limit arguments if necessary based on arity
|
88
|
+
args = args[0, arity] if [0, 1].include?(arity)
|
89
|
+
|
90
|
+
# Call the Method with the arguments and pass the block
|
91
|
+
method.call(*args, **kwargs, &block)
|
80
92
|
when String
|
81
|
-
|
93
|
+
if block_given?
|
94
|
+
if StateMachines::Transition.pause_supported?
|
95
|
+
eval(method, object.instance_eval { binding }, &block)
|
96
|
+
else
|
97
|
+
# Support for JRuby and Truffle Ruby, which don't support binding blocks
|
98
|
+
eigen = class << object; self; end
|
99
|
+
eigen.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
100
|
+
def __temp_eval_method__(*args, &b)
|
101
|
+
#{method}
|
102
|
+
end
|
103
|
+
RUBY
|
104
|
+
result = object.__temp_eval_method__(*args, &block)
|
105
|
+
eigen.send(:remove_method, :__temp_eval_method__)
|
106
|
+
result
|
107
|
+
end
|
108
|
+
else
|
109
|
+
eval(method, object.instance_eval { binding })
|
110
|
+
end
|
82
111
|
else
|
83
112
|
raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
|
84
113
|
end
|
data/lib/state_machines/event.rb
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
1
5
|
module StateMachines
|
2
6
|
# An event defines an action that transitions an attribute from one state to
|
3
7
|
# another. The state that an attribute is transitioned to depends on the
|
@@ -30,13 +34,22 @@ module StateMachines
|
|
30
34
|
#
|
31
35
|
# Configuration options:
|
32
36
|
# * <tt>:human_name</tt> - The human-readable version of this event's name
|
33
|
-
def initialize(machine, name, options =
|
34
|
-
|
37
|
+
def initialize(machine, name, options = nil, human_name: nil, **extra_options) #:nodoc:
|
38
|
+
# Handle both old hash style and new kwargs style for backward compatibility
|
39
|
+
if options.is_a?(Hash)
|
40
|
+
# Old style: initialize(machine, name, {human_name: 'Custom Name'})
|
41
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
|
42
|
+
human_name = options[:human_name]
|
43
|
+
else
|
44
|
+
# New style: initialize(machine, name, human_name: 'Custom Name')
|
45
|
+
raise ArgumentError, "Unexpected positional argument: #{options.inspect}" unless options.nil?
|
46
|
+
StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :human_name) unless extra_options.empty?
|
47
|
+
end
|
35
48
|
|
36
49
|
@machine = machine
|
37
50
|
@name = name
|
38
51
|
@qualified_name = machine.namespace ? :"#{name}_#{machine.namespace}" : name
|
39
|
-
@human_name =
|
52
|
+
@human_name = human_name || @name.to_s.tr('_', ' ')
|
40
53
|
reset
|
41
54
|
|
42
55
|
# Output a warning if another event has a conflicting qualified name
|
@@ -89,7 +102,9 @@ module StateMachines
|
|
89
102
|
|
90
103
|
# Only a certain subset of explicit options are allowed for transition
|
91
104
|
# requirements
|
92
|
-
|
105
|
+
if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
|
106
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless)
|
107
|
+
end
|
93
108
|
|
94
109
|
branches << branch = Branch.new(options.merge(on: name))
|
95
110
|
@known_states |= branch.known_states
|
@@ -119,7 +134,7 @@ module StateMachines
|
|
119
134
|
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
120
135
|
# conditionals defined for each one. Default is true.
|
121
136
|
def transition_for(object, requirements = {})
|
122
|
-
|
137
|
+
StateMachines::OptionsValidator.assert_valid_keys!(requirements, :from, :to, :guard)
|
123
138
|
requirements[:from] = machine.states.match!(object).name unless (custom_from_state = requirements.include?(:from))
|
124
139
|
|
125
140
|
branches.each do |branch|
|
@@ -180,8 +195,8 @@ module StateMachines
|
|
180
195
|
end
|
181
196
|
|
182
197
|
|
183
|
-
def draw(graph, options = {})
|
184
|
-
|
198
|
+
def draw(graph, options = {}, io = $stdout)
|
199
|
+
machine.renderer.draw_event(self, graph, options, io)
|
185
200
|
end
|
186
201
|
|
187
202
|
# Generates a nicely formatted description of this event's contents.
|
@@ -0,0 +1,79 @@
|
|
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, &block)
|
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
|
+
end
|
34
|
+
|
35
|
+
# Evaluate DSL
|
36
|
+
machine.instance_eval(&block) if block_given?
|
37
|
+
else
|
38
|
+
# No existing machine: create a new one
|
39
|
+
machine = new(owner_class, name, options, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
machine
|
43
|
+
end
|
44
|
+
|
45
|
+
def draw(*)
|
46
|
+
raise NotImplementedError
|
47
|
+
end
|
48
|
+
|
49
|
+
# Default messages to use for validation errors in ORM integrations
|
50
|
+
attr_accessor :ignore_method_conflicts
|
51
|
+
|
52
|
+
def default_messages
|
53
|
+
@default_messages ||= {
|
54
|
+
invalid: 'is invalid',
|
55
|
+
invalid_event: 'cannot transition when %s',
|
56
|
+
invalid_transition: 'cannot transition via "%1$s"'
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def default_messages=(messages)
|
61
|
+
@default_messages = messages
|
62
|
+
end
|
63
|
+
|
64
|
+
def replace_messages(message_hash)
|
65
|
+
message_hash.each do |key, value|
|
66
|
+
default_messages[key] = value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
attr_writer :renderer
|
71
|
+
|
72
|
+
def renderer
|
73
|
+
return @renderer if @renderer
|
74
|
+
|
75
|
+
STDIORenderer
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -1,3 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'options_validator'
|
4
|
+
require_relative 'machine/class_methods'
|
5
|
+
|
1
6
|
module StateMachines
|
2
7
|
# Represents a state machine for a particular attribute. State machines
|
3
8
|
# consist of states, events and a set of transitions that define how the
|
@@ -398,65 +403,10 @@ module StateMachines
|
|
398
403
|
# machine's behavior, refer to all constants defined under the
|
399
404
|
# StateMachines::Integrations namespace.
|
400
405
|
class Machine
|
401
|
-
|
406
|
+
extend ClassMethods
|
402
407
|
include EvalHelpers
|
403
408
|
include MatcherHelpers
|
404
409
|
|
405
|
-
class << self
|
406
|
-
# Attempts to find or create a state machine for the given class. For
|
407
|
-
# example,
|
408
|
-
#
|
409
|
-
# StateMachines::Machine.find_or_create(Vehicle)
|
410
|
-
# StateMachines::Machine.find_or_create(Vehicle, :initial => :parked)
|
411
|
-
# StateMachines::Machine.find_or_create(Vehicle, :status)
|
412
|
-
# StateMachines::Machine.find_or_create(Vehicle, :status, :initial => :parked)
|
413
|
-
#
|
414
|
-
# If a machine of the given name already exists in one of the class's
|
415
|
-
# superclasses, then a copy of that machine will be created and stored
|
416
|
-
# in the new owner class (the original will remain unchanged).
|
417
|
-
def find_or_create(owner_class, *args, &block)
|
418
|
-
options = args.last.is_a?(Hash) ? args.pop : {}
|
419
|
-
name = args.first || :state
|
420
|
-
|
421
|
-
# Find an existing machine
|
422
|
-
machine = owner_class.respond_to?(:state_machines) &&
|
423
|
-
(args.first && owner_class.state_machines[name] || !args.first &&
|
424
|
-
owner_class.state_machines.values.first) || nil
|
425
|
-
|
426
|
-
if machine
|
427
|
-
# Only create a new copy if changes are being made to the machine in
|
428
|
-
# a subclass
|
429
|
-
if machine.owner_class != owner_class && (options.any? || block_given?)
|
430
|
-
machine = machine.clone
|
431
|
-
machine.initial_state = options[:initial] if options.include?(:initial)
|
432
|
-
machine.owner_class = owner_class
|
433
|
-
end
|
434
|
-
|
435
|
-
# Evaluate DSL
|
436
|
-
machine.instance_eval(&block) if block_given?
|
437
|
-
else
|
438
|
-
# No existing machine: create a new one
|
439
|
-
machine = new(owner_class, name, options, &block)
|
440
|
-
end
|
441
|
-
|
442
|
-
machine
|
443
|
-
end
|
444
|
-
|
445
|
-
|
446
|
-
def draw(*)
|
447
|
-
fail NotImplementedError
|
448
|
-
end
|
449
|
-
|
450
|
-
# Default messages to use for validation errors in ORM integrations
|
451
|
-
attr_accessor :default_messages
|
452
|
-
attr_accessor :ignore_method_conflicts
|
453
|
-
end
|
454
|
-
@default_messages = {
|
455
|
-
invalid: 'is invalid',
|
456
|
-
invalid_event: 'cannot transition when %s',
|
457
|
-
invalid_transition: 'cannot transition via "%1$s"'
|
458
|
-
}
|
459
|
-
|
460
410
|
# Whether to ignore any conflicts that are detected for helper methods that
|
461
411
|
# get generated for a machine's owner class. Default is false.
|
462
412
|
@ignore_method_conflicts = false
|
@@ -502,7 +452,7 @@ module StateMachines
|
|
502
452
|
# Creates a new state machine for the given attribute
|
503
453
|
def initialize(owner_class, *args, &block)
|
504
454
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
505
|
-
|
455
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
|
506
456
|
|
507
457
|
# Find an integration that matches this machine's owner class
|
508
458
|
if options.include?(:integration)
|
@@ -644,12 +594,12 @@ module StateMachines
|
|
644
594
|
# vehicle.force_idle = false
|
645
595
|
# Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=false>
|
646
596
|
def initial_state(object)
|
647
|
-
states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?(
|
597
|
+
states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?(:@initial_state)
|
648
598
|
end
|
649
599
|
|
650
600
|
# Whether a dynamic initial state is being used in the machine
|
651
601
|
def dynamic_initial_state?
|
652
|
-
instance_variable_defined?(
|
602
|
+
instance_variable_defined?(:@initial_state) && @initial_state.is_a?(Proc)
|
653
603
|
end
|
654
604
|
|
655
605
|
# Initializes the state on the given object. Initial values are only set if
|
@@ -1004,7 +954,7 @@ module StateMachines
|
|
1004
954
|
# options hash which contains at least <tt>:if</tt> condition support.
|
1005
955
|
def state(*names, &block)
|
1006
956
|
options = names.last.is_a?(Hash) ? names.pop : {}
|
1007
|
-
|
957
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :value, :cache, :if, :human_name)
|
1008
958
|
|
1009
959
|
# Store the context so that it can be used for / matched against any state
|
1010
960
|
# that gets added
|
@@ -1053,7 +1003,7 @@ module StateMachines
|
|
1053
1003
|
def read(object, attribute, ivar = false)
|
1054
1004
|
attribute = self.attribute(attribute)
|
1055
1005
|
if ivar
|
1056
|
-
object.instance_variable_defined?("@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
|
1006
|
+
object.instance_variable_defined?(:"@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
|
1057
1007
|
else
|
1058
1008
|
object.send(attribute)
|
1059
1009
|
end
|
@@ -1076,7 +1026,7 @@ module StateMachines
|
|
1076
1026
|
# vehicle.event # => "park"
|
1077
1027
|
def write(object, attribute, value, ivar = false)
|
1078
1028
|
attribute = self.attribute(attribute)
|
1079
|
-
ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value)
|
1029
|
+
ivar ? object.instance_variable_set(:"@#{attribute}", value) : object.send("#{attribute}=", value)
|
1080
1030
|
end
|
1081
1031
|
|
1082
1032
|
# Defines one or more events for the machine and the transitions that can
|
@@ -1307,7 +1257,7 @@ module StateMachines
|
|
1307
1257
|
# end
|
1308
1258
|
def event(*names, &block)
|
1309
1259
|
options = names.last.is_a?(Hash) ? names.pop : {}
|
1310
|
-
|
1260
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
|
1311
1261
|
|
1312
1262
|
# Store the context so that it can be used for / matched against any event
|
1313
1263
|
# that gets added
|
@@ -1749,7 +1699,7 @@ module StateMachines
|
|
1749
1699
|
def after_failure(*args, &block)
|
1750
1700
|
options = (args.last.is_a?(Hash) ? args.pop : {})
|
1751
1701
|
options[:do] = args if args.any?
|
1752
|
-
|
1702
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :on, :do, :if, :unless)
|
1753
1703
|
|
1754
1704
|
add_callback(:failure, options, &block)
|
1755
1705
|
end
|
@@ -1874,8 +1824,12 @@ module StateMachines
|
|
1874
1824
|
end
|
1875
1825
|
|
1876
1826
|
|
1877
|
-
def
|
1878
|
-
|
1827
|
+
def renderer
|
1828
|
+
self.class.renderer
|
1829
|
+
end
|
1830
|
+
|
1831
|
+
def draw(**options)
|
1832
|
+
renderer.draw_machine(self, **options)
|
1879
1833
|
end
|
1880
1834
|
|
1881
1835
|
# Determines whether an action hook was defined for firing attribute-based
|
@@ -1884,7 +1838,7 @@ module StateMachines
|
|
1884
1838
|
@action_hook_defined || !self_only && owner_class.state_machines.any? { |name, machine| machine.action == action && machine != self && machine.action_hook?(true) }
|
1885
1839
|
end
|
1886
1840
|
|
1887
|
-
|
1841
|
+
protected
|
1888
1842
|
|
1889
1843
|
# Runs additional initialization hooks. By default, this is a no-op.
|
1890
1844
|
def after_initialize
|