pluginaweek-state_machine 0.7.6

Sign up to get free protection for your applications and to get access to all the features.
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