pluginaweek-state_machine 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. data/CHANGELOG.rdoc +273 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +466 -0
  4. data/Rakefile +98 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine.rb +429 -0
  28. data/lib/state_machine/assertions.rb +36 -0
  29. data/lib/state_machine/callback.rb +189 -0
  30. data/lib/state_machine/condition_proxy.rb +94 -0
  31. data/lib/state_machine/eval_helpers.rb +67 -0
  32. data/lib/state_machine/event.rb +251 -0
  33. data/lib/state_machine/event_collection.rb +113 -0
  34. data/lib/state_machine/extensions.rb +158 -0
  35. data/lib/state_machine/guard.rb +219 -0
  36. data/lib/state_machine/integrations.rb +68 -0
  37. data/lib/state_machine/integrations/active_record.rb +444 -0
  38. data/lib/state_machine/integrations/active_record/locale.rb +10 -0
  39. data/lib/state_machine/integrations/active_record/observer.rb +41 -0
  40. data/lib/state_machine/integrations/data_mapper.rb +325 -0
  41. data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
  42. data/lib/state_machine/integrations/sequel.rb +292 -0
  43. data/lib/state_machine/machine.rb +1431 -0
  44. data/lib/state_machine/machine_collection.rb +146 -0
  45. data/lib/state_machine/matcher.rb +123 -0
  46. data/lib/state_machine/matcher_helpers.rb +54 -0
  47. data/lib/state_machine/node_collection.rb +152 -0
  48. data/lib/state_machine/state.rb +249 -0
  49. data/lib/state_machine/state_collection.rb +112 -0
  50. data/lib/state_machine/transition.rb +367 -0
  51. data/tasks/state_machine.rake +1 -0
  52. data/tasks/state_machine.rb +30 -0
  53. data/test/classes/switch.rb +11 -0
  54. data/test/functional/state_machine_test.rb +941 -0
  55. data/test/test_helper.rb +4 -0
  56. data/test/unit/assertions_test.rb +40 -0
  57. data/test/unit/callback_test.rb +455 -0
  58. data/test/unit/condition_proxy_test.rb +328 -0
  59. data/test/unit/eval_helpers_test.rb +129 -0
  60. data/test/unit/event_collection_test.rb +293 -0
  61. data/test/unit/event_test.rb +605 -0
  62. data/test/unit/guard_test.rb +862 -0
  63. data/test/unit/integrations/active_record_test.rb +1001 -0
  64. data/test/unit/integrations/data_mapper_test.rb +694 -0
  65. data/test/unit/integrations/sequel_test.rb +486 -0
  66. data/test/unit/integrations_test.rb +42 -0
  67. data/test/unit/invalid_event_test.rb +7 -0
  68. data/test/unit/invalid_transition_test.rb +7 -0
  69. data/test/unit/machine_collection_test.rb +710 -0
  70. data/test/unit/machine_test.rb +1910 -0
  71. data/test/unit/matcher_helpers_test.rb +37 -0
  72. data/test/unit/matcher_test.rb +155 -0
  73. data/test/unit/node_collection_test.rb +207 -0
  74. data/test/unit/state_collection_test.rb +280 -0
  75. data/test/unit/state_machine_test.rb +31 -0
  76. data/test/unit/state_test.rb +795 -0
  77. data/test/unit/transition_test.rb +1113 -0
  78. metadata +161 -0
