hsume2-state_machine 1.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 (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