state_machine 0.8.1 → 0.9.0

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 (42) hide show
  1. data/CHANGELOG.rdoc +17 -0
  2. data/LICENSE +1 -1
  3. data/README.rdoc +162 -23
  4. data/Rakefile +3 -18
  5. data/lib/state_machine.rb +3 -4
  6. data/lib/state_machine/callback.rb +65 -13
  7. data/lib/state_machine/eval_helpers.rb +20 -4
  8. data/lib/state_machine/initializers.rb +4 -0
  9. data/lib/state_machine/initializers/merb.rb +1 -0
  10. data/lib/state_machine/initializers/rails.rb +7 -0
  11. data/lib/state_machine/integrations.rb +21 -6
  12. data/lib/state_machine/integrations/active_model.rb +414 -0
  13. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  14. data/lib/state_machine/integrations/{active_record → active_model}/observer.rb +7 -7
  15. data/lib/state_machine/integrations/active_record.rb +65 -129
  16. data/lib/state_machine/integrations/active_record/locale.rb +4 -11
  17. data/lib/state_machine/integrations/data_mapper.rb +24 -6
  18. data/lib/state_machine/integrations/data_mapper/observer.rb +36 -0
  19. data/lib/state_machine/integrations/mongo_mapper.rb +295 -0
  20. data/lib/state_machine/integrations/sequel.rb +33 -7
  21. data/lib/state_machine/machine.rb +121 -23
  22. data/lib/state_machine/machine_collection.rb +12 -103
  23. data/lib/state_machine/transition.rb +125 -164
  24. data/lib/state_machine/transition_collection.rb +244 -0
  25. data/lib/tasks/state_machine.rb +12 -15
  26. data/test/functional/state_machine_test.rb +11 -1
  27. data/test/unit/callback_test.rb +305 -32
  28. data/test/unit/eval_helpers_test.rb +103 -1
  29. data/test/unit/event_test.rb +2 -1
  30. data/test/unit/guard_test.rb +2 -1
  31. data/test/unit/integrations/active_model_test.rb +909 -0
  32. data/test/unit/integrations/active_record_test.rb +1542 -1292
  33. data/test/unit/integrations/data_mapper_test.rb +1369 -1041
  34. data/test/unit/integrations/mongo_mapper_test.rb +1349 -0
  35. data/test/unit/integrations/sequel_test.rb +1214 -985
  36. data/test/unit/integrations_test.rb +8 -0
  37. data/test/unit/machine_collection_test.rb +140 -513
  38. data/test/unit/machine_test.rb +212 -10
  39. data/test/unit/state_test.rb +2 -1
  40. data/test/unit/transition_collection_test.rb +2098 -0
  41. data/test/unit/transition_test.rb +704 -552
  42. metadata +16 -3
@@ -0,0 +1,11 @@
1
+ {:en => {
2
+ :activemodel => {
3
+ :errors => {
4
+ :messages => {
5
+ :invalid => StateMachine::Machine.default_messages[:invalid],
6
+ :invalid_event => StateMachine::Machine.default_messages[:invalid_event] % ['{{state}}'],
7
+ :invalid_transition => StateMachine::Machine.default_messages[:invalid_transition] % ['{{event}}']
8
+ }
9
+ }
10
+ }
11
+ }}
@@ -1,14 +1,14 @@
1
1
  module StateMachine
2
2
  module Integrations #:nodoc:
3
- module ActiveRecord
4
- # Adds support for invoking callbacks on ActiveRecord observers with more
3
+ module ActiveModel
4
+ # Adds support for invoking callbacks on ActiveModel observers with more
5
5
  # than one argument (e.g. the record *and* the state transition). By
6
- # default, ActiveRecord only supports passing the record into the
6
+ # default, ActiveModel only supports passing the record into the
7
7
  # callbacks.
8
8
  #
9
9
  # For example:
10
10
  #
11
- # class VehicleObserver < ActiveRecord::Observer
11
+ # class VehicleObserver < ActiveModel::Observer
12
12
  # # The default behavior: only pass in the record
13
13
  # def after_save(vehicle)
14
14
  # end
@@ -40,6 +40,6 @@ module StateMachine
40
40
  end
41
41
  end
42
42
 
