hsume2-state_machine 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. data/CHANGELOG.rdoc +413 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +717 -0
  4. data/Rakefile +77 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine.rb +448 -0
  28. data/lib/state_machine/alternate_machine.rb +79 -0
  29. data/lib/state_machine/assertions.rb +36 -0
  30. data/lib/state_machine/branch.rb +224 -0
  31. data/lib/state_machine/callback.rb +236 -0
  32. data/lib/state_machine/condition_proxy.rb +94 -0
  33. data/lib/state_machine/error.rb +13 -0
  34. data/lib/state_machine/eval_helpers.rb +86 -0
  35. data/lib/state_machine/event.rb +304 -0
  36. data/lib/state_machine/event_collection.rb +139 -0
  37. data/lib/state_machine/extensions.rb +149 -0
  38. data/lib/state_machine/initializers.rb +4 -0
  39. data/lib/state_machine/initializers/merb.rb +1 -0
  40. data/lib/state_machine/initializers/rails.rb +25 -0
  41. data/lib/state_machine/integrations.rb +110 -0
  42. data/lib/state_machine/integrations/active_model.rb +502 -0
  43. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  44. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  45. data/lib/state_machine/integrations/active_model/versions.rb +31 -0
  46. data/lib/state_machine/integrations/active_record.rb +424 -0
  47. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  48. data/lib/state_machine/integrations/active_record/versions.rb +143 -0
  49. data/lib/state_machine/integrations/base.rb +91 -0
  50. data/lib/state_machine/integrations/data_mapper.rb +392 -0
  51. data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
  52. data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
  53. data/lib/state_machine/integrations/mongo_mapper.rb +272 -0
  54. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  55. data/lib/state_machine/integrations/mongo_mapper/versions.rb +110 -0
  56. data/lib/state_machine/integrations/mongoid.rb +357 -0
  57. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  58. data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
  59. data/lib/state_machine/integrations/sequel.rb +428 -0
  60. data/lib/state_machine/integrations/sequel/versions.rb +36 -0
  61. data/lib/state_machine/machine.rb +1873 -0
  62. data/lib/state_machine/machine_collection.rb +87 -0
  63. data/lib/state_machine/matcher.rb +123 -0
  64. data/lib/state_machine/matcher_helpers.rb +54 -0
  65. data/lib/state_machine/node_collection.rb +157 -0
  66. data/lib/state_machine/path.rb +120 -0
  67. data/lib/state_machine/path_collection.rb +90 -0
  68. data/lib/state_machine/state.rb +271 -0
  69. data/lib/state_machine/state_collection.rb +112 -0
  70. data/lib/state_machine/transition.rb +458 -0
  71. data/lib/state_machine/transition_collection.rb +244 -0
  72. data/lib/tasks/state_machine.rake +1 -0
  73. data/lib/tasks/state_machine.rb +27 -0
  74. data/test/files/en.yml +17 -0
  75. data/test/files/switch.rb +11 -0
  76. data/test/functional/alternate_state_machine_test.rb +122 -0
  77. data/test/functional/state_machine_test.rb +993 -0
  78. data/test/test_helper.rb +4 -0
  79. data/test/unit/assertions_test.rb +40 -0
  80. data/test/unit/branch_test.rb +890 -0
  81. data/test/unit/callback_test.rb +701 -0
  82. data/test/unit/condition_proxy_test.rb +328 -0
  83. data/test/unit/error_test.rb +43 -0
  84. data/test/unit/eval_helpers_test.rb +222 -0
  85. data/test/unit/event_collection_test.rb +358 -0
  86. data/test/unit/event_test.rb +985 -0
  87. data/test/unit/integrations/active_model_test.rb +1097 -0
  88. data/test/unit/integrations/active_record_test.rb +2021 -0
  89. data/test/unit/integrations/base_test.rb +99 -0
  90. data/test/unit/integrations/data_mapper_test.rb +1909 -0
  91. data/test/unit/integrations/mongo_mapper_test.rb +1611 -0
  92. data/test/unit/integrations/mongoid_test.rb +1591 -0
  93. data/test/unit/integrations/sequel_test.rb +1523 -0
  94. data/test/unit/integrations_test.rb +61 -0
  95. data/test/unit/invalid_event_test.rb +20 -0
  96. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  97. data/test/unit/invalid_transition_test.rb +77 -0
  98. data/test/unit/machine_collection_test.rb +599 -0
  99. data/test/unit/machine_test.rb +3043 -0
  100. data/test/unit/matcher_helpers_test.rb +37 -0
  101. data/test/unit/matcher_test.rb +155 -0
  102. data/test/unit/node_collection_test.rb +217 -0
  103. data/test/unit/path_collection_test.rb +266 -0
  104. data/test/unit/path_test.rb +485 -0
  105. data/test/unit/state_collection_test.rb +310 -0
  106. data/test/unit/state_machine_test.rb +31 -0
  107. data/test/unit/state_test.rb +924 -0
  108. data/test/unit/transition_collection_test.rb +2102 -0
  109. data/test/unit/transition_test.rb +1541 -0
  110. metadata +207 -0
