state_machines 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.idea/.name +1 -0
  4. data/.idea/.rakeTasks +7 -0
  5. data/.idea/cssxfire.xml +9 -0
  6. data/.idea/encodings.xml +5 -0
  7. data/.idea/misc.xml +5 -0
  8. data/.idea/modules.xml +12 -0
  9. data/.idea/scopes/scope_settings.xml +5 -0
  10. data/.idea/state_machine2.iml +34 -0
  11. data/.idea/vcs.xml +9 -0
  12. data/.idea/workspace.xml +1156 -0
  13. data/.rspec +3 -0
  14. data/.travis.yml +8 -0
  15. data/Gemfile +4 -0
  16. data/LICENSE.txt +23 -0
  17. data/README.md +29 -0
  18. data/Rakefile +1 -0
  19. data/lib/state_machines/assertions.rb +40 -0
  20. data/lib/state_machines/branch.rb +187 -0
  21. data/lib/state_machines/callback.rb +220 -0
  22. data/lib/state_machines/core.rb +25 -0
  23. data/lib/state_machines/core_ext/class/state_machine.rb +5 -0
  24. data/lib/state_machines/core_ext.rb +2 -0
  25. data/lib/state_machines/error.rb +13 -0
  26. data/lib/state_machines/eval_helpers.rb +87 -0
  27. data/lib/state_machines/event.rb +246 -0
  28. data/lib/state_machines/event_collection.rb +141 -0
  29. data/lib/state_machines/extensions.rb +148 -0
  30. data/lib/state_machines/helper_module.rb +17 -0
  31. data/lib/state_machines/integrations/base.rb +100 -0
  32. data/lib/state_machines/integrations.rb +113 -0
  33. data/lib/state_machines/machine.rb +2234 -0
  34. data/lib/state_machines/machine_collection.rb +84 -0
  35. data/lib/state_machines/macro_methods.rb +520 -0
  36. data/lib/state_machines/matcher.rb +123 -0
  37. data/lib/state_machines/matcher_helpers.rb +54 -0
  38. data/lib/state_machines/node_collection.rb +221 -0
  39. data/lib/state_machines/path.rb +120 -0
  40. data/lib/state_machines/path_collection.rb +90 -0
  41. data/lib/state_machines/state.rb +276 -0
  42. data/lib/state_machines/state_collection.rb +112 -0
  43. data/lib/state_machines/state_context.rb +138 -0
  44. data/lib/state_machines/transition.rb +470 -0
  45. data/lib/state_machines/transition_collection.rb +245 -0
  46. data/lib/state_machines/version.rb +3 -0
  47. data/lib/state_machines/yard.rb +8 -0
  48. data/lib/state_machines.rb +3 -0
  49. data/spec/errors/default_spec.rb +14 -0
  50. data/spec/errors/with_message_spec.rb +39 -0
  51. data/spec/helpers/helper_spec.rb +14 -0
  52. data/spec/internal/app/models/auto_shop.rb +31 -0
  53. data/spec/internal/app/models/car.rb +19 -0
  54. data/spec/internal/app/models/model_base.rb +6 -0
  55. data/spec/internal/app/models/motorcycle.rb +9 -0
  56. data/spec/internal/app/models/traffic_light.rb +47 -0
  57. data/spec/internal/app/models/vehicle.rb +123 -0
  58. data/spec/machine_spec.rb +3167 -0
  59. data/spec/matcher_helpers_spec.rb +39 -0
  60. data/spec/matcher_spec.rb +157 -0
  61. data/spec/models/auto_shop_spec.rb +41 -0
  62. data/spec/models/car_spec.rb +90 -0
  63. data/spec/models/motorcycle_spec.rb +44 -0
  64. data/spec/models/traffic_light_spec.rb +56 -0
  65. data/spec/models/vehicle_spec.rb +580 -0
  66. data/spec/node_collection_spec.rb +371 -0
  67. data/spec/path_collection_spec.rb +271 -0
  68. data/spec/path_spec.rb +488 -0
  69. data/spec/spec_helper.rb +6 -0
  70. data/spec/state_collection_spec.rb +352 -0
  71. data/spec/state_context_spec.rb +442 -0
  72. data/spec/state_machine_spec.rb +29 -0
  73. data/spec/state_spec.rb +970 -0
  74. data/spec/support/migration_helpers.rb +50 -0
  75. data/spec/support/models.rb +6 -0
  76. data/spec/transition_collection_spec.rb +2199 -0
  77. data/spec/transition_spec.rb +1558 -0
  78. data/state_machines.gemspec +23 -0
  79. metadata +194 -0