43
- ActiveRecord::Observer.class_eval do
44
- include StateMachine::Integrations::ActiveRecord::Observer
45
- end
43
+ ActiveModel::Observer.class_eval do
44
+ include StateMachine::Integrations::ActiveModel::Observer
45
+ end if defined?(ActiveModel::Observer)
@@ -64,6 +64,9 @@ module StateMachine
64
64
  # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id: 1, name: nil, state: "idling">
65
65
  # vehicle.state # => "idling"
66
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
+ #
67
70
  # === Security implications
68
71
  #
69
72
  # Beware that public event attributes mean that events can be fired
@@ -131,6 +134,9 @@ module StateMachine
131
134
  # end
132
135
  # end
133
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
+ #
134
140
  # == Validation errors
135
141
  #
136
142
  # If an event fails to successfully fire because there are no matching
@@ -304,8 +310,9 @@ module StateMachine
304
310
  # events:
305
311
  # park: 'estacionarse'
306
312
  module ActiveRecord
313
+ include ActiveModel
314
+
307
315
  # The default options to use for state machines using this integration
308
- class << self; attr_reader :defaults; end
309
316
  @defaults = {:action => :save}
310
317
 
311
318
  # Should this integration be used for state machines in the given class?
@@ -315,9 +322,12 @@ module StateMachine
315
322
  defined?(::ActiveRecord::Base) && klass <= ::ActiveRecord::Base
316
323
  end
317
324
 
318
- # Loads additional files specific to ActiveRecord
319
325
  def self.extended(base) #:nodoc:
320
- require 'state_machine/integrations/active_record/observer'
326
+ require 'state_machine/integrations/active_model/observer'
327
+
328
+ ::ActiveRecord::Observer.class_eval do
329
+ include StateMachine::Integrations::ActiveModel::Observer
330
+ end unless ::ActiveRecord::Observer.included_modules.include?(StateMachine::Integrations::ActiveModel::Observer)
321
331
 
322
332
  if Object.const_defined?(:I18n)
323
333
  locale = "#{File.dirname(__FILE__)}/active_record/locale.rb"
@@ -325,57 +335,53 @@ module StateMachine
325
335
  end
326
336
  end
327
337
 
328
- # Forces the change in state to be recognized regardless of whether the
329
- # state value actually changed
330
- def write(object, attribute, value)
331
- result = super
332
- if attribute == :state && object.respond_to?("#{self.attribute}_will_change!") && !object.send("#{self.attribute}_changed?")
333
- object.send("#{self.attribute}_will_change!")
334
- end
335
- result
336
- end
337
-
338
338
  # Adds a validation error to the given object
339
339
  def invalidate(object, attribute, message, values = [])
340
- attribute = self.attribute(attribute)
341
-
342
340
  if Object.const_defined?(:I18n)
343
- klasses =
344
- if ::ActiveRecord::VERSION::MAJOR >= 3
345
- object.class.lookup_ancestors
346
- elsif ::ActiveRecord::VERSION::MINOR == 3 && ::ActiveRecord::VERSION::TINY >= 2
347
- object.class.self_and_descendants_from_active_record
348
- else
349
- object.class.self_and_descendents_from_active_record
350
- end
351
-
352
- options = values.inject({}) do |options, (key, value)|
353
- # Generate all possible translation keys
354
- group = key.to_s.pluralize
355
- translations = klasses.map {|klass| :"#{klass.model_name.underscore}.#{name}.#{group}.#{value}"}
356
- translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.to_s])
357
-
358
- options[key] = I18n.translate(translations.shift, :default => translations, :scope => [:activerecord, :state_machines])
359
- options
360
- end
361
-
362
- object.errors.add(attribute, message, options.merge(:default => @messages[message]))
341
+ super
363
342
  else
364
- object.errors.add(attribute, generate_message(message, values))
343
+ object.errors.add(self.attribute(attribute), generate_message(message, values))
365
344
  end
366
345
  end
367
346
 
368
- # Resets any errors previously added when invalidating the given object
369
- def reset(object)
370
- object.errors.clear
371
- end
372
-
373
347
  protected
