verborghs-state_machine 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. data/CHANGELOG.rdoc +360 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +635 -0
  4. data/Rakefile +77 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine/assertions.rb +36 -0
  28. data/lib/state_machine/callback.rb +241 -0
  29. data/lib/state_machine/condition_proxy.rb +106 -0
  30. data/lib/state_machine/eval_helpers.rb +83 -0
  31. data/lib/state_machine/event.rb +267 -0
  32. data/lib/state_machine/event_collection.rb +122 -0
  33. data/lib/state_machine/extensions.rb +149 -0
  34. data/lib/state_machine/guard.rb +230 -0
  35. data/lib/state_machine/initializers/merb.rb +1 -0
  36. data/lib/state_machine/initializers/rails.rb +5 -0
  37. data/lib/state_machine/initializers.rb +4 -0
  38. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  39. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  40. data/lib/state_machine/integrations/active_model.rb +445 -0
  41. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  42. data/lib/state_machine/integrations/active_record.rb +522 -0
  43. data/lib/state_machine/integrations/data_mapper/observer.rb +175 -0
  44. data/lib/state_machine/integrations/data_mapper.rb +379 -0
  45. data/lib/state_machine/integrations/mongo_mapper.rb +309 -0
  46. data/lib/state_machine/integrations/sequel.rb +356 -0
  47. data/lib/state_machine/integrations.rb +83 -0
  48. data/lib/state_machine/machine.rb +1645 -0
  49. data/lib/state_machine/machine_collection.rb +64 -0
  50. data/lib/state_machine/matcher.rb +123 -0
  51. data/lib/state_machine/matcher_helpers.rb +54 -0
  52. data/lib/state_machine/node_collection.rb +152 -0
  53. data/lib/state_machine/state.rb +260 -0
  54. data/lib/state_machine/state_collection.rb +112 -0
  55. data/lib/state_machine/transition.rb +399 -0
  56. data/lib/state_machine/transition_collection.rb +244 -0
  57. data/lib/state_machine.rb +421 -0
  58. data/lib/tasks/state_machine.rake +1 -0
  59. data/lib/tasks/state_machine.rb +27 -0
  60. data/test/files/en.yml +9 -0
  61. data/test/files/switch.rb +11 -0
  62. data/test/functional/state_machine_test.rb +980 -0
  63. data/test/test_helper.rb +4 -0
  64. data/test/unit/assertions_test.rb +40 -0
  65. data/test/unit/callback_test.rb +728 -0
  66. data/test/unit/condition_proxy_test.rb +328 -0
  67. data/test/unit/eval_helpers_test.rb +222 -0
  68. data/test/unit/event_collection_test.rb +324 -0
  69. data/test/unit/event_test.rb +795 -0
  70. data/test/unit/guard_test.rb +909 -0
  71. data/test/unit/integrations/active_model_test.rb +956 -0
  72. data/test/unit/integrations/active_record_test.rb +1918 -0
  73. data/test/unit/integrations/data_mapper_test.rb +1814 -0
  74. data/test/unit/integrations/mongo_mapper_test.rb +1382 -0
  75. data/test/unit/integrations/sequel_test.rb +1492 -0
  76. data/test/unit/integrations_test.rb +50 -0
  77. data/test/unit/invalid_event_test.rb +7 -0
  78. data/test/unit/invalid_transition_test.rb +7 -0
  79. data/test/unit/machine_collection_test.rb +565 -0
  80. data/test/unit/machine_test.rb +2349 -0
  81. data/test/unit/matcher_helpers_test.rb +37 -0
  82. data/test/unit/matcher_test.rb +155 -0
  83. data/test/unit/node_collection_test.rb +207 -0
  84. data/test/unit/state_collection_test.rb +280 -0
  85. data/test/unit/state_machine_test.rb +31 -0
  86. data/test/unit/state_test.rb +848 -0
  87. data/test/unit/transition_collection_test.rb +2098 -0
  88. data/test/unit/transition_test.rb +1384 -0
  89. metadata +176 -0
