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,445 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ # Adds support for integrating state machines with ActiveModel classes.
4
+ #
5
+ # == Examples
6
+ #
7
+ # If using ActiveModel directly within your class, then any one of the
8
+ # following features need to be included in order for the integration to be
9
+ # detected:
10
+ # * ActiveModel::Dirty
11
+ # * ActiveModel::Observing
12
+ # * ActiveModel::Validations
13
+ #
14
+ # Below is an example of a simple state machine defined within an
15
+ # ActiveModel class:
16
+ #
17
+ # class Vehicle
18
+ # include ActiveModel::Dirty
19
+ # include ActiveModel::Observing
20
+ # include ActiveModel::Validations
21
+ #
22
+ # attr_accessor :state
23
+ # define_attribute_methods [:state]
24
+ #
25
+ # state_machine :initial => :parked do
26
+ # event :ignite do
27
+ # transition :parked => :idling
28
+ # end
29
+ # end
30
+ # end
31
+ #
32
+ # The examples in the sections below will use the above class as a
33
+ # reference.
34
+ #
35
+ # == Actions
36
+ #
37
+ # By default, no action will be invoked when a state is transitioned. This
38
+ # means that if you want to save changes when transitioning, you must
39
+ # define the action yourself like so:
40
+ #
41
+ # class Vehicle
42
+ # include ActiveModel::Validations
43
+ # attr_accessor :state
44
+ #
45
+ # state_machine :action => :save do
46
+ # ...
47
+ # end
48
+ #
49
+ # def save
50
+ # # Save changes
51
+ # end
52
+ # end
53
+ #
54
+ # == Validation errors
55
+ #
56
+ # In order to hook in validation support for your model, the
57
+ # ActiveModel::Validations feature must be included. If this is included
58
+ # and an event fails to successfully fire because there are no matching
59
+ # transitions for the object, a validation error is added to the object's
60
+ # state attribute to help in determining why it failed.
61
+ #
62
+ # For example,
63
+ #
64
+ # vehicle = Vehicle.new
65
+ # vehicle.ignite # => false
66
+ # vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
67
+ #
68
+ # == Callbacks
69
+ #
70
+ # All before/after transition callbacks defined for ActiveModel models
71
+ # behave in the same way that other ActiveSupport callbacks behave. The
72
+ # object involved in the transition is passed in as an argument.
73
+ #
74
+ # For example,
75
+ #
76
+ # class Vehicle
77
+ # include ActiveModel::Validations
78
+ # attr_accessor :state
79
+ #
80
+ # state_machine :initial => :parked do
81
+ # before_transition any => :idling do |vehicle|
82
+ # vehicle.put_on_seatbelt
83
+ # end
84
+ #
85
+ # before_transition do |vehicle, transition|
86
+ # # log message
87
+ # end
88
+ #
89
+ # event :ignite do
90
+ # transition :parked => :idling
91
+ # end
92
+ # end
93
+ #
94
+ # def put_on_seatbelt
95
+ # ...
96
+ # end
97
+ # end
98
+ #
99
+ # Note, also, that the transition can be accessed by simply defining
100
+ # additional arguments in the callback block.
101
+ #
102
+ # == Observers
103
+ #
104
+ # In order to hook in observer support for your application, the
105
+ # ActiveModel::Observing feature must be included. Because of the way
106
+ # ActiveModel observers are designed, there is less flexibility around the
107
+ # specific transitions that can be hooked in. However, a large number of
108
+ # hooks *are* supported. For example, if a transition for a object's
109
+ # +state+ attribute changes the state from +parked+ to +idling+ via the
110
+ # +ignite+ event, the following observer methods are supported:
111
+ # * before/after_ignite_from_parked_to_idling
112
+ # * before/after_ignite_from_parked
113
+ # * before/after_ignite_to_idling
114
+ # * before/after_ignite
115
+ # * before/after_transition_state_from_parked_to_idling
116
+ # * before/after_transition_state_from_parked
117
+ # * before/after_transition_state_to_idling
118
+ # * before/after_transition_state
119
+ # * before/after_transition
120
+ #
121
+ # The following class shows an example of some of these hooks:
122
+ #
123
+ # class VehicleObserver < ActiveModel::Observer
124
+ # # Callback for :ignite event *before* the transition is performed
125
+ # def before_ignite(vehicle, transition)
126
+ # # log message
127
+ # end
128
+ #
129
+ # # Callback for :ignite event *after* the transition has been performed
130
+ # def after_ignite(vehicle, transition)
131
+ # # put on seatbelt
132
+ # end
133
+ #
134
+ # # Generic transition callback *before* the transition is performed
135
+ # def after_transition(vehicle, transition)
136
+ # Audit.log(vehicle, transition)
137
+ # end
138
+ # end
139
+ #
140
+ # More flexible transition callbacks can be defined directly within the
141
+ # model as described in StateMachine::Machine#before_transition
142
+ # and StateMachine::Machine#after_transition.
143
+ #
144
+ # To define a single observer for multiple state machines:
145
+ #
146
+ # class StateMachineObserver < ActiveModel::Observer
147
+ # observe Vehicle, Switch, Project
148
+ #
149
+ # def after_transition(object, transition)
150
+ # Audit.log(object, transition)
151
+ # end
152
+ # end
153
+ #
154
+ # == Dirty Attribute Tracking
155
+ #
156
+ # In order to hook in validation support for your model, the
157
+ # ActiveModel::Validations feature must be included. If this is included
158
+ # then state attributes will always be properly marked as changed whether
159
+ # they were a callback or not.
160
+ #
161
+ # For example,
162
+ #
163
+ # class Vehicle
164
+ # include ActiveModel::Dirty
165
+ # attr_accessor :state
166
+ #
167
+ # state_machine :initial => :parked do
168
+ # event :park do
169
+ # transition :parked => :parked
170
+ # end
171
+ # end
172
+ # end
173
+ #
174
+ # vehicle = Vehicle.new
175
+ # vehicle.changed # => []
176
+ # vehicle.park # => true
177
+ # vehicle.changed # => ["state"]
178
+ #
179
+ # == Creating new integrations
180
+ #
181
+ # If you want to integrate state_machine with an ORM that implements parts
182
+ # or all of the ActiveModel API, the following features must be specified:
183
+ # * i18n scope (locale)
184
+ # * Machine defaults
185
+ #
186
+ # For example,
187
+ #
188
+ # module StateMachine::Integrations::MyORM
189
+ # include StateMachine::Integrations::ActiveModel
190
+ #
191
+ # @defaults = {:action = > :persist}
192
+ #
193
+ # def self.matches?(klass)
194
+ # defined?(::MyORM::Base) && klass <= ::MyORM::Base
195
+ # end
196
+ #
197
+ # def self.extended(base)
198
+ # locale = "#{File.dirname(__FILE__)}/my_orm/locale.rb"
199
+ # I18n.load_path << locale unless I18n.load_path.include?(locale)
200
+ # end
201
+ #
202
+ # protected
203
+ # def runs_validation_on_action?
204
+ # action == :persist
205
+ # end
206
+ #
207
+ # def i18n_scope
208
+ # :myorm
209
+ # end
210
+ # end
211
+ #
212
+ # If you wish to implement other features, such as attribute initialization
213
+ # with protected attributes, named scopes, or database transactions, you
214
+ # must add these independent of the ActiveModel integration. See the
215
+ # ActiveRecord implementation for examples of these customizations.
216
+ module ActiveModel
217
+ module ClassMethods
218
+ # The default options to use for state machines using this integration
219
+ attr_reader :defaults
220
+
221
+ # Loads additional files specific to ActiveModel
222
+ def extended(base) #:nodoc:
223
+ require 'state_machine/integrations/active_model/observer'
224
+
225
+ if defined?(I18n)
226
+ locale = "#{File.dirname(__FILE__)}/active_model/locale.rb"
227
+ I18n.load_path.unshift(locale) unless I18n.load_path.include?(locale)
228
+ end
229
+ end
230
+ end
231
+
232
+ def self.included(base) #:nodoc:
233
+ base.class_eval do
234
+ extend ClassMethods
235
+ end
236
+ end
237
+
238
+ extend ClassMethods
239
+
240
+ # Should this integration be used for state machines in the given class?
241
+ # Classes that include ActiveModel::Dirty, ActiveModel::Observing, or
242
+ # ActiveModel::Validations will automatically use the ActiveModel
243
+ # integration.
244
+ def self.matches?(klass)
245
+ features = %w(Dirty Observing Validations)
246
+ defined?(::ActiveModel) && features.any? {|feature| ::ActiveModel.const_defined?(feature) && klass <= ::ActiveModel.const_get(feature)}
247
+ end
248
+
249
+ @defaults = {}
250
+
251
+ # Forces the change in state to be recognized regardless of whether the
252
+ # state value actually changed
253
+ def write(object, attribute, value)
254
+ result = super
255
+ if attribute == :state && supports_dirty_tracking?(object) && !object.send("#{self.attribute}_changed?")
256
+ object.send("#{self.attribute}_will_change!")
257
+ end
258
+ result
259
+ end
260
+
261
+ # Adds a validation error to the given object
262
+ def invalidate(object, attribute, message, values = [])
263
+ if supports_validations?
264
+ attribute = self.attribute(attribute)
265
+ options = values.inject({}) do |options, (key, value)|
266
+ options[key] = value
267
+ options
268
+ end
269
+
270
+ default_options = default_error_message_options(object, attribute, message)
271
+ object.errors.add(attribute, message, options.merge(default_options))
272
+ end
273
+ end
274
+
275
+ # Resets any errors previously added when invalidating the given object
276
+ def reset(object)
277
+ object.errors.clear if supports_validations?
278
+ end
279
+
280
+ protected
281
+ # Whether observers are supported in the integration. Only true if
282
+ # ActiveModel::Observer is available.
283
+ def supports_observers?
284
+ defined?(::ActiveModel::Observing) && owner_class <= ::ActiveModel::Observing
285
+ end
286
+
287
+ # Whether validations are supported in the integration. Only true if
288
+ # the ActiveModel feature is enabled on the owner class.
289
+ def supports_validations?
290
+ defined?(::ActiveModel::Validations) && owner_class <= ::ActiveModel::Validations
291
+ end
292
+
293
+ # Do validations run when the action configured this machine is
294
+ # invoked? This is used to determine whether to fire off attribute-based
295
+ # event transitions when the action is run.
296
+ def runs_validations_on_action?
297
+ false
298
+ end
299
+
300
+ # Whether change (dirty) tracking is supported in the integration.
301
+ # Only true if the ActiveModel feature is enabled on the owner class.
302
+ def supports_dirty_tracking?(object)
303
+ defined?(::ActiveModel::Dirty) && owner_class <= ::ActiveModel::Dirty && object.respond_to?("#{self.attribute}_changed?")
304
+ end
305
+
306
+ # Gets the terminator to use for callbacks
307
+ def callback_terminator
308
+ @terminator ||= lambda {|result| result == false}
309
+ end
310
+
311
+ # Determines the base scope to use when looking up translations
312
+ def i18n_scope
313
+ owner_class.i18n_scope
314
+ end
315
+
316
+ # The default options to use when generating messages for validation
317
+ # errors
318
+ def default_error_message_options(object, attribute, message)
319
+ {:message => @messages[message]}
320
+ end
321
+
322
+ # Translates the given key / value combo. Translation keys are looked
323
+ # up in the following order:
324
+ # * <tt>#{i18n_scope}.state_machines.#{model_name}.#{machine_name}.#{plural_key}.#{value}</tt>
325
+ # * <tt>#{i18n_scope}.state_machines.#{machine_name}.#{plural_key}.#{value}
326
+ # * <tt>#{i18n_scope}.state_machines.#{plural_key}.#{value}</tt>
327
+ #
328
+ # If no keys are found, then the humanized value will be the fallback.
329
+ def translate(klass, key, value)
330
+ ancestors = ancestors_for(klass)
331
+ group = key.to_s.pluralize
332
+ value = value ? value.to_s : 'nil'
333
+
334
+ # Generate all possible translation keys
335
+ translations = ancestors.map {|ancestor| :"#{ancestor.model_name.underscore}.#{name}.#{group}.#{value}"}
336
+ translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase])
337
+ I18n.translate(translations.shift, :default => translations, :scope => [i18n_scope, :state_machines])
338
+ end
339
+
340
+ # Build a list of ancestors for the given class to use when
341
+ # determining which localization key to use for a particular string.
342
+ def ancestors_for(klass)
343
+ klass.lookup_ancestors
344
+ end
345
+
346
+ # Adds the default callbacks for notifying ActiveModel observers
347
+ # before/after a transition has been performed.
348
+ def after_initialize
349
+ if supports_observers?
350
+ callbacks[:before] << Callback.new(:before) {|object, transition| notify(:before, object, transition)}
351
+ callbacks[:after] << Callback.new(:after) {|object, transition| notify(:after, object, transition)}
352
+ end
353
+ end
354
+
355
+ # Skips defining reader/writer methods since this is done automatically
356
+ def define_state_accessor
357
+ name = self.name
358
+
359
+ owner_class.validates_each(attribute) do |object, attr, value|
360
+ machine = object.class.state_machine(name)
361
+ machine.invalidate(object, :state, :invalid) unless machine.states.match(object)
362
+ end if supports_validations?
363
+ end
364
+
365
+ # Adds hooks into validation for automatically firing events
366
+ def define_action_helpers(*args)
367
+ super
368
+
369
+ action = self.action
370
+ @instance_helper_module.class_eval do
371
+ define_method(:valid?) do |*args|
372
+ self.class.state_machines.transitions(self, action, :after => false).perform { super(*args) }
373
+ end
374
+ end if runs_validations_on_action?
375
+ end
376
+
377
+ # Creates a new callback in the callback chain, always inserting it
378
+ # before the default Observer callbacks that were created after
379
+ # initialization.
380
+ def add_callback(type, options, &block)
381
+ options[:terminator] = callback_terminator
382
+
383
+ if supports_observers?
384
+ @callbacks[type == :around ? :before : type].insert(-2, callback = Callback.new(type, options, &block))
385
+ add_states(callback.known_states)
386
+ callback
387
+ else
388
+ super
389
+ end
390
+ end
391
+
392
+ # Configures new states with the built-in humanize scheme
393
+ def add_states(new_states)
394
+ super.each do |state|
395
+ state.human_name = lambda {|state, klass| translate(klass, :state, state.name)}
396
+ end
397
+ end
398
+
399
+ # Configures new event with the built-in humanize scheme
400
+ def add_events(new_events)
401
+ super.each do |event|
402
+ event.human_name = lambda {|event, klass| translate(klass, :event, event.name)}
403
+ end
404
+ end
405
+
406
+ # Notifies observers on the given object that a callback occurred
407
+ # involving the given transition. This will attempt to call the
408
+ # following methods on observers:
409
+ # * #{type}_#{qualified_event}_from_#{from}_to_#{to}
410
+ # * #{type}_#{qualified_event}_from_#{from}
411
+ # * #{type}_#{qualified_event}_to_#{to}
412
+ # * #{type}_#{qualified_event}
413
+ # * #{type}_transition_#{machine_name}_from_#{from}_to_#{to}
414
+ # * #{type}_transition_#{machine_name}_from_#{from}
415
+ # * #{type}_transition_#{machine_name}_to_#{to}
416
+ # * #{type}_transition_#{machine_name}
417
+ # * #{type}_transition
418
+ #
419
+ # This will always return true regardless of the results of the
420
+ # callbacks.
421
+ def notify(type, object, transition)
422
+ name = self.name
423
+ event = transition.qualified_event
424
+ from = transition.from_name
425
+ to = transition.to_name
426
+
427
+ # Machine-specific updates
428
+ ["#{type}_#{event}", "#{type}_transition_#{name}"].each do |event_segment|
429
+ ["_from_#{from}", nil].each do |from_segment|
430
+ ["_to_#{to}", nil].each do |to_segment|
431
+ object.class.changed if object.class.respond_to?(:changed)
432
+ object.class.notify_observers([event_segment, from_segment, to_segment].join, object, transition)
433
+ end
434
+ end
435
+ end
436
+
437
+ # Generic updates
438
+ object.class.changed if object.class.respond_to?(:changed)
439
+ object.class.notify_observers("#{type}_transition", object, transition)
440
+
441
+ true
442
+ end
443
+ end
444
+ end
445
+ end
@@ -0,0 +1,20 @@
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
+
5
+ # Only ActiveRecord 2.3.5+ can pull i18n >= 0.1.3 from system-wide gems (and
6
+ # therefore possibly have I18n::VERSION available)
7
+ begin
8
+ require 'i18n/version'
9
+ rescue Exception => ex
10
+ end unless ::ActiveRecord::VERSION::MAJOR == 2 && (::ActiveRecord::VERSION::MINOR < 3 || ::ActiveRecord::VERSION::TINY < 5)
11
+
12
+ # Only i18n 0.4.0+ has the new %{key} syntax
13
+ if !defined?(I18n::VERSION) || I18n::VERSION < '0.4.0'
14
+ translations[:en][:activerecord][:errors][:messages].each do |key, message|
15
+ message.gsub!('%{', '{{')
16
+ message.gsub!('}', '}}')
17
+ end
18
+ end
19
+
20
+ translations