374
- # Adds the default callbacks for notifying ActiveRecord observers
375
- # before/after a transition has been performed.
376
- def after_initialize
377
- callbacks[:before] << Callback.new {|object, transition| notify(:before, object, transition)}
378
- callbacks[:after] << Callback.new {|object, transition| notify(:after, object, transition)}
348
+ # Always adds observer support
349
+ def supports_observers?
350
+ true
351
+ end
352
+
353
+ # Always adds validation support
354
+ def supports_validations?
355
+ true
356
+ end
357
+
358
+ # Only runs validations on the action if using <tt>:save</tt>
359
+ def runs_validations_on_action?
360
+ action == :save
361
+ end
362
+
363
+ # Only adds dirty tracking support if ActiveRecord supports it
364
+ def supports_dirty_tracking?(object)
365
+ defined?(::ActiveRecord::Dirty) && object.respond_to?("#{self.attribute}_changed?") || super
366
+ end
367
+
368
+ # Always uses the <tt>:activerecord</tt> translation scope
369
+ def i18n_scope
370
+ :activerecord
371
+ end
372
+
373
+ # Attempts to look up a class's ancestors via:
374
+ # * #lookup_ancestors
375
+ # * #self_and_descendants_from_active_record
376
+ # * #self_and_descendents_from_active_record
377
+ def ancestors_for(klass)
378
+ if ::ActiveRecord::VERSION::MAJOR >= 3
379
+ super
380
+ elsif ::ActiveRecord::VERSION::MINOR == 3 && ::ActiveRecord::VERSION::TINY >= 2
381
+ klass.self_and_descendants_from_active_record
382
+ else
383
+ klass.self_and_descendents_from_active_record
384
+ end
379
385
  end
380
386
 
381
387
  # Defines an initialization hook into the owner class for setting the
@@ -414,16 +420,6 @@ module StateMachine
414
420
  end_eval
415
421
  end
416
422
 
417
- # Skips defining reader/writer methods since this is done automatically
418
- def define_state_accessor
419
- name = self.name
420
-
421
- owner_class.validates_each(attribute) do |record, attr, value|
422
- machine = record.class.state_machine(name)
423
- machine.invalidate(record, :state, :invalid) unless machine.states.match(record)
424
- end
425
- end
426
-
427
423
  # Adds support for defining the attribute predicate, while providing
428
424
  # compatibility with the default predicate which determines whether
429
425
  # *anything* is set for the attribute's value
@@ -441,17 +437,7 @@ module StateMachine
441
437
 
442
438
  # Adds hooks into validation for automatically firing events
443
439
  def define_action_helpers
444
- if action == :save
445
- if super(:create_or_update)
446
- @instance_helper_module.class_eval do
447
- define_method(:valid?) do |*args|
448
- self.class.state_machines.fire_event_attributes(self, :save, false) { super(*args) }
449
- end
450
- end
451
- end
452
- else
453
- super
454
- end
440
+ super(action == :save ? :create_or_update : action)
455
441
  end
456
442
 
457
443
  # Creates a scope for finding records *with* a particular state or
@@ -476,83 +462,33 @@ module StateMachine
476
462
  object.class.transaction {raise ::ActiveRecord::Rollback unless yield}
477
463
  end
478
464
 
479
- # Creates a new callback in the callback chain, always inserting it
480
- # before the default Observer callbacks that were created after
481
- # initialization.
482
- def add_callback(type, options, &block)
483
- options[:terminator] = @terminator ||= lambda {|result| result == false}
484
- @callbacks[type].insert(-2, callback = Callback.new(options, &block))
485
- add_states(callback.known_states)
486
-
487
- callback
488
- end
489
-
490
465
  private
491
- # Defines a new named scope with the given name. Since ActiveRecord
492
- # does not allow direct access to the model being used within the
493
- # evaluation of a dynamic named scope, the scope must be generated
494
- # manually. It's necessary to have access to the model so that the
495
- # state names can be translated to their associated values and so that
496
- # inheritance is respected properly.
466
+ # Defines a new named scope with the given name
497
467
  def define_scope(name, scope)
498
- if ::ActiveRecord::VERSION::MAJOR <= 2
468
+ if ::ActiveRecord::VERSION::MAJOR >= 3
469
+ lambda {|model, values| model.where(scope.call(values)[:conditions])}
470
+ else
499
471
  if owner_class.respond_to?(:named_scope)
500
472
  name = name.to_sym
501
473
  machine_name = self.name
502
474
 
