branston 0.3.6 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. data/lib/branston/app/controllers/scenarios_controller.rb +6 -5
  2. data/lib/branston/app/controllers/stories_controller.rb +101 -89
  3. data/lib/branston/app/models/story.rb +30 -1
  4. data/lib/branston/app/models/user.rb +4 -0
  5. data/lib/branston/app/views/iterations/index.html.erb +1 -1
  6. data/lib/branston/app/views/layouts/_header.html.erb +1 -2
  7. data/lib/branston/app/views/scenarios/_scenario.html.erb +6 -3
  8. data/lib/branston/app/views/scenarios/_scenarios.html.erb +4 -2
  9. data/lib/branston/app/views/stories/_form.html.erb +15 -4
  10. data/lib/branston/app/views/stories/_story.html.erb +26 -6
  11. data/lib/branston/app/views/stories/edit.html.erb +3 -3
  12. data/lib/branston/app/views/stories/index.html.erb +22 -3
  13. data/lib/branston/app/views/stories/new.html.erb +2 -2
  14. data/lib/branston/app/views/stories/show.html.erb +3 -3
  15. data/lib/branston/config/routes.rb +7 -4
  16. data/lib/branston/coverage/app-controllers-application_controller_rb.html +1 -1
  17. data/lib/branston/coverage/app-controllers-iterations_controller_rb.html +1 -1
  18. data/lib/branston/coverage/app-controllers-outcomes_controller_rb.html +1 -1
  19. data/lib/branston/coverage/app-controllers-preconditions_controller_rb.html +1 -1
  20. data/lib/branston/coverage/app-controllers-releases_controller_rb.html +1 -1
  21. data/lib/branston/coverage/app-controllers-scenarios_controller_rb.html +18 -12
  22. data/lib/branston/coverage/app-controllers-sessions_controller_rb.html +1 -1
  23. data/lib/branston/coverage/app-controllers-stories_controller_rb.html +193 -121
  24. data/lib/branston/coverage/app-controllers-user_roles_controller_rb.html +1 -1
  25. data/lib/branston/coverage/app-controllers-users_controller_rb.html +1 -1
  26. data/lib/branston/coverage/app-helpers-application_helper_rb.html +1 -1
  27. data/lib/branston/coverage/app-helpers-iterations_helper_rb.html +1 -1
  28. data/lib/branston/coverage/app-helpers-outcomes_helper_rb.html +1 -1
  29. data/lib/branston/coverage/app-helpers-preconditions_helper_rb.html +1 -1
  30. data/lib/branston/coverage/app-helpers-releases_helper_rb.html +1 -1
  31. data/lib/branston/coverage/app-helpers-sessions_helper_rb.html +1 -1
  32. data/lib/branston/coverage/app-helpers-stories_helper_rb.html +1 -1
  33. data/lib/branston/coverage/app-helpers-user_roles_helper_rb.html +1 -1
  34. data/lib/branston/coverage/app-models-iteration_rb.html +1 -1
  35. data/lib/branston/coverage/app-models-outcome_rb.html +1 -1
  36. data/lib/branston/coverage/app-models-participation_rb.html +1 -1
  37. data/lib/branston/coverage/app-models-precondition_rb.html +1 -1
  38. data/lib/branston/coverage/app-models-release_rb.html +1 -1
  39. data/lib/branston/coverage/app-models-scenario_rb.html +1 -1
  40. data/lib/branston/coverage/app-models-story_rb.html +192 -18
  41. data/lib/branston/coverage/app-models-user_rb.html +33 -9
  42. data/lib/branston/coverage/app-models-user_role_rb.html +1 -1
  43. data/lib/branston/coverage/index.html +13 -13
  44. data/lib/branston/coverage/lib-client_rb.html +1 -1
  45. data/lib/branston/coverage/lib-faker_extras_rb.html +1 -1
  46. data/lib/branston/coverage/lib-story_generator_rb.html +1 -1
  47. data/lib/branston/db/development.sqlite3 +0 -0
  48. data/lib/branston/db/migrate/20091223100903_add_status_to_story.rb +11 -0
  49. data/lib/branston/db/pristine.sqlite3 +0 -0
  50. data/lib/branston/db/schema.rb +5 -3
  51. data/lib/branston/db/test.sqlite3 +0 -0
  52. data/lib/branston/lib/branston.rb +4 -2
  53. data/lib/branston/log/development.log +4970 -0
  54. data/lib/branston/log/test.log +88225 -0
  55. data/lib/branston/test/blueprints.rb +10 -7
  56. data/lib/branston/test/functional/scenarios_controller_test.rb +22 -15
  57. data/lib/branston/test/functional/stories_controller_test.rb +51 -30
  58. data/lib/branston/test/unit/story_test.rb +47 -7
  59. data/lib/branston/test/unit/user_test.rb +4 -0
  60. data/lib/branston/tmp/performance/BrowsingTest#test_homepage_process_time_flat.txt +3 -2
  61. data/lib/branston/tmp/performance/BrowsingTest#test_homepage_process_time_graph.html +2041 -1307
  62. data/lib/branston/tmp/performance/BrowsingTest#test_homepage_process_time_tree.txt +7922 -7922
  63. data/lib/branston/vendor/plugins/state_machine/CHANGELOG.rdoc +298 -0
  64. data/lib/branston/vendor/plugins/state_machine/LICENSE +20 -0
  65. data/lib/branston/vendor/plugins/state_machine/README.rdoc +466 -0
  66. data/lib/branston/vendor/plugins/state_machine/Rakefile +98 -0
  67. data/lib/branston/vendor/plugins/state_machine/examples/AutoShop_state.png +0 -0
  68. data/lib/branston/vendor/plugins/state_machine/examples/Car_state.png +0 -0
  69. data/lib/branston/vendor/plugins/state_machine/examples/TrafficLight_state.png +0 -0
  70. data/lib/branston/vendor/plugins/state_machine/examples/Vehicle_state.png +0 -0
  71. data/lib/branston/vendor/plugins/state_machine/examples/auto_shop.rb +11 -0
  72. data/lib/branston/vendor/plugins/state_machine/examples/car.rb +19 -0
  73. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/controller.rb +51 -0
  74. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/model.rb +28 -0
  75. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/view_edit.html.erb +24 -0
  76. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/view_index.html.erb +23 -0
  77. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/view_new.html.erb +13 -0
  78. data/lib/branston/vendor/plugins/state_machine/examples/merb-rest/view_show.html.erb +17 -0
  79. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/controller.rb +43 -0
  80. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/migration.rb +11 -0
  81. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/model.rb +23 -0
  82. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/view_edit.html.erb +25 -0
  83. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/view_index.html.erb +23 -0
  84. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/view_new.html.erb +14 -0
  85. data/lib/branston/vendor/plugins/state_machine/examples/rails-rest/view_show.html.erb +17 -0
  86. data/lib/branston/vendor/plugins/state_machine/examples/traffic_light.rb +7 -0
  87. data/lib/branston/vendor/plugins/state_machine/examples/vehicle.rb +31 -0
  88. data/lib/branston/vendor/plugins/state_machine/init.rb +1 -0
  89. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/assertions.rb +36 -0
  90. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/callback.rb +189 -0
  91. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/condition_proxy.rb +94 -0
  92. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/eval_helpers.rb +67 -0
  93. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/event.rb +252 -0
  94. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/event_collection.rb +122 -0
  95. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/extensions.rb +149 -0
  96. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/guard.rb +230 -0
  97. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/active_record/locale.rb +11 -0
  98. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/active_record/observer.rb +41 -0
  99. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/active_record.rb +492 -0
  100. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
  101. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/data_mapper.rb +351 -0
  102. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations/sequel.rb +322 -0
  103. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/integrations.rb +68 -0
  104. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/machine.rb +1467 -0
  105. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/machine_collection.rb +155 -0
  106. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/matcher.rb +123 -0
  107. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/matcher_helpers.rb +54 -0
  108. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/node_collection.rb +152 -0
  109. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/state.rb +249 -0
  110. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/state_collection.rb +112 -0
  111. data/lib/branston/vendor/plugins/state_machine/lib/state_machine/transition.rb +394 -0
  112. data/lib/branston/vendor/plugins/state_machine/lib/state_machine.rb +388 -0
  113. data/lib/branston/vendor/plugins/state_machine/state_machine.gemspec +30 -0
  114. data/lib/branston/vendor/plugins/state_machine/tasks/state_machine.rake +1 -0
  115. data/lib/branston/vendor/plugins/state_machine/tasks/state_machine.rb +30 -0
  116. data/lib/branston/vendor/plugins/state_machine/test/classes/switch.rb +11 -0
  117. data/lib/branston/vendor/plugins/state_machine/test/functional/state_machine_test.rb +941 -0
  118. data/lib/branston/vendor/plugins/state_machine/test/test_helper.rb +4 -0
  119. data/lib/branston/vendor/plugins/state_machine/test/unit/assertions_test.rb +40 -0
  120. data/lib/branston/vendor/plugins/state_machine/test/unit/callback_test.rb +455 -0
  121. data/lib/branston/vendor/plugins/state_machine/test/unit/condition_proxy_test.rb +328 -0
  122. data/lib/branston/vendor/plugins/state_machine/test/unit/eval_helpers_test.rb +120 -0
  123. data/lib/branston/vendor/plugins/state_machine/test/unit/event_collection_test.rb +326 -0
  124. data/lib/branston/vendor/plugins/state_machine/test/unit/event_test.rb +743 -0
  125. data/lib/branston/vendor/plugins/state_machine/test/unit/guard_test.rb +908 -0
  126. data/lib/branston/vendor/plugins/state_machine/test/unit/integrations/active_record_test.rb +1367 -0
  127. data/lib/branston/vendor/plugins/state_machine/test/unit/integrations/data_mapper_test.rb +962 -0
  128. data/lib/branston/vendor/plugins/state_machine/test/unit/integrations/sequel_test.rb +859 -0
  129. data/lib/branston/vendor/plugins/state_machine/test/unit/integrations_test.rb +42 -0
  130. data/lib/branston/vendor/plugins/state_machine/test/unit/invalid_event_test.rb +7 -0
  131. data/lib/branston/vendor/plugins/state_machine/test/unit/invalid_transition_test.rb +7 -0
  132. data/lib/branston/vendor/plugins/state_machine/test/unit/machine_collection_test.rb +938 -0
  133. data/lib/branston/vendor/plugins/state_machine/test/unit/machine_test.rb +2004 -0
  134. data/lib/branston/vendor/plugins/state_machine/test/unit/matcher_helpers_test.rb +37 -0
  135. data/lib/branston/vendor/plugins/state_machine/test/unit/matcher_test.rb +155 -0
  136. data/lib/branston/vendor/plugins/state_machine/test/unit/node_collection_test.rb +207 -0
  137. data/lib/branston/vendor/plugins/state_machine/test/unit/state_collection_test.rb +280 -0
  138. data/lib/branston/vendor/plugins/state_machine/test/unit/state_machine_test.rb +31 -0
  139. data/lib/branston/vendor/plugins/state_machine/test/unit/state_test.rb +795 -0
  140. data/lib/branston/vendor/plugins/state_machine/test/unit/transition_test.rb +1212 -0
  141. metadata +81 -2
