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,522 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ # Adds support for integrating state machines with ActiveRecord models.
4
+ #
5
+ # == Examples
6
+ #
7
+ # Below is an example of a simple state machine defined within an
8
+ # ActiveRecord model:
9
+ #
10
+ # class Vehicle < ActiveRecord::Base
11
+ # state_machine :initial => :parked do
12
+ # event :ignite do
13
+ # transition :parked => :idling
14
+ # end
15
+ # end
16
+ # end
17
+ #
18
+ # The examples in the sections below will use the above class as a
19
+ # reference.
20
+ #
21
+ # == Actions
22
+ #
23
+ # By default, the action that will be invoked when a state is transitioned
24
+ # is the +save+ action. This will cause the record to save the changes
25
+ # made to the state machine's attribute. *Note* that if any other changes
26
+ # were made to the record prior to transition, then those changes will
27
+ # be saved as well.
28
+ #
29
+ # For example,
30
+ #
31
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
32
+ # vehicle.name = 'Ford Explorer'
33
+ # vehicle.ignite # => true
34
+ # vehicle.reload # => #<Vehicle id: 1, name: "Ford Explorer", state: "idling">
35
+ #
36
+ # == Events
37
+ #
38
+ # As described in StateMachine::InstanceMethods#state_machine, event
39
+ # attributes are created for every machine that allow transitions to be
40
+ # performed automatically when the object's action (in this case, :save)
41
+ # is called.
42
+ #
43
+ # In ActiveRecord, these automated events are run in the following order:
44
+ # * before validation - Run before callbacks and persist new states, then validate
45
+ # * before save - If validation was skipped, run before callbacks and persist new states, then save
46
+ # * after save - Run after callbacks
47
+ #
48
+ # For example,
49
+ #
50
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
51
+ # vehicle.state_event # => nil
52
+ # vehicle.state_event = 'invalid'
53
+ # vehicle.valid? # => false
54
+ # vehicle.errors.full_messages # => ["State event is invalid"]
55
+ #
56
+ # vehicle.state_event = 'ignite'
57
+ # vehicle.valid? # => true
58
+ # vehicle.save # => true
59
+ # vehicle.state # => "idling"
60
+ # vehicle.state_event # => nil
61
+ #
62
+ # Note that this can also be done on a mass-assignment basis:
63
+ #
64
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id: 1, name: nil, state: "idling">
65
+ # vehicle.state # => "idling"
66
+ #
67
+ # This technique is always used for transitioning states when the +save+
68
+ # action (which is the default) is configured for the machine.
69
+ #
70
+ # === Security implications
71
+ #
72
+ # Beware that public event attributes mean that events can be fired
73
+ # whenever mass-assignment is being used. If you want to prevent malicious
74
+ # users from tampering with events through URLs / forms, the attribute
75
+ # should be protected like so:
76
+ #
77
+ # class Vehicle < ActiveRecord::Base
78
+ # attr_protected :state_event
79
+ # # attr_accessible ... # Alternative technique
80
+ #
81
+ # state_machine do
82
+ # ...
83
+ # end
84
+ # end
85
+ #
86
+ # If you want to only have *some* events be able to fire via mass-assignment,
87
+ # you can build two state machines (one public and one protected) like so:
88
+ #
89
+ # class Vehicle < ActiveRecord::Base
90
+ # attr_protected :state_event # Prevent access to events in the first machine
91
+ #
92
+ # state_machine do
93
+ # # Define private events here
94
+ # end
95
+ #
96
+ # # Public machine targets the same state as the private machine
97
+ # state_machine :public_state, :attribute => :state do
98
+ # # Define public events here
99
+ # end
100
+ # end
101
+ #
102
+ # == Transactions
103
+ #
104
+ # In order to ensure that any changes made during transition callbacks
105
+ # are rolled back during a failed attempt, every transition is wrapped
106
+ # within a transaction.
107
+ #
108
+ # For example,
109
+ #
110
+ # class Message < ActiveRecord::Base
111
+ # end
112
+ #
113
+ # Vehicle.state_machine do
114
+ # before_transition do |vehicle, transition|
115
+ # Message.create(:content => transition.inspect)
116
+ # false
117
+ # end
118
+ # end
119
+ #
120
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
121
+ # vehicle.ignite # => false
122
+ # Message.count # => 0
123
+ #
124
+ # *Note* that only before callbacks that halt the callback chain and
125
+ # failed attempts to save the record will result in the transaction being
126
+ # rolled back. If an after callback halts the chain, the previous result
127
+ # still applies and the transaction is *not* rolled back.
128
+ #
129
+ # To turn off transactions:
130
+ #
131
+ # class Vehicle < ActiveRecord::Base
132
+ # state_machine :initial => :parked, :use_transactions => false do
133
+ # ...
134
+ # end
135
+ # end
136
+ #
137
+ # If using the +save+ action for the machine, this option will be ignored as
138
+ # the transaction will be created by ActiveRecord within +save+.
139
+ #
140
+ # == Validation errors
141
+ #
142
+ # If an event fails to successfully fire because there are no matching
143
+ # transitions for the current record, a validation error is added to the
144
+ # record's state attribute to help in determining why it failed and for
145
+ # reporting via the UI.
146
+ #
147
+ # For example,
148
+ #
149
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id: 1, name: nil, state: "idling">
150
+ # vehicle.ignite # => false
151
+ # vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
152
+ #
153
+ # If an event fails to fire because of a validation error on the record and
154
+ # *not* because a matching transition was not available, no error messages
155
+ # will be added to the state attribute.
156
+ #
157
+ # == Scopes
158
+ #
159
+ # To assist in filtering models with specific states, a series of named
160
+ # scopes are defined on the model for finding records with or without a
161
+ # particular set of states.
162
+ #
163
+ # These named scopes are essentially the functional equivalent of the
164
+ # following definitions:
165
+ #
166
+ # class Vehicle < ActiveRecord::Base
167
+ # named_scope :with_states, lambda {|*states| {:conditions => {:state => states}}}
168
+ # # with_states also aliased to with_state
169
+ #
170
+ # named_scope :without_states, lambda {|*states| {:conditions => ['state NOT IN (?)', states]}}
171
+ # # without_states also aliased to without_state
172
+ # end
173
+ #
174
+ # *Note*, however, that the states are converted to their stored values
175
+ # before being passed into the query.
176
+ #
177
+ # Because of the way named scopes work in ActiveRecord, they can be
178
+ # chained like so:
179
+ #
180
+ # Vehicle.with_state(:parked).all(:order => 'id DESC')
181
+ #
182
+ # == Callbacks
183
+ #
184
+ # All before/after transition callbacks defined for ActiveRecord models
185
+ # behave in the same way that other ActiveRecord callbacks behave. The
186
+ # object involved in the transition is passed in as an argument.
187
+ #
188
+ # For example,
189
+ #
190
+ # class Vehicle < ActiveRecord::Base
191
+ # state_machine :initial => :parked do
192
+ # before_transition any => :idling do |vehicle|
193
+ # vehicle.put_on_seatbelt
194
+ # end
195
+ #
196
+ # before_transition do |vehicle, transition|
197
+ # # log message
198
+ # end
199
+ #
200
+ # event :ignite do
201
+ # transition :parked => :idling
202
+ # end
203
+ # end
204
+ #
205
+ # def put_on_seatbelt
206
+ # ...
207
+ # end
208
+ # end
209
+ #
210
+ # Note, also, that the transition can be accessed by simply defining
211
+ # additional arguments in the callback block.
212
+ #
213
+ # == Observers
214
+ #
215
+ # In addition to support for ActiveRecord-like hooks, there is additional
216
+ # support for ActiveRecord observers. Because of the way ActiveRecord
217
+ # observers are designed, there is less flexibility around the specific
218
+ # transitions that can be hooked in. However, a large number of hooks
219
+ # *are* supported. For example, if a transition for a record's +state+
220
+ # attribute changes the state from +parked+ to +idling+ via the +ignite+
221
+ # event, the following observer methods are supported:
222
+ # * before/after_ignite_from_parked_to_idling
223
+ # * before/after_ignite_from_parked
224
+ # * before/after_ignite_to_idling
225
+ # * before/after_ignite
226
+ # * before/after_transition_state_from_parked_to_idling
227
+ # * before/after_transition_state_from_parked
228
+ # * before/after_transition_state_to_idling
229
+ # * before/after_transition_state
230
+ # * before/after_transition
231
+ #
232
+ # The following class shows an example of some of these hooks:
233
+ #
234
+ # class VehicleObserver < ActiveRecord::Observer
235
+ # def before_save(vehicle)
236
+ # # log message
237
+ # end
238
+ #
239
+ # # Callback for :ignite event *before* the transition is performed
240
+ # def before_ignite(vehicle, transition)
241
+ # # log message
242
+ # end
243
+ #
244
+ # # Callback for :ignite event *after* the transition has been performed
245
+ # def after_ignite(vehicle, transition)
246
+ # # put on seatbelt
247
+ # end
248
+ #
249
+ # # Generic transition callback *before* the transition is performed
250
+ # def after_transition(vehicle, transition)
251
+ # Audit.log(vehicle, transition)
252
+ # end
253
+ # end
254
+ #
255
+ # More flexible transition callbacks can be defined directly within the
256
+ # model as described in StateMachine::Machine#before_transition
257
+ # and StateMachine::Machine#after_transition.
258
+ #
259
+ # To define a single observer for multiple state machines:
260
+ #
261
+ # class StateMachineObserver < ActiveRecord::Observer
262
+ # observe Vehicle, Switch, Project
263
+ #
264
+ # def after_transition(record, transition)
265
+ # Audit.log(record, transition)
266
+ # end
267
+ # end
268
+ #
269
+ # == Internationalization
270
+ #
271
+ # In Rails 2.2+, any error message that is generated from performing invalid
272
+ # transitions can be localized. The following default translations are used:
273
+ #
274
+ # en:
275
+ # activerecord:
276
+ # errors:
277
+ # messages:
278
+ # invalid: "is invalid"
279
+ # invalid_event: "cannot transition when %{state}"
280
+ # invalid_transition: "cannot transition via %{event}"
281
+ #
282
+ # Notice that the interpolation syntax is %{key} in Rails 3+. In Rails 2.x,
283
+ # the appropriate syntax is {{key}}.
284
+ #
285
+ # You can override these for a specific model like so:
286
+ #
287
+ # en:
288
+ # activerecord:
289
+ # errors:
290
+ # models:
291
+ # user:
292
+ # invalid: "is not valid"
293
+ #
294
+ # In addition to the above, you can also provide translations for the
295
+ # various states / events in each state machine. Using the Vehicle example,
296
+ # state translations will be looked for using the following keys:
297
+ # * <tt>activerecord.state_machines.vehicle.state.states.parked</tt>
298
+ # * <tt>activerecord.state_machines.state.states.parked
299
+ # * <tt>activerecord.state_machines.states.parked</tt>
300
+ #
301
+ # Event translations will be looked for using the following keys:
302
+ # * <tt>activerecord.state_machines.vehicle.state.events.ignite</tt>
303
+ # * <tt>activerecord.state_machines.state.events.ignite
304
+ # * <tt>activerecord.state_machines.events.ignite</tt>
305
+ #
306
+ # An example translation configuration might look like so:
307
+ #
308
+ # es:
309
+ # activerecord:
310
+ # state_machines:
311
+ # states:
312
+ # parked: 'estacionado'
313
+ # events:
314
+ # park: 'estacionarse'
315
+ module ActiveRecord
316
+ include ActiveModel
317
+
318
+ # The default options to use for state machines using this integration
319
+ @defaults = {:action => :save}
320
+
321
+ # Should this integration be used for state machines in the given class?
322
+ # Classes that inherit from ActiveRecord::Base will automatically use
323
+ # the ActiveRecord integration.
324
+ def self.matches?(klass)
325
+ defined?(::ActiveRecord::Base) && klass <= ::ActiveRecord::Base
326
+ end
327
+
328
+ def self.extended(base) #:nodoc:
329
+ require 'active_record/version'
330
+ require 'state_machine/integrations/active_model/observer'
331
+
332
+ ::ActiveRecord::Observer.class_eval do
333
+ include StateMachine::Integrations::ActiveModel::Observer
334
+ end unless ::ActiveRecord::Observer < StateMachine::Integrations::ActiveModel::Observer
335
+
336
+ if defined?(I18n)
337
+ locale = "#{File.dirname(__FILE__)}/active_record/locale.rb"
338
+ I18n.load_path.unshift(locale) unless I18n.load_path.include?(locale)
339
+ end
340
+ end
341
+
342
+ # Adds a validation error to the given object
343
+ def invalidate(object, attribute, message, values = [])
344
+ if defined?(I18n)
345
+ super
346
+ else
347
+ object.errors.add(self.attribute(attribute), generate_message(message, values))
348
+ end
349
+ end
350
+
351
+ protected
352
+ # Always adds observer support
353
+ def supports_observers?
354
+ true
355
+ end
356
+
357
+ # Always adds validation support
358
+ def supports_validations?
359
+ true
360
+ end
361
+
362
+ # Only runs validations on the action if using <tt>:save</tt>
363
+ def runs_validations_on_action?
364
+ action == :save
365
+ end
366
+
367
+ # Only adds dirty tracking support if ActiveRecord supports it
368
+ def supports_dirty_tracking?(object)
369
+ defined?(::ActiveRecord::Dirty) && object.respond_to?("#{attribute}_changed?") || super
370
+ end
371
+
372
+ # Always uses the <tt>:activerecord</tt> translation scope
373
+ def i18n_scope
374
+ :activerecord
375
+ end
376
+
377
+ # The default options to use when generating messages for validation
378
+ # errors
379
+ def default_error_message_options(object, attribute, message)
380
+ if ::ActiveRecord::VERSION::MAJOR >= 3
381
+ super
382
+ else
383
+ {:default => @messages[message]}
384
+ end
385
+ end
386
+
387
+ # Only allows translation of I18n is available
388
+ def translate(klass, key, value)
389
+ if defined?(I18n)
390
+ super
391
+ else
392
+ value ? value.to_s.humanize.downcase : 'nil'
393
+ end
394
+ end
395
+
396
+ # Attempts to look up a class's ancestors via:
397
+ # * #lookup_ancestors (3.0.0+)
398
+ # * #self_and_descendants_from_active_record (2.3.2 - 2.3.x)
399
+ # * #self_and_descendents_from_active_record (2.0.0 - 2.3.1)
400
+ def ancestors_for(klass)
401
+ if ::ActiveRecord::VERSION::MAJOR >= 3
402
+ super
403
+ elsif ::ActiveRecord::VERSION::MINOR == 3 && ::ActiveRecord::VERSION::TINY >= 2
404
+ klass.self_and_descendants_from_active_record
405
+ else
406
+ klass.self_and_descendents_from_active_record
407
+ end
408
+ end
409
+
410
+ # Defines an initialization hook into the owner class for setting the
411
+ # initial state of the machine *before* any attributes are set on the
412
+ # object
413
+ def define_state_initializer
414
+ @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
415
+ # Ensure that the attributes setter gets used to force initialization
416
+ # of the state machines
417
+ def initialize(attributes = nil, *args)
418
+ attributes ||= {}
419
+ super
420
+ end
421
+
422
+ # Hooks in to attribute initialization to set the states *prior*
423
+ # to the attributes being set
424
+ def attributes=(new_attributes, *args)
425
+ if new_record? && !@initialized_state_machines
426
+ @initialized_state_machines = true
427
+
428
+ ignore = if new_attributes
429
+ attributes = new_attributes.dup
430
+ attributes.stringify_keys!
431
+ if ::ActiveRecord::VERSION::MAJOR >= 3
432
+ sanitize_for_mass_assignment(attributes).keys
433
+ else
434
+ remove_attributes_protected_from_mass_assignment(attributes).keys
435
+ end
436
+ else
437
+ []
438
+ end
439
+
440
+ initialize_state_machines(:dynamic => false, :ignore => ignore)
441
+ super
442
+ initialize_state_machines(:dynamic => true, :ignore => ignore)
443
+ else
444
+ super
445
+ end
446
+ end
447
+ end_eval
448
+ end
449
+
450
+ # Adds support for defining the attribute predicate, while providing
451
+ # compatibility with the default predicate which determines whether
452
+ # *anything* is set for the attribute's value
453
+ def define_state_predicate
454
+ name = self.name
455
+
456
+ # Still use class_eval here instance of define_instance_method since
457
+ # we need to be able to call +super+
458
+ @instance_helper_module.class_eval do
459
+ define_method("#{name}?") do |*args|
460
+ args.empty? ? super(*args) : self.class.state_machine(name).states.matches?(self, *args)
461
+ end
462
+ end
463
+ end
464
+
465
+ # Adds hooks into validation for automatically firing events
466
+ def define_action_helpers
467
+ super(action == :save ? :create_or_update : action)
468
+ end
469
+
470
+ # Creates a scope for finding records *with* a particular state or
471
+ # states for the attribute
472
+ def create_with_scope(name)
473
+ define_scope(name, lambda {|values| {:conditions => {attribute => values}}})
474
+ end
475
+
476
+ # Creates a scope for finding records *without* a particular state or
477
+ # states for the attribute
478
+ def create_without_scope(name)
479
+ define_scope(name, lambda {|values|
480
+ connection = owner_class.connection
481
+ {:conditions => ["#{connection.quote_table_name(owner_class.table_name)}.#{connection.quote_column_name(attribute)} NOT IN (?)", values]}
482
+ })
483
+ end
484
+
485
+ # Runs a new database transaction, rolling back any changes by raising
486
+ # an ActiveRecord::Rollback exception if the yielded block fails
487
+ # (i.e. returns false).
488
+ def transaction(object)
489
+ object.class.transaction {raise ::ActiveRecord::Rollback unless yield}
490
+ end
491
+
492
+ private
493
+ # Defines a new named scope with the given name
494
+ def define_scope(name, scope)
495
+ if ::ActiveRecord::VERSION::MAJOR >= 3
496
+ lambda {|model, values| model.where(scope.call(values)[:conditions])}
497
+ else
498
+ if owner_class.respond_to?(:named_scope)
499
+ name = name.to_sym
500
+ machine_name = self.name
501
+
502
+ # Since ActiveRecord does not allow direct access to the model
503
+ # being used within the evaluation of a dynamic named scope, the
504
+ # scope must be generated manually. It's necessary to have access
505
+ # to the model so that the state names can be translated to their
506
+ # associated values and so that inheritance is respected properly.
507
+ owner_class.named_scope(name)
508
+ owner_class.scopes[name] = lambda do |model, *states|
509
+ machine_states = model.state_machine(machine_name).states
510
+ values = states.flatten.map {|state| machine_states.fetch(state).value}
511
+
512
+ ::ActiveRecord::NamedScope::Scope.new(model, scope.call(values))
513
+ end
514
+ end
515
+
516
+ # Prevent the Machine class from wrapping the scope
517
+ false
518
+ end
519
+ end
520
+ end
521
+ end
522
+ end
@@ -0,0 +1,175 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module DataMapper
4
+ # Adds support for creating before/after transition callbacks within a
5
+ # DataMapper observer. These callbacks behave very similar to
6
+ # before/after hooks during save/update/destroy/etc., but with the
7
+ # following modifications:
8
+ # * Each callback can define a set of transition conditions (i.e. guards)
9
+ # that must be met in order for the callback to get invoked.
10
+ # * An additional transition parameter is available that provides
11
+ # contextual information about the event (see StateMachine::Transition
12
+ # for more information)
13
+ #
14
+ # To define a single observer for multiple state machines:
15
+ #
16
+ # class StateMachineObserver
17
+ # include DataMapper::Observer
18
+ #
19
+ # observe Vehicle, Switch, Project
20
+ #
21
+ # after_transition do |transition|
22
+ # Audit.log(self, transition)
23
+ # end
24
+ # end
25
+ #
26
+ # == Requirements
27
+ #
28
+ # To use this feature of the DataMapper integration, the dm-observer library
29
+ # must be available. This can be installed either directly or indirectly
30
+ # through dm-more. When loading DataMapper, be sure to load the dm-observer
31
+ # library as well like so:
32
+ #
33
+ # require 'rubygems'
34
+ # require 'dm-core'
35
+ # require 'dm-observer'
36
+ #
37
+ # If dm-observer is not available, then this feature will be skipped.
38
+ module Observer
39
+ include MatcherHelpers
40
+
41
+ # Creates a callback that will be invoked *before* a transition is
42
+ # performed, so long as the given configuration options match the
43
+ # transition. Each part of the transition (event, to state, from state)
44
+ # must match in order for the callback to get invoked.
45
+ #
46
+ # See StateMachine::Machine#before_transition for more
47
+ # information about the various configuration options available.
48
+ #
49
+ # == Examples
50
+ #
51
+ # class Vehicle
52
+ # include DataMapper::Resource
53
+ #
54
+ # property :id, Serial
55
+ # property :state, :String
56
+ #
57
+ # state_machine :initial => :parked do
58
+ # event :ignite do
59
+ # transition :parked => :idling
60
+ # end
61
+ # end
62
+ # end
63
+ #
64
+ # class VehicleObserver
65
+ # include DataMapper::Observer
66
+ #
67
+ # observe Vehicle
68
+ #
69
+ # before :save do
70
+ # # log message
71
+ # end
72
+ #
73
+ # # Target all state machines
74
+ # before_transition :parked => :idling, :on => :ignite do
75
+ # # put on seatbelt
76
+ # end
77
+ #
78
+ # # Target a specific state machine
79
+ # before_transition :state, any => :idling do
80
+ # # put on seatbelt
81
+ # end
82
+ #
83
+ # # Target all state machines without requirements
84
+ # before_transition do |transition|
85
+ # # log message
86
+ # end
87
+ # end
88
+ #
89
+ # *Note* that in each of the above +before_transition+ callbacks, the
90
+ # callback is executed within the context of the object (i.e. the
91
+ # Vehicle instance being transition). This means that +self+ refers
92
+ # to the vehicle record within each callback block.
93
+ def before_transition(*args, &block)
94
+ add_transition_callback(:before, *args, &block)
95
+ end
96
+
97
+ # Creates a callback that will be invoked *after* a transition is
98
+ # performed so long as the given configuration options match the
99
+ # transition.
100
+ #
101
+ # See +before_transition+ for a description of the possible configurations
102
+ # for defining callbacks.
103
+ def after_transition(*args, &block)
104
+ add_transition_callback(:after, *args, &block)
105
+ end
106
+
107
+ # Creates a callback that will be invoked *around* a transition so long
108
+ # as the given requirements match the transition.
109
+ #
110
+ # == Examples
111
+ #
112
+ # class Vehicle
113
+ # include DataMapper::Resource
114
+ #
115
+ # property :id, Serial
116
+ # property :state, :String
117
+ #
118
+ # state_machine :initial => :parked do
119
+ # event :ignite do
120
+ # transition :parked => :idling
121
+ # end
122
+ # end
123
+ # end
124
+ #
125
+ # class VehicleObserver
126
+ # include DataMapper::Observer
127
+ #
128
+ # observe Vehicle
129
+ #
130
+ # around_transition do |transition, block|
131
+ # # track start time
132
+ # block.call
133
+ # # track end time
134
+ # end
135
+ # end
136
+ #
137
+ # See +before_transition+ for a description of the possible configurations
138
+ # for defining callbacks.
139
+ def around_transition(*args, &block)
140
+ add_transition_callback(:around, *args, &block)
141
+ end
142
+
143
+ private
144
+ # Adds the transition callback to a specific machine or all of the
145
+ # state machines for each observed class.
146
+ def add_transition_callback(type, *args, &block)
147
+ if args.any? && !args.first.is_a?(Hash)
148
+ # Specific machine(s) being targeted
149
+ names = args
150
+ args = args.last.is_a?(Hash) ? [args.pop] : []
151
+ else
152
+ # Target all state machines
153
+ names = nil
154
+ end
155
+
156
+ # Add the transition callback to each class being observed
157
+ observing.each do |klass|
158
+ state_machines =
159
+ if names
160
+ names.map {|name| klass.state_machines.fetch(name)}
161
+ else
162
+ klass.state_machines.values
163
+ end
164
+
165
+ state_machines.each {|machine| machine.send("#{type}_transition", *args, &block)}
166
+ end if observing
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ DataMapper::Observer::ClassMethods.class_eval do
174
+ include StateMachine::Integrations::DataMapper::Observer
175
+ end