pluginaweek-state_machine 0.7.6

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 +273 -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 +429 -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 +251 -0
  33. data/lib/state_machine/event_collection.rb +113 -0
  34. data/lib/state_machine/extensions.rb +158 -0
  35. data/lib/state_machine/guard.rb +219 -0
  36. data/lib/state_machine/integrations.rb +68 -0
  37. data/lib/state_machine/integrations/active_record.rb +444 -0
  38. data/lib/state_machine/integrations/active_record/locale.rb +10 -0
  39. data/lib/state_machine/integrations/active_record/observer.rb +41 -0
  40. data/lib/state_machine/integrations/data_mapper.rb +325 -0
  41. data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
  42. data/lib/state_machine/integrations/sequel.rb +292 -0
  43. data/lib/state_machine/machine.rb +1431 -0
  44. data/lib/state_machine/machine_collection.rb +146 -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 +367 -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 +129 -0
  60. data/test/unit/event_collection_test.rb +293 -0
  61. data/test/unit/event_test.rb +605 -0
  62. data/test/unit/guard_test.rb +862 -0
  63. data/test/unit/integrations/active_record_test.rb +1001 -0
  64. data/test/unit/integrations/data_mapper_test.rb +694 -0
  65. data/test/unit/integrations/sequel_test.rb +486 -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 +710 -0
  70. data/test/unit/machine_test.rb +1910 -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 +1113 -0
  78. metadata +161 -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
