state_machines 0.10.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 +177 -2
- data/lib/state_machines/branch.rb +16 -15
- data/lib/state_machines/callback.rb +11 -12
- data/lib/state_machines/core.rb +1 -3
- data/lib/state_machines/error.rb +5 -4
- data/lib/state_machines/eval_helpers.rb +83 -45
- data/lib/state_machines/event.rb +37 -27
- data/lib/state_machines/event_collection.rb +4 -5
- data/lib/state_machines/extensions.rb +5 -5
- data/lib/state_machines/helper_module.rb +1 -1
- data/lib/state_machines/integrations/base.rb +1 -1
- data/lib/state_machines/integrations.rb +11 -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 +25 -11
- 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 +75 -618
- data/lib/state_machines/machine_collection.rb +21 -15
- data/lib/state_machines/macro_methods.rb +2 -2
- data/lib/state_machines/matcher.rb +6 -6
- data/lib/state_machines/matcher_helpers.rb +1 -1
- data/lib/state_machines/node_collection.rb +21 -18
- data/lib/state_machines/options_validator.rb +72 -0
- data/lib/state_machines/path.rb +5 -5
- data/lib/state_machines/path_collection.rb +5 -4
- data/lib/state_machines/state.rb +29 -11
- data/lib/state_machines/state_collection.rb +3 -3
- data/lib/state_machines/state_context.rb +9 -8
- data/lib/state_machines/stdio_renderer.rb +16 -16
- data/lib/state_machines/syntax_validator.rb +57 -0
- data/lib/state_machines/test_helper.rb +568 -0
- data/lib/state_machines/transition.rb +43 -41
- data/lib/state_machines/transition_collection.rb +25 -26
- data/lib/state_machines/version.rb +1 -1
- metadata +25 -10
- data/lib/state_machines/assertions.rb +0 -42
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
3
5
|
module StateMachines
|
4
6
|
# Represents a collection of state machines for a class
|
5
7
|
class MachineCollection < Hash
|
@@ -22,21 +24,25 @@ module StateMachines
|
|
22
24
|
# * <tt>:to</tt> - A hash to write the initialized state to instead of
|
23
25
|
# writing to the object. Default is to write directly to the object.
|
24
26
|
def initialize_states(object, options = {}, attributes = {})
|
25
|
-
|
26
|
-
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)
|
27
29
|
|
28
30
|
result = yield if block_given?
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
34
38
|
end
|
35
|
-
end
|
39
|
+
end
|
36
40
|
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
40
46
|
|
41
47
|
result
|
42
48
|
end
|
@@ -50,7 +56,7 @@ module StateMachines
|
|
50
56
|
transitions = events.collect do |event_name|
|
51
57
|
# Find the actual event being run
|
52
58
|
event = nil
|
53
|
-
detect { |
|
59
|
+
detect { |_name, machine| event = machine.events[event_name, :qualified_name] }
|
54
60
|
|
55
61
|
raise(InvalidEvent.new(object, event_name)) unless event
|
56
62
|
|
@@ -64,7 +70,7 @@ module StateMachines
|
|
64
70
|
# Run the events in parallel only if valid transitions were found for
|
65
71
|
# all of them
|
66
72
|
if events.length == transitions.length
|
67
|
-
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
|
68
74
|
else
|
69
75
|
false
|
70
76
|
end
|
@@ -76,14 +82,14 @@ module StateMachines
|
|
76
82
|
#
|
77
83
|
# These should only be fired as a result of the action being run.
|
78
84
|
def transitions(object, action, options = {})
|
79
|
-
transitions = map do |
|
85
|
+
transitions = map do |_name, machine|
|
80
86
|
machine.events.attribute_transition_for(object, true) if machine.action == action
|
81
87
|
end
|
82
88
|
|
83
|
-
AttributeTransitionCollection.new(transitions.compact, {use_transactions: resolve_use_transactions}.merge(options))
|
89
|
+
AttributeTransitionCollection.new(transitions.compact, { use_transactions: resolve_use_transactions }.merge(options))
|
84
90
|
end
|
85
91
|
|
86
|
-
|
92
|
+
protected
|
87
93
|
|
88
94
|
def resolve_use_transactions
|
89
95
|
use_transactions = nil
|
@@ -515,8 +515,8 @@ module StateMachines
|
|
515
515
|
# See StateMachines::Machine for more information about using integrations
|
516
516
|
# and the individual integration docs for information about the actual
|
517
517
|
# scopes that are generated.
|
518
|
-
def state_machine(
|
519
|
-
StateMachines::Machine.find_or_create(self,
|
518
|
+
def state_machine(*, &)
|
519
|
+
StateMachines::Machine.find_or_create(self, *, &)
|
520
520
|
end
|
521
521
|
end
|
522
522
|
end
|
@@ -32,13 +32,13 @@ module StateMachines
|
|
32
32
|
# matcher = StateMachines::AllMatcher.instance - [:parked, :idling]
|
33
33
|
# matcher.matches?(:parked) # => false
|
34
34
|
# matcher.matches?(:first_gear) # => true
|
35
|
-
def -(
|
36
|
-
BlacklistMatcher.new(
|
35
|
+
def -(other)
|
36
|
+
BlacklistMatcher.new(other)
|
37
37
|
end
|
38
|
-
|
38
|
+
alias except -
|
39
39
|
|
40
40
|
# Always returns true
|
41
|
-
def matches?(
|
41
|
+
def matches?(_value, _context = {})
|
42
42
|
true
|
43
43
|
end
|
44
44
|
|
@@ -63,7 +63,7 @@ module StateMachines
|
|
63
63
|
# matcher = StateMachines::WhitelistMatcher.new([:parked, :idling])
|
64
64
|
# matcher.matches?(:parked) # => true
|
65
65
|
# matcher.matches?(:first_gear) # => false
|
66
|
-
def matches?(value,
|
66
|
+
def matches?(value, _context = {})
|
67
67
|
values.include?(value)
|
68
68
|
end
|
69
69
|
|
@@ -83,7 +83,7 @@ module StateMachines
|
|
83
83
|
# matcher = StateMachines::BlacklistMatcher.new([:parked, :idling])
|
84
84
|
# matcher.matches?(:parked) # => false
|
85
85
|
# matcher.matches?(:first_gear) # => true
|
86
|
-
def matches?(value,
|
86
|
+
def matches?(value, _context = {})
|
87
87
|
!values.include?(value)
|
88
88
|
end
|
89
89
|
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
3
5
|
module StateMachines
|
4
6
|
# Represents a collection of nodes in a state machine, be it events or states.
|
5
7
|
# Nodes will not differentiate between the String and Symbol versions of the
|
@@ -18,17 +20,16 @@ module StateMachines
|
|
18
20
|
# hashed indices for in order to perform quick lookups. Default is to
|
19
21
|
# index by the :name attribute
|
20
22
|
def initialize(machine, options = {})
|
21
|
-
|
23
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :index)
|
22
24
|
options = { index: :name }.merge(options)
|
23
25
|
|
24
26
|
@machine = machine
|
25
27
|
@nodes = []
|
26
28
|
@index_names = Array(options[:index])
|
27
|
-
@indices = @index_names.
|
29
|
+
@indices = @index_names.each_with_object({}) do |name, indices|
|
28
30
|
indices[name] = {}
|
29
31
|
indices[:"#{name}_to_s"] = {}
|
30
32
|
indices[:"#{name}_to_sym"] = {}
|
31
|
-
indices
|
32
33
|
end
|
33
34
|
@default_index = Array(options[:index]).first
|
34
35
|
@contexts = []
|
@@ -36,14 +37,16 @@ module StateMachines
|
|
36
37
|
|
37
38
|
# Creates a copy of this collection such that modifications don't affect
|
38
39
|
# the original collection
|
39
|
-
def initialize_copy(orig)
|
40
|
+
def initialize_copy(orig) # :nodoc:
|
40
41
|
super
|
41
42
|
|
42
43
|
nodes = @nodes
|
43
44
|
contexts = @contexts
|
44
45
|
@nodes = []
|
45
46
|
@contexts = []
|
46
|
-
@indices = @indices.
|
47
|
+
@indices = @indices.each_with_object({}) do |(name, *), indices|
|
48
|
+
indices[name] = {}
|
49
|
+
end
|
47
50
|
|
48
51
|
# Add nodes *prior* to copying over the contexts so that they don't get
|
49
52
|
# evaluated multiple times
|
@@ -114,8 +117,8 @@ module StateMachines
|
|
114
117
|
# ...produces:
|
115
118
|
#
|
116
119
|
# parked -- idling --
|
117
|
-
def each
|
118
|
-
@nodes.each
|
120
|
+
def each(&)
|
121
|
+
@nodes.each(&)
|
119
122
|
self
|
120
123
|
end
|
121
124
|
|
@@ -143,9 +146,9 @@ module StateMachines
|
|
143
146
|
# If the key cannot be found, then nil will be returned.
|
144
147
|
def [](key, index_name = @default_index)
|
145
148
|
index(index_name)[key] ||
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
+
index(:"#{index_name}_to_s")[key.to_s] ||
|
150
|
+
(to_sym?(key) && index(:"#{index_name}_to_sym")[:"#{key}"]) ||
|
151
|
+
nil
|
149
152
|
end
|
150
153
|
|
151
154
|
# Gets the node indexed by the given key. By default, this will look up the
|
@@ -161,17 +164,17 @@ module StateMachines
|
|
161
164
|
#
|
162
165
|
# collection['invalid', :value] # => IndexError: "invalid" is an invalid value
|
163
166
|
def fetch(key, index_name = @default_index)
|
164
|
-
self[key, index_name] ||
|
167
|
+
self[key, index_name] || raise(IndexError, "#{key.inspect} is an invalid #{index_name}")
|
165
168
|
end
|
166
169
|
|
167
|
-
|
170
|
+
protected
|
168
171
|
|
169
172
|
# Gets the given index. If the index does not exist, then an ArgumentError
|
170
173
|
# is raised.
|
171
174
|
def index(name)
|
172
|
-
|
175
|
+
raise ArgumentError, 'No indices configured' unless @indices.any?
|
173
176
|
|
174
|
-
@indices[name] ||
|
177
|
+
@indices[name] || raise(ArgumentError, "Invalid index: #{name.inspect}")
|
175
178
|
end
|
176
179
|
|
177
180
|
# Gets the value for the given attribute on the node
|
@@ -203,10 +206,10 @@ module StateMachines
|
|
203
206
|
new_key = value(node, name)
|
204
207
|
|
205
208
|
# Only replace the key if it's changed
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
209
|
+
return unless old_key != new_key
|
210
|
+
|
211
|
+
remove_from_index(name, old_key)
|
212
|
+
add_to_index(name, new_key, node)
|
210
213
|
end
|
211
214
|
|
212
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,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
3
5
|
module StateMachines
|
4
6
|
# A path represents a sequence of transitions that can be run for a particular
|
5
7
|
# object. Paths can walk to new transitions, revealing all of the possible
|
6
8
|
# branches that can be encountered in the object's state machine.
|
7
9
|
class Path < Array
|
8
|
-
|
9
|
-
|
10
10
|
# The object whose state machine is being walked
|
11
11
|
attr_reader :object
|
12
12
|
|
@@ -22,7 +22,7 @@ module StateMachines
|
|
22
22
|
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
23
23
|
# conditionals defined for each one
|
24
24
|
def initialize(object, machine, options = {})
|
25
|
-
|
25
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :target, :guard)
|
26
26
|
|
27
27
|
@object = object
|
28
28
|
@machine = machine
|
@@ -30,7 +30,7 @@ module StateMachines
|
|
30
30
|
@guard = options[:guard]
|
31
31
|
end
|
32
32
|
|
33
|
-
def initialize_copy(orig)
|
33
|
+
def initialize_copy(orig) # :nodoc:
|
34
34
|
super
|
35
35
|
@transitions = nil
|
36
36
|
end
|
@@ -88,7 +88,7 @@ module StateMachines
|
|
88
88
|
!empty? && (@target ? to_name == @target : transitions.empty?)
|
89
89
|
end
|
90
90
|
|
91
|
-
|
91
|
+
private
|
92
92
|
|
93
93
|
# Calculates the number of times the given state has been walked to
|
94
94
|
def times_walked_to(state)
|
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
3
5
|
module StateMachines
|
4
6
|
# Represents a collection of paths that are generated based on a set of
|
5
7
|
# requirements regarding what states to start and end on
|
6
8
|
class PathCollection < Array
|
7
|
-
|
8
9
|
# The object whose state machine is being walked
|
9
10
|
attr_reader :object
|
10
11
|
|
@@ -26,8 +27,8 @@ module StateMachines
|
|
26
27
|
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
27
28
|
# conditionals defined for each one
|
28
29
|
def initialize(object, machine, options = {})
|
29
|
-
options = {deep: false, from: machine.states.match!(object).name}.merge(options)
|
30
|
-
|
30
|
+
options = { deep: false, from: machine.states.match!(object).name }.merge(options)
|
31
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :deep, :guard)
|
31
32
|
|
32
33
|
@object = object
|
33
34
|
@machine = machine
|
@@ -69,7 +70,7 @@ module StateMachines
|
|
69
70
|
flat_map(&:events).uniq
|
70
71
|
end
|
71
72
|
|
72
|
-
|
73
|
+
private
|
73
74
|
|
74
75
|
# Gets the initial set of paths to walk
|
75
76
|
def initial_paths
|
data/lib/state_machines/state.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
3
5
|
module StateMachines
|
4
6
|
# A state defines a value that an attribute can be in after being transitioned
|
5
7
|
# 0 or more times. States can represent a value of any type in Ruby, though
|
@@ -51,17 +53,33 @@ module StateMachines
|
|
51
53
|
# (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
|
52
54
|
# By default, the configured value is matched.
|
53
55
|
# * <tt>:human_name</tt> - The human-readable version of this state's name
|
54
|
-
def initialize(machine, name, options =
|
55
|
-
|
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
|
56
74
|
|
57
75
|
@machine = machine
|
58
76
|
@name = name
|
59
77
|
@qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name
|
60
|
-
@human_name =
|
61
|
-
@value =
|
62
|
-
@cache =
|
63
|
-
@matcher =
|
64
|
-
@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
|
65
83
|
@context = StateContext.new(self)
|
66
84
|
|
67
85
|
return unless name
|
@@ -183,14 +201,14 @@ module StateMachines
|
|
183
201
|
#
|
184
202
|
# This can be called multiple times. Each time a new context is created,
|
185
203
|
# a new module will be included in the owner class.
|
186
|
-
def context(&
|
204
|
+
def context(&)
|
187
205
|
# Include the context
|
188
206
|
context = @context
|
189
207
|
machine.owner_class.class_eval { include context }
|
190
208
|
|
191
209
|
# Evaluate the method definitions and track which ones were added
|
192
210
|
old_methods = context_methods
|
193
|
-
context.class_eval(&
|
211
|
+
context.class_eval(&)
|
194
212
|
new_methods = context_methods.to_a.reject { |(name, method)| old_methods[name] == method }
|
195
213
|
|
196
214
|
# Alias new methods so that the only execute when the object is in this state
|
@@ -221,13 +239,13 @@ module StateMachines
|
|
221
239
|
#
|
222
240
|
# If the method has never been defined for this state, then a NoMethodError
|
223
241
|
# will be raised.
|
224
|
-
def call(object, method, *args, &
|
242
|
+
def call(object, method, *args, &)
|
225
243
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
226
244
|
options = { method_name: method }.merge(options)
|
227
245
|
state = machine.states.match!(object)
|
228
246
|
|
229
247
|
if state == self && object.respond_to?(method)
|
230
|
-
object.send(method, *args, &
|
248
|
+
object.send(method, *args, &)
|
231
249
|
elsif (method_missing = options[:method_missing])
|
232
250
|
# Dispatch to the superclass since the object either isn't in this state
|
233
251
|
# or this state doesn't handle the method
|
@@ -3,8 +3,8 @@
|
|
3
3
|
module StateMachines
|
4
4
|
# Represents a collection of states in a state machine
|
5
5
|
class StateCollection < NodeCollection
|
6
|
-
def initialize(machine)
|
7
|
-
super(machine, index: [
|
6
|
+
def initialize(machine) # :nodoc:
|
7
|
+
super(machine, index: %i[name qualified_name value])
|
8
8
|
end
|
9
9
|
|
10
10
|
# Determines whether the given object is in a specific state. If the
|
@@ -103,7 +103,7 @@ module StateMachines
|
|
103
103
|
order
|
104
104
|
end
|
105
105
|
|
106
|
-
|
106
|
+
private
|
107
107
|
|
108
108
|
# Gets the value for the given attribute on the node
|
109
109
|
def value(node, attribute)
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'options_validator'
|
4
|
+
|
3
5
|
module StateMachines
|
4
6
|
# Represents a module which will get evaluated within the context of a state.
|
5
7
|
#
|
@@ -52,7 +54,6 @@ module StateMachines
|
|
52
54
|
# vehicle.simulate = true
|
53
55
|
# vehicle.moving? # => false
|
54
56
|
class StateContext < Module
|
55
|
-
|
56
57
|
include EvalHelpers
|
57
58
|
|
58
59
|
# The state machine for which this context's state is defined
|
@@ -68,7 +69,7 @@ module StateMachines
|
|
68
69
|
|
69
70
|
state_name = state.name
|
70
71
|
machine_name = machine.name
|
71
|
-
@condition =
|
72
|
+
@condition = ->(object) { object.class.state_machine(machine_name).states.matches?(object, state_name) }
|
72
73
|
end
|
73
74
|
|
74
75
|
# Creates a new transition that determines what to change the current state
|
@@ -88,15 +89,15 @@ module StateMachines
|
|
88
89
|
# See StateMachines::Machine#transition for a description of the possible
|
89
90
|
# configurations for defining transitions.
|
90
91
|
def transition(options)
|
91
|
-
|
92
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :on, :if, :unless)
|
92
93
|
raise ArgumentError, 'Must specify :on event' unless options[:on]
|
93
94
|
raise ArgumentError, 'Must specify either :to or :from state' unless !options[:to] ^ !options[:from]
|
94
95
|
|
95
|
-
machine.transition(options.merge(options[:to] ? {from: state.name} : {to: state.name}))
|
96
|
+
machine.transition(options.merge(options[:to] ? { from: state.name } : { to: state.name }))
|
96
97
|
end
|
97
98
|
|
98
99
|
# Hooks in condition-merging to methods that don't exist in this module
|
99
|
-
def method_missing(*args, &
|
100
|
+
def method_missing(*args, &)
|
100
101
|
# Get the configuration
|
101
102
|
if args.last.is_a?(Hash)
|
102
103
|
options = args.last
|
@@ -121,13 +122,13 @@ module StateMachines
|
|
121
122
|
object = condition_args.first || self
|
122
123
|
|
123
124
|
proxy.evaluate_method(object, proxy_condition) &&
|
124
|
-
|
125
|
-
|
125
|
+
Array(if_condition).all? { |condition| proxy.evaluate_method(object, condition) } &&
|
126
|
+
!Array(unless_condition).any? { |condition| proxy.evaluate_method(object, condition) }
|
126
127
|
end
|
127
128
|
|
128
129
|
# Evaluate the method on the owner class with the condition proxied
|
129
130
|
# through
|
130
|
-
machine.owner_class.send(*args, &
|
131
|
+
machine.owner_class.send(*args, &)
|
131
132
|
end
|
132
133
|
end
|
133
134
|
end
|
@@ -13,9 +13,9 @@ module StateMachines
|
|
13
13
|
end
|
14
14
|
|
15
15
|
module_function def draw_states(machine:, io: $stdout)
|
16
|
-
io.puts
|
16
|
+
io.puts ' States:'
|
17
17
|
if machine.states.to_a.empty?
|
18
|
-
io.puts
|
18
|
+
io.puts ' - None'
|
19
19
|
else
|
20
20
|
machine.states.each do |state|
|
21
21
|
io.puts " - #{state.name}"
|
@@ -23,31 +23,31 @@ module StateMachines
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
module_function def draw_event(event,
|
26
|
+
module_function def draw_event(event, _graph, options: {}, io: $stdout)
|
27
27
|
io = io || options[:io] || $stdout
|
28
28
|
io.puts " Event: #{event.name}"
|
29
29
|
end
|
30
30
|
|
31
|
-
module_function def draw_branch(branch,
|
31
|
+
module_function def draw_branch(branch, _graph, _event, options: {}, io: $stdout)
|
32
32
|
io = io || options[:io] || $stdout
|
33
33
|
io.puts " Branch: #{branch.inspect}"
|
34
34
|
end
|
35
35
|
|
36
|
-
module_function def draw_state(state,
|
36
|
+
module_function def draw_state(state, _graph, options: {}, io: $stdout)
|
37
37
|
io = io || options[:io] || $stdout
|
38
38
|
io.puts " State: #{state.name}"
|
39
39
|
end
|
40
40
|
|
41
41
|
module_function def draw_events(machine:, io: $stdout)
|
42
|
-
io.puts
|
42
|
+
io.puts ' Events:'
|
43
43
|
if machine.events.to_a.empty?
|
44
|
-
io.puts
|
44
|
+
io.puts ' - None'
|
45
45
|
else
|
46
46
|
machine.events.each do |event|
|
47
47
|
io.puts " - #{event.name}"
|
48
48
|
event.branches.each do |branch|
|
49
49
|
branch.state_requirements.each do |requirement|
|
50
|
-
out = +
|
50
|
+
out = +' - '
|
51
51
|
out << "#{draw_requirement(requirement[:from])} => #{draw_requirement(requirement[:to])}"
|
52
52
|
out << " IF #{branch.if_condition}" if branch.if_condition
|
53
53
|
out << " UNLESS #{branch.unless_condition}" if branch.unless_condition
|
@@ -60,14 +60,14 @@ module StateMachines
|
|
60
60
|
|
61
61
|
module_function def draw_requirement(requirement)
|
62
62
|
case requirement
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
63
|
+
when StateMachines::BlacklistMatcher
|
64
|
+
"ALL EXCEPT #{requirement.values.join(', ')}"
|
65
|
+
when StateMachines::AllMatcher
|
66
|
+
'ALL'
|
67
|
+
when StateMachines::LoopbackMatcher
|
68
|
+
'SAME'
|
69
|
+
else
|
70
|
+
requirement.values.join(', ')
|
71
71
|
end
|
72
72
|
end
|
73
73
|
end
|