503
- # Create the scope and then override it with state translation
475
+ # Since ActiveRecord does not allow direct access to the model
476
+ # being used within the evaluation of a dynamic named scope, the
477
+ # scope must be generated manually. It's necessary to have access
478
+ # to the model so that the state names can be translated to their
479
+ # associated values and so that inheritance is respected properly.
504
480
  owner_class.named_scope(name)
505
- owner_class.scopes[name] = lambda do |klass, *states|
506
- machine_states = klass.state_machine(machine_name).states
481
+ owner_class.scopes[name] = lambda do |model, *states|
482
+ machine_states = model.state_machine(machine_name).states
507
483
  values = states.flatten.map {|state| machine_states.fetch(state).value}
508
484
 
509
- ::ActiveRecord::NamedScope::Scope.new(klass, scope.call(values))
485
+ ::ActiveRecord::NamedScope::Scope.new(model, scope.call(values))
510
486
  end
511
487
  end
512
488
 
513
489
  # Prevent the Machine class from wrapping the scope
514
490
  false
515
- else
516
- lambda {|klass, values| klass.where(scope.call(values)[:conditions])}
517
- end
518
- end
519
-
520
- # Notifies observers on the given object that a callback occurred
521
- # involving the given transition. This will attempt to call the
522
- # following methods on observers:
523
- # * #{type}_#{qualified_event}_from_#{from}_to_#{to}
524
- # * #{type}_#{qualified_event}_from_#{from}
525
- # * #{type}_#{qualified_event}_to_#{to}
526
- # * #{type}_#{qualified_event}
527
- # * #{type}_transition_#{machine_name}_from_#{from}_to_#{to}
528
- # * #{type}_transition_#{machine_name}_from_#{from}
529
- # * #{type}_transition_#{machine_name}_to_#{to}
530
- # * #{type}_transition_#{machine_name}
531
- # * #{type}_transition
532
- #
533
- # This will always return true regardless of the results of the
534
- # callbacks.
535
- def notify(type, object, transition)
536
- name = self.name
537
- event = transition.qualified_event
538
- from = transition.from_name
539
- to = transition.to_name
540
-
541
- # Machine-specific updates
542
- ["#{type}_#{event}", "#{type}_transition_#{name}"].each do |event_segment|
543
- ["_from_#{from}", nil].each do |from_segment|
544
- ["_to_#{to}", nil].each do |to_segment|
545
- object.class.changed
546
- object.class.notify_observers([event_segment, from_segment, to_segment].join, object, transition)
547
- end
548
- end
549
491
  end
550
-
551
- # Generic updates
552
- object.class.changed
553
- object.class.notify_observers("#{type}_transition", object, transition)
554
-
555
- true
556
492
  end
557
493
  end
558
494
  end
@@ -1,11 +1,4 @@
1
- {:en => {
2
- :activerecord => {
3
- :errors => {
4
- :messages => {
5
- :invalid => StateMachine::Machine.default_messages[:invalid],
6
- :invalid_event => StateMachine::Machine.default_messages[:invalid_event] % ['{{state}}'],
7
- :invalid_transition => StateMachine::Machine.default_messages[:invalid_transition] % ['{{event}}']
8
- }
9
- }
10
- }
11
- }}
1
+ filename = "#{File.dirname(__FILE__)}/../active_model/locale.rb"
2
+ translations = eval(IO.read(filename), binding, filename)
3
+ translations[:en][:activerecord] = translations[:en].delete(:activemodel)
4
+ translations
@@ -70,6 +70,9 @@ module StateMachine
70
70
  # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id=1 name=nil state="idling">
71
71
  # vehicle.state # => "idling"
72
72
  #
73
+ # This technique is always used for transitioning states when the +save+
74
+ # action (which is the default) is configured for the machine.
75
+ #
73
76
  # === Security implications
74
77
  #
75
78
  # Beware that public event attributes mean that events can be fired
@@ -140,6 +143,10 @@ module StateMachine
140
143
  # end
141
144
  # end
142
145
  #
146
+ # If using the +save+ action for the machine, this option will be ignored as
147
+ # the transaction behavior will depend on the +save+ implementation within
148
+ # DataMapper.
149
+ #
143
150
  # == Validation errors
144
151
  #
145
152
  # If an event fails to successfully fire because there are no matching
@@ -256,9 +263,12 @@ module StateMachine
256
263
  def write(object, attribute, value)
257
264
  result = super
258
265
  if attribute == :state && owner_class.properties.detect {|property| property.name == self.attribute}