@@ -0,0 +1,17 @@
1
+ <p>
2
+ <b>Name:</b>
3
+ <%=h @user.name %>
4
+ </p>
5
+
6
+ <p>
7
+ <b>State:</b>
8
+ <%=h @user.state %>
9
+ </p>
10
+
11
+ <p>
12
+ <b>Access State:</b>
13
+ <%=h @user.access_state %>
14
+ </p>
15
+
16
+ <%= link_to 'Edit', edit_user_path(@user) %> |
17
+ <%= link_to 'Back', users_path %>
@@ -0,0 +1,7 @@
1
+ class TrafficLight
2
+ state_machine :initial => :stop do
3
+ event :cycle do
4
+ transition :stop => :proceed, :proceed => :caution, :caution => :stop
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ class Vehicle
2
+ state_machine :initial => :parked do
3
+ event :park do
4
+ transition [:idling, :first_gear] => :parked
5
+ end
6
+
7
+ event :ignite do
8
+ transition :stalled => same, :parked => :idling
9
+ end
10
+
11
+ event :idle do
12
+ transition :first_gear => :idling
13
+ end
14
+
15
+ event :shift_up do
16
+ transition :idling => :first_gear, :first_gear => :second_gear, :second_gear => :third_gear
17
+ end
18
+
19
+ event :shift_down do
20
+ transition :third_gear => :second_gear, :second_gear => :first_gear
21
+ end
22
+
23
+ event :crash do
24
+ transition [:first_gear, :second_gear, :third_gear] => :stalled
25
+ end
26
+
27
+ event :repair do
28
+ transition :stalled => :parked
29
+ end
30
+ end
31
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'state_machine'
@@ -0,0 +1,429 @@
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 for the given attribute. The default
9
+ # attribute, if not specified, is <tt>:state</tt>.
10
+ #
11
+ # Configuration options:
12
+ # * <tt>:initial</tt> - The initial state of the attribute. This can be a
13
+ # static state or a lambda block which will be evaluated at runtime
14
+ # (e.g. lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling}).
15
+ # Default is nil.
16
+ # * <tt>:action</tt> - The instance method to invoke when an object
17
+ # transitions. Default is nil unless otherwise specified by the
18
+ # configured integration.
19
+ # * <tt>:as</tt> - The name to use for prefixing all generated machine
20
+ # instance / class methods (e.g. if the attribute is +state_id+, then
21
+ # "state" would generate :state_name, :state_transitions, etc. instead of
22
+ # :state_id_name and :state_id_transitions)
23
+ # * <tt>:namespace</tt> - The name to use for namespacing all generated
24
+ # state / event instance methods (e.g. "heater" would generate
25
+ # :turn_on_heater and :turn_off_heater for the :turn_on/:turn_off events).
26
+ # Default is nil.
27
+ # * <tt>:integration</tt> - The name of the integration to use for adding
28
+ # library-specific behavior to the machine. Built-in integrations
29
+ # include :data_mapper, :active_record, and :sequel. By default, this
30
+ # is determined automatically.
31
+ #
32
+ # Configuration options relevant to ORM integrations:
33
+ # * <tt>:plural</tt> - The pluralized name of the attribute. By default,
34
+ # this will attempt to call +pluralize+ on the attribute. If this
35
+ # method is not available, an "s" is appended. This is used for
36
+ # generating scopes.
37
+ # * <tt>:messages</tt> - The error messages to use when invalidating
38
+ # objects due to failed transitions. Messages include:
39
+ # * <tt>:invalid</tt>
40
+ # * <tt>:invalid_event</tt>
41
+ # * <tt>:invalid_transition</tt>
42
+ # * <tt>:use_transactions</tt> - Whether transactions should be used when
43
+ # firing events. Default is true unless otherwise specified by the
44
+ # configured integration.
45
+ #
46
+ # This also expects a block which will be used to actually configure the
47
+ # states, events and transitions for the state machine. *Note* that this
48
+ # block will be executed within the context of the state machine. As a
49
+ # result, you will not be able to access any class methods unless you refer
50
+ # to them directly (i.e. specifying the class name).
51
+ #
52
+ # For examples on the types of configured state machines and blocks, see
53
+ # the section below.
54
+ #
55
+ # == Examples
56
+ #
57
+ # With the default attribute and no configuration:
58
+ #
59
+ # class Vehicle
60
+ # state_machine do
61
+ # event :park do
62
+ # ...
63
+ # end
64
+ # end
65
+ # end
66
+ #
67
+ # The above example will define a state machine for the +state+ attribute
68
+ # on the class. Every vehicle will start without an initial state.
69
+ #
70
+ # With a custom attribute:
71
+ #
72
+ # class Vehicle
73
+ # state_machine :status do
74
+ # ...
75
+ # end
76
+ # end
77
+ #
78
+ # With a static initial state:
79
+ #
80
+ # class Vehicle
81
+ # state_machine :status, :initial => :parked do
82
+ # ...
83
+ # end
84
+ # end
85
+ #
86
+ # With a dynamic initial state:
87
+ #
88
+ # class Vehicle
89
+ # state_machine :status, :initial => lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling} do
90
+ # ...
91
+ # end
92
+ # end
93
+ #
94
+ # == Instance Methods
95
+ #
96
+ # The following instance methods will be automatically generated by the
97
+ # state machine. Any existing methods will not be overwritten.
98
+ # * <tt>state</tt> - Gets the current value for the attribute
99
+ # * <tt>state=(value)</tt> - Sets the current value for the attribute
100
+ # * <tt>state?(name)</tt> - Checks the given state name against the current
101
+ # state. If the name is not a known state, then an ArgumentError is raised.
102
+ # * <tt>state_name</tt> - Gets the name of the state for the current value
103
+ # * <tt>state_events</tt> - Gets the list of events that can be fired on
104
+ # the current object's state (uses the *unqualified* event names)
105
+ # * <tt>state_transitions</tt> - Gets the list of possible transitions
106
+ # that can be made on the current object's state
107
+ #
108
+ # For example,
109
+ #
110
+ # class Vehicle
111
+ # state_machine :state, :initial => :parked do
112
+ # event :ignite do
113
+ # transition :parked => :idling
114
+ # end
115
+ #
116
+ # event :park do
117
+ # transition :idling => :parked
118
+ # end
119
+ # end
120
+ # end
121
+ #
122
+ # vehicle = Vehicle.new
123
+ # vehicle.state # => "parked"
124
+ # vehicle.state_name # => :parked
125
+ # vehicle.state?(:parked) # => true
126
+ #
127
+ # # Changing state
128
+ # vehicle.state = 'idling'
129
+ # vehicle.state # => "idling"
130
+ # vehicle.state_name # => :idling
131
+ # vehicle.state?(:parked) # => false
132
+ #
133
+ # # Getting current event / transition availability
134
+ # vehicle.state_events # => [:park]
135
+ # vehicle.park # => true
136
+ # vehicle.state_events # => [:ignite]
137
+ #
138
+ # vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
139
+ # vehicle.ignite
140
+ # vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
141
+ #
142
+ # == Attribute initialization
143
+ #
144
+ # For most classes, the initial values for state machine attributes are
145
+ # automatically assigned when a new object is created. However, this
146
+ # behavior will *not* work if the class defines an +initialize+ method
147
+ # without properly calling +super+.
148
+ #
149
+ # For example,
150
+ #
151
+ # class Vehicle
152
+ # state_machine :state, :initial => :parked do
153
+ # ...
154
+ # end
155
+ # end
156
+ #
157
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
158
+ # vehicle.state # => "parked"
159
+ #
160
+ # In the above example, no +initialize+ method is defined. As a result,
161
+ # the default behavior of initializing the state machine attributes is used.
162
+ #
163
+ # In the following example, a custom +initialize+ method is defined:
164
+ #
165
+ # class Vehicle
166
+ # state_machine :state, :initial => :parked do
167
+ # ...
168
+ # end
169
+ #
170
+ # def initialize
171
+ # end
172
+ # end
173
+ #
174
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c77678>
175
+ # vehicle.state # => nil
176
+ #
177
+ # Since the +initialize+ method is defined, the state machine attributes
178
+ # never get initialized. In order to ensure that all initialization hooks
179
+ # are called, the custom method *must* call +super+ without any arguments
180
+ # like so:
181
+ #
182
+ # class Vehicle
183
+ # state_machine :state, :initial => :parked do
184
+ # ...
185
+ # end
186
+ #
187
+ # def initialize(attributes = {})
188
+ # ...
189
+ # super()
190
+ # end
191
+ # end
192
+ #
193
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
194
+ # vehicle.state # => "parked"
195
+ #
196
+ # Because of the way the inclusion of modules works in Ruby, calling
197
+ # <tt>super()</tt> will not only call the superclass's +initialize+, but
198
+ # also +initialize+ on all included modules. This allows the original state
199
+ # machine hook to get called properly.
200
+ #
201
+ # If you want to avoid calling the superclass's constructor, but still want
202
+ # to initialize the state machine attributes:
203
+ #
204
+ # class Vehicle
205
+ # state_machine :state, :initial => :parked do
206
+ # ...
207
+ # end
208
+ #
209
+ # def initialize(attributes = {})
210
+ # ...
211
+ # initialize_state_machines
212
+ # end
213
+ # end
214
+ #
215
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
216
+ # vehicle.state # => "parked"
217
+ #
218
+ # == States
219
+ #
220
+ # All of the valid states for the machine are automatically tracked based
221
+ # on the events, transitions, and callbacks defined for the machine. If
222
+ # there are additional states that are never referenced, these should be
223
+ # explicitly added using the StateMachine::Machine#state or
224
+ # StateMachine::Machine#other_states helpers.
225
+ #
226
+ # When a new state is defined, a predicate method for that state is
227
+ # generated on the class. For example,
228
+ #
229
+ # class Vehicle
230
+ # state_machine :initial => :parked do
231
+ # event :ignite do
232
+ # transition all => :idling
233
+ # end
234
+ # end
235
+ # end
236
+ #
237
+ # ...will generate the following instance methods (assuming they're not
238
+ # already defined in the class):
239
+ # * <tt>parked?</tt>
240
+ # * <tt>idling?</tt>
241
+ #
242
+ # Each predicate method will return true if it matches the object's
243
+ # current state. Otherwise, it will return false.
244
+ #
245
+ # == Events and Transitions
246
+ #
247
+ # Events defined on the machine are the interface to transitioning states
248
+ # for an object. Events can be fired either directly (through the method
249
+ # generated for the event) or indirectly (through attributes defined on
250
+ # the machine).
251
+ #
252
+ # For example,
253
+ #
254
+ # class Vehicle
255
+ # include DataMapper::Resource
256
+ # property :id, Integer, :serial => true
257
+ #
258
+ # state_machine :initial => :parked do
259
+ # event :ignite do
260
+ # transition :parked => :idling
261
+ # end
262
+ # end
263
+ #
264
+ # state_machine :alarm_state, :initial => :active do
265
+ # event :disable do
266
+ # transition all => :off
267
+ # end
268
+ # end
269
+ # end
270
+ #
271
+ # # Fire +ignite+ event directly
272
+ # vehicle = Vehicle.create # => #<Vehicle id=1 state="parked" alarm_state="active">
273
+ # vehicle.ignite # => true
274
+ # vehicle.state # => "idling"
275
+ # vehicle.alarm_state # => "active"
276
+ #
277
+ # # Fire +disable+ event automatically
278
+ # vehicle.alarm_state_event = 'disable'
279
+ # vehicle.save # => true
280
+ # vehicle.alarm_state # => "off"
281
+ #
282
+ # In the above example, the +state+ attribute is transitioned using the
283
+ # +ignite+ action that's generated from the state machine. On the other
284
+ # hand, the +alarm_state+ attribute is transitioned using the +alarm_state_event+
285
+ # attribute that automatically gets fired when the machine's action (+save+)
286
+ # is invoked.
287
+ #
288
+ # For more information about how to configure an event and its associated
289
+ # transitions, see StateMachine::Machine#event.
290
+ #
291
+ # == Defining callbacks
292
+ #
293
+ # Within the +state_machine+ block, you can also define callbacks for
294
+ # transitions. For more information about defining these callbacks,
295
+ # see StateMachine::Machine#before_transition and
296
+ # StateMachine::Machine#after_transition.
297
+ #
298
+ # == Attribute aliases
299
+ #
300
+ # When a state machine is defined, several methods are generated scoped by
301
+ # the name of the attribute, such as (if the attribute were "state"):
302
+ # * <tt>state_name</tt>
303
+ # * <tt>state_event</tt>
304
+ # * <tt>state_transitions</tt>
305
+ # * etc.
306
+ #
307
+ # If the attribute for the machine were something less common, such as
308
+ # "state_id" or "state_value", this makes for more awkward scoped methods.
309
+ #
310
+ # Rather than scope based on the attribute, these methods can be customized
311
+ # using the <tt>:as</tt> option as essentially an alias.
312
+ #
313
+ # For example,
314
+ #
315
+ # class Vehicle
316
+ # state_machine :state_id, :as => :state do
317
+ # event :turn_on do
318
+ # transition all => :on
319
+ # end
320
+ #
321
+ # event :turn_off do
322
+ # transition all => :off
323
+ # end
324
+ #
325
+ # state :on, :value => 1
326
+ # state :off, :value => 2
327
+ # end
328
+ # end
329
+ #
330
+ # ...will generate the following methods:
331
+ # * <tt>state_name</tt>
332
+ # * <tt>state_event</tt>
333
+ # * <tt>state_transitions</tt>
334
+ #
335
+ # ...instead of:
336
+ # * <tt>state_id_name</tt>
337
+ # * <tt>state_id_event</tt>
338
+ # * <tt>state_id_transitions</tt>
339
+ #
340
+ # However, it will continue to read and write to the +state_id+ attribute.
341
+ #
342
+ # == Namespaces
343
+ #
344
+ # When a namespace is configured for a state machine, the name provided
345
+ # will be used in generating the instance methods for interacting with
346
+ # events/states in the machine. This is particularly useful when a class
347
+ # has multiple state machines and it would be difficult to differentiate
348
+ # between the various states / events.
349
+ #
350
+ # For example,
351
+ #
352
+ # class Vehicle
353
+ # state_machine :heater_state, :initial => :off, :namespace => 'heater' do
354
+ # event :turn_on do
355
+ # transition all => :on
356
+ # end
357
+ #
358
+ # event :turn_off do
359
+ # transition all => :off
360
+ # end
361
+ # end
362
+ #
363
+ # state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
364
+ # event :turn_on do
365
+ # transition all => :active
366
+ # end
367
+ #
368
+ # event :turn_off do
369
+ # transition all => :off
370
+ # end
371
+ # end
372
+ # end
373
+ #
374
+ # The above class defines two state machines: +heater_state+ and +alarm_state+.
375
+ # For the +heater_state+ machine, the following methods are generated since
376
+ # it's namespaced by "heater":
377
+ # * <tt>can_turn_on_heater?</tt>
378
+ # * <tt>turn_on_heater</tt>
379
+ # * ...
380
+ # * <tt>can_turn_off_heater?</tt>
381
+ # * <tt>turn_off_heater</tt>
382
+ # * ..
383
+ # * <tt>heater_off?</tt>
384
+ # * <tt>heater_on?</tt>
385
+ #
386
+ # As shown, each method is unique to the state machine so that the states
387
+ # and events don't conflict. The same goes for the +alarm_state+ machine:
388
+ # * <tt>can_turn_on_alarm?</tt>
389
+ # * <tt>turn_on_alarm</tt>
390
+ # * ...
391
+ # * <tt>can_turn_off_alarm?</tt>
392
+ # * <tt>turn_off_alarm</tt>
393
+ # * ..
394
+ # * <tt>alarm_active?</tt>
395
+ # * <tt>alarm_off?</tt>
396
+ #
397
+ # == Scopes
398
+ #
399
+ # For integrations that support it, a group of default scope filters will
400
+ # be automatically created for assisting in finding objects that have the
401
+ # attribute set to the value for a given set of states.
402
+ #
403
+ # For example,
404
+ #
405
+ # Vehicle.with_state(:parked) # => All vehicles where the state is parked
406
+ # Vehicle.with_states(:parked, :idling) # => All vehicles where the state is either parked or idling
407
+ #
408
+ # Vehicle.without_state(:parked) # => All vehicles where the state is *not* parked
409
+ # Vehicle.without_states(:parked, :idling) # => All vehicles where the state is *not* parked or idling
410
+ #
411
+ # *Note* that if class methods already exist with those names (i.e.
412
+ # :with_state, :with_states, :without_state, or :without_states), then a
413
+ # scope will not be defined for that name.
414
+ #
415
+ # See StateMachine::Machine for more information about using
416
+ # integrations and the individual integration docs for information about
417
+ # the actual scopes that are generated.
418
+ def state_machine(*args, &block)
419
+ StateMachine::Machine.find_or_create(self, *args, &block)
420
+ end
421
+ end
422
+ end
423
+
424
+ Class.class_eval do
425
+ include StateMachine::MacroMethods
426
+ end
427
+
428
+ # Register rake tasks for supported libraries
429
+ Merb::Plugins.add_rakefiles("#{File.dirname(__FILE__)}/../tasks/state_machine") if defined?(Merb::Plugins)