@@ -0,0 +1,123 @@
1
+ require 'singleton'
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 -(blacklist)
36
+ BlacklistMatcher.new(blacklist)
37
+ end
38
+
39
+ # Always returns true
40
+ def matches?(value, context = {})
41
+ true
42
+ end
43
+
44
+ # Always returns the given set of values
45
+ def filter(values)
46
+ values
47
+ end
48
+
49
+ # A human-readable description of this matcher. Always "all".
50
+ def description
51
+ 'all'
52
+ end
53
+ end
54
+
55
+ # Matches a specific set of values
56
+ class WhitelistMatcher < Matcher
57
+ # Checks whether the given value exists within the whitelist configured
58
+ # for this matcher.
59
+ #
60
+ # == Examples
61
+ #
62
+ # matcher = StateMachines::WhitelistMatcher.new([:parked, :idling])
63
+ # matcher.matches?(:parked) # => true
64
+ # matcher.matches?(:first_gear) # => false
65
+ def matches?(value, context = {})
66
+ values.include?(value)
67
+ end
68
+
69
+ # A human-readable description of this matcher
70
+ def description
71
+ values.length == 1 ? values.first.inspect : values.inspect
72
+ end
73
+ end
74
+
75
+ # Matches everything but a specific set of values
76
+ class BlacklistMatcher < Matcher
77
+ # Checks whether the given value exists outside the blacklist configured
78
+ # for this matcher.
79
+ #
80
+ # == Examples
81
+ #
82
+ # matcher = StateMachines::BlacklistMatcher.new([:parked, :idling])
83
+ # matcher.matches?(:parked) # => false
84
+ # matcher.matches?(:first_gear) # => true
85
+ def matches?(value, context = {})
86
+ !values.include?(value)
87
+ end
88
+
89
+ # Finds all values that are *not* within the blacklist configured for this
90
+ # matcher
91
+ def filter(values)
92
+ values - self.values
93
+ end
94
+
95
+ # A human-readable description of this matcher
96
+ def description
97
+ "all - #{values.length == 1 ? values.first.inspect : values.inspect}"
98
+ end
99
+ end
100
+
101
+ # Matches a loopback of two values within a context. Since there is no
102
+ # configuration for this type of matcher, it must be used as a singleton.
103
+ class LoopbackMatcher < Matcher
104
+ include Singleton
105
+
106
+ # Checks whether the given value matches what the value originally was.
107
+ # This value should be defined in the context.
108
+ #
109
+ # == Examples
110
+ #
111
+ # matcher = StateMachines::LoopbackMatcher.instance
112
+ # matcher.matches?(:parked, :from => :parked) # => true
113
+ # matcher.matches?(:parked, :from => :idling) # => false
114
+ def matches?(value, context)
115
+ context[:from] == value
116
+ end
117
+
118
+ # A human-readable description of this matcher. Always "same".
119
+ def description
120
+ 'same'
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,54 @@
1
+ module StateMachines
2
+ # Provides a set of helper methods for generating matchers
3
+ module MatcherHelpers
4
+ # Represents a state that matches all known states in a machine.
5
+ #
6
+ # == Examples
7
+ #
8
+ # class Vehicle
9
+ # state_machine do
10
+ # before_transition any => :parked, :do => lambda {...}
11
+ # before_transition all - :parked => all - :idling, :do => lambda {}
12
+ #
13
+ # event :park
14
+ # transition all => :parked
15
+ # end
16
+ #
17
+ # event :crash
18
+ # transition all - :parked => :stalled
19
+ # end
20
+ # end
21
+ # end
22
+ #
23
+ # In the above example, +all+ will match the following states since they
24
+ # are known:
25
+ # * +parked+
26
+ # * +stalled+
27
+ # * +idling+
28
+ def all
29
+ AllMatcher.instance
30
+ end
31
+ alias_method :any, :all
32
+
33
+ # Represents a state that matches the original +from+ state. This is useful
34
+ # for defining transitions which are loopbacks.
35
+ #
36
+ # == Examples
37
+ #
38
+ # class Vehicle
39
+ # state_machine do
40
+ # event :ignite
41
+ # transition [:idling, :first_gear] => same
42
+ # end
43
+ # end
44
+ # end
45
+ #
46
+ # In the above example, +same+ will match whichever the from state is. In
47
+ # the case of the +ignite+ event, it is essential the same as the following:
48
+ #
49
+ # transition :parked => :parked, :first_gear => :first_gear
50
+ def same
51
+ LoopbackMatcher.instance
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,221 @@
1
+ require 'state_machines/assertions'
2
+
3
+ module StateMachines
4
+ # Represents a collection of nodes in a state machine, be it events or states.
5
+ # Nodes will not differentiate between the String and Symbol versions of the
6
+ # values being indexed.
7
+ class NodeCollection
8
+ include Enumerable
9
+
10
+ # The machine associated with the nodes
11
+ attr_reader :machine
12
+
13
+ # Creates a new collection of nodes for the given state machine. By default,
14
+ # the collection is empty.
15
+ #
16
+ # Configuration options:
17
+ # * <tt>:index</tt> - One or more attributes to automatically generate
18
+ # hashed indices for in order to perform quick lookups. Default is to
19
+ # index by the :name attribute
20
+ def initialize(machine, options = {})
21
+ options.assert_valid_keys(:index)
22
+ options = { index: :name }.merge(options)
23
+
24
+ @machine = machine
25
+ @nodes = []
26
+ @index_names = Array(options[:index])
27
+ @indices = @index_names.reduce({}) do |indices, name|
28
+ indices[name] = {}
29
+ indices[:"#{name}_to_s"] = {}
30
+ indices[:"#{name}_to_sym"] = {}
31
+ indices
32
+ end
33
+ @default_index = Array(options[:index]).first
34
+ @contexts = []
35
+ end
36
+
37
+ # Creates a copy of this collection such that modifications don't affect
38
+ # the original collection
39
+ def initialize_copy(orig) #:nodoc:
40
+ super
41
+
42
+ nodes = @nodes
43
+ contexts = @contexts
44
+ @nodes = []
45
+ @contexts = []
46
+ @indices = @indices.reduce({}) { |indices, (name, *)| indices[name] = {}; indices }
47
+
48
+ # Add nodes *prior* to copying over the contexts so that they don't get
49
+ # evaluated multiple times
50
+ concat(nodes.map { |n| n.dup })
51
+ @contexts = contexts.dup
52
+ end
53
+
54
+ # Changes the current machine associated with the collection. In turn, this
55
+ # will change the state machine associated with each node in the collection.
56
+ def machine=(new_machine)
57
+ @machine = new_machine
58
+ each { |node| node.machine = new_machine }
59
+ end
60
+
61
+ # Gets the number of nodes in this collection
62
+ def length
63
+ @nodes.length
64
+ end
65
+
66
+ # Gets the set of unique keys for the given index
67
+ def keys(index_name = @default_index)
68
+ index(index_name).keys
69
+ end
70
+
71
+ # Tracks a context that should be evaluated for any nodes that get added
72
+ # which match the given set of nodes. Matchers can be used so that the
73
+ # context can get added once and evaluated after multiple adds.
74
+ def context(nodes, &block)
75
+ nodes = nodes.first.is_a?(Matcher) ? nodes.first : WhitelistMatcher.new(nodes)
76
+ @contexts << context = { nodes: nodes, block: block }
77
+
78
+ # Evaluate the new context for existing nodes
79
+ each { |node| eval_context(context, node) }
80
+
81
+ context
82
+ end
83
+
84
+ # Adds a new node to the collection. By doing so, this will also add it to
85
+ # the configured indices. This will also evaluate any existings contexts
86
+ # that match the new node.
87
+ def <<(node)
88
+ @nodes << node
89
+ @index_names.each { |name| add_to_index(name, value(node, name), node) }
90
+ @contexts.each { |context| eval_context(context, node) }
91
+ self
92
+ end
93
+
94
+ # Appends a group of nodes to the collection
95
+ def concat(nodes)
96
+ nodes.each { |node| self << node }
97
+ end
98
+
99
+ # Updates the indexed keys for the given node. If the node's attribute
100
+ # has changed since it was added to the collection, the old indexed keys
101
+ # will be replaced with the updated ones.
102
+ def update(node)
103
+ @index_names.each { |name| update_index(name, node) }
104
+ end
105
+
106
+ # Calls the block once for each element in self, passing that element as a
107
+ # parameter.
108
+ #
109
+ # states = StateMachines::NodeCollection.new
110
+ # states << StateMachines::State.new(machine, :parked)
111
+ # states << StateMachines::State.new(machine, :idling)
112
+ # states.each {|state| puts state.name, ' -- '}
113
+ #
114
+ # ...produces:
115
+ #
116
+ # parked -- idling --
117
+ def each
118
+ @nodes.each { |node| yield node }
119
+ self
120
+ end
121
+
122
+ # Gets the node at the given index.
123
+ #
124
+ # states = StateMachines::NodeCollection.new
125
+ # states << StateMachines::State.new(machine, :parked)
126
+ # states << StateMachines::State.new(machine, :idling)
127
+ #
128
+ # states.at(0).name # => :parked
129
+ # states.at(1).name # => :idling
130
+ def at(index)
131
+ @nodes[index]
132
+ end
133
+
134
+ # Gets the node indexed by the given key. By default, this will look up the
135
+ # key in the first index configured for the collection. A custom index can
136
+ # be specified like so:
137
+ #
138
+ # collection['parked', :value]
139
+ #
140
+ # The above will look up the "parked" key in a hash indexed by each node's
141
+ # +value+ attribute.
142
+ #
143
+ # If the key cannot be found, then nil will be returned.
144
+ def [](key, index_name = @default_index)
145
+ index(index_name)[key] ||
146
+ index(:"#{index_name}_to_s")[key.to_s] ||
147
+ to_sym?(key) && index(:"#{index_name}_to_sym")[:"#{key}"] ||
148
+ nil
149
+ end
150
+
151
+ # Gets the node indexed by the given key. By default, this will look up the
152
+ # key in the first index configured for the collection. A custom index can
153
+ # be specified like so:
154
+ #
155
+ # collection['parked', :value]
156
+ #
157
+ # The above will look up the "parked" key in a hash indexed by each node's
158
+ # +value+ attribute.
159
+ #
160
+ # If the key cannot be found, then an IndexError exception will be raised:
161
+ #
162
+ # collection['invalid', :value] # => IndexError: "invalid" is an invalid value
163
+ def fetch(key, index_name = @default_index)
164
+ self[key, index_name] || fail(IndexError, "#{key.inspect} is an invalid #{index_name}")
165
+ end
166
+
167
+ protected
168
+ # Gets the given index. If the index does not exist, then an ArgumentError
169
+ # is raised.
170
+ def index(name)
171
+ fail ArgumentError, 'No indices configured' unless @indices.any?
172
+ @indices[name] || fail(ArgumentError, "Invalid index: #{name.inspect}")
173
+ end
174
+
175
+ # Gets the value for the given attribute on the node
176
+ def value(node, attribute)
177
+ node.send(attribute)
178
+ end
179
+
180
+ # Adds the given key / node combination to an index, including the string
181
+ # and symbol versions of the index
182
+ def add_to_index(name, key, node)
183
+ index(name)[key] = node
184
+ index(:"#{name}_to_s")[key.to_s] = node
185
+ index(:"#{name}_to_sym")[:"#{key}"] = node if to_sym?(key)
186
+ end
187
+
188
+ # Removes the given key from an index, including the string and symbol
189
+ # versions of the index
190
+ def remove_from_index(name, key)
191
+ index(name).delete(key)
192
+ index(:"#{name}_to_s").delete(key.to_s)
193
+ index(:"#{name}_to_sym").delete(:"#{key}") if to_sym?(key)
194
+ end
195
+
196
+ # Updates the node for the given index, including the string and symbol
197
+ # versions of the index
198
+ def update_index(name, node)
199
+ index = self.index(name)
200
+ old_key = index.key(node)
201
+ new_key = value(node, name)
202
+
203
+ # Only replace the key if it's changed
204
+ if old_key != new_key
205
+ remove_from_index(name, old_key)
206
+ add_to_index(name, new_key, node)
207
+ end
208
+ end
209
+
210
+ # Determines whether the given value can be converted to a symbol
211
+ def to_sym?(value)
212
+ "#{value}" != ''
213
+ end
214
+
215
+ # Evaluates the given context for a particular node. This will only
216
+ # evaluate the context if the node matches.
217
+ def eval_context(context, node)
218
+ node.context(&context[:block]) if context[:nodes].matches?(node.name)
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,120 @@
1
+ module StateMachines
2
+ # A path represents a sequence of transitions that can be run for a particular
3
+ # object. Paths can walk to new transitions, revealing all of the possible
4
+ # branches that can be encountered in the object's state machine.
5
+ class Path < Array
6
+
7
+
8
+ # The object whose state machine is being walked
9
+ attr_reader :object
10
+
11
+ # The state machine this path is walking
12
+ attr_reader :machine
13
+
14
+ # Creates a new transition path for the given object. Initially this is an
15
+ # empty path. In order to start walking the path, it must be populated with
16
+ # an initial transition.
17
+ #
18
+ # Configuration options:
19
+ # * <tt>:target</tt> - The target state to end the path on
20
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
21
+ # conditionals defined for each one
22
+ def initialize(object, machine, options = {})
23
+ options.assert_valid_keys(:target, :guard)
24
+
25
+ @object = object
26
+ @machine = machine
27
+ @target = options[:target]
28
+ @guard = options[:guard]
29
+ end
30
+
31
+ def initialize_copy(orig) #:nodoc:
32
+ super
33
+ @transitions = nil
34
+ end
35
+
36
+ # The initial state name for this path
37
+ def from_name
38
+ first && first.from_name
39
+ end
40
+
41
+ # Lists all of the from states that can be reached through this path.
42
+ #
43
+ # For example,
44
+ #
45
+ # path.to_states # => [:parked, :idling, :first_gear, ...]
46
+ def from_states
47
+ map {|transition| transition.from_name}.uniq
48
+ end
49
+
50
+ # The end state name for this path. If a target state was specified for
51
+ # the path, then that will be returned if the path is complete.
52
+ def to_name
53
+ last && last.to_name
54
+ end
55
+
56
+ # Lists all of the to states that can be reached through this path.
57
+ #
58
+ # For example,
59
+ #
60
+ # path.to_states # => [:parked, :idling, :first_gear, ...]
61
+ def to_states
62
+ map {|transition| transition.to_name}.uniq
63
+ end
64
+
65
+ # Lists all of the events that can be fired through this path.
66
+ #
67
+ # For example,
68
+ #
69
+ # path.events # => [:park, :ignite, :shift_up, ...]
70
+ def events
71
+ map {|transition| transition.event}.uniq
72
+ end
73
+
74
+ # Walks down the next transitions at the end of this path. This will only
75
+ # walk down paths that are considered valid.
76
+ def walk
77
+ transitions.each {|transition| yield dup.push(transition)}
78
+ end
79
+
80
+ # Determines whether or not this path has completed. A path is considered
81
+ # complete when one of the following conditions is met:
82
+ # * The last transition in the path ends on the target state
83
+ # * There are no more transitions remaining to walk and there is no target
84
+ # state
85
+ def complete?
86
+ !empty? && (@target ? to_name == @target : transitions.empty?)
87
+ end
88
+
89
+ private
90
+ # Calculates the number of times the given state has been walked to
91
+ def times_walked_to(state)
92
+ select {|transition| transition.to_name == state}.length
93
+ end
94
+
95
+ # Determines whether the given transition has been recently walked down in
96
+ # this path. If a target is configured for this path, then this will only
97
+ # look at transitions walked down since the target was last reached.
98
+ def recently_walked?(transition)
99
+ transitions = self
100
+ if @target && @target != to_name && target_transition = detect {|t| t.to_name == @target}
101
+ transitions = transitions[index(target_transition) + 1..-1]
102
+ end
103
+ transitions.include?(transition)
104
+ end
105
+
106
+ # Determines whether it's possible to walk to the given transition from
107
+ # the current path. A transition can be walked to if:
108
+ # * It has not been recently walked and
109
+ # * If a target is specified, it has not been walked to twice yet
110
+ def can_walk_to?(transition)
111
+ !recently_walked?(transition) && (!@target || times_walked_to(@target) < 2)
112
+ end
113
+
114
+ # Get the next set of transitions that can be walked to starting from the
115
+ # end of this path
116
+ def transitions
117
+ @transitions ||= empty? ? [] : machine.events.transitions_for(object, :from => to_name, :guard => @guard).select {|transition| can_walk_to?(transition)}
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,90 @@
1
+ require 'state_machines/path'
2
+
3
+ module StateMachines
4
+ # Represents a collection of paths that are generated based on a set of
5
+ # requirements regarding what states to start and end on
6
+ class PathCollection < Array
7
+
8
+
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
+ options.assert_valid_keys( :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
+ map {|path| path.from_states}.flatten.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
+ map {|path| path.to_states}.flatten.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
+ map {|path| path.events}.flatten.uniq
71
+ end
72
+
73
+ private
74
+ # Gets the initial set of paths to walk
75
+ def initial_paths
76
+ machine.events.transitions_for(object, :from => from_name, :guard => @guard).map do |transition|
77
+ path = Path.new(object, machine, :target => to_name, :guard => @guard)
78
+ path << transition
79
+ path
80
+ end
81
+ end
82
+
83
+ # Walks down the given path. Each new path that matches the configured
84
+ # requirements will be added to this collection.
85
+ def walk(path)
86
+ self << path if path.complete?
87
+ path.walk {|next_path| walk(next_path)} unless to_name && path.complete? && !@deep
88
+ end
89
+ end
90
+ end