spree-state_machine 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.travis.yml +12 -0
  4. data/.yardopts +5 -0
  5. data/CHANGELOG.md +502 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE +20 -0
  8. data/README.md +1246 -0
  9. data/Rakefile +20 -0
  10. data/examples/AutoShop_state.png +0 -0
  11. data/examples/Car_state.png +0 -0
  12. data/examples/Gemfile +5 -0
  13. data/examples/Gemfile.lock +14 -0
  14. data/examples/TrafficLight_state.png +0 -0
  15. data/examples/Vehicle_state.png +0 -0
  16. data/examples/auto_shop.rb +13 -0
  17. data/examples/car.rb +21 -0
  18. data/examples/doc/AutoShop.html +2856 -0
  19. data/examples/doc/AutoShop_state.png +0 -0
  20. data/examples/doc/Car.html +919 -0
  21. data/examples/doc/Car_state.png +0 -0
  22. data/examples/doc/TrafficLight.html +2230 -0
  23. data/examples/doc/TrafficLight_state.png +0 -0
  24. data/examples/doc/Vehicle.html +7921 -0
  25. data/examples/doc/Vehicle_state.png +0 -0
  26. data/examples/doc/_index.html +136 -0
  27. data/examples/doc/class_list.html +47 -0
  28. data/examples/doc/css/common.css +1 -0
  29. data/examples/doc/css/full_list.css +55 -0
  30. data/examples/doc/css/style.css +322 -0
  31. data/examples/doc/file_list.html +46 -0
  32. data/examples/doc/frames.html +13 -0
  33. data/examples/doc/index.html +136 -0
  34. data/examples/doc/js/app.js +205 -0
  35. data/examples/doc/js/full_list.js +173 -0
  36. data/examples/doc/js/jquery.js +16 -0
  37. data/examples/doc/method_list.html +734 -0
  38. data/examples/doc/top-level-namespace.html +105 -0
  39. data/examples/merb-rest/controller.rb +51 -0
  40. data/examples/merb-rest/model.rb +28 -0
  41. data/examples/merb-rest/view_edit.html.erb +24 -0
  42. data/examples/merb-rest/view_index.html.erb +23 -0
  43. data/examples/merb-rest/view_new.html.erb +13 -0
  44. data/examples/merb-rest/view_show.html.erb +17 -0
  45. data/examples/rails-rest/controller.rb +43 -0
  46. data/examples/rails-rest/migration.rb +7 -0
  47. data/examples/rails-rest/model.rb +23 -0
  48. data/examples/rails-rest/view__form.html.erb +34 -0
  49. data/examples/rails-rest/view_edit.html.erb +6 -0
  50. data/examples/rails-rest/view_index.html.erb +25 -0
  51. data/examples/rails-rest/view_new.html.erb +5 -0
  52. data/examples/rails-rest/view_show.html.erb +19 -0
  53. data/examples/traffic_light.rb +9 -0
  54. data/examples/vehicle.rb +33 -0
  55. data/lib/state_machine/assertions.rb +36 -0
  56. data/lib/state_machine/branch.rb +225 -0
  57. data/lib/state_machine/callback.rb +236 -0
  58. data/lib/state_machine/core.rb +7 -0
  59. data/lib/state_machine/core_ext/class/state_machine.rb +5 -0
  60. data/lib/state_machine/core_ext.rb +2 -0
  61. data/lib/state_machine/error.rb +13 -0
  62. data/lib/state_machine/eval_helpers.rb +87 -0
  63. data/lib/state_machine/event.rb +257 -0
  64. data/lib/state_machine/event_collection.rb +141 -0
  65. data/lib/state_machine/extensions.rb +149 -0
  66. data/lib/state_machine/graph.rb +92 -0
  67. data/lib/state_machine/helper_module.rb +17 -0
  68. data/lib/state_machine/initializers/rails.rb +25 -0
  69. data/lib/state_machine/initializers.rb +4 -0
  70. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  71. data/lib/state_machine/integrations/active_model/observer.rb +33 -0
  72. data/lib/state_machine/integrations/active_model/observer_update.rb +42 -0
  73. data/lib/state_machine/integrations/active_model/versions.rb +31 -0
  74. data/lib/state_machine/integrations/active_model.rb +585 -0
  75. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  76. data/lib/state_machine/integrations/active_record/versions.rb +123 -0
  77. data/lib/state_machine/integrations/active_record.rb +525 -0
  78. data/lib/state_machine/integrations/base.rb +100 -0
  79. data/lib/state_machine/integrations.rb +121 -0
  80. data/lib/state_machine/machine.rb +2287 -0
  81. data/lib/state_machine/machine_collection.rb +74 -0
  82. data/lib/state_machine/macro_methods.rb +522 -0
  83. data/lib/state_machine/matcher.rb +123 -0
  84. data/lib/state_machine/matcher_helpers.rb +54 -0
  85. data/lib/state_machine/node_collection.rb +222 -0
  86. data/lib/state_machine/path.rb +120 -0
  87. data/lib/state_machine/path_collection.rb +90 -0
  88. data/lib/state_machine/state.rb +297 -0
  89. data/lib/state_machine/state_collection.rb +112 -0
  90. data/lib/state_machine/state_context.rb +138 -0
  91. data/lib/state_machine/transition.rb +470 -0
  92. data/lib/state_machine/transition_collection.rb +245 -0
  93. data/lib/state_machine/version.rb +3 -0
  94. data/lib/state_machine/yard/handlers/base.rb +32 -0
  95. data/lib/state_machine/yard/handlers/event.rb +25 -0
  96. data/lib/state_machine/yard/handlers/machine.rb +344 -0
  97. data/lib/state_machine/yard/handlers/state.rb +25 -0
  98. data/lib/state_machine/yard/handlers/transition.rb +47 -0
  99. data/lib/state_machine/yard/handlers.rb +12 -0
  100. data/lib/state_machine/yard/templates/default/class/html/setup.rb +30 -0
  101. data/lib/state_machine/yard/templates/default/class/html/state_machines.erb +12 -0
  102. data/lib/state_machine/yard/templates.rb +3 -0
  103. data/lib/state_machine/yard.rb +8 -0
  104. data/lib/state_machine.rb +8 -0
  105. data/lib/yard-state_machine.rb +2 -0
  106. data/state_machine.gemspec +22 -0
  107. data/test/files/en.yml +17 -0
  108. data/test/files/switch.rb +15 -0
  109. data/test/functional/state_machine_test.rb +1066 -0
  110. data/test/test_helper.rb +7 -0
  111. data/test/unit/assertions_test.rb +40 -0
  112. data/test/unit/branch_test.rb +969 -0
  113. data/test/unit/callback_test.rb +704 -0
  114. data/test/unit/error_test.rb +43 -0
  115. data/test/unit/eval_helpers_test.rb +270 -0
  116. data/test/unit/event_collection_test.rb +398 -0
  117. data/test/unit/event_test.rb +1196 -0
  118. data/test/unit/graph_test.rb +98 -0
  119. data/test/unit/helper_module_test.rb +17 -0
  120. data/test/unit/integrations/active_model_test.rb +1245 -0
  121. data/test/unit/integrations/active_record_test.rb +2551 -0
  122. data/test/unit/integrations/base_test.rb +104 -0
  123. data/test/unit/integrations_test.rb +71 -0
  124. data/test/unit/invalid_event_test.rb +20 -0
  125. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  126. data/test/unit/invalid_transition_test.rb +115 -0
  127. data/test/unit/machine_collection_test.rb +603 -0
  128. data/test/unit/machine_test.rb +3395 -0
  129. data/test/unit/matcher_helpers_test.rb +37 -0
  130. data/test/unit/matcher_test.rb +155 -0
  131. data/test/unit/node_collection_test.rb +362 -0
  132. data/test/unit/path_collection_test.rb +266 -0
  133. data/test/unit/path_test.rb +485 -0
  134. data/test/unit/state_collection_test.rb +352 -0
  135. data/test/unit/state_context_test.rb +441 -0
  136. data/test/unit/state_machine_test.rb +31 -0
  137. data/test/unit/state_test.rb +1101 -0
  138. data/test/unit/transition_collection_test.rb +2168 -0
  139. data/test/unit/transition_test.rb +1558 -0
  140. metadata +264 -0
