joelind-state_machine 0.8.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 (78) hide show
  1. data/CHANGELOG.rdoc +297 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +466 -0
  4. data/Rakefile +98 -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 +388 -0
  28. data/lib/state_machine/assertions.rb +36 -0
  29. data/lib/state_machine/callback.rb +189 -0
  30. data/lib/state_machine/condition_proxy.rb +94 -0
  31. data/lib/state_machine/eval_helpers.rb +67 -0
  32. data/lib/state_machine/event.rb +252 -0
  33. data/lib/state_machine/event_collection.rb +122 -0
  34. data/lib/state_machine/extensions.rb +149 -0
  35. data/lib/state_machine/guard.rb +230 -0
  36. data/lib/state_machine/integrations.rb +68 -0
  37. data/lib/state_machine/integrations/active_record.rb +492 -0
  38. data/lib/state_machine/integrations/active_record/locale.rb +11 -0
  39. data/lib/state_machine/integrations/active_record/observer.rb +41 -0
  40. data/lib/state_machine/integrations/data_mapper.rb +351 -0
  41. data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
  42. data/lib/state_machine/integrations/sequel.rb +322 -0
  43. data/lib/state_machine/machine.rb +1467 -0
  44. data/lib/state_machine/machine_collection.rb +155 -0
  45. data/lib/state_machine/matcher.rb +123 -0
  46. data/lib/state_machine/matcher_helpers.rb +54 -0
  47. data/lib/state_machine/node_collection.rb +152 -0
  48. data/lib/state_machine/state.rb +249 -0
  49. data/lib/state_machine/state_collection.rb +112 -0
  50. data/lib/state_machine/transition.rb +394 -0
  51. data/tasks/state_machine.rake +1 -0
  52. data/tasks/state_machine.rb +30 -0
  53. data/test/classes/switch.rb +11 -0
  54. data/test/functional/state_machine_test.rb +941 -0
  55. data/test/test_helper.rb +4 -0
  56. data/test/unit/assertions_test.rb +40 -0
  57. data/test/unit/callback_test.rb +455 -0
  58. data/test/unit/condition_proxy_test.rb +328 -0
  59. data/test/unit/eval_helpers_test.rb +120 -0
  60. data/test/unit/event_collection_test.rb +326 -0
  61. data/test/unit/event_test.rb +743 -0
  62. data/test/unit/guard_test.rb +908 -0
  63. data/test/unit/integrations/active_record_test.rb +1374 -0
  64. data/test/unit/integrations/data_mapper_test.rb +962 -0
  65. data/test/unit/integrations/sequel_test.rb +859 -0
  66. data/test/unit/integrations_test.rb +42 -0
  67. data/test/unit/invalid_event_test.rb +7 -0
  68. data/test/unit/invalid_transition_test.rb +7 -0
  69. data/test/unit/machine_collection_test.rb +938 -0
  70. data/test/unit/machine_test.rb +2004 -0
  71. data/test/unit/matcher_helpers_test.rb +37 -0
  72. data/test/unit/matcher_test.rb +155 -0
  73. data/test/unit/node_collection_test.rb +207 -0
  74. data/test/unit/state_collection_test.rb +280 -0
  75. data/test/unit/state_machine_test.rb +31 -0
  76. data/test/unit/state_test.rb +795 -0
  77. data/test/unit/transition_test.rb +1212 -0
  78. metadata +163 -0