259
- if ::DataMapper::VERSION =~ /^(0\.\d\.)/ # Match anything < 0.10
266
+ if ::DataMapper::VERSION =~ /^0\.\d\./ # Match anything < 0.10
260
267
  object.original_values[self.attribute] = "#{value}-ignored" if object.original_values[self.attribute] == value
261
268
  else
269
+ # Force the resource's state to Dirty for 0.10.13+
270
+ object.persisted_state = ::DataMapper::Resource::State::Dirty.new(object) unless ::DataMapper::VERSION =~ /^0\.10\.[0-2]$/ || object.persisted_state.respond_to?(:original_attributes)
271
+
262
272
  property = owner_class.properties[self.attribute]
263
273
  object.original_attributes[property] = "#{value}-ignored" unless object.original_attributes.include?(property)
264
274
  end
@@ -284,7 +294,7 @@ module StateMachine
284
294
 
285
295
  # Pluralizes the name using the built-in inflector
286
296
  def pluralize(word)
287
- Extlib::Inflection.pluralize(word.to_s)
297
+ defined?(Extlib::Inflection) ? Extlib::Inflection.pluralize(word.to_s) : super
288
298
  end
289
299
 
290
300
  # Defines an initialization hook into the owner class for setting the
@@ -316,12 +326,20 @@ module StateMachine
316
326
 
317
327
  # Adds hooks into validation for automatically firing events
318
328
  def define_action_helpers
319
- if super && action == :save && supports_validations?
320
- @instance_helper_module.class_eval do
321
- define_method(:valid?) do |*args|
322
- self.class.state_machines.fire_event_attributes(self, :save, false) { super(*args) }
329
+ # 0.9.4 - 0.9.6 fails to run after callbacks when validations are
330
+ # enabled because of the way dm-validations integrates
331
+ return if ::DataMapper::VERSION =~ /^0\.9\.[4-6]/ && supports_validations?
332
+
333
+ if action == :save
334
+ if super(::DataMapper::VERSION =~ /^0\.\d\./ ? :save : :save_self) && supports_validations?
335
+ @instance_helper_module.class_eval do
336
+ define_method(:valid?) do |*args|
337
+ self.class.state_machines.transitions(self, :save, :after => false).perform { super(*args) }
338
+ end
323
339
  end
324
340
  end
341
+ else
342
+ super
325
343
  end
326
344
  end
327
345
 
@@ -104,6 +104,42 @@ module StateMachine
104
104
  add_transition_callback(:after, *args, &block)
105
105
  end
106
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
+
107
143
  private
108
144
  # Adds the transition callback to a specific machine or all of the
109
145
  # state machines for each observed class.