@@ -0,0 +1,87 @@
1
+ require 'state_machine/assertions'
2
+
3
+ module StateMachine
4
+ # Represents a collection of state machines for a class
5
+ class MachineCollection < Hash
6
+ include Assertions
7
+
8
+ # Initializes the state of each machine in the given object. This can allow
9
+ # states to be initialized in two groups: static and dynamic. For example:
10
+ #
11
+ # machines.initialize_states(object) do
12
+ # # After static state initialization, before dynamic state initialization
13
+ # end
14
+ #
15
+ # If no block is provided, then all states will still be initialized.
16
+ #
17
+ # Valid configuration options:
18
+ # * <tt>:static</tt> - Whether to initialize static states. If set to
19
+ # :force, the state will be initialized regardless of its current value.
20
+ # Default is :force.
21
+ # * <tt>:dynamic</tt> - Whether to initialize dynamic states. If set to
22
+ # :force, the state will be initialized regardless of its current value.
23
+ # Default is true.
24
+ # * <tt>:to</tt> - A hash to write the initialized state to instead of
25
+ # writing to the object. Default is to write directly to the object.
26
+ def initialize_states(object, options = {})
27
+ assert_valid_keys(options, :static, :dynamic, :to)
28
+ options = {:static => :force, :dynamic => true}.merge(options)
29
+
30
+ each_value do |machine|
31
+ machine.initialize_state(object, :force => options[:static] == :force, :to => options[:to]) unless machine.dynamic_initial_state?
32
+ end if options[:static]
33
+
34
+ result = yield if block_given?
35
+
36
+ each_value do |machine|
37
+ machine.initialize_state(object, :force => options[:dynamic] == :force, :to => options[:to]) if machine.dynamic_initial_state?
38
+ end if options[:dynamic]
39
+
40
+ result
41
+ end
42
+
43
+ # Runs one or more events in parallel on the given object. See
44
+ # StateMachine::InstanceMethods#fire_events for more information.
45
+ def fire_events(object, *events)
46
+ run_action = [true, false].include?(events.last) ? events.pop : true
47
+
48
+ # Generate the transitions to run for each event
49
+ transitions = events.collect do |event_name|
50
+ # Find the actual event being run
51
+ event = nil
52
+ detect {|name, machine| event = machine.events[event_name, :qualified_name]}
53
+
54
+ raise(InvalidEvent.new(object, event_name)) unless event
55
+
56
+ # Get the transition that will be performed for the event
57
+ unless transition = event.transition_for(object)
58
+ machine = event.machine
59
+ event.on_failure(object)
60
+ end
61
+
62
+ transition
63
+ end.compact
64
+
65
+ # Run the events in parallel only if valid transitions were found for
66
+ # all of them
67
+ if events.length == transitions.length
68
+ TransitionCollection.new(transitions, :actions => run_action).perform
69
+ else
70
+ false
71
+ end
72
+ end
73
+
74
+ # Builds the collection of transitions for all event attributes defined on
75
+ # the given object. This will only include events whose machine actions
76
+ # match the one specified.
77
+ #
78
+ # These should only be fired as a result of the action being run.
79
+ def transitions(object, action, options = {})
80
+ transitions = map do |name, machine|
81
+ machine.events.attribute_transition_for(object, true) if machine.action == action
82
+ end
83
+
84
+ AttributeTransitionCollection.new(transitions.compact, options)
85
+ end
86
+ end
87
+ end
@@ -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,157 @@
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
+ class NodeCollection
6
+ include Enumerable
7
+ include Assertions
8
+
9
+ # The machine associated with the nodes
10
+ attr_reader :machine
11
+
12
+ # Creates a new collection of nodes for the given state machine. By default,
13
+ # the collection is empty.
14
+ #
15
+ # Configuration options:
16
+ # * <tt>:index</tt> - One or more attributes to automatically generate
17
+ # hashed indices for in order to perform quick lookups. Default is to
18
+ # index by the :name attribute
19
+ def initialize(machine, options = {})
20
+ assert_valid_keys(options, :index)
21
+ options = {:index => :name}.merge(options)
22
+
23
+ @machine = machine
24
+ @nodes = []
25
+ @indices = Array(options[:index]).inject({}) {|indices, attribute| indices[attribute] = {}; indices}
26
+ @default_index = Array(options[:index]).first
27
+ end
28
+
29
+ # Creates a copy of this collection such that modifications don't affect
30
+ # the original collection
31
+ def initialize_copy(orig) #:nodoc:
32
+ super
33
+
34
+ nodes = @nodes
35
+ @nodes = []
36
+ @indices = @indices.inject({}) {|indices, (name, index)| indices[name] = {}; indices}
37
+ concat(nodes.map {|n| n.dup})
38
+ end
39
+
40
+ # Changes the current machine associated with the collection. In turn, this
41
+ # will change the state machine associated with each node in the collection.
42
+ def machine=(new_machine)
43
+ @machine = new_machine
44
+ each {|node| node.machine = new_machine}
45
+ end
46
+
47
+ # Gets the number of nodes in this collection
48
+ def length
49
+ @nodes.length
50
+ end
51
+
52
+ # Gets the set of unique keys for the given index
53
+ def keys(index_name = @default_index)
54
+ index(index_name).keys
55
+ end
56
+
57
+ # Adds a new node to the collection. By doing so, this will also add it to
58
+ # the configured indices.
59
+ def <<(node)
60
+ @nodes << node
61
+ @indices.each {|attribute, index| index[value(node, attribute)] = node}
62
+ self
63
+ end
64
+
65
+ # Appends a group of nodes to the collection
66
+ def concat(nodes)
67
+ nodes.each {|node| self << node}
68
+ end
69
+
70
+ # Updates the indexed keys for the given node. If the node's attribute
71
+ # has changed since it was added to the collection, the old indexed keys
72
+ # will be replaced with the updated ones.
73
+ def update(node)
74
+ @indices.each do |attribute, index|
75
+ old_key = RUBY_VERSION < '1.9' ? index.index(node) : index.key(node)
76
+ new_key = value(node, attribute)
77
+
78
+ # Only replace the key if it's changed
79
+ if old_key != new_key
80
+ index.delete(old_key)
81
+ index[new_key] = node
82
+ end
83
+ end
84
+ end
85
+
86
+ # Calls the block once for each element in self, passing that element as a
87
+ # parameter.
88
+ #
89
+ # states = StateMachine::NodeCollection.new
90
+ # states << StateMachine::State.new(machine, :parked)
91
+ # states << StateMachine::State.new(machine, :idling)
92
+ # states.each {|state| puts state.name, ' -- '}
93
+ #
94
+ # ...produces:
95
+ #
96
+ # parked -- idling --
97
+ def each
98
+ @nodes.each {|node| yield node}
99
+ self
100
+ end
101
+
102
+ # Gets the node at the given index.
103
+ #
104
+ # states = StateMachine::NodeCollection.new
105
+ # states << StateMachine::State.new(machine, :parked)
106
+ # states << StateMachine::State.new(machine, :idling)
107
+ #
108
+ # states.at(0).name # => :parked
109
+ # states.at(1).name # => :idling
110
+ def at(index)
111
+ @nodes[index]
112
+ end
113
+
114
+ # Gets the node indexed by the given key. By default, this will look up the
115
+ # key in the first index configured for the collection. A custom index can
116
+ # be specified like so:
117
+ #
118
+ # collection['parked', :value]
119
+ #
120
+ # The above will look up the "parked" key in a hash indexed by each node's
121
+ # +value+ attribute.
122
+ #
123
+ # If the key cannot be found, then nil will be returned.
124
+ def [](key, index_name = @default_index)
125
+ index(index_name)[key]
126
+ end
127
+
128
+ # Gets the node indexed by the given key. By default, this will look up the
129
+ # key in the first index configured for the collection. A custom index can
130
+ # be specified like so:
131
+ #
132
+ # collection['parked', :value]
133
+ #
134
+ # The above will look up the "parked" key in a hash indexed by each node's
135
+ # +value+ attribute.
136
+ #
137
+ # If the key cannot be found, then an IndexError exception will be raised:
138
+ #
139
+ # collection['invalid', :value] # => IndexError: "invalid" is an invalid value
140
+ def fetch(key, index_name = @default_index)
141
+ self[key, index_name] || raise(IndexError, "#{key.inspect} is an invalid #{index_name}")
142
+ end
143
+
144
+ private
145
+ # Gets the given index. If the index does not exist, then an ArgumentError
146
+ # is raised.
147
+ def index(name)
148
+ raise ArgumentError, 'No indices configured' unless @indices.any?
149
+ @indices[name] || raise(ArgumentError, "Invalid index: #{name.inspect}")
150
+ end
151
+
152
+ # Gets the value for the given attribute on the node
153
+ def value(node, attribute)
154
+ node.send(attribute)
155
+ end
156
+ end
157
+ 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