state_machines 0.6.0 → 0.30.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 +205 -14
- data/lib/state_machines/branch.rb +20 -17
- data/lib/state_machines/callback.rb +13 -12
- data/lib/state_machines/core.rb +3 -3
- 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 +7 -4
- data/lib/state_machines/eval_helpers.rb +93 -26
- data/lib/state_machines/event.rb +41 -29
- data/lib/state_machines/event_collection.rb +6 -5
- data/lib/state_machines/extensions.rb +7 -5
- data/lib/state_machines/helper_module.rb +3 -1
- data/lib/state_machines/integrations/base.rb +3 -1
- data/lib/state_machines/integrations.rb +13 -14
- data/lib/state_machines/machine/action_hooks.rb +53 -0
- data/lib/state_machines/machine/callbacks.rb +59 -0
- data/lib/state_machines/machine/class_methods.rb +93 -0
- data/lib/state_machines/machine/configuration.rb +124 -0
- data/lib/state_machines/machine/event_methods.rb +59 -0
- data/lib/state_machines/machine/helper_generators.rb +125 -0
- data/lib/state_machines/machine/integration.rb +70 -0
- data/lib/state_machines/machine/parsing.rb +77 -0
- data/lib/state_machines/machine/rendering.rb +17 -0
- data/lib/state_machines/machine/scoping.rb +44 -0
- data/lib/state_machines/machine/state_methods.rb +101 -0
- data/lib/state_machines/machine/utilities.rb +85 -0
- data/lib/state_machines/machine/validation.rb +39 -0
- data/lib/state_machines/machine.rb +83 -673
- data/lib/state_machines/machine_collection.rb +23 -15
- data/lib/state_machines/macro_methods.rb +4 -2
- data/lib/state_machines/matcher.rb +8 -5
- data/lib/state_machines/matcher_helpers.rb +3 -1
- data/lib/state_machines/node_collection.rb +23 -18
- data/lib/state_machines/options_validator.rb +72 -0
- data/lib/state_machines/path.rb +7 -5
- data/lib/state_machines/path_collection.rb +7 -4
- data/lib/state_machines/state.rb +76 -47
- data/lib/state_machines/state_collection.rb +5 -3
- data/lib/state_machines/state_context.rb +11 -8
- data/lib/state_machines/stdio_renderer.rb +74 -0
- data/lib/state_machines/syntax_validator.rb +57 -0
- data/lib/state_machines/test_helper.rb +568 -0
- data/lib/state_machines/transition.rb +45 -41
- data/lib/state_machines/transition_collection.rb +27 -26
- data/lib/state_machines/version.rb +3 -1
- data/lib/state_machines.rb +4 -1
- metadata +32 -16
- data/lib/state_machines/assertions.rb +0 -40
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
1
5
|
module StateMachines
|
2
6
|
# Represents a collection of state machines for a class
|
3
7
|
class MachineCollection < Hash
|
@@ -20,21 +24,25 @@ module StateMachines
|
|
20
24
|
# * <tt>:to</tt> - A hash to write the initialized state to instead of
|
21
25
|
# writing to the object. Default is to write directly to the object.
|
22
26
|
def initialize_states(object, options = {}, attributes = {})
|
23
|
-
|
24
|
-
options = {static: true, dynamic: true}.merge(options)
|
27
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :static, :dynamic, :to)
|
28
|
+
options = { static: true, dynamic: true }.merge(options)
|
25
29
|
|
26
30
|
result = yield if block_given?
|
27
31
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
+
if options[:static]
|
33
|
+
each_value do |machine|
|
34
|
+
unless machine.dynamic_initial_state?
|
35
|
+
force = options[:static] == :force || !attributes.keys.map(&:to_sym).include?(machine.attribute)
|
36
|
+
machine.initialize_state(object, force: force, to: options[:to])
|
37
|
+
end
|
32
38
|
end
|
33
|
-
end
|
39
|
+
end
|
34
40
|
|
35
|
-
|
36
|
-
|
37
|
-
|
41
|
+
if options[:dynamic]
|
42
|
+
each_value do |machine|
|
43
|
+
machine.initialize_state(object, force: options[:dynamic] == :force, to: options[:to]) if machine.dynamic_initial_state?
|
44
|
+
end
|
45
|
+
end
|
38
46
|
|
39
47
|
result
|
40
48
|
end
|
@@ -48,7 +56,7 @@ module StateMachines
|
|
48
56
|
transitions = events.collect do |event_name|
|
49
57
|
# Find the actual event being run
|
50
58
|
event = nil
|
51
|
-
detect { |
|
59
|
+
detect { |_name, machine| event = machine.events[event_name, :qualified_name] }
|
52
60
|
|
53
61
|
raise(InvalidEvent.new(object, event_name)) unless event
|
54
62
|
|
@@ -62,7 +70,7 @@ module StateMachines
|
|
62
70
|
# Run the events in parallel only if valid transitions were found for
|
63
71
|
# all of them
|
64
72
|
if events.length == transitions.length
|
65
|
-
TransitionCollection.new(transitions, {use_transactions: resolve_use_transactions, actions: run_action}).perform
|
73
|
+
TransitionCollection.new(transitions, { use_transactions: resolve_use_transactions, actions: run_action }).perform
|
66
74
|
else
|
67
75
|
false
|
68
76
|
end
|
@@ -74,14 +82,14 @@ module StateMachines
|
|
74
82
|
#
|
75
83
|
# These should only be fired as a result of the action being run.
|
76
84
|
def transitions(object, action, options = {})
|
77
|
-
transitions = map do |
|
85
|
+
transitions = map do |_name, machine|
|
78
86
|
machine.events.attribute_transition_for(object, true) if machine.action == action
|
79
87
|
end
|
80
88
|
|
81
|
-
AttributeTransitionCollection.new(transitions.compact, {use_transactions: resolve_use_transactions}.merge(options))
|
89
|
+
AttributeTransitionCollection.new(transitions.compact, { use_transactions: resolve_use_transactions }.merge(options))
|
82
90
|
end
|
83
91
|
|
84
|
-
|
92
|
+
protected
|
85
93
|
|
86
94
|
def resolve_use_transactions
|
87
95
|
use_transactions = nil
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# A state machine is a model of behavior composed of states, events, and
|
2
4
|
# transitions. This helper adds support for defining this type of
|
3
5
|
# functionality on any Ruby class.
|
@@ -513,8 +515,8 @@ module StateMachines
|
|
513
515
|
# See StateMachines::Machine for more information about using integrations
|
514
516
|
# and the individual integration docs for information about the actual
|
515
517
|
# scopes that are generated.
|
516
|
-
def state_machine(
|
517
|
-
StateMachines::Machine.find_or_create(self,
|
518
|
+
def state_machine(*, &)
|
519
|
+
StateMachines::Machine.find_or_create(self, *, &)
|
518
520
|
end
|
519
521
|
end
|
520
522
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module StateMachines
|
2
4
|
# Provides a general strategy pattern for determining whether a match is found
|
3
5
|
# for a value. The algorithm that actually determines the match depends on
|
@@ -30,12 +32,13 @@ module StateMachines
|
|
30
32
|
# matcher = StateMachines::AllMatcher.instance - [:parked, :idling]
|
31
33
|
# matcher.matches?(:parked) # => false
|
32
34
|
# matcher.matches?(:first_gear) # => true
|
33
|
-
def -(
|
34
|
-
BlacklistMatcher.new(
|
35
|
+
def -(other)
|
36
|
+
BlacklistMatcher.new(other)
|
35
37
|
end
|
38
|
+
alias except -
|
36
39
|
|
37
40
|
# Always returns true
|
38
|
-
def matches?(
|
41
|
+
def matches?(_value, _context = {})
|
39
42
|
true
|
40
43
|
end
|
41
44
|
|
@@ -60,7 +63,7 @@ module StateMachines
|
|
60
63
|
# matcher = StateMachines::WhitelistMatcher.new([:parked, :idling])
|
61
64
|
# matcher.matches?(:parked) # => true
|
62
65
|
# matcher.matches?(:first_gear) # => false
|
63
|
-
def matches?(value,
|
66
|
+
def matches?(value, _context = {})
|
64
67
|
values.include?(value)
|
65
68
|
end
|
66
69
|
|
@@ -80,7 +83,7 @@ module StateMachines
|
|
80
83
|
# matcher = StateMachines::BlacklistMatcher.new([:parked, :idling])
|
81
84
|
# matcher.matches?(:parked) # => false
|
82
85
|
# matcher.matches?(:first_gear) # => true
|
83
|
-
def matches?(value,
|
86
|
+
def matches?(value, _context = {})
|
84
87
|
!values.include?(value)
|
85
88
|
end
|
86
89
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module StateMachines
|
2
4
|
# Provides a set of helper methods for generating matchers
|
3
5
|
module MatcherHelpers
|
@@ -28,7 +30,7 @@ module StateMachines
|
|
28
30
|
def all
|
29
31
|
AllMatcher.instance
|
30
32
|
end
|
31
|
-
|
33
|
+
alias any all
|
32
34
|
|
33
35
|
# Represents a state that matches the original +from+ state. This is useful
|
34
36
|
# for defining transitions which are loopbacks.
|
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
1
5
|
module StateMachines
|
2
6
|
# Represents a collection of nodes in a state machine, be it events or states.
|
3
7
|
# Nodes will not differentiate between the String and Symbol versions of the
|
@@ -16,17 +20,16 @@ module StateMachines
|
|
16
20
|
# hashed indices for in order to perform quick lookups. Default is to
|
17
21
|
# index by the :name attribute
|
18
22
|
def initialize(machine, options = {})
|
19
|
-
|
23
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :index)
|
20
24
|
options = { index: :name }.merge(options)
|
21
25
|
|
22
26
|
@machine = machine
|
23
27
|
@nodes = []
|
24
28
|
@index_names = Array(options[:index])
|
25
|
-
@indices = @index_names.
|
29
|
+
@indices = @index_names.each_with_object({}) do |name, indices|
|
26
30
|
indices[name] = {}
|
27
31
|
indices[:"#{name}_to_s"] = {}
|
28
32
|
indices[:"#{name}_to_sym"] = {}
|
29
|
-
indices
|
30
33
|
end
|
31
34
|
@default_index = Array(options[:index]).first
|
32
35
|
@contexts = []
|
@@ -34,14 +37,16 @@ module StateMachines
|
|
34
37
|
|
35
38
|
# Creates a copy of this collection such that modifications don't affect
|
36
39
|
# the original collection
|
37
|
-
def initialize_copy(orig)
|
40
|
+
def initialize_copy(orig) # :nodoc:
|
38
41
|
super
|
39
42
|
|
40
43
|
nodes = @nodes
|
41
44
|
contexts = @contexts
|
42
45
|
@nodes = []
|
43
46
|
@contexts = []
|
44
|
-
@indices = @indices.
|
47
|
+
@indices = @indices.each_with_object({}) do |(name, *), indices|
|
48
|
+
indices[name] = {}
|
49
|
+
end
|
45
50
|
|
46
51
|
# Add nodes *prior* to copying over the contexts so that they don't get
|
47
52
|
# evaluated multiple times
|
@@ -112,8 +117,8 @@ module StateMachines
|
|
112
117
|
# ...produces:
|
113
118
|
#
|
114
119
|
# parked -- idling --
|
115
|
-
def each
|
116
|
-
@nodes.each
|
120
|
+
def each(&)
|
121
|
+
@nodes.each(&)
|
117
122
|
self
|
118
123
|
end
|
119
124
|
|
@@ -141,9 +146,9 @@ module StateMachines
|
|
141
146
|
# If the key cannot be found, then nil will be returned.
|
142
147
|
def [](key, index_name = @default_index)
|
143
148
|
index(index_name)[key] ||
|
144
|
-
|
145
|
-
|
146
|
-
|
149
|
+
index(:"#{index_name}_to_s")[key.to_s] ||
|
150
|
+
(to_sym?(key) && index(:"#{index_name}_to_sym")[:"#{key}"]) ||
|
151
|
+
nil
|
147
152
|
end
|
148
153
|
|
149
154
|
# Gets the node indexed by the given key. By default, this will look up the
|
@@ -159,17 +164,17 @@ module StateMachines
|
|
159
164
|
#
|
160
165
|
# collection['invalid', :value] # => IndexError: "invalid" is an invalid value
|
161
166
|
def fetch(key, index_name = @default_index)
|
162
|
-
self[key, index_name] ||
|
167
|
+
self[key, index_name] || raise(IndexError, "#{key.inspect} is an invalid #{index_name}")
|
163
168
|
end
|
164
169
|
|
165
|
-
|
170
|
+
protected
|
166
171
|
|
167
172
|
# Gets the given index. If the index does not exist, then an ArgumentError
|
168
173
|
# is raised.
|
169
174
|
def index(name)
|
170
|
-
|
175
|
+
raise ArgumentError, 'No indices configured' unless @indices.any?
|
171
176
|
|
172
|
-
@indices[name] ||
|
177
|
+
@indices[name] || raise(ArgumentError, "Invalid index: #{name.inspect}")
|
173
178
|
end
|
174
179
|
|
175
180
|
# Gets the value for the given attribute on the node
|
@@ -201,10 +206,10 @@ module StateMachines
|
|
201
206
|
new_key = value(node, name)
|
202
207
|
|
203
208
|
# Only replace the key if it's changed
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
209
|
+
return unless old_key != new_key
|
210
|
+
|
211
|
+
remove_from_index(name, old_key)
|
212
|
+
add_to_index(name, new_key, node)
|
208
213
|
end
|
209
214
|
|
210
215
|
# Determines whether the given value can be converted to a symbol
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
# Define the module if it doesn't exist yet
|
5
|
+
# Module for validating options without monkey-patching Hash
|
6
|
+
# Provides the same functionality as the Hash monkey patch but in a cleaner way
|
7
|
+
module OptionsValidator
|
8
|
+
class << self
|
9
|
+
# Validates that all keys in the options hash are in the list of valid keys
|
10
|
+
#
|
11
|
+
# @param options [Hash] The options hash to validate
|
12
|
+
# @param valid_keys [Array<Symbol>] List of valid key names
|
13
|
+
# @param caller_info [String] Information about the calling method for better error messages
|
14
|
+
# @raise [ArgumentError] If any invalid keys are found
|
15
|
+
def assert_valid_keys!(options, *valid_keys, caller_info: nil)
|
16
|
+
return if options.empty?
|
17
|
+
|
18
|
+
valid_keys.flatten!
|
19
|
+
invalid_keys = options.keys - valid_keys
|
20
|
+
|
21
|
+
return if invalid_keys.empty?
|
22
|
+
|
23
|
+
caller_context = caller_info ? " in #{caller_info}" : ''
|
24
|
+
raise ArgumentError, "Unknown key#{'s' if invalid_keys.length > 1}: #{invalid_keys.map(&:inspect).join(', ')}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}#{caller_context}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Validates that at most one of the exclusive keys is present in the options hash
|
28
|
+
#
|
29
|
+
# @param options [Hash] The options hash to validate
|
30
|
+
# @param exclusive_keys [Array<Symbol>] List of mutually exclusive keys
|
31
|
+
# @param caller_info [String] Information about the calling method for better error messages
|
32
|
+
# @raise [ArgumentError] If more than one exclusive key is found
|
33
|
+
def assert_exclusive_keys!(options, *exclusive_keys, caller_info: nil)
|
34
|
+
return if options.empty?
|
35
|
+
|
36
|
+
conflicting_keys = exclusive_keys & options.keys
|
37
|
+
return if conflicting_keys.length <= 1
|
38
|
+
|
39
|
+
caller_context = caller_info ? " in #{caller_info}" : ''
|
40
|
+
raise ArgumentError, "Conflicting keys: #{conflicting_keys.join(', ')}#{caller_context}"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Validates options using a more convenient interface that works with both
|
44
|
+
# hash-style and kwargs-style method definitions
|
45
|
+
#
|
46
|
+
# @param valid_keys [Array<Symbol>] List of valid key names
|
47
|
+
# @param exclusive_key_groups [Array<Array<Symbol>>] Groups of mutually exclusive keys
|
48
|
+
# @param caller_info [String] Information about the calling method
|
49
|
+
# @return [Proc] A validation proc that can be called with options
|
50
|
+
def validator(valid_keys: [], exclusive_key_groups: [], caller_info: nil)
|
51
|
+
proc do |options|
|
52
|
+
assert_valid_keys!(options, *valid_keys, caller_info: caller_info) unless valid_keys.empty?
|
53
|
+
|
54
|
+
exclusive_key_groups.each do |group|
|
55
|
+
assert_exclusive_keys!(options, *group, caller_info: caller_info)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Helper method for backwards compatibility - allows gradual migration
|
61
|
+
# from Hash monkey patch to this module
|
62
|
+
#
|
63
|
+
# @param options [Hash] The options to validate
|
64
|
+
# @param valid_keys [Array<Symbol>] Valid keys
|
65
|
+
# @return [Hash] The same options hash (for chaining)
|
66
|
+
def validate_and_return(options, *valid_keys)
|
67
|
+
assert_valid_keys!(options, *valid_keys)
|
68
|
+
options
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/state_machines/path.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
1
5
|
module StateMachines
|
2
6
|
# A path represents a sequence of transitions that can be run for a particular
|
3
7
|
# object. Paths can walk to new transitions, revealing all of the possible
|
4
8
|
# branches that can be encountered in the object's state machine.
|
5
9
|
class Path < Array
|
6
|
-
|
7
|
-
|
8
10
|
# The object whose state machine is being walked
|
9
11
|
attr_reader :object
|
10
12
|
|
@@ -20,7 +22,7 @@ module StateMachines
|
|
20
22
|
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
21
23
|
# conditionals defined for each one
|
22
24
|
def initialize(object, machine, options = {})
|
23
|
-
|
25
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :target, :guard)
|
24
26
|
|
25
27
|
@object = object
|
26
28
|
@machine = machine
|
@@ -28,7 +30,7 @@ module StateMachines
|
|
28
30
|
@guard = options[:guard]
|
29
31
|
end
|
30
32
|
|
31
|
-
def initialize_copy(orig)
|
33
|
+
def initialize_copy(orig) # :nodoc:
|
32
34
|
super
|
33
35
|
@transitions = nil
|
34
36
|
end
|
@@ -86,7 +88,7 @@ module StateMachines
|
|
86
88
|
!empty? && (@target ? to_name == @target : transitions.empty?)
|
87
89
|
end
|
88
90
|
|
89
|
-
|
91
|
+
private
|
90
92
|
|
91
93
|
# Calculates the number of times the given state has been walked to
|
92
94
|
def times_walked_to(state)
|
@@ -1,8 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
1
5
|
module StateMachines
|
2
6
|
# Represents a collection of paths that are generated based on a set of
|
3
7
|
# requirements regarding what states to start and end on
|
4
8
|
class PathCollection < Array
|
5
|
-
|
6
9
|
# The object whose state machine is being walked
|
7
10
|
attr_reader :object
|
8
11
|
|
@@ -24,8 +27,8 @@ module StateMachines
|
|
24
27
|
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
25
28
|
# conditionals defined for each one
|
26
29
|
def initialize(object, machine, options = {})
|
27
|
-
options = {deep: false, from: machine.states.match!(object).name}.merge(options)
|
28
|
-
|
30
|
+
options = { deep: false, from: machine.states.match!(object).name }.merge(options)
|
31
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :deep, :guard)
|
29
32
|
|
30
33
|
@object = object
|
31
34
|
@machine = machine
|
@@ -67,7 +70,7 @@ module StateMachines
|
|
67
70
|
flat_map(&:events).uniq
|
68
71
|
end
|
69
72
|
|
70
|
-
|
73
|
+
private
|
71
74
|
|
72
75
|
# Gets the initial set of paths to walk
|
73
76
|
def initial_paths
|
data/lib/state_machines/state.rb
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
1
5
|
module StateMachines
|
2
6
|
# A state defines a value that an attribute can be in after being transitioned
|
3
7
|
# 0 or more times. States can represent a value of any type in Ruby, though
|
@@ -8,7 +12,6 @@ module StateMachines
|
|
8
12
|
# StateMachines::Machine#state for more information about how state-driven
|
9
13
|
# behavior can be utilized.
|
10
14
|
class State
|
11
|
-
|
12
15
|
# The state machine for which this state is defined
|
13
16
|
attr_reader :machine
|
14
17
|
|
@@ -31,7 +34,7 @@ module StateMachines
|
|
31
34
|
|
32
35
|
# Whether or not this state is the initial state to use for new objects
|
33
36
|
attr_accessor :initial
|
34
|
-
|
37
|
+
alias initial? initial
|
35
38
|
|
36
39
|
# A custom lambda block for determining whether a given value matches this
|
37
40
|
# state
|
@@ -50,38 +53,58 @@ module StateMachines
|
|
50
53
|
# (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
|
51
54
|
# By default, the configured value is matched.
|
52
55
|
# * <tt>:human_name</tt> - The human-readable version of this state's name
|
53
|
-
def initialize(machine, name, options =
|
54
|
-
|
56
|
+
def initialize(machine, name, options = nil, initial: false, value: :__not_provided__, cache: nil, if: nil, human_name: nil, **extra_options) # :nodoc:
|
57
|
+
# Handle both old hash style and new kwargs style for backward compatibility
|
58
|
+
if options.is_a?(Hash)
|
59
|
+
# Old style: initialize(machine, name, {initial: true, value: 'foo'})
|
60
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :initial, :value, :cache, :if, :human_name)
|
61
|
+
initial = options.fetch(:initial, false)
|
62
|
+
value = options.include?(:value) ? options[:value] : :__not_provided__
|
63
|
+
cache = options[:cache]
|
64
|
+
if_condition = options[:if]
|
65
|
+
human_name = options[:human_name]
|
66
|
+
else
|
67
|
+
# New style: initialize(machine, name, initial: true, value: 'foo')
|
68
|
+
# options parameter should be nil in this case
|
69
|
+
raise ArgumentError, "Unexpected positional argument: #{options.inspect}" unless options.nil?
|
70
|
+
|
71
|
+
StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :initial, :value, :cache, :if, :human_name) unless extra_options.empty?
|
72
|
+
if_condition = binding.local_variable_get(:if) # 'if' is a keyword, need special handling
|
73
|
+
end
|
55
74
|
|
56
75
|
@machine = machine
|
57
76
|
@name = name
|
58
77
|
@qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name
|
59
|
-
@human_name =
|
60
|
-
@value =
|
61
|
-
@cache =
|
62
|
-
@matcher =
|
63
|
-
@initial =
|
78
|
+
@human_name = human_name || (@name ? @name.to_s.tr('_', ' ') : 'nil')
|
79
|
+
@value = value == :__not_provided__ ? name&.to_s : value
|
80
|
+
@cache = cache
|
81
|
+
@matcher = if_condition
|
82
|
+
@initial = initial == true
|
64
83
|
@context = StateContext.new(self)
|
65
84
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
85
|
+
return unless name
|
86
|
+
|
87
|
+
conflicting_machines = machine.owner_class.state_machines.select do |_other_name, other_machine|
|
88
|
+
other_machine != machine && other_machine.states[qualified_name, :qualified_name]
|
89
|
+
end
|
90
|
+
|
91
|
+
# Output a warning if another machine has a conflicting qualified name
|
92
|
+
# for a different attribute
|
93
|
+
if (conflict = conflicting_machines.detect do |_other_name, other_machine|
|
94
|
+
other_machine.attribute != machine.attribute
|
95
|
+
end)
|
96
|
+
_name, other_machine = conflict
|
97
|
+
warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
|
98
|
+
elsif conflicting_machines.empty?
|
99
|
+
# Only bother adding predicates when another machine for the same
|
100
|
+
# attribute hasn't already done so
|
101
|
+
add_predicate
|
79
102
|
end
|
80
103
|
end
|
81
104
|
|
82
105
|
# Creates a copy of this state, excluding the context to prevent conflicts
|
83
106
|
# across different machines.
|
84
|
-
def initialize_copy(orig)
|
107
|
+
def initialize_copy(orig) # :nodoc:
|
85
108
|
super
|
86
109
|
@context = StateContext.new(self)
|
87
110
|
end
|
@@ -96,7 +119,7 @@ module StateMachines
|
|
96
119
|
# Any objects in a final state will remain so forever given the current
|
97
120
|
# machine's definition.
|
98
121
|
def final?
|
99
|
-
|
122
|
+
machine.events.none? do |event|
|
100
123
|
event.branches.any? do |branch|
|
101
124
|
branch.state_requirements.any? do |requirement|
|
102
125
|
requirement[:from].matches?(name) && !requirement[:to].matches?(name, from: name)
|
@@ -126,7 +149,7 @@ module StateMachines
|
|
126
149
|
# description or just the internal name
|
127
150
|
def description(options = {})
|
128
151
|
label = options[:human_name] ? human_name : name
|
129
|
-
description = label ? label.to_s : label.inspect
|
152
|
+
description = +(label ? label.to_s : label.inspect)
|
130
153
|
description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
|
131
154
|
description
|
132
155
|
end
|
@@ -178,27 +201,27 @@ module StateMachines
|
|
178
201
|
#
|
179
202
|
# This can be called multiple times. Each time a new context is created,
|
180
203
|
# a new module will be included in the owner class.
|
181
|
-
def context(&
|
204
|
+
def context(&)
|
182
205
|
# Include the context
|
183
206
|
context = @context
|
184
207
|
machine.owner_class.class_eval { include context }
|
185
208
|
|
186
209
|
# Evaluate the method definitions and track which ones were added
|
187
210
|
old_methods = context_methods
|
188
|
-
context.class_eval(&
|
189
|
-
new_methods = context_methods.to_a.
|
211
|
+
context.class_eval(&)
|
212
|
+
new_methods = context_methods.to_a.reject { |(name, method)| old_methods[name] == method }
|
190
213
|
|
191
214
|
# Alias new methods so that the only execute when the object is in this state
|
192
215
|
new_methods.each do |(method_name, _method)|
|
193
216
|
context_name = context_name_for(method_name)
|
194
|
-
context.class_eval <<-
|
217
|
+
context.class_eval <<-END_EVAL, __FILE__, __LINE__ + 1
|
195
218
|
alias_method :"#{context_name}", :#{method_name}
|
196
219
|
def #{method_name}(*args, &block)
|
197
220
|
state = self.class.state_machine(#{machine.name.inspect}).states.fetch(#{name.inspect})
|
198
221
|
options = {:method_missing => lambda {super(*args, &block)}, :method_name => #{method_name.inspect}}
|
199
222
|
state.call(self, :"#{context_name}", *(args + [options]), &block)
|
200
223
|
end
|
201
|
-
|
224
|
+
END_EVAL
|
202
225
|
end
|
203
226
|
|
204
227
|
true
|
@@ -216,31 +239,30 @@ module StateMachines
|
|
216
239
|
#
|
217
240
|
# If the method has never been defined for this state, then a NoMethodError
|
218
241
|
# will be raised.
|
219
|
-
def call(object, method, *args, &
|
242
|
+
def call(object, method, *args, &)
|
220
243
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
221
|
-
options = {method_name: method}.merge(options)
|
244
|
+
options = { method_name: method }.merge(options)
|
222
245
|
state = machine.states.match!(object)
|
223
246
|
|
224
247
|
if state == self && object.respond_to?(method)
|
225
|
-
object.send(method, *args, &
|
226
|
-
elsif method_missing = options[:method_missing]
|
248
|
+
object.send(method, *args, &)
|
249
|
+
elsif (method_missing = options[:method_missing])
|
227
250
|
# Dispatch to the superclass since the object either isn't in this state
|
228
251
|
# or this state doesn't handle the method
|
229
252
|
begin
|
230
253
|
method_missing.call
|
231
|
-
rescue NoMethodError =>
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
end
|
254
|
+
rescue NoMethodError => e
|
255
|
+
raise unless e.name.to_s == options[:method_name].to_s && e.args == args
|
256
|
+
|
257
|
+
# No valid context for this method
|
258
|
+
raise InvalidContext.new(object,
|
259
|
+
"State #{state.name.inspect} for #{machine.name.inspect} is not a valid context for calling ##{options[:method_name]}")
|
238
260
|
end
|
239
261
|
end
|
240
262
|
end
|
241
263
|
|
242
|
-
def draw(graph, options = {})
|
243
|
-
|
264
|
+
def draw(graph, options = {}, io = $stdout)
|
265
|
+
machine.renderer.draw_state(self, graph, options, io)
|
244
266
|
end
|
245
267
|
|
246
268
|
# Generates a nicely formatted description of this state's contents.
|
@@ -254,7 +276,7 @@ module StateMachines
|
|
254
276
|
"#<#{self.class} #{attributes.map { |attr, value| "#{attr}=#{value.inspect}" } * ' '}>"
|
255
277
|
end
|
256
278
|
|
257
|
-
|
279
|
+
private
|
258
280
|
|
259
281
|
# Should the value be cached after it's evaluated for the first time?
|
260
282
|
def cache_value?
|
@@ -264,9 +286,16 @@ module StateMachines
|
|
264
286
|
# Adds a predicate method to the owner class so long as a name has
|
265
287
|
# actually been configured for the state
|
266
288
|
def add_predicate
|
267
|
-
|
268
|
-
|
269
|
-
|
289
|
+
predicate_method = "#{qualified_name}?"
|
290
|
+
|
291
|
+
if machine.send(:owner_class_ancestor_has_method?, :instance, predicate_method)
|
292
|
+
warn "Instance method #{predicate_method.inspect} is already defined in #{machine.owner_class.ancestors.first.inspect}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
|
293
|
+
elsif machine.send(:owner_class_has_method?, :instance, predicate_method)
|
294
|
+
warn "Instance method #{predicate_method.inspect} is already defined in #{machine.owner_class.inspect}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
|
295
|
+
else
|
296
|
+
machine.define_helper(:instance, predicate_method) do |machine, object|
|
297
|
+
machine.states.matches?(object, name)
|
298
|
+
end
|
270
299
|
end
|
271
300
|
end
|
272
301
|
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module StateMachines
|
2
4
|
# Represents a collection of states in a state machine
|
3
5
|
class StateCollection < NodeCollection
|
4
|
-
def initialize(machine)
|
5
|
-
super(machine, index: [
|
6
|
+
def initialize(machine) # :nodoc:
|
7
|
+
super(machine, index: %i[name qualified_name value])
|
6
8
|
end
|
7
9
|
|
8
10
|
# Determines whether the given object is in a specific state. If the
|
@@ -101,7 +103,7 @@ module StateMachines
|
|
101
103
|
order
|
102
104
|
end
|
103
105
|
|
104
|
-
|
106
|
+
private
|
105
107
|
|
106
108
|
# Gets the value for the given attribute on the node
|
107
109
|
def value(node, attribute)
|