@@ -0,0 +1,295 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ # Adds support for integrating state machines with MongoMapper models.
4
+ #
5
+ # == Examples
6
+ #
7
+ # Below is an example of a simple state machine defined within a
8
+ # MongoMapper model:
9
+ #
10
+ # class Vehicle
11
+ # include MongoMapper::Document
12
+ #
13
+ # state_machine :initial => :parked do
14
+ # event :ignite do
15
+ # transition :parked => :idling
16
+ # end
17
+ # end
18
+ # end
19
+ #
20
+ # The examples in the sections below will use the above class as a
21
+ # reference.
22
+ #
23
+ # == Actions
24
+ #
25
+ # By default, the action that will be invoked when a state is transitioned
26
+ # is the +save+ action. This will cause the record to save the changes
27
+ # made to the state machine's attribute. *Note* that if any other changes
28
+ # were made to the record prior to transition, then those changes will
29
+ # be saved as well.
30
+ #
31
+ # For example,
32
+ #
33
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
34
+ # vehicle.name = 'Ford Explorer'
35
+ # vehicle.ignite # => true
36
+ # vehicle.reload # => #<Vehicle id: 1, name: "Ford Explorer", state: "idling">
37
+ #
38
+ # == Events
39
+ #
40
+ # As described in StateMachine::InstanceMethods#state_machine, event
41
+ # attributes are created for every machine that allow transitions to be
42
+ # performed automatically when the object's action (in this case, :save)
43
+ # is called.
44
+ #
45
+ # In MongoMapper, these automated events are run in the following order:
46
+ # * before validation - Run before callbacks and persist new states, then validate
47
+ # * before save - If validation was skipped, run before callbacks and persist new states, then save
48
+ # * after save - Run after callbacks
49
+ #
50
+ # For example,
51
+ #
52
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
53
+ # vehicle.state_event # => nil
54
+ # vehicle.state_event = 'invalid'
55
+ # vehicle.valid? # => false
56
+ # vehicle.errors.full_messages # => ["State event is invalid"]
57
+ #
58
+ # vehicle.state_event = 'ignite'
59
+ # vehicle.valid? # => true
60
+ # vehicle.save # => true
61
+ # vehicle.state # => "idling"
62
+ # vehicle.state_event # => nil
63
+ #
64
+ # Note that this can also be done on a mass-assignment basis:
65
+ #
66
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id: 1, name: nil, state: "idling">
67
+ # vehicle.state # => "idling"
68
+ #
69
+ # This technique is always used for transitioning states when the +save+
70
+ # action (which is the default) is configured for the machine.
71
+ #
72
+ # === Security implications
73
+ #
74
+ # Beware that public event attributes mean that events can be fired
75
+ # whenever mass-assignment is being used. If you want to prevent malicious
76
+ # users from tampering with events through URLs / forms, the attribute
77
+ # should be protected like so:
78
+ #
79
+ # class Vehicle
80
+ # include MongoMapper::Document
81
+ #
82
+ # attr_protected :state_event
83
+ # # attr_accessible ... # Alternative technique
84
+ #
85
+ # state_machine do
86
+ # ...
87
+ # end
88
+ # end
89
+ #
90
+ # If you want to only have *some* events be able to fire via mass-assignment,
91
+ # you can build two state machines (one public and one protected) like so:
92
+ #
93
+ # class Vehicle
94
+ # include MongoMapper::Document
95
+ #
96
+ # attr_protected :state_event # Prevent access to events in the first machine
97
+ #
98
+ # state_machine do
99
+ # # Define private events here
100
+ # end
101
+ #
102
+ # # Public machine targets the same state as the private machine
103
+ # state_machine :public_state, :attribute => :state do
104
+ # # Define public events here
105
+ # end
106
+ # end
107
+ #
108
+ # == Validation errors
109
+ #
110
+ # If an event fails to successfully fire because there are no matching
111
+ # transitions for the current record, a validation error is added to the
112
+ # record's state attribute to help in determining why it failed and for
113
+ # reporting via the UI.
114
+ #
115
+ # For example,
116
+ #
117
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id: 1, name: nil, state: "idling">
118
+ # vehicle.ignite # => false
119
+ # vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
120
+ #
121
+ # If an event fails to fire because of a validation error on the record and
122
+ # *not* because a matching transition was not available, no error messages
123
+ # will be added to the state attribute.
124
+ #
125
+ # == Scopes
126
+ #
127
+ # To assist in filtering models with specific states, a series of basic
128
+ # scopes are defined on the model for finding records with or without a
129
+ # particular set of states.
130
+ #
131
+ # These scopes are essentially the functional equivalent of the following
132
+ # definitions:
133
+ #
134
+ # class Vehicle
135
+ # include MongoMapper::Document
136
+ #
137
+ # def self.with_states(*states)
138
+ # all(:conditions => {:state => {'$in' => states}})
139
+ # end
140
+ # # with_states also aliased to with_state
141
+ #
142
+ # def self.without_states(*states)
143
+ # all(:conditions => {:state => {'$nin' => states}})
144
+ # end
145
+ # # without_states also aliased to without_state
146
+ # end
147
+ #
148
+ # *Note*, however, that the states are converted to their stored values
149
+ # before being passed into the query.
150
+ #
151
+ # Because of the way named scopes work in MongoMapper, they *cannot* be
152
+ # chained.
153
+ #
154
+ # == Callbacks
155
+ #
156
+ # All before/after transition callbacks defined for MongoMapper models
157
+ # behave in the same way that other MongoMapper callbacks behave. The
158
+ # object involved in the transition is passed in as an argument.
159
+ #
160
+ # For example,
161
+ #
162
+ # class Vehicle
163
+ # include MongoMapper::Document
164
+ #
165
+ # state_machine :initial => :parked do
166
+ # before_transition any => :idling do |vehicle|
167
+ # vehicle.put_on_seatbelt
168
+ # end
169
+ #
170
+ # before_transition do |vehicle, transition|
171
+ # # log message
172
+ # end
173
+ #
174
+ # event :ignite do
175
+ # transition :parked => :idling
176
+ # end
177
+ # end
178
+ #
179
+ # def put_on_seatbelt
180
+ # ...
181
+ # end
182
+ # end
183
+ #
184
+ # Note, also, that the transition can be accessed by simply defining
185
+ # additional arguments in the callback block.
186
+ module MongoMapper
187
+ include ActiveModel
188
+
189
+ # The default options to use for state machines using this integration
190
+ @defaults = {:action => :save}
191
+
192
+ # Should this integration be used for state machines in the given class?
193
+ # Classes that include MongoMapper::Document will automatically use the
194
+ # MongoMapper integration.
195
+ def self.matches?(klass)
196
+ defined?(::MongoMapper::Document) && klass <= ::MongoMapper::Document
197
+ end
198
+
199
+ # Adds a validation error to the given object (no i18n support)
200
+ def invalidate(object, attribute, message, values = [])
201
+ object.errors.add(self.attribute(attribute), generate_message(message, values))
202
+ end
203
+
204
+ protected
205
+ # Does not support observers
206
+ def supports_observers?
207
+ false
208
+ end
209
+
210
+ # Always adds validation support
211
+ def supports_validations?
212
+ true
213
+ end
214
+
215
+ # Only runs validations on the action if using <tt>:save</tt>
216
+ def runs_validations_on_action?
217
+ action == :save
218
+ end
219
+
220
+ # Always adds dirty tracking support
221
+ def supports_dirty_tracking?(object)
222
+ true
223
+ end
224
+
225
+ # Don't allow callback terminators
226
+ def callback_terminator
227
+ end
228
+
229
+ # Defines an initialization hook into the owner class for setting the
230
+ # initial state of the machine *before* any attributes are set on the
231
+ # object
232
+ def define_state_initializer
233
+ @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
234
+ def initialize(attrs = {}, *args)
235
+ from_database = args.first
236
+
237
+ if !from_database && (!attrs || !attrs.stringify_keys.key?('_id'))
238
+ filtered = respond_to?(:filter_protected_attrs) ? filter_protected_attrs(attrs) : attrs
239
+ ignore = filtered ? filtered.keys : []
240
+
241
+ initialize_state_machines(:dynamic => false, :ignore => ignore)
242
+ super
243
+ initialize_state_machines(:dynamic => true, :ignore => ignore)
244
+ else
245
+ super
246
+ end
247
+ end
248
+ end_eval
249
+ end
250
+
251
+ # Skips defining reader/writer methods since this is done automatically
252
+ def define_state_accessor
253
+ owner_class.key(attribute, String) unless owner_class.keys.include?(attribute)
254
+
255
+ name = self.name
256
+ owner_class.validates_each(attribute, :logic => lambda {
257
+ machine = self.class.state_machine(name)
258
+ machine.invalidate(self, :state, :invalid) unless machine.states.match(self)
259
+ })
260
+ end
261
+
262
+ # Adds support for defining the attribute predicate, while providing
263
+ # compatibility with the default predicate which determines whether
264
+ # *anything* is set for the attribute's value
265
+ def define_state_predicate
266
+ name = self.name
267
+
268
+ # Still use class_eval here instance of define_instance_method since
269
+ # we need to be able to call +super+
270
+ @instance_helper_module.class_eval do
271
+ define_method("#{name}?") do |*args|
272
+ args.empty? ? super(*args) : self.class.state_machine(name).states.matches?(self, *args)
273
+ end
274
+ end
275
+ end
276
+
277
+ # Adds hooks into validation for automatically firing events
278
+ def define_action_helpers
279
+ super(action == :save ? :create_or_update : action)
280
+ end
281
+
282
+ # Creates a scope for finding records *with* a particular state or
283
+ # states for the attribute
284
+ def create_with_scope(name)
285
+ lambda {|model, values| model.all(:conditions => {attribute => {'$in' => values}})}
286
+ end
287
+
288
+ # Creates a scope for finding records *without* a particular state or
289
+ # states for the attribute
290
+ def create_without_scope(name)
291
+ lambda {|model, values| model.all(:conditions => {attribute => {'$nin' => values}})}
292
+ end
293
+ end
294
+ end
295
+ end