@@ -0,0 +1,244 @@
1
+ module StateMachine
2
+ # Represents a collection of transitions in a state machine
3
+ class TransitionCollection < Array
4
+ include Assertions
5
+
6
+ # Whether to skip running the action for each transition's machine
7
+ attr_reader :skip_actions
8
+
9
+ # Whether to skip running the after callbacks
10
+ attr_reader :skip_after
11
+
12
+ # Whether transitions should wrapped around a transaction block
13
+ attr_reader :use_transaction
14
+
15
+ # Creates a new collection of transitions that can be run in parallel. Each
16
+ # transition *must* be for a different attribute.
17
+ #
18
+ # Configuration options:
19
+ # * <tt>:actions</tt> - Whether to run the action configured for each transition
20
+ # * <tt>:after</tt> - Whether to run after callbacks
21
+ # * <tt>:transaction</tt> - Whether to wrap transitions within a transaction
22
+ def initialize(transitions = [], options = {})
23
+ super(transitions)
24
+
25
+ # Determine the validity of the transitions as a whole
26
+ @valid = all?
27
+ reject! {|transition| !transition}
28
+
29
+ attributes = map {|transition| transition.attribute}.uniq
30
+ raise ArgumentError, 'Cannot perform multiple transitions in parallel for the same state machine attribute' if attributes.length != length
31
+
32
+ assert_valid_keys(options, :actions, :after, :transaction)
33
+ options = {:actions => true, :after => true, :transaction => true}.merge(options)
34
+ @skip_actions = !options[:actions]
35
+ @skip_after = !options[:after]
36
+ @use_transaction = options[:transaction]
37
+ end
38
+
39
+ # Runs each of the collection's transitions in parallel.
40
+ #
41
+ # All transitions will run through the following steps:
42
+ # 1. Before callbacks
43
+ # 2. Persist state
44
+ # 3. Invoke action
45
+ # 4. After callbacks (if configured)
46
+ # 5. Rollback (if action is unsuccessful)
47
+ #
48
+ # If a block is passed to this method, that block will be called instead
49
+ # of invoking each transition's action.
50
+ def perform(&block)
51
+ reset
52
+
53
+ if valid?
54
+ if use_event_attributes? && !block_given?
55
+ each do |transition|
56
+ transition.transient = true
57
+ transition.machine.write(object, :event_transition, transition)
58
+ end
59
+
60
+ run_actions
61
+ else
62
+ within_transaction do
63
+ catch(:halt) { run_callbacks(&block) }
64
+ rollback unless success?
65
+ end
66
+ end
67
+ end
68
+
69
+ if actions.length == 1 && results.include?(actions.first)
70
+ results[actions.first]
71
+ else
72
+ success?
73
+ end
74
+ end
75
+
76
+ private
77
+ attr_reader :results #:nodoc:
78
+
79
+ # Is this a valid set of transitions? If the collection was creating with
80
+ # any +false+ values for transitions, then the the collection will be
81
+ # marked as invalid.
82
+ def valid?
83
+ @valid
84
+ end
85
+
86
+ # Did each transition perform successfully? This will only be true if the
87
+ # following requirements are met:
88
+ # * No +before+ callbacks halt
89
+ # * All actions run successfully (always true if skipping actions)
90
+ def success?
91
+ @success
92
+ end
93
+
94
+ # Gets the object being transitioned
95
+ def object
96
+ first.object
97
+ end
98
+
99
+ # Gets the list of actions to run. If configured to skip actions, then
100
+ # this will return an empty collection.
101
+ def actions
102
+ empty? ? [nil] : map {|transition| transition.action}.uniq
103
+ end
104
+
105
+ # Determines whether an event attribute be used to trigger the transitions
106
+ # in this collection or whether the transitions be run directly *outside*
107
+ # of the action.
108
+ def use_event_attributes?
109
+ !skip_actions && !skip_after && actions.all? && actions.length == 1 && first.machine.action_helper_defined?
110
+ end
111
+
112
+ # Resets any information tracked from previous attempts to perform the
113
+ # collection
114
+ def reset
115
+ @results = {}
116
+ @success = false
117
+ end
118
+
119
+ # Runs each transition's callbacks recursively. Once all before callbacks
120
+ # have been executed, the transitions will then be persisted and the
121
+ # configured actions will be run.
122
+ #
123
+ # If any transition fails to run its callbacks, :halt will be thrown.
124
+ def run_callbacks(index = 0, &block)
125
+ if transition = self[index]
126
+ throw :halt unless transition.run_callbacks(:after => !skip_after) do
127
+ run_callbacks(index + 1, &block)
128
+ {:result => results[transition.action], :success => success?}
129
+ end
130
+ else
131
+ persist
132
+ run_actions(&block)
133
+ end
134
+ end
135
+
136
+ # Transitions the current value of the object's states to those specified by
137
+ # each transition
138
+ def persist
139
+ each {|transition| transition.persist}
140
+ end
141
+
142
+ # Runs the actions for each transition. If a block is given method, then it
143
+ # will be called instead of invoking each transition's action.
144
+ #
145
+ # The results of the actions will be used to determine #success?.
146
+ def run_actions
147
+ catch_exceptions do
148
+ @success = if block_given?
149
+ result = yield
150
+ actions.each {|action| results[action] = result}
151
+ !!result
152
+ else
153
+ actions.compact.each {|action| !skip_actions && results[action] = object.send(action)}
154
+ results.values.all?
155
+ end
156
+ end
157
+ end
158
+
159
+ # Rolls back changes made to the object's states via each transition
160
+ def rollback
161
+ each {|transition| transition.rollback}
162
+ end
163
+
164
+ # Wraps the given block with a rescue handler so that any exceptions that
165
+ # occur will automatically result in the transition rolling back any changes
166
+ # that were made to the object involved.
167
+ def catch_exceptions
168
+ begin
169
+ yield
170
+ rescue Exception
171
+ rollback
172
+ raise
173
+ end
174
+ end
175
+
176
+ # Runs a block within a transaction for the object being transitioned. If
177
+ # transactions are disabled, then this is a no-op.
178
+ def within_transaction
179
+ if use_transaction && !empty?
180
+ first.within_transaction do
181
+ yield
182
+ success?
183
+ end
184
+ else
185
+ yield
186
+ end
187
+ end
188
+ end
189
+
190
+ # Represents a collection of transitions that were generated from attribute-
191
+ # based events
192
+ class AttributeTransitionCollection < TransitionCollection
193
+ def initialize(transitions = [], options = {}) #:nodoc:
194
+ super(transitions, {:transaction => false, :actions => false}.merge(options))
195
+ end
196
+
197
+ private
198
+ # Hooks into running transition callbacks so that event / event transition
199
+ # attributes can be properly updated
200
+ def run_callbacks(index = 0)
201
+ if index == 0
202
+ # Clears any traces of the event attribute to prevent it from being
203
+ # evaluated multiple times if actions are nested
204
+ each do |transition|
205
+ transition.machine.write(object, :event, nil)
206
+ transition.machine.write(object, :event_transition, nil)
207
+ end
208
+
209
+ # Rollback only if exceptions occur during before callbacks
210
+ begin
211
+ super
212
+ rescue Exception
213
+ rollback unless @before_run
214
+ raise
215
+ end
216
+
217
+ # Persists transitions on the object if partial transition was successful.
218
+ # This allows us to reference them later to complete the transition with
219
+ # after callbacks.
220
+ each {|transition| transition.machine.write(object, :event_transition, transition)} if skip_after && success?
221
+ else
222
+ super
223
+ end
224
+ end
225
+
226
+ # Tracks that before callbacks have now completed
227
+ def persist
228
+ @before_run = true
229
+ super
230
+ end
231
+
232
+ # Resets callback tracking
233
+ def reset
234
+ super
235
+ @before_run = false
236
+ end
237
+
238
+ # Resets the event attribute so it can be re-evaluated if attempted again
239
+ def rollback
240
+ super
241
+ each {|transition| transition.machine.write(object, :event, transition.event) unless transition.transient?}
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,421 @@
1
+ require 'state_machine/machine'
2
+
3
+ # A state machine is a model of behavior composed of states, events, and
4
+ # transitions. This helper adds support for defining this type of
5
+ # functionality on any Ruby class.
6
+ module StateMachine
7
+ module MacroMethods
8
+ # Creates a new state machine with the given name. The default name, if not
9
+ # specified, is <tt>:state</tt>.
10
+ #
11
+ # Configuration options:
12
+ # * <tt>:attribute</tt> - The name of the attribute to store the state value
13
+ # in. By default, this is the same as the name of the machine.
14
+ # * <tt>:initial</tt> - The initial state of the attribute. This can be a
15
+ # static state or a lambda block which will be evaluated at runtime
16
+ # (e.g. lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling}).
17
+ # Default is nil.
18
+ # * <tt>:action</tt> - The instance method to invoke when an object
19
+ # transitions. Default is nil unless otherwise specified by the
20
+ # configured integration.
21
+ # * <tt>:namespace</tt> - The name to use for namespacing all generated
22
+ # state / event instance methods (e.g. "heater" would generate
23
+ # :turn_on_heater and :turn_off_heater for the :turn_on/:turn_off events).
24
+ # Default is nil.
25
+ # * <tt>:integration</tt> - The name of the integration to use for adding
26
+ # library-specific behavior to the machine. Built-in integrations
27
+ # include :active_model, :active_record, :data_mapper, :mongo_mapper, and
28
+ # :sequel. By default, this is determined automatically.
29
+ #
30
+ # Configuration options relevant to ORM integrations:
31
+ # * <tt>:plural</tt> - The pluralized name of the attribute. By default,
32
+ # this will attempt to call +pluralize+ on the attribute. If this
33
+ # method is not available, an "s" is appended. This is used for
34
+ # generating scopes.
35
+ # * <tt>:messages</tt> - The error messages to use when invalidating
36
+ # objects due to failed transitions. Messages include:
37
+ # * <tt>:invalid</tt>
38
+ # * <tt>:invalid_event</tt>
39
+ # * <tt>:invalid_transition</tt>
40
+ # * <tt>:use_transactions</tt> - Whether transactions should be used when
41
+ # firing events. Default is true unless otherwise specified by the
42
+ # configured integration.
43
+ #
44
+ # This also expects a block which will be used to actually configure the
45
+ # states, events and transitions for the state machine. *Note* that this
46
+ # block will be executed within the context of the state machine. As a
47
+ # result, you will not be able to access any class methods unless you refer
48
+ # to them directly (i.e. specifying the class name).
49
+ #
50
+ # For examples on the types of state machine configurations and blocks, see
51
+ # the section below.
52
+ #
53
+ # == Examples
54
+ #
55
+ # With the default name/attribute and no configuration:
56
+ #
57
+ # class Vehicle
58
+ # state_machine do
59
+ # event :park do
60
+ # ...
61
+ # end
62
+ # end
63
+ # end
64
+ #
65
+ # The above example will define a state machine named "state" that will
66
+ # store the value in the +state+ attribute. Every vehicle will start
67
+ # without an initial state.
68
+ #
69
+ # With a custom name / attribute:
70
+ #
71
+ # class Vehicle
72
+ # state_machine :status, :attribute => :status_value do
73
+ # ...
74
+ # end
75
+ # end
76
+ #
77
+ # With a static initial state:
78
+ #
79
+ # class Vehicle
80
+ # state_machine :status, :initial => :parked do
81
+ # ...
82
+ # end
83
+ # end
84
+ #
85
+ # With a dynamic initial state:
86
+ #
87
+ # class Vehicle
88
+ # state_machine :status, :initial => lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling} do
89
+ # ...
90
+ # end
91
+ # end
92
+ #
93
+ # == Class Methods
94
+ #
95
+ # The following class methods will be automatically generated by the
96
+ # state machine based on the *name* of the machine. Any existing methods
97
+ # will not be overwritten.
98
+ # * <tt>human_state_name(state)</tt> - Gets the humanized value for the
99
+ # given state. This may be generated by internationalization libraries if
100
+ # supported by the integration.
101
+ # * <tt>human_state_event_name(event)</tt> - Gets the humanized value for
102
+ # the given event. This may be generated by internationalization
103
+ # libraries if supported by the integration.
104
+ #
105
+ # For example,
106
+ #
107
+ # class Vehicle
108
+ # state_machine :state, :initial => :parked do
109
+ # event :ignite do
110
+ # transition :parked => :idling
111
+ # end
112
+ #
113
+ # event :shift_up do
114
+ # transition :idling => :first_gear
115
+ # end
116
+ # end
117
+ # end
118
+ #
119
+ # Vehicle.human_state_name(:parked) # => "parked"
120
+ # Vehicle.human_state_name(:first_gear) # => "first gear"
121
+ # Vehicle.human_state_event_name(:park) # => "park"
122
+ # Vehicle.human_state_event_name(:shift_up) # => "shift up"
123
+ #
124
+ # == Instance Methods
125
+ #
126
+ # The following instance methods will be automatically generated by the
127
+ # state machine based on the *name* of the machine. Any existing methods
128
+ # will not be overwritten.
129
+ # * <tt>state</tt> - Gets the current value for the attribute
130
+ # * <tt>state=(value)</tt> - Sets the current value for the attribute
131
+ # * <tt>state?(name)</tt> - Checks the given state name against the current
132
+ # state. If the name is not a known state, then an ArgumentError is raised.
133
+ # * <tt>state_name</tt> - Gets the name of the state for the current value
134
+ # * <tt>human_state_name</tt> - Gets the human-readable name of the state
135
+ # for the current value
136
+ # * <tt>state_events</tt> - Gets the list of events that can be fired on
137
+ # the current object's state (uses the *unqualified* event names)
138
+ # * <tt>state_transitions(requirements = {})</tt> - Gets the list of possible
139
+ # transitions that can be made on the current object's state. Additional
140
+ # requirements, such as the :from / :to state and :on event can be specified
141
+ # to restrict the transitions to select. By default, the current state
142
+ # will be used for the :from state.
143
+ #
144
+ # For example,
145
+ #
146
+ # class Vehicle
147
+ # state_machine :state, :initial => :parked do
148
+ # event :ignite do
149
+ # transition :parked => :idling
150
+ # end
151
+ #
152
+ # event :park do
153
+ # transition :idling => :parked
154
+ # end
155
+ # end
156
+ # end
157
+ #
158
+ # vehicle = Vehicle.new
159
+ # vehicle.state # => "parked"
160
+ # vehicle.state_name # => :parked
161
+ # vehicle.human_state_name # => "parked"
162
+ # vehicle.state?(:parked) # => true
163
+ #
164
+ # # Changing state
165
+ # vehicle.state = 'idling'
166
+ # vehicle.state # => "idling"
167
+ # vehicle.state_name # => :idling
168
+ # vehicle.state?(:parked) # => false
169
+ #
170
+ # # Getting current event / transition availability
171
+ # vehicle.state_events # => [:park]
172
+ # vehicle.park # => true
173
+ # vehicle.state_events # => [:ignite]
174
+ #
175
+ # vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
176
+ # vehicle.ignite
177
+ # vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
178
+ #
179
+ # == Attribute initialization
180
+ #
181
+ # For most classes, the initial values for state machine attributes are
182
+ # automatically assigned when a new object is created. However, this
183
+ # behavior will *not* work if the class defines an +initialize+ method
184
+ # without properly calling +super+.
185
+ #
186
+ # For example,
187
+ #
188
+ # class Vehicle
189
+ # state_machine :state, :initial => :parked do
190
+ # ...
191
+ # end
192
+ # end
193
+ #
194
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
195
+ # vehicle.state # => "parked"
196
+ #
197
+ # In the above example, no +initialize+ method is defined. As a result,
198
+ # the default behavior of initializing the state machine attributes is used.
199
+ #
200
+ # In the following example, a custom +initialize+ method is defined:
201
+ #
202
+ # class Vehicle
203
+ # state_machine :state, :initial => :parked do
204
+ # ...
205
+ # end
206
+ #
207
+ # def initialize
208
+ # end
209
+ # end
210
+ #
211
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c77678>
212
+ # vehicle.state # => nil
213
+ #
214
+ # Since the +initialize+ method is defined, the state machine attributes
215
+ # never get initialized. In order to ensure that all initialization hooks
216
+ # are called, the custom method *must* call +super+ without any arguments
217
+ # like so:
218
+ #
219
+ # class Vehicle
220
+ # state_machine :state, :initial => :parked do
221
+ # ...
222
+ # end
223
+ #
224
+ # def initialize(attributes = {})
225
+ # ...
226
+ # super()
227
+ # end
228
+ # end
229
+ #
230
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
231
+ # vehicle.state # => "parked"
232
+ #
233
+ # Because of the way the inclusion of modules works in Ruby, calling
234
+ # <tt>super()</tt> will not only call the superclass's +initialize+, but
235
+ # also +initialize+ on all included modules. This allows the original state
236
+ # machine hook to get called properly.
237
+ #
238
+ # If you want to avoid calling the superclass's constructor, but still want
239
+ # to initialize the state machine attributes:
240
+ #
241
+ # class Vehicle
242
+ # state_machine :state, :initial => :parked do
243
+ # ...
244
+ # end
245
+ #
246
+ # def initialize(attributes = {})
247
+ # ...
248
+ # initialize_state_machines
249
+ # end
250
+ # end
251
+ #
252
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
253
+ # vehicle.state # => "parked"
254
+ #
255
+ # == States
256
+ #
257
+ # All of the valid states for the machine are automatically tracked based
258
+ # on the events, transitions, and callbacks defined for the machine. If
259
+ # there are additional states that are never referenced, these should be
260
+ # explicitly added using the StateMachine::Machine#state or
261
+ # StateMachine::Machine#other_states helpers.
262
+ #
263
+ # When a new state is defined, a predicate method for that state is
264
+ # generated on the class. For example,
265
+ #
266
+ # class Vehicle
267
+ # state_machine :initial => :parked do
268
+ # event :ignite do
269
+ # transition all => :idling
270
+ # end
271
+ # end
272
+ # end
273
+ #
274
+ # ...will generate the following instance methods (assuming they're not
275
+ # already defined in the class):
276
+ # * <tt>parked?</tt>
277
+ # * <tt>idling?</tt>
278
+ #
279
+ # Each predicate method will return true if it matches the object's
280
+ # current state. Otherwise, it will return false.
281
+ #
282
+ # == Events and Transitions
283
+ #
284
+ # Events defined on the machine are the interface to transitioning states
285
+ # for an object. Events can be fired either directly (through the method
286
+ # generated for the event) or indirectly (through attributes defined on
287
+ # the machine).
288
+ #
289
+ # For example,
290
+ #
291
+ # class Vehicle
292
+ # include DataMapper::Resource
293
+ # property :id, Serial
294
+ #
295
+ # state_machine :initial => :parked do
296
+ # event :ignite do
297
+ # transition :parked => :idling
298
+ # end
299
+ # end
300
+ #
301
+ # state_machine :alarm_state, :initial => :active do
302
+ # event :disable do
303
+ # transition all => :off
304
+ # end
305
+ # end
306
+ # end
307
+ #
308
+ # # Fire +ignite+ event directly
309
+ # vehicle = Vehicle.create # => #<Vehicle id=1 state="parked" alarm_state="active">
310
+ # vehicle.ignite # => true
311
+ # vehicle.state # => "idling"
312
+ # vehicle.alarm_state # => "active"
313
+ #
314
+ # # Fire +disable+ event automatically
315
+ # vehicle.alarm_state_event = 'disable'
316
+ # vehicle.save # => true
317
+ # vehicle.alarm_state # => "off"
318
+ #
319
+ # In the above example, the +state+ attribute is transitioned using the
320
+ # +ignite+ action that's generated from the state machine. On the other
321
+ # hand, the +alarm_state+ attribute is transitioned using the +alarm_state_event+
322
+ # attribute that automatically gets fired when the machine's action (+save+)
323
+ # is invoked.
324
+ #
325
+ # For more information about how to configure an event and its associated
326
+ # transitions, see StateMachine::Machine#event.
327
+ #
328
+ # == Defining callbacks
329
+ #
330
+ # Within the +state_machine+ block, you can also define callbacks for
331
+ # transitions. For more information about defining these callbacks,
332
+ # see StateMachine::Machine#before_transition, StateMachine::Machine#after_transition,
333
+ # and StateMachine::Machine#around_transition.
334
+ #
335
+ # == Namespaces
336
+ #
337
+ # When a namespace is configured for a state machine, the name provided
338
+ # will be used in generating the instance methods for interacting with
339
+ # states/events in the machine. This is particularly useful when a class
340
+ # has multiple state machines and it would be difficult to differentiate
341
+ # between the various states / events.
342
+ #
343
+ # For example,
344
+ #
345
+ # class Vehicle
346
+ # state_machine :heater_state, :initial => :off, :namespace => 'heater' do
347
+ # event :turn_on do
348
+ # transition all => :on
349
+ # end
350
+ #
351
+ # event :turn_off do
352
+ # transition all => :off
353
+ # end
354
+ # end
355
+ #
356
+ # state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
357
+ # event :turn_on do
358
+ # transition all => :active
359
+ # end
360
+ #
361
+ # event :turn_off do
362
+ # transition all => :off
363
+ # end
364
+ # end
365
+ # end
366
+ #
367
+ # The above class defines two state machines: +heater_state+ and +alarm_state+.
368
+ # For the +heater_state+ machine, the following methods are generated since
369
+ # it's namespaced by "heater":
370
+ # * <tt>can_turn_on_heater?</tt>
371
+ # * <tt>turn_on_heater</tt>
372
+ # * ...
373
+ # * <tt>can_turn_off_heater?</tt>
374
+ # * <tt>turn_off_heater</tt>
375
+ # * ..
376
+ # * <tt>heater_off?</tt>
377
+ # * <tt>heater_on?</tt>
378
+ #
379
+ # As shown, each method is unique to the state machine so that the states
380
+ # and events don't conflict. The same goes for the +alarm_state+ machine:
381
+ # * <tt>can_turn_on_alarm?</tt>
382
+ # * <tt>turn_on_alarm</tt>
383
+ # * ...
384
+ # * <tt>can_turn_off_alarm?</tt>
385
+ # * <tt>turn_off_alarm</tt>
386
+ # * ..
387
+ # * <tt>alarm_active?</tt>
388
+ # * <tt>alarm_off?</tt>
389
+ #
390
+ # == Scopes
391
+ #
392
+ # For integrations that support it, a group of default scope filters will
393
+ # be automatically created for assisting in finding objects that have the
394
+ # attribute set to one of a given set of states.
395
+ #
396
+ # For example,
397
+ #
398
+ # Vehicle.with_state(:parked) # => All vehicles where the state is parked
399
+ # Vehicle.with_states(:parked, :idling) # => All vehicles where the state is either parked or idling
400
+ #
401
+ # Vehicle.without_state(:parked) # => All vehicles where the state is *not* parked
402
+ # Vehicle.without_states(:parked, :idling) # => All vehicles where the state is *not* parked or idling
403
+ #
404
+ # *Note* that if class methods already exist with those names (i.e.
405
+ # :with_state, :with_states, :without_state, or :without_states), then a
406
+ # scope will not be defined for that name.
407
+ #
408
+ # See StateMachine::Machine for more information about using integrations
409
+ # and the individual integration docs for information about the actual
410
+ # scopes that are generated.
411
+ def state_machine(*args, &block)
412
+ StateMachine::Machine.find_or_create(self, *args, &block)
413
+ end
414
+ end
415
+ end
416
+
417
+ Class.class_eval do
418
+ include StateMachine::MacroMethods
419
+ end
420
+
421
+ require 'state_machine/initializers'
@@ -0,0 +1 @@
1
+ require File.join("#{File.dirname(__FILE__)}/state_machine")
@@ -0,0 +1,27 @@
1
+ namespace :state_machine do
2
+ desc 'Draws a set of state machines using GraphViz. Target files to load with FILE=x,y,z; Machine class with CLASS=x,y,z; Font name with FONT=x; Image format with FORMAT=x; Orientation with ORIENTATION=x'
3
+ task :draw do
4
+ if defined?(Rails)
5
+ Rake::Task['environment'].invoke
6
+ elsif defined?(Merb)
7
+ Rake::Task['merb_env'].invoke
8
+
9
+ # Fix ruby-graphviz being incompatible with Merb's process title
10
+ $0 = 'rake'
11
+ else
12
+ # Load the library
13
+ $:.unshift(File.dirname(__FILE__) + '/..')
14
+ require 'state_machine'
15
+ end
16
+
17
+ # Build drawing options
18
+ options = {}
19
+ options[:file] = ENV['FILE'] if ENV['FILE']
20
+ options[:path] = ENV['TARGET'] if ENV['TARGET']
21
+ options[:format] = ENV['FORMAT'] if ENV['FORMAT']
22
+ options[:font] = ENV['FONT'] if ENV['FONT']
23
+ options[:orientation] = ENV['ORIENTATION'] if ENV['ORIENTATION']
24
+
25
+ StateMachine::Machine.draw(ENV['CLASS'], options)
26
+ end
27
+ end