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,379 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ # Adds support for integrating state machines with DataMapper resources.
4
+ #
5
+ # == Examples
6
+ #
7
+ # Below is an example of a simple state machine defined within a
8
+ # DataMapper resource:
9
+ #
10
+ # class Vehicle
11
+ # include DataMapper::Resource
12
+ #
13
+ # property :id, Serial
14
+ # property :name, String
15
+ # property :state, String
16
+ #
17
+ # state_machine :initial => :parked do
18
+ # event :ignite do
19
+ # transition :parked => :idling
20
+ # end
21
+ # end
22
+ # end
23
+ #
24
+ # The examples in the sections below will use the above class as a
25
+ # reference.
26
+ #
27
+ # == Actions
28
+ #
29
+ # By default, the action that will be invoked when a state is transitioned
30
+ # is the +save+ action. This will cause the resource to save the changes
31
+ # made to the state machine's attribute. *Note* that if any other changes
32
+ # were made to the resource prior to transition, then those changes will
33
+ # be saved as well.
34
+ #
35
+ # For example,
36
+ #
37
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
38
+ # vehicle.name = 'Ford Explorer'
39
+ # vehicle.ignite # => true
40
+ # vehicle.reload # => #<Vehicle id=1 name="Ford Explorer" state="idling">
41
+ #
42
+ # == Events
43
+ #
44
+ # As described in StateMachine::InstanceMethods#state_machine, event
45
+ # attributes are created for every machine that allow transitions to be
46
+ # performed automatically when the object's action (in this case, :save)
47
+ # is called.
48
+ #
49
+ # In DataMapper, these automated events are run in the following order:
50
+ # * before validation - If validation feature loaded, run before callbacks and persist new states, then validate
51
+ # * before save - If validation feature was skipped/not loaded, run before callbacks and persist new states, then save
52
+ # * after save - Run after callbacks
53
+ #
54
+ # For example,
55
+ #
56
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
57
+ # vehicle.state_event # => nil
58
+ # vehicle.state_event = 'invalid'
59
+ # vehicle.valid? # => false
60
+ # vehicle.errors # => #<DataMapper::Validate::ValidationErrors:0xb7a48b54 @errors={"state_event"=>["is invalid"]}>
61
+ #
62
+ # vehicle.state_event = 'ignite'
63
+ # vehicle.valid? # => true
64
+ # vehicle.save # => true
65
+ # vehicle.state # => "idling"
66
+ # vehicle.state_event # => nil
67
+ #
68
+ # Note that this can also be done on a mass-assignment basis:
69
+ #
70
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id=1 name=nil state="idling">
71
+ # vehicle.state # => "idling"
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
+ #
76
+ # === Security implications
77
+ #
78
+ # Beware that public event attributes mean that events can be fired
79
+ # whenever mass-assignment is being used. If you want to prevent malicious
80
+ # users from tampering with events through URLs / forms, the attribute
81
+ # should be protected like so:
82
+ #
83
+ # class Vehicle
84
+ # include DataMapper::Resource
85
+ # ...
86
+ #
87
+ # state_machine do
88
+ # ...
89
+ # end
90
+ # protected :state_event
91
+ # end
92
+ #
93
+ # If you want to only have *some* events be able to fire via mass-assignment,
94
+ # you can build two state machines (one public and one protected) like so:
95
+ #
96
+ # class Vehicle
97
+ # include DataMapper::Resource
98
+ # ...
99
+ #
100
+ # state_machine do
101
+ # # Define private events here
102
+ # end
103
+ # protected :state_event= # Prevent access to events in the first machine
104
+ #
105
+ # # Allow both machines to share the same state
106
+ # state_machine :public_state, :attribute => :state do
107
+ # # Define public events here
108
+ # end
109
+ # end
110
+ #
111
+ # == Transactions
112
+ #
113
+ # By default, the use of transactions during an event transition is
114
+ # turned off to be consistent with DataMapper. This means that if
115
+ # changes are made to the database during a before callback, but the
116
+ # transition fails to complete, those changes will *not* be rolled back.
117
+ #
118
+ # For example,
119
+ #
120
+ # class Message
121
+ # include DataMapper::Resource
122
+ #
123
+ # property :id, Serial
124
+ # property :content, String
125
+ # end
126
+ #
127
+ # Vehicle.state_machine do
128
+ # before_transition do |transition|
129
+ # Message.create(:content => transition.inspect)
130
+ # throw :halt
131
+ # end
132
+ # end
133
+ #
134
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
135
+ # vehicle.ignite # => false
136
+ # Message.all.count # => 1
137
+ #
138
+ # To turn on transactions:
139
+ #
140
+ # class Vehicle < ActiveRecord::Base
141
+ # state_machine :initial => :parked, :use_transactions => true do
142
+ # ...
143
+ # end
144
+ # end
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
+ #
150
+ # == Validation errors
151
+ #
152
+ # If an event fails to successfully fire because there are no matching
153
+ # transitions for the current record, a validation error is added to the
154
+ # record's state attribute to help in determining why it failed and for
155
+ # reporting via the UI.
156
+ #
157
+ # For example,
158
+ #
159
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id=1 name=nil state="idling">
160
+ # vehicle.ignite # => false
161
+ # vehicle.errors.full_messages # => ["cannot transition via \"ignite\""]
162
+ #
163
+ # If an event fails to fire because of a validation error on the record and
164
+ # *not* because a matching transition was not available, no error messages
165
+ # will be added to the state attribute.
166
+ #
167
+ # == Scopes
168
+ #
169
+ # To assist in filtering models with specific states, a series of class
170
+ # methods are defined on the model for finding records with or without a
171
+ # particular set of states.
172
+ #
173
+ # These named scopes are the functional equivalent of the following
174
+ # definitions:
175
+ #
176
+ # class Vehicle
177
+ # include DataMapper::Resource
178
+ #
179
+ # property :id, Serial
180
+ # property :state, String
181
+ #
182
+ # class << self
183
+ # def with_states(*states)
184
+ # all(:state => states.flatten)
185
+ # end
186
+ # alias_method :with_state, :with_states
187
+ #
188
+ # def without_states(*states)
189
+ # all(:state.not => states.flatten)
190
+ # end
191
+ # alias_method :without_state, :without_states
192
+ # end
193
+ # end
194
+ #
195
+ # *Note*, however, that the states are converted to their stored values
196
+ # before being passed into the query.
197
+ #
198
+ # Because of the way scopes work in DataMapper, they can be chained like
199
+ # so:
200
+ #
201
+ # Vehicle.with_state(:parked).all(:order => [:id.desc])
202
+ #
203
+ # == Callbacks / Observers
204
+ #
205
+ # All before/after transition callbacks defined for DataMapper resources
206
+ # behave in the same way that other DataMapper hooks behave. Rather than
207
+ # passing in the record as an argument to the callback, the callback is
208
+ # instead bound to the object and evaluated within its context.
209
+ #
210
+ # For example,
211
+ #
212
+ # class Vehicle
213
+ # include DataMapper::Resource
214
+ #
215
+ # property :id, Serial
216
+ # property :state, String
217
+ #
218
+ # state_machine :initial => :parked do
219
+ # before_transition any => :idling do
220
+ # put_on_seatbelt
221
+ # end
222
+ #
223
+ # before_transition do |transition|
224
+ # # log message
225
+ # end
226
+ #
227
+ # event :ignite do
228
+ # transition :parked => :idling
229
+ # end
230
+ # end
231
+ #
232
+ # def put_on_seatbelt
233
+ # ...
234
+ # end
235
+ # end
236
+ #
237
+ # Note, also, that the transition can be accessed by simply defining
238
+ # additional arguments in the callback block.
239
+ #
240
+ # In addition to support for DataMapper-like hooks, there is additional
241
+ # support for DataMapper observers. See StateMachine::Integrations::DataMapper::Observer
242
+ # for more information.
243
+ module DataMapper
244
+ # The default options to use for state machines using this integration
245
+ class << self; attr_reader :defaults; end
246
+ @defaults = {:action => :save, :use_transactions => false}
247
+
248
+ # Should this integration be used for state machines in the given class?
249
+ # Classes that include DataMapper::Resource will automatically use the
250
+ # DataMapper integration.
251
+ def self.matches?(klass)
252
+ defined?(::DataMapper::Resource) && klass <= ::DataMapper::Resource
253
+ end
254
+
255
+ # Loads additional files specific to DataMapper
256
+ def self.extended(base) #:nodoc:
257
+ require 'dm-core/version' unless ::DataMapper.const_defined?('VERSION')
258
+ require 'state_machine/integrations/data_mapper/observer' if ::DataMapper.const_defined?('Observer')
259
+ end
260
+
261
+ # Forces the change in state to be recognized regardless of whether the
262
+ # state value actually changed
263
+ def write(object, attribute, value)
264
+ if attribute == :state
265
+ result = super
266
+
267
+ # Change original attributes in 0.9.4 - 0.10.2
268
+ if ::DataMapper::VERSION =~ /^0\.9\./
269
+ object.original_values[self.attribute] = "#{value}-ignored" if object.original_values[self.attribute] == value
270
+ elsif ::DataMapper::VERSION =~ /^0\.10\./
271
+ property = owner_class.properties[self.attribute]
272
+ object.original_attributes[property] = "#{value}-ignored" unless object.original_attributes.include?(property)
273
+ else
274
+ object.persisted_state = ::DataMapper::Resource::State::Dirty.new(object) if object.persisted_state.is_a?(::DataMapper::Resource::State::Clean)
275
+ property = owner_class.properties[self.attribute]
276
+ object.persisted_state.original_attributes[property] = value unless object.persisted_state.original_attributes.include?(property)
277
+ end
278
+ else
279
+ result = super
280
+ end
281
+
282
+ result
283
+ end
284
+
285
+ # Adds a validation error to the given object
286
+ def invalidate(object, attribute, message, values = [])
287
+ object.errors.add(self.attribute(attribute), generate_message(message, values)) if supports_validations?
288
+ end
289
+
290
+ # Resets any errors previously added when invalidating the given object
291
+ def reset(object)
292
+ object.errors.clear if supports_validations?
293
+ end
294
+
295
+ protected
296
+ # Is validation support currently loaded?
297
+ def supports_validations?
298
+ @supports_validations ||= ::DataMapper.const_defined?('Validate')
299
+ end
300
+
301
+ # Pluralizes the name using the built-in inflector
302
+ def pluralize(word)
303
+ defined?(Extlib::Inflection) ? Extlib::Inflection.pluralize(word.to_s) : super
304
+ end
305
+
306
+ # Defines an initialization hook into the owner class for setting the
307
+ # initial state of the machine *before* any attributes are set on the
308
+ # object
309
+ def define_state_initializer
310
+ @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
311
+ def initialize(attributes = {}, *args)
312
+ ignore = attributes ? attributes.keys : []
313
+ initialize_state_machines(:dynamic => false, :ignore => ignore)
314
+ super
315
+ initialize_state_machines(:dynamic => true, :ignore => ignore)
316
+ end
317
+ end_eval
318
+ end
319
+
320
+ # Skips defining reader/writer methods since this is done automatically
321
+ def define_state_accessor
322
+ owner_class.property(attribute, String) unless owner_class.properties.detect {|property| property.name == attribute}
323
+
324
+ if supports_validations?
325
+ name = self.name
326
+ owner_class.validates_with_block(attribute) do
327
+ machine = self.class.state_machine(name)
328
+ machine.states.match(self) ? true : [false, machine.generate_message(:invalid)]
329
+ end
330
+ end
331
+ end
332
+
333
+ # Adds hooks into validation for automatically firing events
334
+ def define_action_helpers
335
+ # 0.9.4 - 0.9.6 fails to run after callbacks when validations are
336
+ # enabled because of the way dm-validations integrates
337
+ return if ::DataMapper::VERSION =~ /^0\.9\.[4-6]/ && supports_validations?
338
+
339
+ if action == :save
340
+ if super(::DataMapper::VERSION =~ /^0\.\d\./ ? :save : :save_self) && supports_validations?
341
+ @instance_helper_module.class_eval do
342
+ define_method(:valid?) do |*args|
343
+ self.class.state_machines.transitions(self, :save, :after => false).perform { super(*args) }
344
+ end
345
+ end
346
+ end
347
+ else
348
+ super
349
+ end
350
+ end
351
+
352
+ # Creates a scope for finding records *with* a particular state or
353
+ # states for the attribute
354
+ def create_with_scope(name)
355
+ lambda {|resource, values| resource.all(attribute => values)}
356
+ end
357
+
358
+ # Creates a scope for finding records *without* a particular state or
359
+ # states for the attribute
360
+ def create_without_scope(name)
361
+ lambda {|resource, values| resource.all(attribute.to_sym.not => values)}
362
+ end
363
+
364
+ # Runs a new database transaction, rolling back any changes if the
365
+ # yielded block fails (i.e. returns false).
366
+ def transaction(object)
367
+ object.class.transaction {|t| t.rollback unless yield}
368
+ end
369
+
370
+ # Creates a new callback in the callback chain, always ensuring that
371
+ # it's configured to bind to the object as this is the convention for
372
+ # DataMapper/Extlib callbacks
373
+ def add_callback(type, options, &block)
374
+ options[:bind_to_object] = true
375
+ super
376
+ end
377
+ end
378
+ end
379
+ end
@@ -0,0 +1,309 @@
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
+ # Don't allow translations
230
+ def translate(klass, key, value)
231
+ value.to_s.humanize.downcase
232
+ end
233
+
234
+ # Defines an initialization hook into the owner class for setting the
235
+ # initial state of the machine *before* any attributes are set on the
236
+ # object
237
+ def define_state_initializer
238
+ @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
239
+ def initialize(attrs = {}, *args)
240
+ from_database = args.first
241
+
242
+ if !from_database && (!attrs || !attrs.stringify_keys.key?('_id'))
243
+ filtered = respond_to?(:filter_protected_attrs) ? filter_protected_attrs(attrs) : attrs
244
+ ignore = filtered ? filtered.keys : []
245
+
246
+ initialize_state_machines(:dynamic => false, :ignore => ignore)
247
+ super
248
+ initialize_state_machines(:dynamic => true, :ignore => ignore)
249
+ else
250
+ super
251
+ end
252
+ end
253
+ end_eval
254
+ end
255
+
256
+ # Skips defining reader/writer methods since this is done automatically
257
+ def define_state_accessor
258
+ owner_class.key(attribute, String) unless owner_class.keys.include?(attribute)
259
+
260
+ name = self.name
261
+ owner_class.validates_each(attribute, :logic => lambda {|*|
262
+ machine = self.class.state_machine(name)
263
+ machine.invalidate(self, :state, :invalid) unless machine.states.match(self)
264
+ })
265
+ end
266
+
267
+ # Adds support for defining the attribute predicate, while providing
268
+ # compatibility with the default predicate which determines whether
269
+ # *anything* is set for the attribute's value
270
+ def define_state_predicate
271
+ name = self.name
272
+
273
+ # Still use class_eval here instance of define_instance_method since
274
+ # we need to be able to call +super+
275
+ @instance_helper_module.class_eval do
276
+ define_method("#{name}?") do |*args|
277
+ args.empty? ? super(*args) : self.class.state_machine(name).states.matches?(self, *args)
278
+ end
279
+ end
280
+ end
281
+
282
+ # Adds hooks into validation for automatically firing events
283
+ def define_action_helpers
284
+ super(action == :save ? :create_or_update : action)
285
+ end
286
+
287
+ # Creates a scope for finding records *with* a particular state or
288
+ # states for the attribute
289
+ def create_with_scope(name)
290
+ define_scope(name, lambda {|values| {:conditions => {attribute => {'$in' => values}}}})
291
+ end
292
+
293
+ # Creates a scope for finding records *without* a particular state or
294
+ # states for the attribute
295
+ def create_without_scope(name)
296
+ define_scope(name, lambda {|values| {:conditions => {attribute => {'$nin' => values}}}})
297
+ end
298
+
299
+ # Defines a new scope with the given name
300
+ def define_scope(name, scope)
301
+ if defined?(::MongoMapper::Version) && ::MongoMapper::Version >= '0.8.0'
302
+ lambda {|model, values| model.query.merge(model.query(scope.call(values)))}
303
+ else
304
+ lambda {|model, values| model.all(scope.call(values))}
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end