@@ -0,0 +1,1467 @@
1
+ require 'state_machine/extensions'
2
+ require 'state_machine/assertions'
3
+ require 'state_machine/integrations'
4
+
5
+ require 'state_machine/state'
6
+ require 'state_machine/event'
7
+ require 'state_machine/callback'
8
+ require 'state_machine/node_collection'
9
+ require 'state_machine/state_collection'
10
+ require 'state_machine/event_collection'
11
+ require 'state_machine/matcher_helpers'
12
+
13
+ module StateMachine
14
+ # Represents a state machine for a particular attribute. State machines
15
+ # consist of states, events and a set of transitions that define how the
16
+ # state changes after a particular event is fired.
17
+ #
18
+ # A state machine will not know all of the possible states for an object
19
+ # unless they are referenced *somewhere* in the state machine definition.
20
+ # As a result, any unused states should be defined with the +other_states+
21
+ # or +state+ helper.
22
+ #
23
+ # == Actions
24
+ #
25
+ # When an action is configured for a state machine, it is invoked when an
26
+ # object transitions via an event. The success of the event becomes
27
+ # dependent on the success of the action. If the action is successful, then
28
+ # the transitioned state remains persisted. However, if the action fails
29
+ # (by returning false), the transitioned state will be rolled back.
30
+ #
31
+ # For example,
32
+ #
33
+ # class Vehicle
34
+ # attr_accessor :fail, :saving_state
35
+ #
36
+ # state_machine :initial => :parked, :action => :save do
37
+ # event :ignite do
38
+ # transition :parked => :idling
39
+ # end
40
+ #
41
+ # event :park do
42
+ # transition :idling => :parked
43
+ # end
44
+ # end
45
+ #
46
+ # def save
47
+ # @saving_state = state
48
+ # fail != true
49
+ # end
50
+ # end
51
+ #
52
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
53
+ # vehicle.save # => true
54
+ # vehicle.saving_state # => "parked" # The state was "parked" was save was called
55
+ #
56
+ # # Successful event
57
+ # vehicle.ignite # => true
58
+ # vehicle.saving_state # => "idling" # The state was "idling" when save was called
59
+ # vehicle.state # => "idling"
60
+ #
61
+ # # Failed event
62
+ # vehicle.fail = true
63
+ # vehicle.park # => false
64
+ # vehicle.saving_state # => "parked"
65
+ # vehicle.state # => "idling"
66
+ #
67
+ # As shown, even though the state is set prior to calling the +save+ action
68
+ # on the object, it will be rolled back to the original state if the action
69
+ # fails. *Note* that this will also be the case if an exception is raised
70
+ # while calling the action.
71
+ #
72
+ # === Indirect transitions
73
+ #
74
+ # In addition to the action being run as the _result_ of an event, the action
75
+ # can also be used to run events itself. For example, using the above as an
76
+ # example:
77
+ #
78
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
79
+ #
80
+ # vehicle.state_event = 'ignite'
81
+ # vehicle.save # => true
82
+ # vehicle.state # => "idling"
83
+ # vehicle.state_event # => nil
84
+ #
85
+ # As can be seen, the +save+ action automatically invokes the event stored in
86
+ # the +state_event+ attribute (<tt>:ignite</tt> in this case).
87
+ #
88
+ # One important note about using this technique for running transitions is
89
+ # that if the class in which the state machine is defined *also* defines the
90
+ # action being invoked (and not a superclass), then it must manually run the
91
+ # StateMachine hook that checks for event attributes.
92
+ #
93
+ # For example, in ActiveRecord, DataMapper, and Sequel, the default action
94
+ # (+save+) is already defined in a base class. As a result, when a state
95
+ # machine is defined in a model / resource, StateMachine can automatically
96
+ # hook into the +save+ action.
97
+ #
98
+ # On the other hand, the Vehicle class from above defined its own +save+
99
+ # method (and there is no +save+ method in its superclass). As a result, it
100
+ # must be modified like so:
101
+ #
102
+ # def save
103
+ # self.class.state_machines.fire_event_attributes(self, :save) do
104
+ # @saving_state = state
105
+ # fail != true
106
+ # end
107
+ # end
108
+ #
109
+ # This will add in the functionality for firing the event stored in the
110
+ # +state_event+ attribute.
111
+ #
112
+ # == Callbacks
113
+ #
114
+ # Callbacks are supported for hooking before and after every possible
115
+ # transition in the machine. Each callback is invoked in the order in which
116
+ # it was defined. See StateMachine::Machine#before_transition and
117
+ # StateMachine::Machine#after_transition for documentation on how to define
118
+ # new callbacks.
119
+ #
120
+ # *Note* that callbacks only get executed within the context of an event. As
121
+ # a result, if a class has an initial state when it's created, any callbacks
122
+ # that would normally get executed when the object enters that state will
123
+ # *not* get triggered.
124
+ #
125
+ # For example,
126
+ #
127
+ # class Vehicle
128
+ # state_machine :initial => :parked do
129
+ # after_transition all => :parked do
130
+ # raise ArgumentError
131
+ # end
132
+ # ...
133
+ # end
134
+ # end
135
+ #
136
+ # vehicle = Vehicle.new # => #<Vehicle id: 1, state: "parked">
137
+ # vehicle.save # => true (no exception raised)
138
+ #
139
+ # If you need callbacks to get triggered when an object is created, this
140
+ # should be done by either:
141
+ # * Use a <tt>before :save</tt> or equivalent hook, or
142
+ # * Set an initial state of nil and use the correct event to create the
143
+ # object with the proper state, resulting in callbacks being triggered and
144
+ # the object getting persisted
145
+ #
146
+ # === Canceling callbacks
147
+ #
148
+ # Callbacks can be canceled by throwing :halt at any point during the
149
+ # callback. For example,
150
+ #
151
+ # ...
152
+ # throw :halt
153
+ # ...
154
+ #
155
+ # If a +before+ callback halts the chain, the associated transition and all
156
+ # later callbacks are canceled. If an +after+ callback halts the chain,
157
+ # the later callbacks are canceled, but the transition is still successful.
158
+ #
159
+ # *Note* that if a +before+ callback fails and the bang version of an event
160
+ # was invoked, an exception will be raised instead of returning false. For
161
+ # example,
162
+ #
163
+ # class Vehicle
164
+ # state_machine :initial => :parked do
165
+ # before_transition any => :idling, :do => lambda {|vehicle| throw :halt}
166
+ # ...
167
+ # end
168
+ # end
169
+ #
170
+ # vehicle = Vehicle.new
171
+ # vehicle.park # => false
172
+ # vehicle.park! # => StateMachine::InvalidTransition: Cannot transition state via :park from "idling"
173
+ #
174
+ # == Observers
175
+ #
176
+ # Observers, in the sense of external classes and *not* Ruby's Observable
177
+ # mechanism, can hook into state machines as well. Such observers use the
178
+ # same callback api that's used internally.
179
+ #
180
+ # Below are examples of defining observers for the following state machine:
181
+ #
182
+ # class Vehicle
183
+ # state_machine do
184
+ # event :park do
185
+ # transition :idling => :parked
186
+ # end
187
+ # ...
188
+ # end
189
+ # ...
190
+ # end
191
+ #
192
+ # Event/Transition behaviors:
193
+ #
194
+ # class VehicleObserver
195
+ # def self.before_park(vehicle, transition)
196
+ # logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}"
197
+ # end
198
+ #
199
+ # def self.after_park(vehicle, transition, result)
200
+ # logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}"
201
+ # end
202
+ #
203
+ # def self.before_transition(vehicle, transition)
204
+ # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}"
205
+ # end
206
+ #
207
+ # def self.after_transition(vehicle, transition)
208
+ # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
209
+ # end
210
+ # end
211
+ #
212
+ # Vehicle.state_machine do
213
+ # before_transition :on => :park, :do => VehicleObserver.method(:before_park)
214
+ # before_transition VehicleObserver.method(:before_transition)
215
+ #
216
+ # after_transition :on => :park, :do => VehicleObserver.method(:after_park)
217
+ # after_transition VehicleObserver.method(:after_transition)
218
+ # end
219
+ #
220
+ # One common callback is to record transitions for all models in the system
221
+ # for auditing/debugging purposes. Below is an example of an observer that
222
+ # can easily automate this process for all models:
223
+ #
224
+ # class StateMachineObserver
225
+ # def self.before_transition(object, transition)
226
+ # Audit.log_transition(object.attributes)
227
+ # end
228
+ # end
229
+ #
230
+ # [Vehicle, Switch, Project].each do |klass|
231
+ # klass.state_machines.each do |attribute, machine|
232
+ # machine.before_transition StateMachineObserver.method(:before_transition)
233
+ # end
234
+ # end
235
+ #
236
+ # Additional observer-like behavior may be exposed by the various integrations
237
+ # available. See below for more information on integrations.
238
+ #
239
+ # == Overriding instance / class methods
240
+ #
241
+ # Hooking in behavior to the generated instance / class methods from the
242
+ # state machine, events, and states is very simple because of the way these
243
+ # methods are generated on the class. Using the class's ancestors, the
244
+ # original generated method can be referred to via +super+. For example,
245
+ #
246
+ # class Vehicle
247
+ # state_machine do
248
+ # event :park do
249
+ # ...
250
+ # end
251
+ # end
252
+ #
253
+ # def park(*args)
254
+ # logger.info "..."
255
+ # super
256
+ # end
257
+ # end
258
+ #
259
+ # In the above example, the +park+ instance method that's generated on the
260
+ # Vehicle class (by the associated event) is overridden with custom behavior.
261
+ # Once this behavior is complete, the original method from the state machine
262
+ # is invoked by simply calling +super+.
263
+ #
264
+ # The same technique can be used for +state+, +state_name+, and all other
265
+ # instance *and* class methods on the Vehicle class.
266
+ #
267
+ # == Integrations
268
+ #
269
+ # By default, state machines are library-agnostic, meaning that they work
270
+ # on any Ruby class and have no external dependencies. However, there are
271
+ # certain libraries which expose additional behavior that can be taken
272
+ # advantage of by state machines.
273
+ #
274
+ # This library is built to work out of the box with a few popular Ruby
275
+ # libraries that allow for additional behavior to provide a cleaner and
276
+ # smoother experience. This is especially the case for objects backed by a
277
+ # database that may allow for transactions, persistent storage,
278
+ # search/filters, callbacks, etc.
279
+ #
280
+ # When a state machine is defined for classes using any of the above libraries,
281
+ # it will try to automatically determine the integration to use (Agnostic,
282
+ # ActiveRecord, DataMapper, or Sequel) based on the class definition. To
283
+ # see how each integration affects the machine's behavior, refer to all
284
+ # constants defined under the StateMachine::Integrations namespace.
285
+ class Machine
286
+ include Assertions
287
+ include MatcherHelpers
288
+
289
+ class << self
290
+ # Attempts to find or create a state machine for the given class. For
291
+ # example,
292
+ #
293
+ # StateMachine::Machine.find_or_create(Vehicle)
294
+ # StateMachine::Machine.find_or_create(Vehicle, :initial => :parked)
295
+ # StateMachine::Machine.find_or_create(Vehicle, :status)
296
+ # StateMachine::Machine.find_or_create(Vehicle, :status, :initial => :parked)
297
+ #
298
+ # If a machine of the given name already exists in one of the class's
299
+ # superclasses, then a copy of that machine will be created and stored
300
+ # in the new owner class (the original will remain unchanged).
301
+ def find_or_create(owner_class, *args, &block)
302
+ options = args.last.is_a?(Hash) ? args.pop : {}
303
+ name = args.first || :state
304
+
305
+ # Find an existing machine
306
+ if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[name]
307
+ # Only create a new copy if changes are being made to the machine in
308
+ # a subclass
309
+ if machine.owner_class != owner_class && (options.any? || block_given?)
310
+ machine = machine.clone
311
+ machine.initial_state = options[:initial] if options.include?(:initial)
312
+ machine.owner_class = owner_class
313
+ end
314
+
315
+ # Evaluate DSL
316
+ machine.instance_eval(&block) if block_given?
317
+ else
318
+ # No existing machine: create a new one
319
+ machine = new(owner_class, name, options, &block)
320
+ end
321
+
322
+ machine
323
+ end
324
+
325
+ # Draws the state machines defined in the given classes using GraphViz.
326
+ # The given classes must be a comma-delimited string of class names.
327
+ #
328
+ # Configuration options:
329
+ # * <tt>:file</tt> - A comma-delimited string of files to load that
330
+ # contain the state machine definitions to draw
331
+ # * <tt>:path</tt> - The path to write the graph file to
332
+ # * <tt>:format</tt> - The image format to generate the graph in
333
+ # * <tt>:font</tt> - The name of the font to draw state names in
334
+ def draw(class_names, options = {})
335
+ raise ArgumentError, 'At least one class must be specified' unless class_names && class_names.split(',').any?
336
+
337
+ # Load any files
338
+ if files = options.delete(:file)
339
+ files.split(',').each {|file| require file}
340
+ end
341
+
342
+ class_names.split(',').each do |class_name|
343
+ # Navigate through the namespace structure to get to the class
344
+ klass = Object
345
+ class_name.split('::').each do |name|
346
+ klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name)
347
+ end
348
+
349
+ # Draw each of the class's state machines
350
+ klass.state_machines.each_value do |machine|
351
+ machine.draw(options)
352
+ end
353
+ end
354
+ end
355
+ end
356
+
357
+ # Default messages to use for validation errors in ORM integrations
358
+ class << self; attr_accessor :default_messages; end
359
+ @default_messages = {
360
+ :invalid => 'is invalid',
361
+ :invalid_event => 'cannot transition when %s',
362
+ :invalid_transition => 'cannot transition via "%s"'
363
+ }
364
+
365
+ # The class that the machine is defined in
366
+ attr_accessor :owner_class
367
+
368
+ # The name of the machine, used for scoping methods generated for the
369
+ # machine as a whole (not states or events)
370
+ attr_reader :name
371
+
372
+ # The events that trigger transitions. These are sorted, by default, in
373
+ # the order in which they were defined.
374
+ attr_reader :events
375
+
376
+ # A list of all of the states known to this state machine. This will pull
377
+ # states from the following sources:
378
+ # * Initial state
379
+ # * State behaviors
380
+ # * Event transitions (:to, :from, and :except_from options)
381
+ # * Transition callbacks (:to, :from, :except_to, and :except_from options)
382
+ # * Unreferenced states (using +other_states+ helper)
383
+ #
384
+ # These are sorted, by default, in the order in which they were referenced.
385
+ attr_reader :states
386
+
387
+ # The callbacks to invoke before/after a transition is performed
388
+ #
389
+ # Maps :before => callbacks and :after => callbacks
390
+ attr_reader :callbacks
391
+
392
+ # The action to invoke when an object transitions
393
+ attr_reader :action
394
+
395
+ # An identifier that forces all methods (including state predicates and
396
+ # event methods) to be generated with the value prefixed or suffixed,
397
+ # depending on the context.
398
+ attr_reader :namespace
399
+
400
+ # Whether the machine will use transactions when firing events
401
+ attr_reader :use_transactions
402
+
403
+ # Creates a new state machine for the given attribute
404
+ def initialize(owner_class, *args, &block)
405
+ options = args.last.is_a?(Hash) ? args.pop : {}
406
+ assert_valid_keys(options, :attribute, :initial, :action, :plural, :namespace, :integration, :messages, :use_transactions)
407
+
408
+ # Find an integration that matches this machine's owner class
409
+ if integration = options[:integration] ? StateMachine::Integrations.find(options[:integration]) : StateMachine::Integrations.match(owner_class)
410
+ extend integration
411
+ options = integration.defaults.merge(options) if integration.respond_to?(:defaults)
412
+ end
413
+
414
+ # Add machine-wide defaults
415
+ options = {:use_transactions => true}.merge(options)
416
+
417
+ # Set machine configuration
418
+ @name = args.first || :state
419
+ @attribute = options[:attribute] || @name
420
+ @events = EventCollection.new(self)
421
+ @states = StateCollection.new(self)
422
+ @callbacks = {:before => [], :after => []}
423
+ @namespace = options[:namespace]
424
+ @messages = options[:messages] || {}
425
+ @action = options[:action]
426
+ @use_transactions = options[:use_transactions]
427
+ self.owner_class = owner_class
428
+ self.initial_state = options[:initial]
429
+
430
+ # Define class integration
431
+ define_helpers
432
+ define_scopes(options[:plural])
433
+ after_initialize
434
+
435
+ # Evaluate DSL
436
+ instance_eval(&block) if block_given?
437
+ end
438
+
439
+ # Creates a copy of this machine in addition to copies of each associated
440
+ # event/states/callback, so that the modifications to those collections do
441
+ # not affect the original machine.
442
+ def initialize_copy(orig) #:nodoc:
443
+ super
444
+
445
+ @events = @events.dup
446
+ @events.machine = self
447
+ @states = @states.dup
448
+ @states.machine = self
449
+ @callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup}
450
+ end
451
+
452
+ # Sets the class which is the owner of this state machine. Any methods
453
+ # generated by states, events, or other parts of the machine will be defined
454
+ # on the given owner class.
455
+ def owner_class=(klass)
456
+ @owner_class = klass
457
+
458
+ # Create modules for extending the class with state/event-specific methods
459
+ class_helper_module = @class_helper_module = Module.new
460
+ instance_helper_module = @instance_helper_module = Module.new
461
+ owner_class.class_eval do
462
+ extend class_helper_module
463
+ include instance_helper_module
464
+ end
465
+
466
+ # Add class-/instance-level methods to the owner class for state initialization
467
+ unless owner_class.included_modules.include?(StateMachine::InstanceMethods)
468
+ owner_class.class_eval do
469
+ extend StateMachine::ClassMethods
470
+ include StateMachine::InstanceMethods
471
+ end
472
+
473
+ define_state_initializer
474
+ end
475
+
476
+ # Record this machine as matched to the name in the current owner class.
477
+ # This will override any machines mapped to the same name in any superclasses.
478
+ owner_class.state_machines[name] = self
479
+ end
480
+
481
+ # Sets the initial state of the machine. This can be either the static name
482
+ # of a state or a lambda block which determines the initial state at
483
+ # creation time.
484
+ def initial_state=(new_initial_state)
485
+ @initial_state = new_initial_state
486
+ add_states([@initial_state]) unless dynamic_initial_state?
487
+
488
+ # Update all states to reflect the new initial state
489
+ states.each {|state| state.initial = (state.name == @initial_state)}
490
+ end
491
+
492
+ # Gets the actual name of the attribute on the machine's owner class that
493
+ # stores data with the given name.
494
+ def attribute(name = :state)
495
+ name == :state ? @attribute : :"#{self.name}_#{name}"
496
+ end
497
+
498
+ # Defines a new instance method with the given name on the machine's owner
499
+ # class. If the method is already defined in the class, then this will not
500
+ # override it.
501
+ #
502
+ # Example:
503
+ #
504
+ # machine.define_instance_method(:state_name) do |machine, object|
505
+ # machine.states.match(object)
506
+ # end
507
+ def define_instance_method(method, &block)
508
+ name = self.name
509
+
510
+ @instance_helper_module.class_eval do
511
+ define_method(method) do |*args|
512
+ block.call(self.class.state_machine(name), self, *args)
513
+ end
514
+ end
515
+ end
516
+ attr_reader :instance_helper_module
517
+
518
+ # Defines a new class method with the given name on the machine's owner
519
+ # class. If the method is already defined in the class, then this will not
520
+ # override it.
521
+ #
522
+ # Example:
523
+ #
524
+ # machine.define_class_method(:states) do |machine, klass|
525
+ # machine.states.keys
526
+ # end
527
+ def define_class_method(method, &block)
528
+ name = self.name
529
+
530
+ @class_helper_module.class_eval do
531
+ define_method(method) do |*args|
532
+ block.call(self.state_machine(name), self, *args)
533
+ end
534
+ end
535
+ end
536
+
537
+ # Gets the initial state of the machine for the given object. If a dynamic
538
+ # initial state was configured for this machine, then the object will be
539
+ # passed into the lambda block to help determine the actual state.
540
+ #
541
+ # == Examples
542
+ #
543
+ # With a static initial state:
544
+ #
545
+ # class Vehicle
546
+ # state_machine :initial => :parked do
547
+ # ...
548
+ # end
549
+ # end
550
+ #
551
+ # vehicle = Vehicle.new
552
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
553
+ #
554
+ # With a dynamic initial state:
555
+ #
556
+ # class Vehicle
557
+ # attr_accessor :force_idle
558
+ #
559
+ # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do
560
+ # ...
561
+ # end
562
+ # end
563
+ #
564
+ # vehicle = Vehicle.new
565
+ #
566
+ # vehicle.force_idle = true
567
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=false>
568
+ #
569
+ # vehicle.force_idle = false
570
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=false>
571
+ def initial_state(object)
572
+ states.fetch(dynamic_initial_state? ? @initial_state.call(object) : @initial_state)
573
+ end
574
+
575
+ # Whether a dynamic initial state is being used in the machine
576
+ def dynamic_initial_state?
577
+ @initial_state.is_a?(Proc)
578
+ end
579
+
580
+ # Customizes the definition of one or more states in the machine.
581
+ #
582
+ # Configuration options:
583
+ # * <tt>:value</tt> - The actual value to store when an object transitions
584
+ # to the state. Default is the name (stringified).
585
+ # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
586
+ # then setting this to true will cache the evaluated result
587
+ # * <tt>:if</tt> - Determines whether an object's value matches the state
588
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
589
+ # By default, the configured value is matched.
590
+ #
591
+ # == Customizing the stored value
592
+ #
593
+ # Whenever a state is automatically discovered in the state machine, its
594
+ # default value is assumed to be the stringified version of the name. For
595
+ # example,
596
+ #
597
+ # class Vehicle
598
+ # state_machine :initial => :parked do
599
+ # event :ignite do
600
+ # transition :parked => :idling
601
+ # end
602
+ # end
603
+ # end
604
+ #
605
+ # In the above state machine, there are two states automatically discovered:
606
+ # :parked and :idling. These states, by default, will store their stringified
607
+ # equivalents when an object moves into that state (e.g. "parked" / "idling").
608
+ #
609
+ # For legacy systems or when tying state machines into existing frameworks,
610
+ # it's oftentimes necessary to need to store a different value for a state
611
+ # than the default. In order to continue taking advantage of an expressive
612
+ # state machine and helper methods, every defined state can be re-configured
613
+ # with a custom stored value. For example,
614
+ #
615
+ # class Vehicle
616
+ # state_machine :initial => :parked do
617
+ # event :ignite do
618
+ # transition :parked => :idling
619
+ # end
620
+ #
621
+ # state :idling, :value => 'IDLING'
622
+ # state :parked, :value => 'PARKED
623
+ # end
624
+ # end
625
+ #
626
+ # This is also useful if being used in association with a database and,
627
+ # instead of storing the state name in a column, you want to store the
628
+ # state's foreign key:
629
+ #
630
+ # class VehicleState < ActiveRecord::Base
631
+ # end
632
+ #
633
+ # class Vehicle < ActiveRecord::Base
634
+ # state_machine :attribute => :state_id, :initial => :parked do
635
+ # event :ignite do
636
+ # transition :parked => :idling
637
+ # end
638
+ #
639
+ # states.each do |state|
640
+ # self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
641
+ # end
642
+ # end
643
+ # end
644
+ #
645
+ # In the above example, each known state is configured to store it's
646
+ # associated database id in the +state_id+ attribute. Also, notice that a
647
+ # lambda block is used to define the state's value. This is required in
648
+ # situations (like testing) where the model is loaded without any existing
649
+ # data (i.e. no VehicleState records available).
650
+ #
651
+ # One caveat to the above example is to keep performance in mind. To avoid
652
+ # constant db hits for looking up the VehicleState ids, the value is cached
653
+ # by specifying the <tt>:cache</tt> option. Alternatively, a custom
654
+ # caching strategy can be used like so:
655
+ #
656
+ # class VehicleState < ActiveRecord::Base
657
+ # cattr_accessor :cache_store
658
+ # self.cache_store = ActiveSupport::Cache::MemoryStore.new
659
+ #
660
+ # def self.find_by_name(name)
661
+ # cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
662
+ # end
663
+ # end
664
+ #
665
+ # === Dynamic values
666
+ #
667
+ # In addition to customizing states with other value types, lambda blocks
668
+ # can also be specified to allow for a state's value to be determined
669
+ # dynamically at runtime. For example,
670
+ #
671
+ # class Vehicle
672
+ # state_machine :purchased_at, :initial => :available do
673
+ # event :purchase do
674
+ # transition all => :purchased
675
+ # end
676
+ #
677
+ # event :restock do
678
+ # transition all => :available
679
+ # end
680
+ #
681
+ # state :available, :value => nil
682
+ # state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
683
+ # end
684
+ # end
685
+ #
686
+ # In the above definition, the <tt>:purchased</tt> state is customized with
687
+ # both a dynamic value *and* a value matcher.
688
+ #
689
+ # When an object transitions to the purchased state, the value's lambda
690
+ # block will be called. This will get the current time and store it in the
691
+ # object's +purchased_at+ attribute.
692
+ #
693
+ # *Note* that the custom matcher is very important here. Since there's no
694
+ # way for the state machine to figure out an object's state when it's set to
695
+ # a runtime value, it must be explicitly defined. If the <tt>:if</tt> option
696
+ # were not configured for the state, then an ArgumentError exception would
697
+ # be raised at runtime, indicating that the state machine could not figure
698
+ # out what the current state of the object was.
699
+ #
700
+ # == Behaviors
701
+ #
702
+ # Behaviors define a series of methods to mixin with objects when the current
703
+ # state matches the given one(s). This allows instance methods to behave
704
+ # a specific way depending on what the value of the object's state is.
705
+ #
706
+ # For example,
707
+ #
708
+ # class Vehicle
709
+ # attr_accessor :driver
710
+ # attr_accessor :passenger
711
+ #
712
+ # state_machine :initial => :parked do
713
+ # event :ignite do
714
+ # transition :parked => :idling
715
+ # end
716
+ #
717
+ # state :parked do
718
+ # def speed
719
+ # 0
720
+ # end
721
+ #
722
+ # def rotate_driver
723
+ # driver = self.driver
724
+ # self.driver = passenger
725
+ # self.passenger = driver
726
+ # true
727
+ # end
728
+ # end
729
+ #
730
+ # state :idling, :first_gear do
731
+ # def speed
732
+ # 20
733
+ # end
734
+ #
735
+ # def rotate_driver
736
+ # self.state = 'parked'
737
+ # rotate_driver
738
+ # end
739
+ # end
740
+ #
741
+ # other_states :backing_up
742
+ # end
743
+ # end
744
+ #
745
+ # In the above example, there are two dynamic behaviors defined for the
746
+ # class:
747
+ # * +speed+
748
+ # * +rotate_driver+
749
+ #
750
+ # Each of these behaviors are instance methods on the Vehicle class. However,
751
+ # which method actually gets invoked is based on the current state of the
752
+ # object. Using the above class as the example:
753
+ #
754
+ # vehicle = Vehicle.new
755
+ # vehicle.driver = 'John'
756
+ # vehicle.passenger = 'Jane'
757
+ #
758
+ # # Behaviors in the "parked" state
759
+ # vehicle.state # => "parked"
760
+ # vehicle.speed # => 0
761
+ # vehicle.rotate_driver # => true
762
+ # vehicle.driver # => "Jane"
763
+ # vehicle.passenger # => "John"
764
+ #
765
+ # vehicle.ignite # => true
766
+ #
767
+ # # Behaviors in the "idling" state
768
+ # vehicle.state # => "idling"
769
+ # vehicle.speed # => 20
770
+ # vehicle.rotate_driver # => true
771
+ # vehicle.driver # => "John"
772
+ # vehicle.passenger # => "Jane"
773
+ #
774
+ # As can be seen, both the +speed+ and +rotate_driver+ instance method
775
+ # implementations changed how they behave based on what the current state
776
+ # of the vehicle was.
777
+ #
778
+ # === Invalid behaviors
779
+ #
780
+ # If a specific behavior has not been defined for a state, then a
781
+ # NoMethodError exception will be raised, indicating that that method would
782
+ # not normally exist for an object with that state.
783
+ #
784
+ # Using the example from before:
785
+ #
786
+ # vehicle = Vehicle.new
787
+ # vehicle.state = 'backing_up'
788
+ # vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
789
+ #
790
+ # == State-aware class methods
791
+ #
792
+ # In addition to defining scopes for instance methods that are state-aware,
793
+ # the same can be done for certain types of class methods.
794
+ #
795
+ # Some libraries have support for class-level methods that only run certain
796
+ # behaviors based on a conditions hash passed in. For example:
797
+ #
798
+ # class Vehicle < ActiveRecord::Base
799
+ # state_machine do
800
+ # ...
801
+ # state :first_gear, :second_gear, :third_gear do
802
+ # validates_presence_of :speed
803
+ # validates_inclusion_of :speed, :in => 0..25, :if => :in_school_zone?
804
+ # end
805
+ # end
806
+ # end
807
+ #
808
+ # In the above ActiveRecord model, two validations have been defined which
809
+ # will *only* run when the Vehicle object is in one of the three states:
810
+ # +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless
811
+ # conditions can continue to be used.
812
+ #
813
+ # This functionality is not library-specific and can work for any class-level
814
+ # method that is defined like so:
815
+ #
816
+ # def validates_presence_of(attribute, options = {})
817
+ # ...
818
+ # end
819
+ #
820
+ # The minimum requirement is that the last argument in the method be an
821
+ # options hash which contains at least <tt>:if</tt> condition support.
822
+ def state(*names, &block)
823
+ options = names.last.is_a?(Hash) ? names.pop : {}
824
+ assert_valid_keys(options, :value, :cache, :if)
825
+
826
+ states = add_states(names)
827
+ states.each do |state|
828
+ if options.include?(:value)
829
+ state.value = options[:value]
830
+ self.states.update(state)
831
+ end
832
+
833
+ state.cache = options[:cache] if options.include?(:cache)
834
+ state.matcher = options[:if] if options.include?(:if)
835
+ state.context(&block) if block_given?
836
+ end
837
+
838
+ states.length == 1 ? states.first : states
839
+ end
840
+ alias_method :other_states, :state
841
+
842
+ # Gets the current value stored in the given object's attribute.
843
+ #
844
+ # For example,
845
+ #
846
+ # class Vehicle
847
+ # state_machine :initial => :parked do
848
+ # ...
849
+ # end
850
+ # end
851
+ #
852
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
853
+ # Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state
854
+ # Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
855
+ def read(object, attribute, ivar = false)
856
+ attribute = self.attribute(attribute)
857
+ ivar ? object.instance_variable_get("@#{attribute}") : object.send(attribute)
858
+ end
859
+
860
+ # Sets a new value in the given object's attribute.
861
+ #
862
+ # For example,
863
+ #
864
+ # class Vehicle
865
+ # state_machine :initial => :parked do
866
+ # ...
867
+ # end
868
+ # end
869
+ #
870
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
871
+ # Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling'
872
+ # Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
873
+ # vehicle.state # => "idling"
874
+ # vehicle.event # => "park"
875
+ def write(object, attribute, value)
876
+ object.send("#{self.attribute(attribute)}=", value)
877
+ end
878
+
879
+ # Defines one or more events for the machine and the transitions that can
880
+ # be performed when those events are run.
881
+ #
882
+ # This method is also aliased as +on+ for improved compatibility with
883
+ # using a domain-specific language.
884
+ #
885
+ # == Instance methods
886
+ #
887
+ # The following instance methods are generated when a new event is defined
888
+ # (the "park" event is used as an example):
889
+ # * <tt>can_park?</tt> - Checks whether the "park" event can be fired given
890
+ # the current state of the object. This will *not* run validations in
891
+ # ORM integrations. To check whether an event can fire *and* passes
892
+ # validations, use event attributes (e.g. state_event) as described in the
893
+ # "Events" documentation of each ORM integration.
894
+ # * <tt>park_transition</tt> - Gets the next transition that would be
895
+ # performed if the "park" event were to be fired now on the object or nil
896
+ # if no transitions can be performed.
897
+ # * <tt>park(run_action = true)</tt> - Fires the "park" event, transitioning
898
+ # from the current state to the next valid state.
899
+ # * <tt>park!(run_action = true)</tt> - Fires the "park" event, transitioning
900
+ # from the current state to the next valid state. If the transition fails,
901
+ # then a StateMachine::InvalidTransition error will be raised.
902
+ #
903
+ # With a namespace of "car", the above names map to the following methods:
904
+ # * <tt>can_park_car?</tt>
905
+ # * <tt>park_car_transition</tt>
906
+ # * <tt>park_car</tt>
907
+ # * <tt>park_car!</tt>
908
+ #
909
+ # == Defining transitions
910
+ #
911
+ # +event+ requires a block which allows you to define the possible
912
+ # transitions that can happen as a result of that event. For example,
913
+ #
914
+ # event :park, :stop do
915
+ # transition :idling => :parked
916
+ # end
917
+ #
918
+ # event :first_gear do
919
+ # transition :parked => :first_gear, :if => :seatbelt_on?
920
+ # end
921
+ #
922
+ # See StateMachine::Event#transition for more information on
923
+ # the possible options that can be passed in.
924
+ #
925
+ # *Note* that this block is executed within the context of the actual event
926
+ # object. As a result, you will not be able to reference any class methods
927
+ # on the model without referencing the class itself. For example,
928
+ #
929
+ # class Vehicle
930
+ # def self.safe_states
931
+ # [:parked, :idling, :stalled]
932
+ # end
933
+ #
934
+ # state_machine do
935
+ # event :park do
936
+ # transition Vehicle.safe_states => :parked
937
+ # end
938
+ # end
939
+ # end
940
+ #
941
+ # == Defining additional arguments
942
+ #
943
+ # Additional arguments on event actions can be defined like so:
944
+ #
945
+ # class Vehicle
946
+ # state_machine do
947
+ # event :park do
948
+ # ...
949
+ # end
950
+ # end
951
+ #
952
+ # def park(kind = :parallel, *args)
953
+ # take_deep_breath if kind == :parallel
954
+ # super
955
+ # end
956
+ #
957
+ # def take_deep_breath
958
+ # sleep 3
959
+ # end
960
+ # end
961
+ #
962
+ # Note that +super+ is called instead of <tt>super(*args)</tt>. This
963
+ # allows the entire arguments list to be accessed by transition callbacks
964
+ # through StateMachine::Transition#args like so:
965
+ #
966
+ # after_transition :on => :park do |vehicle, transition|
967
+ # kind = *transition.args
968
+ # ...
969
+ # end
970
+ #
971
+ # == Example
972
+ #
973
+ # class Vehicle
974
+ # state_machine do
975
+ # # The park, stop, and halt events will all share the given transitions
976
+ # event :park, :stop, :halt do
977
+ # transition [:idling, :backing_up] => :parked
978
+ # end
979
+ #
980
+ # event :stop do
981
+ # transition :first_gear => :idling
982
+ # end
983
+ #
984
+ # event :ignite do
985
+ # transition :parked => :idling
986
+ # end
987
+ # end
988
+ # end
989
+ def event(*names, &block)
990
+ events = names.collect do |name|
991
+ unless event = self.events[name]
992
+ self.events << event = Event.new(self, name)
993
+ end
994
+
995
+ if block_given?
996
+ event.instance_eval(&block)
997
+ add_states(event.known_states)
998
+ end
999
+
1000
+ event
1001
+ end
1002
+
1003
+ events.length == 1 ? events.first : events
1004
+ end
1005
+ alias_method :on, :event
1006
+
1007
+ # Creates a callback that will be invoked *before* a transition is
1008
+ # performed so long as the given requirements match the transition.
1009
+ #
1010
+ # == The callback
1011
+ #
1012
+ # Callbacks must be defined as either an argument, in the :do option, or
1013
+ # as a block. For example,
1014
+ #
1015
+ # class Vehicle
1016
+ # state_machine do
1017
+ # before_transition :set_alarm
1018
+ # before_transition :set_alarm, all => :parked
1019
+ # before_transition all => :parked, :do => :set_alarm
1020
+ # before_transition all => :parked do |vehicle, transition|
1021
+ # vehicle.set_alarm
1022
+ # end
1023
+ # ...
1024
+ # end
1025
+ # end
1026
+ #
1027
+ # Notice that the first three callbacks are the same in terms of how the
1028
+ # methods to invoke are defined. However, using the <tt>:do</tt> can
1029
+ # provide for a more fluid DSL.
1030
+ #
1031
+ # In addition, multiple callbacks can be defined like so:
1032
+ #
1033
+ # class Vehicle
1034
+ # state_machine do
1035
+ # before_transition :set_alarm, :lock_doors, all => :parked
1036
+ # before_transition all => :parked, :do => [:set_alarm, :lock_doors]
1037
+ # before_transition :set_alarm do |vehicle, transition|
1038
+ # vehicle.lock_doors
1039
+ # end
1040
+ # end
1041
+ # end
1042
+ #
1043
+ # Notice that the different ways of configuring methods can be mixed.
1044
+ #
1045
+ # == State requirements
1046
+ #
1047
+ # Callbacks can require that the machine be transitioning from and to
1048
+ # specific states. These requirements use a Hash syntax to map beginning
1049
+ # states to ending states. For example,
1050
+ #
1051
+ # before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
1052
+ #
1053
+ # In this case, the +set_alarm+ callback will only be called if the machine
1054
+ # is transitioning from +parked+ to +idling+ or from +idling+ to +parked+.
1055
+ #
1056
+ # To help define state requirements, a set of helpers are available for
1057
+ # slightly more complex matching:
1058
+ # * <tt>all</tt> - Matches every state/event in the machine
1059
+ # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state/event except those specified
1060
+ # * <tt>any</tt> - An alias for +all+ (matches every state/event in the machine)
1061
+ # * <tt>same</tt> - Matches the same state being transitioned from
1062
+ #
1063
+ # See StateMachine::MatcherHelpers for more information.
1064
+ #
1065
+ # Examples:
1066
+ #
1067
+ # before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
1068
+ # before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
1069
+ # before_transition all => :parked, :do => ... # Matches all states to parked
1070
+ # before_transition any => same, :do => ... # Matches every loopback
1071
+ #
1072
+ # == Event requirements
1073
+ #
1074
+ # In addition to state requirements, an event requirement can be defined so
1075
+ # that the callback is only invoked on specific events using the +on+
1076
+ # option. This can also use the same matcher helpers as the state
1077
+ # requirements.
1078
+ #
1079
+ # Examples:
1080
+ #
1081
+ # before_transition :on => :ignite, :do => ... # Matches only on ignite
1082
+ # before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
1083
+ # before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
1084
+ #
1085
+ # == Result requirements
1086
+ #
1087
+ # By default, after_transition callbacks will only be run if the transition
1088
+ # was performed successfully. A transition is successful if the machine's
1089
+ # action is not configured or does not return false when it is invoked.
1090
+ # In order to include failed attempts when running an after_transition
1091
+ # callback, the :include_failures option can be specified like so:
1092
+ #
1093
+ # after_transition :include_failures => true, :do => ... # Runs on all attempts to transition, including failures
1094
+ # after_transition :do => ... # Runs only on successful attempts to transition
1095
+ #
1096
+ # == Verbose Requirements
1097
+ #
1098
+ # Requirements can also be defined using verbose options rather than the
1099
+ # implicit Hash syntax and helper methods described above.
1100
+ #
1101
+ # Configuration options:
1102
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
1103
+ # are specified, then all states will match.
1104
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
1105
+ # specified, then all states will match.
1106
+ # * <tt>:on</tt> - One or more events that fired the transition. If none
1107
+ # are specified, then all events will match.
1108
+ # * <tt>:except_from</tt> - One or more states *not* being transitioned from
1109
+ # * <tt>:except_to</tt> - One more states *not* being transitioned to
1110
+ # * <tt>:except_on</tt> - One or more events that *did not* fire the transition
1111
+ #
1112
+ # Examples:
1113
+ #
1114
+ # before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
1115
+ # before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
1116
+ #
1117
+ # == Conditions
1118
+ #
1119
+ # In addition to the state/event requirements, a condition can also be
1120
+ # defined to help determine whether the callback should be invoked.
1121
+ #
1122
+ # Configuration options:
1123
+ # * <tt>:if</tt> - A method, proc or string to call to determine if the
1124
+ # callback should occur (e.g. :if => :allow_callbacks, or
1125
+ # :if => lambda {|user| user.signup_step > 2}). The method, proc or string
1126
+ # should return or evaluate to a true or false value.
1127
+ # * <tt>:unless</tt> - A method, proc or string to call to determine if the
1128
+ # callback should not occur (e.g. :unless => :skip_callbacks, or
1129
+ # :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
1130
+ # string should return or evaluate to a true or false value.
1131
+ #
1132
+ # Examples:
1133
+ #
1134
+ # before_transition :parked => :idling, :if => :moving?, :do => ...
1135
+ # before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
1136
+ #
1137
+ # === Accessing the transition
1138
+ #
1139
+ # In addition to passing the object being transitioned, the actual
1140
+ # transition describing the context (e.g. event, from, to) can be accessed
1141
+ # as well. This additional argument is only passed if the callback allows
1142
+ # for it.
1143
+ #
1144
+ # For example,
1145
+ #
1146
+ # class Vehicle
1147
+ # # Only specifies one parameter (the object being transitioned)
1148
+ # before_transition all => :parked do |vehicle|
1149
+ # vehicle.set_alarm
1150
+ # end
1151
+ #
1152
+ # # Specifies 2 parameters (object being transitioned and actual transition)
1153
+ # before_transition all => :parked do |vehicle, transition|
1154
+ # vehicle.set_alarm(transition)
1155
+ # end
1156
+ # end
1157
+ #
1158
+ # *Note* that the object in the callback will only be passed in as an
1159
+ # argument if callbacks are configured to *not* be bound to the object
1160
+ # involved. This is the default and may change on a per-integration basis.
1161
+ #
1162
+ # See StateMachine::Transition for more information about the
1163
+ # attributes available on the transition.
1164
+ #
1165
+ # == Examples
1166
+ #
1167
+ # Below is an example of a class with one state machine and various types
1168
+ # of +before+ transitions defined for it:
1169
+ #
1170
+ # class Vehicle
1171
+ # state_machine do
1172
+ # # Before all transitions
1173
+ # before_transition :update_dashboard
1174
+ #
1175
+ # # Before specific transition:
1176
+ # before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
1177
+ #
1178
+ # # With conditional callback:
1179
+ # before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
1180
+ #
1181
+ # # Using helpers:
1182
+ # before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
1183
+ # ...
1184
+ # end
1185
+ # end
1186
+ #
1187
+ # As can be seen, any number of transitions can be created using various
1188
+ # combinations of configuration options.
1189
+ def before_transition(*args, &block)
1190
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1191
+ options[:do] = args if args.any?
1192
+ add_callback(:before, options, &block)
1193
+ end
1194
+
1195
+ # Creates a callback that will be invoked *after* a transition is
1196
+ # performed so long as the given requirements match the transition.
1197
+ #
1198
+ # See +before_transition+ for a description of the possible configurations
1199
+ # for defining callbacks.
1200
+ def after_transition(*args, &block)
1201
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1202
+ options[:do] = args if args.any?
1203
+ add_callback(:after, options, &block)
1204
+ end
1205
+
1206
+ # Marks the given object as invalid with the given message.
1207
+ #
1208
+ # By default, this is a no-op.
1209
+ def invalidate(object, attribute, message, values = [])
1210
+ end
1211
+
1212
+ # Resets any errors previously added when invalidating the given object.
1213
+ #
1214
+ # By default, this is a no-op.
1215
+ def reset(object)
1216
+ end
1217
+
1218
+ # Generates the message to use when invalidating the given object after
1219
+ # failing to transition on a specific event
1220
+ def generate_message(name, values = [])
1221
+ (@messages[name] || self.class.default_messages[name]) % values.map {|value| value.last}
1222
+ end
1223
+
1224
+ # Runs a transaction, rolling back any changes if the yielded block fails.
1225
+ #
1226
+ # This is only applicable to integrations that involve databases. By
1227
+ # default, this will not run any transactions since the changes aren't
1228
+ # taking place within the context of a database.
1229
+ def within_transaction(object)
1230
+ if use_transactions
1231
+ transaction(object) { yield }
1232
+ else
1233
+ yield
1234
+ end
1235
+ end
1236
+
1237
+ # Draws a directed graph of the machine for visualizing the various events,
1238
+ # states, and their transitions.
1239
+ #
1240
+ # This requires both the Ruby graphviz gem and the graphviz library be
1241
+ # installed on the system.
1242
+ #
1243
+ # Configuration options:
1244
+ # * <tt>:name</tt> - The name of the file to write to (without the file extension).
1245
+ # Default is "#{owner_class.name}_#{name}"
1246
+ # * <tt>:path</tt> - The path to write the graph file to. Default is the
1247
+ # current directory (".").
1248
+ # * <tt>:format</tt> - The image format to generate the graph in.
1249
+ # Default is "png'.
1250
+ # * <tt>:font</tt> - The name of the font to draw state names in.
1251
+ # Default is "Arial".
1252
+ # * <tt>:orientation</tt> - The direction of the graph ("portrait" or
1253
+ # "landscape"). Default is "portrait".
1254
+ # * <tt>:output</tt> - Whether to generate the output of the graph
1255
+ def draw(options = {})
1256
+ options = {
1257
+ :name => "#{owner_class.name}_#{name}",
1258
+ :path => '.',
1259
+ :format => 'png',
1260
+ :font => 'Arial',
1261
+ :orientation => 'portrait',
1262
+ :output => true
1263
+ }.merge(options)
1264
+ assert_valid_keys(options, :name, :path, :format, :font, :orientation, :output)
1265
+
1266
+ begin
1267
+ # Load the graphviz library
1268
+ require 'rubygems'
1269
+ require 'graphviz'
1270
+
1271
+ graph = GraphViz.new('G',
1272
+ :output => options[:format],
1273
+ :file => File.join(options[:path], "#{options[:name]}.#{options[:format]}"),
1274
+ :rankdir => options[:orientation] == 'landscape' ? 'LR' : 'TB'
1275
+ )
1276
+
1277
+ # Add nodes
1278
+ states.by_priority.each do |state|
1279
+ node = state.draw(graph)
1280
+ node.fontname = options[:font]
1281
+ end
1282
+
1283
+ # Add edges
1284
+ events.each do |event|
1285
+ edges = event.draw(graph)
1286
+ edges.each {|edge| edge.fontname = options[:font]}
1287
+ end
1288
+
1289
+ # Generate the graph
1290
+ graph.output if options[:output]
1291
+ graph
1292
+ rescue LoadError
1293
+ $stderr.puts 'Cannot draw the machine. `gem install ruby-graphviz` and try again.'
1294
+ false
1295
+ end
1296
+ end
1297
+
1298
+ protected
1299
+ # Runs additional initialization hooks. By default, this is a no-op.
1300
+ def after_initialize
1301
+ end
1302
+
1303
+ # Adds helper methods for interacting with the state machine, including
1304
+ # for states, events, and transitions
1305
+ def define_helpers
1306
+ define_state_accessor
1307
+ define_state_predicate
1308
+ define_event_helpers
1309
+ define_action_helpers if action
1310
+
1311
+ # Gets the state name for the current value
1312
+ define_instance_method(attribute(:name)) do |machine, object|
1313
+ machine.states.match!(object).name
1314
+ end
1315
+ end
1316
+
1317
+ # Defines the initial values for state machine attributes. Static values
1318
+ # are set prior to the original initialize method and dynamic values are
1319
+ # set *after* the initialize method in case it is dependent on it.
1320
+ def define_state_initializer
1321
+ @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
1322
+ def initialize(*args)
1323
+ initialize_state_machines(:dynamic => false)
1324
+ super
1325
+ initialize_state_machines(:dynamic => true)
1326
+ end
1327
+ end_eval
1328
+ end
1329
+
1330
+ # Adds reader/writer methods for accessing the state attribute
1331
+ def define_state_accessor
1332
+ attribute = self.attribute
1333
+
1334
+ @instance_helper_module.class_eval do
1335
+ attr_accessor attribute
1336
+ end
1337
+ end
1338
+
1339
+ # Adds predicate method to the owner class for determining the name of the
1340
+ # current state
1341
+ def define_state_predicate
1342
+ define_instance_method("#{name}?") do |machine, object, state|
1343
+ machine.states.matches?(object, state)
1344
+ end
1345
+ end
1346
+
1347
+ # Adds helper methods for getting information about this state machine's
1348
+ # events
1349
+ def define_event_helpers
1350
+ # Gets the events that are allowed to fire on the current object
1351
+ define_instance_method(attribute(:events)) do |machine, object|
1352
+ machine.events.valid_for(object).map {|event| event.name}
1353
+ end
1354
+
1355
+ # Gets the next possible transitions that can be run on the current
1356
+ # object
1357
+ define_instance_method(attribute(:transitions)) do |machine, object, *args|
1358
+ machine.events.transitions_for(object, *args)
1359
+ end
1360
+
1361
+ # Add helpers for interacting with the action
1362
+ if action
1363
+ # Tracks the event / transition to invoke when the action is called
1364
+ event_attribute = attribute(:event)
1365
+ event_transition_attribute = attribute(:event_transition)
1366
+ @instance_helper_module.class_eval do
1367
+ attr_writer event_attribute
1368
+
1369
+ protected
1370
+ attr_accessor event_transition_attribute
1371
+ end
1372
+
1373
+ # Interpret non-blank events as present
1374
+ define_instance_method(attribute(:event)) do |machine, object|
1375
+ event = machine.read(object, :event, true)
1376
+ event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
1377
+ end
1378
+ end
1379
+ end
1380
+
1381
+ # Adds helper methods for automatically firing events when an action
1382
+ # is invoked
1383
+ def define_action_helpers(action_hook = self.action)
1384
+ action = self.action
1385
+ private_method = owner_class.private_method_defined?(action_hook)
1386
+
1387
+ if (owner_class.method_defined?(action_hook) || private_method) && !owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self}
1388
+ # Action is defined and hasn't already been overridden by another machine
1389
+ @instance_helper_module.class_eval do
1390
+ # Override the default action to invoke the before / after hooks
1391
+ define_method(action_hook) do |*args|
1392
+ self.class.state_machines.fire_event_attributes(self, action) { super(*args) }
1393
+ end
1394
+
1395
+ private action_hook if private_method
1396
+ end
1397
+
1398
+ true
1399
+ else
1400
+ # Action already defined: don't add integration-specific hooks
1401
+ false
1402
+ end
1403
+ end
1404
+
1405
+ # Defines the with/without scope helpers for this attribute. Both the
1406
+ # singular and plural versions of the attribute are defined for each
1407
+ # scope helper. A custom plural can be specified if it cannot be
1408
+ # automatically determined by either calling +pluralize+ on the attribute
1409
+ # name or adding an "s" to the end of the name.
1410
+ def define_scopes(custom_plural = nil)
1411
+ plural = custom_plural || (name.to_s.respond_to?(:pluralize) ? name.to_s.pluralize : "#{name}s")
1412
+
1413
+ [name, plural].uniq.each do |name|
1414
+ [:with, :without].each do |kind|
1415
+ method = "#{kind}_#{name}"
1416
+
1417
+ if scope = send("create_#{kind}_scope", method)
1418
+ # Converts state names to their corresponding values so that they
1419
+ # can be looked up properly
1420
+ define_class_method(method) do |machine, klass, *states|
1421
+ values = states.flatten.map {|state| machine.states.fetch(state).value}
1422
+ scope.call(klass, values)
1423
+ end
1424
+ end
1425
+ end
1426
+ end
1427
+ end
1428
+
1429
+ # Creates a scope for finding objects *with* a particular value or values
1430
+ # for the attribute.
1431
+ #
1432
+ # By default, this is a no-op.
1433
+ def create_with_scope(name)
1434
+ end
1435
+
1436
+ # Creates a scope for finding objects *without* a particular value or
1437
+ # values for the attribute.
1438
+ #
1439
+ # By default, this is a no-op.
1440
+ def create_without_scope(name)
1441
+ end
1442
+
1443
+ # Always yields
1444
+ def transaction(object)
1445
+ yield
1446
+ end
1447
+
1448
+ # Adds a new transition callback of the given type.
1449
+ def add_callback(type, options, &block)
1450
+ callbacks[type] << callback = Callback.new(options, &block)
1451
+ add_states(callback.known_states)
1452
+ callback
1453
+ end
1454
+
1455
+ # Tracks the given set of states in the list of all known states for
1456
+ # this machine
1457
+ def add_states(new_states)
1458
+ new_states.map do |new_state|
1459
+ unless state = states[new_state]
1460
+ states << state = State.new(self, new_state)
1461
+ end
1462
+
1463
+ state
1464
+ end
1465
+ end
1466
+ end
1467
+ end