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,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ # Provides a general strategy pattern for determining whether a match is found
5
+ # for a value. The algorithm that actually determines the match depends on
6
+ # the matcher in use.
7
+ class Matcher
8
+ # The list of values against which queries are matched
9
+ attr_reader :values
10
+
11
+ # Creates a new matcher for querying against the given set of values
12
+ def initialize(values = [])
13
+ @values = values.is_a?(Array) ? values : [values]
14
+ end
15
+
16
+ # Generates a subset of values that exists in both the set of values being
17
+ # filtered and the values configured for the matcher
18
+ def filter(values)
19
+ self.values & values
20
+ end
21
+ end
22
+
23
+ # Matches any given value. Since there is no configuration for this type of
24
+ # matcher, it must be used as a singleton.
25
+ class AllMatcher < Matcher
26
+ include Singleton
27
+
28
+ # Generates a blacklist matcher based on the given set of values
29
+ #
30
+ # == Examples
31
+ #
32
+ # matcher = StateMachines::AllMatcher.instance - [:parked, :idling]
33
+ # matcher.matches?(:parked) # => false
34
+ # matcher.matches?(:first_gear) # => true
35
+ def -(other)
36
+ BlacklistMatcher.new(other)
37
+ end
38
+ alias except -
39
+
40
+ # Always returns true
41
+ def matches?(_value, _context = {})
42
+ true
43
+ end
44
+
45
+ # Always returns the given set of values
46
+ def filter(values)
47
+ values
48
+ end
49
+
50
+ # A human-readable description of this matcher. Always "all".
51
+ def description
52
+ 'all'
53
+ end
54
+ end
55
+
56
+ # Matches a specific set of values
57
+ class WhitelistMatcher < Matcher
58
+ # Checks whether the given value exists within the whitelist configured
59
+ # for this matcher.
60
+ #
61
+ # == Examples
62
+ #
63
+ # matcher = StateMachines::WhitelistMatcher.new([:parked, :idling])
64
+ # matcher.matches?(:parked) # => true
65
+ # matcher.matches?(:first_gear) # => false
66
+ def matches?(value, _context = {})
67
+ values.include?(value)
68
+ end
69
+
70
+ # A human-readable description of this matcher
71
+ def description
72
+ values.length == 1 ? values.first.inspect : values.inspect
73
+ end
74
+ end
75
+
76
+ # Matches everything but a specific set of values
77
+ class BlacklistMatcher < Matcher
78
+ # Checks whether the given value exists outside the blacklist configured
79
+ # for this matcher.
80
+ #
81
+ # == Examples
82
+ #
83
+ # matcher = StateMachines::BlacklistMatcher.new([:parked, :idling])
84
+ # matcher.matches?(:parked) # => false
85
+ # matcher.matches?(:first_gear) # => true
86
+ def matches?(value, _context = {})
87
+ !values.include?(value)
88
+ end
89
+
90
+ # Finds all values that are *not* within the blacklist configured for this
91
+ # matcher
92
+ def filter(values)
93
+ values - self.values
94
+ end
95
+
96
+ # A human-readable description of this matcher
97
+ def description
98
+ "all - #{values.length == 1 ? values.first.inspect : values.inspect}"
99
+ end
100
+ end
101
+
102
+ # Matches a loopback of two values within a context. Since there is no
103
+ # configuration for this type of matcher, it must be used as a singleton.
104
+ class LoopbackMatcher < Matcher
105
+ include Singleton
106
+
107
+ # Checks whether the given value matches what the value originally was.
108
+ # This value should be defined in the context.
109
+ #
110
+ # == Examples
111
+ #
112
+ # matcher = StateMachines::LoopbackMatcher.instance
113
+ # matcher.matches?(:parked, :from => :parked) # => true
114
+ # matcher.matches?(:parked, :from => :idling) # => false
115
+ def matches?(value, context)
116
+ context[:from] == value
117
+ end
118
+
119
+ # A human-readable description of this matcher. Always "same".
120
+ def description
121
+ 'same'
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ # Provides a set of helper methods for generating matchers
5
+ module MatcherHelpers
6
+ # Represents a state that matches all known states in a machine.
7
+ #
8
+ # == Examples
9
+ #
10
+ # class Vehicle
11
+ # state_machine do
12
+ # before_transition any => :parked, :do => lambda {...}
13
+ # before_transition all - :parked => all - :idling, :do => lambda {}
14
+ #
15
+ # event :park
16
+ # transition all => :parked
17
+ # end
18
+ #
19
+ # event :crash
20
+ # transition all - :parked => :stalled
21
+ # end
22
+ # end
23
+ # end
24
+ #
25
+ # In the above example, +all+ will match the following states since they
26
+ # are known:
27
+ # * +parked+
28
+ # * +stalled+
29
+ # * +idling+
30
+ def all
31
+ AllMatcher.instance
32
+ end
33
+ alias any all
34
+
35
+ # Represents a state that matches the original +from+ state. This is useful
36
+ # for defining transitions which are loopbacks.
37
+ #
38
+ # == Examples
39
+ #
40
+ # class Vehicle
41
+ # state_machine do
42
+ # event :ignite
43
+ # transition [:idling, :first_gear] => same
44
+ # end
45
+ # end
46
+ # end
47
+ #
48
+ # In the above example, +same+ will match whichever the from state is. In
49
+ # the case of the +ignite+ event, it is essential the same as the following:
50
+ #
51
+ # transition :idling => :idling, :first_gear => :first_gear
52
+ def same
53
+ LoopbackMatcher.instance
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+
5
+ module StateMachines
6
+ # Represents a collection of nodes in a state machine, be it events or states.
7
+ # Nodes will not differentiate between the String and Symbol versions of the
8
+ # values being indexed.
9
+ class NodeCollection
10
+ include Enumerable
11
+
12
+ # The machine associated with the nodes
13
+ attr_reader :machine
14
+
15
+ # Creates a new collection of nodes for the given state machine. By default,
16
+ # the collection is empty.
17
+ #
18
+ # Configuration options:
19
+ # * <tt>:index</tt> - One or more attributes to automatically generate
20
+ # hashed indices for in order to perform quick lookups. Default is to
21
+ # index by the :name attribute
22
+ def initialize(machine, options = {})
23
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :index)
24
+ options = { index: :name }.merge(options)
25
+
26
+ @machine = machine
27
+ @nodes = []
28
+ @index_names = Array(options[:index])
29
+ @indices = @index_names.each_with_object({}) do |name, indices|
30
+ indices[name] = {}
31
+ indices[:"#{name}_to_s"] = {}
32
+ indices[:"#{name}_to_sym"] = {}
33
+ end
34
+ @default_index = Array(options[:index]).first
35
+ @contexts = []
36
+ end
37
+
38
+ # Creates a copy of this collection such that modifications don't affect
39
+ # the original collection
40
+ def initialize_copy(orig) # :nodoc:
41
+ super
42
+
43
+ nodes = @nodes
44
+ contexts = @contexts
45
+ @nodes = []
46
+ @contexts = []
47
+ @indices = @indices.each_with_object({}) do |(name, *), indices|
48
+ indices[name] = {}
49
+ end
50
+
51
+ # Add nodes *prior* to copying over the contexts so that they don't get
52
+ # evaluated multiple times
53
+ concat(nodes.map { |n| n.dup })
54
+ @contexts = contexts.dup
55
+ end
56
+
57
+ # Changes the current machine associated with the collection. In turn, this
58
+ # will change the state machine associated with each node in the collection.
59
+ def machine=(new_machine)
60
+ @machine = new_machine
61
+ each { |node| node.machine = new_machine }
62
+ end
63
+
64
+ # Gets the number of nodes in this collection
65
+ def length
66
+ @nodes.length
67
+ end
68
+
69
+ # Gets the set of unique keys for the given index
70
+ def keys(index_name = @default_index)
71
+ index(index_name).keys
72
+ end
73
+
74
+ # Tracks a context that should be evaluated for any nodes that get added
75
+ # which match the given set of nodes. Matchers can be used so that the
76
+ # context can get added once and evaluated after multiple adds.
77
+ def context(nodes, &block)
78
+ nodes = nodes.first.is_a?(Matcher) ? nodes.first : WhitelistMatcher.new(nodes)
79
+ @contexts << context = { nodes: nodes, block: block }
80
+
81
+ # Evaluate the new context for existing nodes
82
+ each { |node| eval_context(context, node) }
83
+
84
+ context
85
+ end
86
+
87
+ # Adds a new node to the collection. By doing so, this will also add it to
88
+ # the configured indices. This will also evaluate any existings contexts
89
+ # that match the new node.
90
+ def <<(node)
91
+ @nodes << node
92
+ @index_names.each { |name| add_to_index(name, value(node, name), node) }
93
+ @contexts.each { |context| eval_context(context, node) }
94
+ self
95
+ end
96
+
97
+ # Appends a group of nodes to the collection
98
+ def concat(nodes)
99
+ nodes.each { |node| self << node }
100
+ end
101
+
102
+ # Updates the indexed keys for the given node. If the node's attribute
103
+ # has changed since it was added to the collection, the old indexed keys
104
+ # will be replaced with the updated ones.
105
+ def update(node)
106
+ @index_names.each { |name| update_index(name, node) }
107
+ end
108
+
109
+ # Calls the block once for each element in self, passing that element as a
110
+ # parameter.
111
+ #
112
+ # states = StateMachines::NodeCollection.new
113
+ # states << StateMachines::State.new(machine, :parked)
114
+ # states << StateMachines::State.new(machine, :idling)
115
+ # states.each {|state| puts state.name, ' -- '}
116
+ #
117
+ # ...produces:
118
+ #
119
+ # parked -- idling --
120
+ def each(&)
121
+ @nodes.each(&)
122
+ self
123
+ end
124
+
125
+ # Gets the node at the given index.
126
+ #
127
+ # states = StateMachines::NodeCollection.new
128
+ # states << StateMachines::State.new(machine, :parked)
129
+ # states << StateMachines::State.new(machine, :idling)
130
+ #
131
+ # states.at(0).name # => :parked
132
+ # states.at(1).name # => :idling
133
+ def at(index)
134
+ @nodes[index]
135
+ end
136
+
137
+ # Gets the node indexed by the given key. By default, this will look up the
138
+ # key in the first index configured for the collection. A custom index can
139
+ # be specified like so:
140
+ #
141
+ # collection['parked', :value]
142
+ #
143
+ # The above will look up the "parked" key in a hash indexed by each node's
144
+ # +value+ attribute.
145
+ #
146
+ # If the key cannot be found, then nil will be returned.
147
+ def [](key, index_name = @default_index)
148
+ index(index_name)[key] ||
149
+ index(:"#{index_name}_to_s")[key.to_s] ||
150
+ (to_sym?(key) && index(:"#{index_name}_to_sym")[:"#{key}"]) ||
151
+ nil
152
+ end
153
+
154
+ # Gets the node indexed by the given key. By default, this will look up the
155
+ # key in the first index configured for the collection. A custom index can
156
+ # be specified like so:
157
+ #
158
+ # collection['parked', :value]
159
+ #
160
+ # The above will look up the "parked" key in a hash indexed by each node's
161
+ # +value+ attribute.
162
+ #
163
+ # If the key cannot be found, then an IndexError exception will be raised:
164
+ #
165
+ # collection['invalid', :value] # => IndexError: "invalid" is an invalid value
166
+ def fetch(key, index_name = @default_index)
167
+ self[key, index_name] || raise(IndexError, "#{key.inspect} is an invalid #{index_name}")
168
+ end
169
+
170
+ protected
171
+
172
+ # Gets the given index. If the index does not exist, then an ArgumentError
173
+ # is raised.
174
+ def index(name)
175
+ raise ArgumentError, 'No indices configured' unless @indices.any?
176
+
177
+ @indices[name] || raise(ArgumentError, "Invalid index: #{name.inspect}")
178
+ end
179
+
180
+ # Gets the value for the given attribute on the node
181
+ def value(node, attribute)
182
+ node.send(attribute)
183
+ end
184
+
185
+ # Adds the given key / node combination to an index, including the string
186
+ # and symbol versions of the index
187
+ def add_to_index(name, key, node)
188
+ index(name)[key] = node
189
+ index(:"#{name}_to_s")[key.to_s] = node
190
+ index(:"#{name}_to_sym")[:"#{key}"] = node if to_sym?(key)
191
+ end
192
+
193
+ # Removes the given key from an index, including the string and symbol
194
+ # versions of the index
195
+ def remove_from_index(name, key)
196
+ index(name).delete(key)
197
+ index(:"#{name}_to_s").delete(key.to_s)
198
+ index(:"#{name}_to_sym").delete(:"#{key}") if to_sym?(key)
199
+ end
200
+
201
+ # Updates the node for the given index, including the string and symbol
202
+ # versions of the index
203
+ def update_index(name, node)
204
+ index = self.index(name)
205
+ old_key = index.key(node)
206
+ new_key = value(node, name)
207
+
208
+ # Only replace the key if it's changed
209
+ return unless old_key != new_key
210
+
211
+ remove_from_index(name, old_key)
212
+ add_to_index(name, new_key, node)
213
+ end
214
+
215
+ # Determines whether the given value can be converted to a symbol
216
+ def to_sym?(value)
217
+ "#{value}" != ''
218
+ end
219
+
220
+ # Evaluates the given context for a particular node. This will only
221
+ # evaluate the context if the node matches.
222
+ def eval_context(context, node)
223
+ node.context(&context[:block]) if context[:nodes].matches?(node.name)
224
+ end
225
+ end
226
+ end
@@ -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
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+
5
+ module StateMachines
6
+ # A path represents a sequence of transitions that can be run for a particular
7
+ # object. Paths can walk to new transitions, revealing all of the possible
8
+ # branches that can be encountered in the object's state machine.
9
+ class Path < Array
10
+ # The object whose state machine is being walked
11
+ attr_reader :object
12
+
13
+ # The state machine this path is walking
14
+ attr_reader :machine
15
+
16
+ # Creates a new transition path for the given object. Initially this is an
17
+ # empty path. In order to start walking the path, it must be populated with
18
+ # an initial transition.
19
+ #
20
+ # Configuration options:
21
+ # * <tt>:target</tt> - The target state to end the path on
22
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
23
+ # conditionals defined for each one
24
+ def initialize(object, machine, options = {})
25
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :target, :guard)
26
+
27
+ @object = object
28
+ @machine = machine
29
+ @target = options[:target]
30
+ @guard = options[:guard]
31
+ end
32
+
33
+ def initialize_copy(orig) # :nodoc:
34
+ super
35
+ @transitions = nil
36
+ end
37
+
38
+ # The initial state name for this path
39
+ def from_name
40
+ first&.from_name
41
+ end
42
+
43
+ # Lists all of the from states that can be reached through this path.
44
+ #
45
+ # For example,
46
+ #
47
+ # path.to_states # => [:parked, :idling, :first_gear, ...]
48
+ def from_states
49
+ map { |transition| transition.from_name }.uniq
50
+ end
51
+
52
+ # The end state name for this path. If a target state was specified for
53
+ # the path, then that will be returned if the path is complete.
54
+ def to_name
55
+ last&.to_name
56
+ end
57
+
58
+ # Lists all of the to states that can be reached through this path.
59
+ #
60
+ # For example,
61
+ #
62
+ # path.to_states # => [:parked, :idling, :first_gear, ...]
63
+ def to_states
64
+ map { |transition| transition.to_name }.uniq
65
+ end
66
+
67
+ # Lists all of the events that can be fired through this path.
68
+ #
69
+ # For example,
70
+ #
71
+ # path.events # => [:park, :ignite, :shift_up, ...]
72
+ def events
73
+ map { |transition| transition.event }.uniq
74
+ end
75
+
76
+ # Walks down the next transitions at the end of this path. This will only
77
+ # walk down paths that are considered valid.
78
+ def walk
79
+ transitions.each { |transition| yield dup.push(transition) }
80
+ end
81
+
82
+ # Determines whether or not this path has completed. A path is considered
83
+ # complete when one of the following conditions is met:
84
+ # * The last transition in the path ends on the target state
85
+ # * There are no more transitions remaining to walk and there is no target
86
+ # state
87
+ def complete?
88
+ !empty? && (@target ? to_name == @target : transitions.empty?)
89
+ end
90
+
91
+ private
92
+
93
+ # Calculates the number of times the given state has been walked to
94
+ def times_walked_to(state)
95
+ select { |transition| transition.to_name == state }.length
96
+ end
97
+
98
+ # Determines whether the given transition has been recently walked down in
99
+ # this path. If a target is configured for this path, then this will only
100
+ # look at transitions walked down since the target was last reached.
101
+ def recently_walked?(transition)
102
+ transitions = self
103
+ if @target && @target != to_name && (target_transition = detect { |t| t.to_name == @target })
104
+ transitions = transitions[index(target_transition) + 1..-1]
105
+ end
106
+ transitions.include?(transition)
107
+ end
108
+
109
+ # Determines whether it's possible to walk to the given transition from
110
+ # the current path. A transition can be walked to if:
111
+ # * It has not been recently walked and
112
+ # * If a target is specified, it has not been walked to twice yet
113
+ def can_walk_to?(transition)
114
+ !recently_walked?(transition) && (!@target || times_walked_to(@target) < 2)
115
+ end
116
+
117
+ # Get the next set of transitions that can be walked to starting from the
118
+ # end of this path
119
+ def transitions
120
+ @transitions ||= empty? ? [] : machine.events.transitions_for(object, from: to_name, guard: @guard).select { |transition| can_walk_to?(transition) }
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+
5
+ module StateMachines
6
+ # Represents a collection of paths that are generated based on a set of
7
+ # requirements regarding what states to start and end on
8
+ class PathCollection < Array
9
+ # The object whose state machine is being walked
10
+ attr_reader :object
11
+
12
+ # The state machine these path are walking
13
+ attr_reader :machine
14
+
15
+ # The initial state to start each path from
16
+ attr_reader :from_name
17
+
18
+ # The target state for each path
19
+ attr_reader :to_name
20
+
21
+ # Creates a new collection of paths with the given requirements.
22
+ #
23
+ # Configuration options:
24
+ # * <tt>:from</tt> - The initial state to start from
25
+ # * <tt>:to</tt> - The target end state
26
+ # * <tt>:deep</tt> - Whether to enable deep searches for the target state.
27
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
28
+ # conditionals defined for each one
29
+ def initialize(object, machine, options = {})
30
+ options = { deep: false, from: machine.states.match!(object).name }.merge(options)
31
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :deep, :guard)
32
+
33
+ @object = object
34
+ @machine = machine
35
+ @from_name = machine.states.fetch(options[:from]).name
36
+ @to_name = options[:to] && machine.states.fetch(options[:to]).name
37
+ @guard = options[:guard]
38
+ @deep = options[:deep]
39
+
40
+ initial_paths.each { |path| walk(path) }
41
+ end
42
+
43
+ # Lists all of the states that can be transitioned from through the paths in
44
+ # this collection.
45
+ #
46
+ # For example,
47
+ #
48
+ # paths.from_states # => [:parked, :idling, :first_gear, ...]
49
+ def from_states
50
+ flat_map(&:from_states).uniq
51
+ end
52
+
53
+ # Lists all of the states that can be transitioned to through the paths in
54
+ # this collection.
55
+ #
56
+ # For example,
57
+ #
58
+ # paths.to_states # => [:idling, :first_gear, :second_gear, ...]
59
+ def to_states
60
+ flat_map(&:to_states).uniq
61
+ end
62
+
63
+ # Lists all of the events that can be fired through the paths in this
64
+ # collection.
65
+ #
66
+ # For example,
67
+ #
68
+ # paths.events # => [:park, :ignite, :shift_up, ...]
69
+ def events
70
+ flat_map(&:events).uniq
71
+ end
72
+
73
+ private
74
+
75
+ # Gets the initial set of paths to walk
76
+ def initial_paths
77
+ machine.events.transitions_for(object, from: from_name, guard: @guard).map do |transition|
78
+ path = Path.new(object, machine, target: to_name, guard: @guard)
79
+ path << transition
80
+ path
81
+ end
82
+ end
83
+
84
+ # Walks down the given path. Each new path that matches the configured
85
+ # requirements will be added to this collection.
86
+ def walk(path)
87
+ self << path if path.complete?
88
+ path.walk { |next_path| walk(next_path) } unless to_name && path.complete? && !@deep
89
+ end
90
+ end
91
+ end