@@ -0,0 +1,123 @@
1
+ require 'singleton'
2
+
3
+ module StateMachine
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 = StateMachine::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 = StateMachine::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 = StateMachine::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 = StateMachine::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 StateMachine
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,222 @@
1
+ require 'state_machine/assertions'
2
+
3
+ module StateMachine
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
+ include Assertions
10
+
11
+ # The machine associated with the nodes
12
+ attr_reader :machine
13
+
14
+ # Creates a new collection of nodes for the given state machine. By default,
15
+ # the collection is empty.
16
+ #
17
+ # Configuration options:
18
+ # * <tt>:index</tt> - One or more attributes to automatically generate
19
+ # hashed indices for in order to perform quick lookups. Default is to
20
+ # index by the :name attribute
21
+ def initialize(machine, options = {})
22
+ assert_valid_keys(options, :index)
23
+ options = {:index => :name}.merge(options)
24
+
25
+ @machine = machine
26
+ @nodes = []
27
+ @index_names = Array(options[:index])
28
+ @indices = @index_names.inject({}) do |indices, name|
29
+ indices[name] = {}
30
+ indices[:"#{name}_to_s"] = {}
31
+ indices[:"#{name}_to_sym"] = {}
32
+ indices
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.inject({}) {|indices, (name, *)| indices[name] = {}; indices}
48
+
49
+ # Add nodes *prior* to copying over the contexts so that they don't get
50
+ # evaluated multiple times
51
+ concat(nodes.map {|n| n.dup})
52
+ @contexts = contexts.dup
53
+ end
54
+
55
+ # Changes the current machine associated with the collection. In turn, this
56
+ # will change the state machine associated with each node in the collection.
57
+ def machine=(new_machine)
58
+ @machine = new_machine
59
+ each {|node| node.machine = new_machine}
60
+ end
61
+
62
+ # Gets the number of nodes in this collection
63
+ def length
64
+ @nodes.length
65
+ end
66
+
67
+ # Gets the set of unique keys for the given index
68
+ def keys(index_name = @default_index)
69
+ index(index_name).keys
70
+ end
71
+
72
+ # Tracks a context that should be evaluated for any nodes that get added
73
+ # which match the given set of nodes. Matchers can be used so that the
74
+ # context can get added once and evaluated after multiple adds.
75
+ def context(nodes, &block)
76
+ nodes = nodes.first.is_a?(Matcher) ? nodes.first : WhitelistMatcher.new(nodes)
77
+ @contexts << context = {:nodes => nodes, :block => block}
78
+
79
+ # Evaluate the new context for existing nodes
80
+ each {|node| eval_context(context, node)}
81
+
82
+ context
83
+ end
84
+
85
+ # Adds a new node to the collection. By doing so, this will also add it to
86
+ # the configured indices. This will also evaluate any existings contexts
87
+ # that match the new node.
88
+ def <<(node)
89
+ @nodes << node
90
+ @index_names.each {|name| add_to_index(name, value(node, name), node)}
91
+ @contexts.each {|context| eval_context(context, node)}
92
+ self
93
+ end
94
+
95
+ # Appends a group of nodes to the collection
96
+ def concat(nodes)
97
+ nodes.each {|node| self << node}
98
+ end
99
+
100
+ # Updates the indexed keys for the given node. If the node's attribute
101
+ # has changed since it was added to the collection, the old indexed keys
102
+ # will be replaced with the updated ones.
103
+ def update(node)
104
+ @index_names.each {|name| update_index(name, node)}
105
+ end
106
+
107
+ # Calls the block once for each element in self, passing that element as a
108
+ # parameter.
109
+ #
110
+ # states = StateMachine::NodeCollection.new
111
+ # states << StateMachine::State.new(machine, :parked)
112
+ # states << StateMachine::State.new(machine, :idling)
113
+ # states.each {|state| puts state.name, ' -- '}
114
+ #
115
+ # ...produces:
116
+ #
117
+ # parked -- idling --
118
+ def each
119
+ @nodes.each {|node| yield node}
120
+ self
121
+ end
122
+
123
+ # Gets the node at the given index.
124
+ #
125
+ # states = StateMachine::NodeCollection.new
126
+ # states << StateMachine::State.new(machine, :parked)
127
+ # states << StateMachine::State.new(machine, :idling)
128
+ #
129
+ # states.at(0).name # => :parked
130
+ # states.at(1).name # => :idling
131
+ def at(index)
132
+ @nodes[index]
133
+ end
134
+
135
+ # Gets the node indexed by the given key. By default, this will look up the
136
+ # key in the first index configured for the collection. A custom index can
137
+ # be specified like so:
138
+ #
139
+ # collection['parked', :value]
140
+ #
141
+ # The above will look up the "parked" key in a hash indexed by each node's
142
+ # +value+ attribute.
143
+ #
144
+ # If the key cannot be found, then nil will be returned.
145
+ def [](key, index_name = @default_index)
146
+ self.index(index_name)[key] ||
147
+ self.index(:"#{index_name}_to_s")[key.to_s] ||
148
+ to_sym?(key) && self.index(:"#{index_name}_to_sym")[:"#{key}"] ||
149
+ nil
150
+ end
151
+
152
+ # Gets the node indexed by the given key. By default, this will look up the
153
+ # key in the first index configured for the collection. A custom index can
154
+ # be specified like so:
155
+ #
156
+ # collection['parked', :value]
157
+ #
158
+ # The above will look up the "parked" key in a hash indexed by each node's
159
+ # +value+ attribute.
160
+ #
161
+ # If the key cannot be found, then an IndexError exception will be raised:
162
+ #
163
+ # collection['invalid', :value] # => IndexError: "invalid" is an invalid value
164
+ def fetch(key, index_name = @default_index)
165
+ self[key, index_name] || raise(IndexError, "#{key.inspect} is an invalid #{index_name}")
166
+ end
167
+
168
+ protected
169
+ # Gets the given index. If the index does not exist, then an ArgumentError
170
+ # is raised.
171
+ def index(name)
172
+ raise ArgumentError, 'No indices configured' unless @indices.any?
173
+ @indices[name] || raise(ArgumentError, "Invalid index: #{name.inspect}")
174
+ end
175
+
176
+ # Gets the value for the given attribute on the node
177
+ def value(node, attribute)
178
+ node.send(attribute)
179
+ end
180
+
181
+ # Adds the given key / node combination to an index, including the string
182
+ # and symbol versions of the index
183
+ def add_to_index(name, key, node)
184
+ index(name)[key] = node
185
+ index(:"#{name}_to_s")[key.to_s] = node
186
+ index(:"#{name}_to_sym")[:"#{key}"] = node if to_sym?(key)
187
+ end
188
+
189
+ # Removes the given key from an index, including the string and symbol
190
+ # versions of the index
191
+ def remove_from_index(name, key)
192
+ index(name).delete(key)
193
+ index(:"#{name}_to_s").delete(key.to_s)
194
+ index(:"#{name}_to_sym").delete(:"#{key}") if to_sym?(key)
195
+ end
196
+
197
+ # Updates the node for the given index, including the string and symbol
198
+ # versions of the index
199
+ def update_index(name, node)
200
+ index = self.index(name)
201
+ old_key = RUBY_VERSION < '1.9' ? index.index(node) : index.key(node)
202
+ new_key = value(node, name)
203
+
204
+ # Only replace the key if it's changed
205
+ if old_key != new_key
206
+ remove_from_index(name, old_key)
207
+ add_to_index(name, new_key, node)
208
+ end
209
+ end
210
+
211
+ # Determines whether the given value can be converted to a symbol
212
+ def to_sym?(value)
213
+ "#{value}" != ''
214
+ end
215
+
216
+ # Evaluates the given context for a particular node. This will only
217
+ # evaluate the context if the node matches.
218
+ def eval_context(context, node)
219
+ node.context(&context[:block]) if context[:nodes].matches?(node.name)
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,120 @@
1
+ module StateMachine
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
+ include Assertions
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
+ assert_valid_keys(options, :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_machine/path'
2
+
3
+ module StateMachine
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
+ include Assertions
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
+ 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
+ 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