@@ -0,0 +1,249 @@
1
+ require 'state_machine/assertions'
2
+ require 'state_machine/condition_proxy'
3
+
4
+ module StateMachine
5
+ # A state defines a value that an attribute can be in after being transitioned
6
+ # 0 or more times. States can represent a value of any type in Ruby, though
7
+ # the most common (and default) type is String.
8
+ #
9
+ # In addition to defining the machine's value, a state can also define a
10
+ # behavioral context for an object when that object is in the state. See
11
+ # StateMachine::Machine#state for more information about how state-driven
12
+ # behavior can be utilized.
13
+ class State
14
+ include Assertions
15
+
16
+ # The state machine for which this state is defined
17
+ attr_accessor :machine
18
+
19
+ # The unique identifier for the state used in event and callback definitions
20
+ attr_reader :name
21
+
22
+ # The fully-qualified identifier for the state, scoped by the machine's
23
+ # namespace
24
+ attr_reader :qualified_name
25
+
26
+ # The value that is written to a machine's attribute when an object
27
+ # transitions into this state
28
+ attr_writer :value
29
+
30
+ # Whether this state's value should be cached after being evaluated
31
+ attr_accessor :cache
32
+
33
+ # Whether or not this state is the initial state to use for new objects
34
+ attr_accessor :initial
35
+ alias_method :initial?, :initial
36
+
37
+ # A custom lambda block for determining whether a given value matches this
38
+ # state
39
+ attr_accessor :matcher
40
+
41
+ # Tracks all of the methods that have been defined for the machine's owner
42
+ # class when objects are in this state.
43
+ #
44
+ # Maps :method_name => UnboundMethod
45
+ attr_reader :methods
46
+
47
+ # Creates a new state within the context of the given machine.
48
+ #
49
+ # Configuration options:
50
+ # * <tt>:initial</tt> - Whether this state is the beginning state for the
51
+ # machine. Default is false.
52
+ # * <tt>:value</tt> - The value to store when an object transitions to this
53
+ # state. Default is the name (stringified).
54
+ # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
55
+ # then setting this to true will cache the evaluated result
56
+ # * <tt>:if</tt> - Determines whether a value matches this state
57
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
58
+ # By default, the configured value is matched.
59
+ def initialize(machine, name, options = {}) #:nodoc:
60
+ assert_valid_keys(options, :initial, :value, :cache, :if)
61
+
62
+ @machine = machine
63
+ @name = name
64
+ @qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name
65
+ @value = options.include?(:value) ? options[:value] : name && name.to_s
66
+ @cache = options[:cache]
67
+ @matcher = options[:if]
68
+ @methods = {}
69
+ @initial = options[:initial] == true
70
+
71
+ add_predicate
72
+ end
73
+
74
+ # Creates a copy of this state in addition to the list of associated
75
+ # methods to prevent conflicts across different states.
76
+ def initialize_copy(orig) #:nodoc:
77
+ super
78
+ @methods = methods.dup
79
+ end
80
+
81
+ # Determines whether there are any states that can be transitioned to from
82
+ # this state. If there are none, then this state is considered *final*.
83
+ # Any objects in a final state will remain so forever given the current
84
+ # machine's definition.
85
+ def final?
86
+ !machine.events.any? do |event|
87
+ event.guards.any? do |guard|
88
+ guard.state_requirements.any? do |requirement|
89
+ requirement[:from].matches?(name) && !requirement[:to].matches?(name, :from => name)
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # Generates a human-readable description of this state's name / value:
96
+ #
97
+ # For example,
98
+ #
99
+ # State.new(machine, :parked).description # => "parked"
100
+ # State.new(machine, :parked, :value => :parked).description # => "parked"
101
+ # State.new(machine, :parked, :value => nil).description # => "parked (nil)"
102
+ # State.new(machine, :parked, :value => 1).description # => "parked (1)"
103
+ # State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
104
+ def description
105
+ description = name ? name.to_s : name.inspect
106
+ description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
107
+ description
108
+ end
109
+
110
+ # The value that represents this state. This will optionally evaluate the
111
+ # original block if it's a lambda block. Otherwise, the static value is
112
+ # returned.
113
+ #
114
+ # For example,
115
+ #
116
+ # State.new(machine, :parked, :value => 1).value # => 1
117
+ # State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
118
+ # State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => <Proc:0xb6ea7ca0@...>
119
+ def value(eval = true)
120
+ if @value.is_a?(Proc) && eval
121
+ if cache_value?
122
+ @value = @value.call
123
+ machine.states.update(self)
124
+ @value
125
+ else
126
+ @value.call
127
+ end
128
+ else
129
+ @value
130
+ end
131
+ end
132
+
133
+ # Determines whether this state matches the given value. If no matcher is
134
+ # configured, then this will check whether the values are equivalent.
135
+ # Otherwise, the matcher will determine the result.
136
+ #
137
+ # For example,
138
+ #
139
+ # # Without a matcher
140
+ # state = State.new(machine, :parked, :value => 1)
141
+ # state.matches?(1) # => true
142
+ # state.matches?(2) # => false
143
+ #
144
+ # # With a matcher
145
+ # state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
146
+ # state.matches?(nil) # => false
147
+ # state.matches?(Time.now) # => true
148
+ def matches?(other_value)
149
+ matcher ? matcher.call(other_value) : other_value == value
150
+ end
151
+
152
+ # Defines a context for the state which will be enabled on instances of
153
+ # the owner class when the machine is in this state.
154
+ #
155
+ # This can be called multiple times. Each time a new context is created,
156
+ # a new module will be included in the owner class.
157
+ def context(&block)
158
+ owner_class = machine.owner_class
159
+ machine_name = machine.name
160
+ name = self.name
161
+
162
+ # Evaluate the method definitions
163
+ context = ConditionProxy.new(owner_class, lambda {|object| object.class.state_machine(machine_name).states.matches?(object, name)})
164
+ context.class_eval(&block)
165
+ context.instance_methods.each do |method|
166
+ methods[method.to_sym] = context.instance_method(method)
167
+
168
+ # Calls the method defined by the current state of the machine
169
+ context.class_eval <<-end_eval, __FILE__, __LINE__
170
+ def #{method}(*args, &block)
171
+ self.class.state_machine(#{machine_name.inspect}).states.match!(self).call(self, #{method.inspect}, *args, &block)
172
+ end
173
+ end_eval
174
+ end
175
+
176
+ # Include the context so that it can be bound to the owner class (the
177
+ # context is considered an ancestor, so it's allowed to be bound)
178
+ owner_class.class_eval { include context }
179
+
180
+ context
181
+ end
182
+
183
+ # Calls a method defined in this state's context on the given object. All
184
+ # arguments and any block will be passed into the method defined.
185
+ #
186
+ # If the method has never been defined for this state, then a NoMethodError
187
+ # will be raised.
188
+ def call(object, method, *args, &block)
189
+ if context_method = methods[method.to_sym]
190
+ # Method is defined by the state: proxy it through
191
+ context_method.bind(object).call(*args, &block)
192
+ else
193
+ # Raise exception as if the method never existed on the original object
194
+ raise NoMethodError, "undefined method '#{method}' for #{object} with #{name || 'nil'} #{machine.name}"
195
+ end
196
+ end
197
+
198
+ # Draws a representation of this state on the given machine. This will
199
+ # create a new node on the graph with the following properties:
200
+ # * +label+ - The human-friendly description of the state.
201
+ # * +width+ - The width of the node. Always 1.
202
+ # * +height+ - The height of the node. Always 1.
203
+ # * +shape+ - The actual shape of the node. If the state is a final
204
+ # state, then "doublecircle", otherwise "ellipse".
205
+ #
206
+ # The actual node generated on the graph will be returned.
207
+ def draw(graph)
208
+ node = graph.add_node(name ? name.to_s : 'nil',
209
+ :label => description,
210
+ :width => '1',
211
+ :height => '1',
212
+ :shape => final? ? 'doublecircle' : 'ellipse'
213
+ )
214
+
215
+ # Add open arrow for initial state
216
+ graph.add_edge(graph.add_node('starting_state', :shape => 'point'), node) if initial?
217
+
218
+ node
219
+ end
220
+
221
+ # Generates a nicely formatted description of this state's contents.
222
+ #
223
+ # For example,
224
+ #
225
+ # state = StateMachine::State.new(machine, :parked, :value => 1, :initial => true)
226
+ # state # => #<StateMachine::State name=:parked value=1 initial=true context=[]>
227
+ def inspect
228
+ attributes = [[:name, name], [:value, @value], [:initial, initial?], [:context, methods.keys]]
229
+ "#<#{self.class} #{attributes.map {|attr, value| "#{attr}=#{value.inspect}"} * ' '}>"
230
+ end
231
+
232
+ private
233
+ # Should the value be cached after it's evaluated for the first time?
234
+ def cache_value?
235
+ @cache
236
+ end
237
+
238
+ # Adds a predicate method to the owner class so long as a name has
239
+ # actually been configured for the state
240
+ def add_predicate
241
+ return unless name
242
+
243
+ # Checks whether the current value matches this state
244
+ machine.define_instance_method("#{qualified_name}?") do |machine, object|
245
+ machine.states.matches?(object, name)
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,112 @@
1
+ require 'state_machine/node_collection'
2
+
3
+ module StateMachine
4
+ # Represents a collection of states in a state machine
5
+ class StateCollection < NodeCollection
6
+ def initialize(machine) #:nodoc:
7
+ super(machine, :index => [:name, :value])
8
+ end
9
+
10
+ # Determines whether the given object is in a specific state. If the
11
+ # object's current value doesn't match the state, then this will return
12
+ # false, otherwise true. If the given state is unknown, then an IndexError
13
+ # will be raised.
14
+ #
15
+ # == Examples
16
+ #
17
+ # class Vehicle
18
+ # state_machine :initial => :parked do
19
+ # other_states :idling
20
+ # end
21
+ # end
22
+ #
23
+ # states = Vehicle.state_machine.states
24
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
25
+ #
26
+ # states.matches?(vehicle, :parked) # => true
27
+ # states.matches?(vehicle, :idling) # => false
28
+ # states.matches?(vehicle, :invalid) # => IndexError: :invalid is an invalid key for :name index
29
+ def matches?(object, name)
30
+ fetch(name).matches?(machine.read(object, :state))
31
+ end
32
+
33
+ # Determines the current state of the given object as configured by this
34
+ # state machine. This will attempt to find a known state that matches
35
+ # the value of the attribute on the object.
36
+ #
37
+ # == Examples
38
+ #
39
+ # class Vehicle
40
+ # state_machine :initial => :parked do
41
+ # other_states :idling
42
+ # end
43
+ # end
44
+ #
45
+ # states = Vehicle.state_machine.states
46
+ #
47
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
48
+ # states.match(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
49
+ #
50
+ # vehicle.state = 'idling'
51
+ # states.match(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=true>
52
+ #
53
+ # vehicle.state = 'invalid'
54
+ # states.match(vehicle) # => nil
55
+ def match(object)
56
+ value = machine.read(object, :state)
57
+ self[value, :value] || detect {|state| state.matches?(value)}
58
+ end
59
+
60
+ # Determines the current state of the given object as configured by this
61
+ # state machine. If no state is found, then an ArgumentError will be
62
+ # raised.
63
+ #
64
+ # == Examples
65
+ #
66
+ # class Vehicle
67
+ # state_machine :initial => :parked do
68
+ # other_states :idling
69
+ # end
70
+ # end
71
+ #
72
+ # states = Vehicle.state_machine.states
73
+ #
74
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
75
+ # states.match!(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
76
+ #
77
+ # vehicle.state = 'invalid'
78
+ # states.match!(vehicle) # => ArgumentError: "invalid" is not a known state value
79
+ def match!(object)
80
+ match(object) || raise(ArgumentError, "#{machine.read(object, :state).inspect} is not a known #{machine.name} value")
81
+ end
82
+
83
+ # Gets the order in which states should be displayed based on where they
84
+ # were first referenced. This will order states in the following priority:
85
+ #
86
+ # 1. Initial state
87
+ # 2. Event transitions (:from, :except_from, :to, :except_to options)
88
+ # 3. States with behaviors
89
+ # 4. States referenced via +state+ or +other_states+
90
+ # 5. States referenced in callbacks
91
+ #
92
+ # This order will determine how the GraphViz visualizations are rendered.
93
+ def by_priority
94
+ order = select {|state| state.initial}.map {|state| state.name}
95
+
96
+ machine.events.each {|event| order += event.known_states}
97
+ order += select {|state| state.methods.any?}.map {|state| state.name}
98
+ order += keys(:name) - machine.callbacks.values.flatten.map {|callback| callback.known_states}.flatten
99
+ order += keys(:name)
100
+
101
+ order.uniq!
102
+ order.map! {|name| self[name]}
103
+ order
104
+ end
105
+
106
+ private
107
+ # Gets the value for the given attribute on the node
108
+ def value(node, attribute)
109
+ attribute == :value ? node.value(false) : super
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,394 @@
1
+ module StateMachine
2
+ # An invalid transition was attempted
3
+ class InvalidTransition < StandardError
4
+ end
5
+
6
+ # A transition represents a state change for a specific attribute.
7
+ #
8
+ # Transitions consist of:
9
+ # * An event
10
+ # * A starting state
11
+ # * An ending state
12
+ class Transition
13
+ class << self
14
+ # Runs one or more transitions in parallel. All transitions will run
15
+ # through the following steps:
16
+ # 1. Before callbacks
17
+ # 2. Persist state
18
+ # 3. Invoke action
19
+ # 4. After callbacks (if configured)
20
+ # 5. Rollback (if action is unsuccessful)
21
+ #
22
+ # Configuration options:
23
+ # * <tt>:action</tt> - Whether to run the action configured for each transition
24
+ # * <tt>:after</tt> - Whether to run after callbacks
25
+ #
26
+ # If a block is passed to this method, that block will be called instead
27
+ # of invoking each transition's action.
28
+ def perform(transitions, options = {})
29
+ # Validate that the transitions are for separate machines / attributes
30
+ attributes = transitions.map {|transition| transition.attribute}.uniq
31
+ raise ArgumentError, 'Cannot perform multiple transitions in parallel for the same state machine attribute' if attributes.length != transitions.length
32
+
33
+ success = false
34
+
35
+ # Run before callbacks. If any callback halts, then the entire chain
36
+ # is halted for every transition.
37
+ if transitions.all? {|transition| transition.before}
38
+ # Persist the new state for each attribute
39
+ transitions.each {|transition| transition.persist}
40
+
41
+ # Run the actions associated with each machine
42
+ begin
43
+ results = {}
44
+ success =
45
+ if block_given?
46
+ # Block was given: use the result for each transition
47
+ result = yield
48
+ transitions.each {|transition| results[transition.action] = result}
49
+ !!result
50
+ elsif options[:action] == false
51
+ # Skip the action
52
+ true
53
+ else
54
+ # Run each transition's action (only once)
55
+ object = transitions.first.object
56
+ transitions.all? do |transition|
57
+ action = transition.action
58
+ action && !results.include?(action) ? results[action] = object.send(action) : true
59
+ end
60
+ end
61
+ rescue Exception
62
+ # Action failed: rollback
63
+ transitions.each {|transition| transition.rollback}
64
+ raise
65
+ end
66
+
67
+ # Run after callbacks even when the actions failed. The :after option
68
+ # is ignored if the transitions were unsuccessful.
69
+ transitions.each {|transition| transition.after(results[transition.action], success)} unless options[:after] == false && success
70
+
71
+ # Rollback the transitions if the transaction was unsuccessful
72
+ transitions.each {|transition| transition.rollback} unless success
73
+ end
74
+
75
+ success
76
+ end
77
+
78
+ # Runs one or more transitions within a transaction. See StateMachine::Transition.perform
79
+ # for more information.
80
+ def perform_within_transaction(transitions, options = {})
81
+ success = false
82
+ transitions.first.within_transaction do
83
+ success = perform(transitions, options)
84
+ end
85
+
86
+ success
87
+ end
88
+ end
89
+
90
+ # The object being transitioned
91
+ attr_reader :object
92
+
93
+ # The state machine for which this transition is defined
94
+ attr_reader :machine
95
+
96
+ # The event that triggered the transition
97
+ attr_reader :event
98
+
99
+ # The fully-qualified name of the event that triggered the transition
100
+ attr_reader :qualified_event
101
+
102
+ # The original state value *before* the transition
103
+ attr_reader :from
104
+
105
+ # The original state name *before* the transition
106
+ attr_reader :from_name
107
+
108
+ # The original fully-qualified state name *before* transition
109
+ attr_reader :qualified_from_name
110
+
111
+ # The new state value *after* the transition
112
+ attr_reader :to
113
+
114
+ # The new state name *after* the transition
115
+ attr_reader :to_name
116
+
117
+ # The new fully-qualified state name *after* the transition
118
+ attr_reader :qualified_to_name
119
+
120
+ # The arguments passed in to the event that triggered the transition
121
+ # (does not include the +run_action+ boolean argument if specified)
122
+ attr_accessor :args
123
+
124
+ # The result of invoking the action associated with the machine
125
+ attr_reader :result
126
+
127
+ # Creates a new, specific transition
128
+ def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
129
+ @object = object
130
+ @machine = machine
131
+ @args = []
132
+
133
+ # Event information
134
+ event = machine.events.fetch(event)
135
+ @event = event.name
136
+ @qualified_event = event.qualified_name
137
+
138
+ # From state information
139
+ from_state = machine.states.fetch(from_name)
140
+ @from = read_state ? machine.read(object, :state) : from_state.value
141
+ @from_name = from_state.name
142
+ @qualified_from_name = from_state.qualified_name
143
+
144
+ # To state information
145
+ to_state = machine.states.fetch(to_name)
146
+ @to = to_state.value
147
+ @to_name = to_state.name
148
+ @qualified_to_name = to_state.qualified_name
149
+ end
150
+
151
+ # The attribute which this transition's machine is defined for
152
+ def attribute
153
+ machine.attribute
154
+ end
155
+
156
+ # The action that will be run when this transition is performed
157
+ def action
158
+ machine.action
159
+ end
160
+
161
+ # Does this transition represent a loopback (i.e. the from and to state
162
+ # are the same)
163
+ #
164
+ # == Example
165
+ #
166
+ # machine = StateMachine.new(Vehicle)
167
+ # StateMachine::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback? # => true
168
+ # StateMachine::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback? # => false
169
+ def loopback?
170
+ from_name == to_name
171
+ end
172
+
173
+ # A hash of all the core attributes defined for this transition with their
174
+ # names as keys and values of the attributes as values.
175
+ #
176
+ # == Example
177
+ #
178
+ # machine = StateMachine.new(Vehicle)
179
+ # transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
180
+ # transition.attributes # => {:object => #<Vehicle:0xb7d60ea4>, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}
181
+ def attributes
182
+ @attributes ||= {:object => object, :attribute => attribute, :event => event, :from => from, :to => to}
183
+ end
184
+
185
+ # Runs the actual transition and any before/after callbacks associated
186
+ # with the transition. The action associated with the transition/machine
187
+ # can be skipped by passing in +false+.
188
+ #
189
+ # == Examples
190
+ #
191
+ # class Vehicle
192
+ # state_machine :action => :save do
193
+ # ...
194
+ # end
195
+ # end
196
+ #
197
+ # vehicle = Vehicle.new
198
+ # transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
199
+ # transition.perform # => Runs the +save+ action after setting the state attribute
200
+ # transition.perform(false) # => Only sets the state attribute
201
+ def perform(*args)
202
+ run_action = [true, false].include?(args.last) ? args.pop : true
203
+ self.args = args
204
+
205
+ # Run the transition
206
+ self.class.perform_within_transaction([self], :action => run_action)
207
+ end
208
+
209
+ # Runs a block within a transaction for the object being transitioned.
210
+ # By default, transactions are a no-op unless otherwise defined by the
211
+ # machine's integration.
212
+ def within_transaction
213
+ machine.within_transaction(object) do
214
+ yield
215
+ end
216
+ end
217
+
218
+ # Runs the machine's +before+ callbacks for this transition. Only
219
+ # callbacks that are configured to match the event, from state, and to
220
+ # state will be invoked.
221
+ #
222
+ # Once the callbacks are run, they cannot be run again until this transition
223
+ # is reset.
224
+ #
225
+ # == Example
226
+ #
227
+ # class Vehicle
228
+ # state_machine do
229
+ # before_transition :on => :ignite, :do => lambda {|vehicle| ...}
230
+ # end
231
+ # end
232
+ #
233
+ # vehicle = Vehicle.new
234
+ # transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
235
+ # transition.before
236
+ def before
237
+ result = false
238
+
239
+ catch(:halt) do
240
+ unless @before_run
241
+ callback(:before)
242
+ @before_run = true
243
+ end
244
+
245
+ result = true
246
+ end
247
+
248
+ result
249
+ end
250
+
251
+ # Transitions the current value of the state to that specified by the
252
+ # transition. Once the state is persisted, it cannot be persisted again
253
+ # until this transition is reset.
254
+ #
255
+ # == Example
256
+ #
257
+ # class Vehicle
258
+ # state_machine do
259
+ # event :ignite do
260
+ # transition :parked => :idling
261
+ # end
262
+ # end
263
+ # end
264
+ #
265
+ # vehicle = Vehicle.new
266
+ # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
267
+ # transition.persist
268
+ #
269
+ # vehicle.state # => 'idling'
270
+ def persist
271
+ unless @persisted
272
+ machine.write(object, :state, to)
273
+ @persisted = true
274
+ end
275
+ end
276
+
277
+ # Runs the machine's +after+ callbacks for this transition. Only
278
+ # callbacks that are configured to match the event, from state, and to
279
+ # state will be invoked.
280
+ #
281
+ # The result can be used to indicate whether the associated machine action
282
+ # was executed successfully.
283
+ #
284
+ # Once the callbacks are run, they cannot be run again until this transition
285
+ # is reset.
286
+ #
287
+ # == Halting
288
+ #
289
+ # If any callback throws a <tt>:halt</tt> exception, it will be caught
290
+ # and the callback chain will be automatically stopped. However, this
291
+ # exception will not bubble up to the caller since +after+ callbacks
292
+ # should never halt the execution of a +perform+.
293
+ #
294
+ # == Example
295
+ #
296
+ # class Vehicle
297
+ # state_machine do
298
+ # after_transition :on => :ignite, :do => lambda {|vehicle| ...}
299
+ #
300
+ # event :ignite do
301
+ # transition :parked => :idling
302
+ # end
303
+ # end
304
+ # end
305
+ #
306
+ # vehicle = Vehicle.new
307
+ # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
308
+ # transition.after(true)
309
+ def after(result = nil, success = true)
310
+ @result = result
311
+
312
+ catch(:halt) do
313
+ unless @after_run
314
+ callback(:after, :success => success)
315
+ @after_run = true
316
+ end
317
+ end
318
+
319
+ true
320
+ end
321
+
322
+ # Rolls back changes made to the object's state via this transition. This
323
+ # will revert the state back to the +from+ value.
324
+ #
325
+ # == Example
326
+ #
327
+ # class Vehicle
328
+ # state_machine :initial => :parked do
329
+ # event :ignite do
330
+ # transition :parked => :idling
331
+ # end
332
+ # end
333
+ # end
334
+ #
335
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7b7f568 @state="parked">
336
+ # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
337
+ #
338
+ # # Persist the new state
339
+ # vehicle.state # => "parked"
340
+ # transition.persist
341
+ # vehicle.state # => "idling"
342
+ #
343
+ # # Roll back to the original state
344
+ # transition.rollback
345
+ # vehicle.state # => "parked"
346
+ def rollback
347
+ reset
348
+ machine.write(object, :state, from)
349
+ end
350
+
351
+ # Resets any tracking of which callbacks have already been run and whether
352
+ # the state has already been persisted
353
+ def reset
354
+ @before_run = @persisted = @after_run = false
355
+ end
356
+
357
+ # Generates a nicely formatted description of this transitions's contents.
358
+ #
359
+ # For example,
360
+ #
361
+ # transition = StateMachine::Transition.new(object, machine, :ignite, :parked, :idling)
362
+ # transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
363
+ def inspect
364
+ "#<#{self.class} #{%w(attribute event from from_name to to_name).map {|attr| "#{attr}=#{send(attr).inspect}"} * ' '}>"
365
+ end
366
+
367
+ protected
368
+ # Gets a hash of the context defining this unique transition (including
369
+ # event, from state, and to state).
370
+ #
371
+ # == Example
372
+ #
373
+ # machine = StateMachine.new(Vehicle)
374
+ # transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
375
+ # transition.context # => {:on => :ignite, :from => :parked, :to => :idling}
376
+ def context
377
+ @context ||= {:on => event, :from => from_name, :to => to_name}
378
+ end
379
+
380
+ # Runs the callbacks of the given type for this transition. This will
381
+ # only invoke callbacks that exactly match the event, from state, and
382
+ # to state that describe this transition.
383
+ #
384
+ # Additional callback parameters can be specified. By default, this
385
+ # transition is also passed into callbacks.
386
+ def callback(type, context = {})
387
+ context = self.context.merge(context)
388
+
389
+ machine.callbacks[type].each do |callback|
390
+ callback.call(object, context, self)
391
+ end
392
+ end
393
+ end
394
+ end