+ attribute = machine.attribute
160
+ name = self.name
161
+
162
+ # Evaluate the method definitions
163
+ context = ConditionProxy.new(owner_class, lambda {|object| object.class.state_machine(attribute).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(#{attribute.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} in state #{machine.states.match(object).name.inspect}"
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,367 @@
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
+ # Always run after callbacks regardless of whether the actions failed
68
+ transitions.each {|transition| transition.after(results[transition.action])} unless options[:after] == false
69
+
70
+ # Rollback the transitions if the transaction was unsuccessful
71
+ transitions.each {|transition| transition.rollback} unless success
72
+ end
73
+
74
+ success
75
+ end
76
+
77
+ # Runs one or more transitions within a transaction. See StateMachine::Transition.perform
78
+ # for more information.
79
+ def perform_within_transaction(transitions, options = {})
80
+ success = false
81
+ transitions.first.within_transaction do
82
+ success = perform(transitions, options)
83
+ end
84
+
85
+ success
86
+ end
87
+ end
88
+
89
+ # The object being transitioned
90
+ attr_reader :object
91
+
92
+ # The state machine for which this transition is defined
93
+ attr_reader :machine
94
+
95
+ # The event that triggered the transition
96
+ attr_reader :event
97
+
98
+ # The fully-qualified name of the event that triggered the transition
99
+ attr_reader :qualified_event
100
+
101
+ # The original state value *before* the transition
102
+ attr_reader :from
103
+
104
+ # The original state name *before* the transition
105
+ attr_reader :from_name
106
+
107
+ # The original fully-qualified state name *before* transition
108
+ attr_reader :qualified_from_name
109
+
110
+ # The new state value *after* the transition
111
+ attr_reader :to
112
+
113
+ # The new state name *after* the transition
114
+ attr_reader :to_name
115
+
116
+ # The new fully-qualified state name *after* the transition
117
+ attr_reader :qualified_to_name
118
+
119
+ # The arguments passed in to the event that triggered the transition
120
+ # (does not include the +run_action+ boolean argument if specified)
121
+ attr_accessor :args
122
+
123
+ # The result of invoking the action associated with the machine
124
+ attr_reader :result
125
+
126
+ # Creates a new, specific transition
127
+ def initialize(object, machine, event, from_name, to_name) #:nodoc:
128
+ @object = object
129
+ @machine = machine
130
+ @args = []
131
+
132
+ # Event information
133
+ event = machine.events.fetch(event)
134
+ @event = event.name
135
+ @qualified_event = event.qualified_name
136
+
137
+ # From state information
138
+ from_state = machine.states.fetch(from_name)
139
+ @from = machine.read(object, :state)
140
+ @from_name = from_state.name
141
+ @qualified_from_name = from_state.qualified_name
142
+
143
+ # To state information
144
+ to_state = machine.states.fetch(to_name)
145
+ @to = to_state.value
146
+ @to_name = to_state.name
147
+ @qualified_to_name = to_state.qualified_name
148
+ end
149
+
150
+ # The attribute which this transition's machine is defined for
151
+ def attribute
152
+ machine.attribute
153
+ end
154
+
155
+ # The action that will be run when this transition is performed
156
+ def action
157
+ machine.action
158
+ end
159
+
160
+ # Does this transition represent a loopback (i.e. the from and to state
161
+ # are the same)
162
+ #
163
+ # == Example
164
+ #
165
+ # machine = StateMachine.new(Vehicle)
166
+ # StateMachine::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback? # => true
167
+ # StateMachine::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback? # => false
168
+ def loopback?
169
+ from_name == to_name
170
+ end
171
+
172
+ # A hash of all the core attributes defined for this transition with their
173
+ # names as keys and values of the attributes as values.
174
+ #
175
+ # == Example
176
+ #
177
+ # machine = StateMachine.new(Vehicle)
178
+ # transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
179
+ # transition.attributes # => {:object => #<Vehicle:0xb7d60ea4>, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}
180
+ def attributes
181
+ @attributes ||= {:object => object, :attribute => attribute, :event => event, :from => from, :to => to}
182
+ end
183
+
184
+ # Runs the actual transition and any before/after callbacks associated
185
+ # with the transition. The action associated with the transition/machine
186
+ # can be skipped by passing in +false+.
187
+ #
188
+ # == Examples
189
+ #
190
+ # class Vehicle
191
+ # state_machine :action => :save do
192
+ # ...
193
+ # end
194
+ # end
195
+ #
196
+ # vehicle = Vehicle.new
197
+ # transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
198
+ # transition.perform # => Runs the +save+ action after setting the state attribute
199
+ # transition.perform(false) # => Only sets the state attribute
200
+ def perform(*args)
201
+ run_action = [true, false].include?(args.last) ? args.pop : true
202
+ self.args = args
203
+
204
+ # Run the transition
205
+ self.class.perform_within_transaction([self], :action => run_action)
206
+ end
207
+
208
+ # Runs a block within a transaction for the object being transitioned.
209
+ # By default, transactions are a no-op unless otherwise defined by the
210
+ # machine's integration.
211
+ def within_transaction
212
+ machine.within_transaction(object) do
213
+ yield
214
+ end
215
+ end
216
+
217
+ # Runs the machine's +before+ callbacks for this transition. Only
218
+ # callbacks that are configured to match the event, from state, and to
219
+ # state will be invoked.
220
+ #
221
+ # == Example
222
+ #
223
+ # class Vehicle
224
+ # state_machine do
225
+ # before_transition :on => :ignite, :do => lambda {|vehicle| ...}
226
+ # end
227
+ # end
228
+ #
229
+ # vehicle = Vehicle.new
230
+ # transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
231
+ # transition.before
232
+ def before
233
+ result = false
234
+
235
+ catch(:halt) do
236
+ callback(:before)
237
+ result = true
238
+ end
239
+
240
+ result
241
+ end
242
+
243
+ # Transitions the current value of the state to that specified by the
244
+ # transition.
245
+ #
246
+ # == Example
247
+ #
248
+ # class Vehicle
249
+ # state_machine do
250
+ # event :ignite do
251
+ # transition :parked => :idling
252
+ # end
253
+ # end
254
+ # end
255
+ #
256
+ # vehicle = Vehicle.new
257
+ # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
258
+ # transition.persist
259
+ #
260
+ # vehicle.state # => 'idling'
261
+ def persist
262
+ machine.write(object, :state, to)
263
+ end
264
+
265
+ # Runs the machine's +after+ callbacks for this transition. Only
266
+ # callbacks that are configured to match the event, from state, and to
267
+ # state will be invoked.
268
+ #
269
+ # The result is used to indicate whether the associated machine action
270
+ # was executed successfully.
271
+ #
272
+ # == Halting
273
+ #
274
+ # If any callback throws a <tt>:halt</tt> exception, it will be caught
275
+ # and the callback chain will be automatically stopped. However, this
276
+ # exception will not bubble up to the caller since +after+ callbacks
277
+ # should never halt the execution of a +perform+.
278
+ #
279
+ # == Example
280
+ #
281
+ # class Vehicle
282
+ # state_machine do
283
+ # after_transition :on => :ignite, :do => lambda {|vehicle| ...}
284
+ #
285
+ # event :ignite do
286
+ # transition :parked => :idling
287
+ # end
288
+ # end
289
+ # end
290
+ #
291
+ # vehicle = Vehicle.new
292
+ # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
293
+ # transition.after(true)
294
+ def after(result = nil)
295
+ @result = result
296
+
297
+ catch(:halt) do
298
+ callback(:after)
299
+ end
300
+
301
+ true
302
+ end
303
+
304
+ # Rolls back changes made to the object's state via this transition. This
305
+ # will revert the state back to the +from+ value.
306
+ #
307
+ # == Example
308
+ #
309
+ # class Vehicle
310
+ # state_machine :initial => :parked do
311
+ # event :ignite do
312
+ # transition :parked => :idling
313
+ # end
314
+ # end
315
+ # end
316
+ #
317
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7b7f568 @state="parked">
318
+ # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
319
+ #
320
+ # # Persist the new state
321
+ # vehicle.state # => "parked"
322
+ # transition.persist
323
+ # vehicle.state # => "idling"
324
+ #
325
+ # # Roll back to the original state
326
+ # transition.rollback
327
+ # vehicle.state # => "parked"
328
+ def rollback
329
+ machine.write(object, :state, from)
330
+ end
331
+
332
+ # Generates a nicely formatted description of this transitions's contents.
333
+ #
334
+ # For example,
335
+ #
336
+ # transition = StateMachine::Transition.new(object, machine, :ignite, :parked, :idling)
337
+ # transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
338
+ def inspect
339
+ "#<#{self.class} #{%w(attribute event from from_name to to_name).map {|attr| "#{attr}=#{send(attr).inspect}"} * ' '}>"
340
+ end
341
+
342
+ protected
343
+ # Gets a hash of the context defining this unique transition (including
344
+ # event, from state, and to state).
345
+ #
346
+ # == Example
347
+ #
348
+ # machine = StateMachine.new(Vehicle)
349
+ # transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
350
+ # transition.context # => {:on => :ignite, :from => :parked, :to => :idling}
351
+ def context
352
+ @context ||= {:on => event, :from => from_name, :to => to_name}
353
+ end
354
+
355
+ # Runs the callbacks of the given type for this transition. This will
356
+ # only invoke callbacks that exactly match the event, from state, and
357
+ # to state that describe this transition.
358
+ #
359
+ # Additional callback parameters can be specified. By default, this
360
+ # transition is also passed into callbacks.
361
+ def callback(type)
362
+ machine.callbacks[type].each do |callback|
363
+ callback.call(object, context, self)
364
+ end
365
+ end
366
+ end
367
+ end