hsume2-state_machine 1.0.1

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 (110) hide show
  1. data/CHANGELOG.rdoc +413 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +717 -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.rb +448 -0
  28. data/lib/state_machine/alternate_machine.rb +79 -0
  29. data/lib/state_machine/assertions.rb +36 -0
  30. data/lib/state_machine/branch.rb +224 -0
  31. data/lib/state_machine/callback.rb +236 -0
  32. data/lib/state_machine/condition_proxy.rb +94 -0
  33. data/lib/state_machine/error.rb +13 -0
  34. data/lib/state_machine/eval_helpers.rb +86 -0
  35. data/lib/state_machine/event.rb +304 -0
  36. data/lib/state_machine/event_collection.rb +139 -0
  37. data/lib/state_machine/extensions.rb +149 -0
  38. data/lib/state_machine/initializers.rb +4 -0
  39. data/lib/state_machine/initializers/merb.rb +1 -0
  40. data/lib/state_machine/initializers/rails.rb +25 -0
  41. data/lib/state_machine/integrations.rb +110 -0
  42. data/lib/state_machine/integrations/active_model.rb +502 -0
  43. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  44. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  45. data/lib/state_machine/integrations/active_model/versions.rb +31 -0
  46. data/lib/state_machine/integrations/active_record.rb +424 -0
  47. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  48. data/lib/state_machine/integrations/active_record/versions.rb +143 -0
  49. data/lib/state_machine/integrations/base.rb +91 -0
  50. data/lib/state_machine/integrations/data_mapper.rb +392 -0
  51. data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
  52. data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
  53. data/lib/state_machine/integrations/mongo_mapper.rb +272 -0
  54. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  55. data/lib/state_machine/integrations/mongo_mapper/versions.rb +110 -0
  56. data/lib/state_machine/integrations/mongoid.rb +357 -0
  57. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  58. data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
  59. data/lib/state_machine/integrations/sequel.rb +428 -0
  60. data/lib/state_machine/integrations/sequel/versions.rb +36 -0
  61. data/lib/state_machine/machine.rb +1873 -0
  62. data/lib/state_machine/machine_collection.rb +87 -0
  63. data/lib/state_machine/matcher.rb +123 -0
  64. data/lib/state_machine/matcher_helpers.rb +54 -0
  65. data/lib/state_machine/node_collection.rb +157 -0
  66. data/lib/state_machine/path.rb +120 -0
  67. data/lib/state_machine/path_collection.rb +90 -0
  68. data/lib/state_machine/state.rb +271 -0
  69. data/lib/state_machine/state_collection.rb +112 -0
  70. data/lib/state_machine/transition.rb +458 -0
  71. data/lib/state_machine/transition_collection.rb +244 -0
  72. data/lib/tasks/state_machine.rake +1 -0
  73. data/lib/tasks/state_machine.rb +27 -0
  74. data/test/files/en.yml +17 -0
  75. data/test/files/switch.rb +11 -0
  76. data/test/functional/alternate_state_machine_test.rb +122 -0
  77. data/test/functional/state_machine_test.rb +993 -0
  78. data/test/test_helper.rb +4 -0
  79. data/test/unit/assertions_test.rb +40 -0
  80. data/test/unit/branch_test.rb +890 -0
  81. data/test/unit/callback_test.rb +701 -0
  82. data/test/unit/condition_proxy_test.rb +328 -0
  83. data/test/unit/error_test.rb +43 -0
  84. data/test/unit/eval_helpers_test.rb +222 -0
  85. data/test/unit/event_collection_test.rb +358 -0
  86. data/test/unit/event_test.rb +985 -0
  87. data/test/unit/integrations/active_model_test.rb +1097 -0
  88. data/test/unit/integrations/active_record_test.rb +2021 -0
  89. data/test/unit/integrations/base_test.rb +99 -0
  90. data/test/unit/integrations/data_mapper_test.rb +1909 -0
  91. data/test/unit/integrations/mongo_mapper_test.rb +1611 -0
  92. data/test/unit/integrations/mongoid_test.rb +1591 -0
  93. data/test/unit/integrations/sequel_test.rb +1523 -0
  94. data/test/unit/integrations_test.rb +61 -0
  95. data/test/unit/invalid_event_test.rb +20 -0
  96. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  97. data/test/unit/invalid_transition_test.rb +77 -0
  98. data/test/unit/machine_collection_test.rb +599 -0
  99. data/test/unit/machine_test.rb +3043 -0
  100. data/test/unit/matcher_helpers_test.rb +37 -0
  101. data/test/unit/matcher_test.rb +155 -0
  102. data/test/unit/node_collection_test.rb +217 -0
  103. data/test/unit/path_collection_test.rb +266 -0
  104. data/test/unit/path_test.rb +485 -0
  105. data/test/unit/state_collection_test.rb +310 -0
  106. data/test/unit/state_machine_test.rb +31 -0
  107. data/test/unit/state_test.rb +924 -0
  108. data/test/unit/transition_collection_test.rb +2102 -0
  109. data/test/unit/transition_test.rb +1541 -0
  110. metadata +207 -0
@@ -0,0 +1,4 @@
1
+ filename = "#{File.dirname(__FILE__)}/../active_model/locale.rb"
2
+ translations = eval(IO.read(filename), binding, filename)
3
+ translations[:en][:mongo_mapper] = translations[:en].delete(:activemodel)
4
+ translations
@@ -0,0 +1,110 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module MongoMapper
4
+ version '0.5.x - 0.6.x' do
5
+ def self.active?
6
+ !defined?(::MongoMapper::Plugins)
7
+ end
8
+
9
+ def filter_attributes(object, attributes)
10
+ attributes
11
+ end
12
+
13
+ def define_state_initializer
14
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
15
+ def initialize(*args)
16
+ attrs, * = args
17
+ attrs && attrs.stringify_keys.key?('_id') ? super : self.class.state_machines.initialize_states(self) { super }
18
+ end
19
+ end_eval
20
+ end
21
+ end
22
+
23
+ version '0.5.x - 0.7.x' do
24
+ def self.active?
25
+ !defined?(::MongoMapper::Version) || ::MongoMapper::Version < '0.8.0'
26
+ end
27
+
28
+ def define_scope(name, scope)
29
+ lambda {|model, values| model.all(scope.call(values))}
30
+ end
31
+ end
32
+
33
+ version '0.5.x - 0.8.x' do
34
+ def self.active?
35
+ !defined?(::MongoMapper::Version) || ::MongoMapper::Version < '0.9.0'
36
+ end
37
+
38
+ def invalidate(object, attribute, message, values = [])
39
+ object.errors.add(self.attribute(attribute), generate_message(message, values))
40
+ end
41
+
42
+ def define_state_accessor
43
+ owner_class.key(attribute, String) unless owner_class.keys.include?(attribute)
44
+
45
+ name = self.name
46
+ owner_class.validates_each(attribute, :logic => lambda {|*|
47
+ machine = self.class.state_machine(name)
48
+ machine.invalidate(self, :state, :invalid) unless machine.states.match(self)
49
+ })
50
+ end
51
+
52
+ def action_hook
53
+ action == :save ? :create_or_update : super
54
+ end
55
+
56
+ def load_locale
57
+ end
58
+
59
+ def supports_observers?
60
+ false
61
+ end
62
+
63
+ def supports_validations?
64
+ true
65
+ end
66
+
67
+ def supports_dirty_tracking?(object)
68
+ true
69
+ end
70
+
71
+ def callback_terminator
72
+ end
73
+
74
+ def translate(klass, key, value)
75
+ value.to_s.humanize.downcase
76
+ end
77
+ end
78
+
79
+ version '0.7.x - 0.8.3' do
80
+ def self.active?
81
+ # Only 0.8.x and up has a Version string available, so Plugins is used
82
+ # to detect when 0.7.x is active
83
+ defined?(::MongoMapper::Plugins) && (!defined?(::MongoMapper::Version) || ::MongoMapper::Version <= '0.8.3')
84
+ end
85
+
86
+ def define_state_initializer
87
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
88
+ def initialize(*args)
89
+ attrs, from_db = args
90
+ from_db ? super : self.class.state_machines.initialize_states(self) { super }
91
+ end
92
+ end_eval
93
+ end
94
+ end
95
+
96
+ # Assumes MongoMapper 0.10+ uses ActiveModel 3.1+
97
+ version '0.9.x' do
98
+ def self.active?
99
+ defined?(::MongoMapper::Version) && ::MongoMapper::Version =~ /^0\.9\./
100
+ end
101
+
102
+ def define_action_hook
103
+ # +around+ callbacks don't have direct access to results until AS 3.1
104
+ owner_class.set_callback(:save, :after, 'value', :prepend => true) if action_hook == :save
105
+ super
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,357 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ # Adds support for integrating state machines with Mongoid models.
4
+ #
5
+ # == Examples
6
+ #
7
+ # Below is an example of a simple state machine defined within a
8
+ # Mongoid model:
9
+ #
10
+ # class Vehicle
11
+ # include Mongoid::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: 4d70e028b876bb54d9000003, name: nil, state: "parked">
34
+ # vehicle.name = 'Ford Explorer'
35
+ # vehicle.ignite # => true
36
+ # vehicle.reload # => #<Vehicle _id: 4d70e028b876bb54d9000003, 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 Mongoid, 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: 4d70e028b876bb54d9000003, 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: 4d70e028b876bb54d9000003, 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 Mongoid::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 Mongoid::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: 4d70e028b876bb54d9000003, 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 Mongoid::Document
136
+ #
137
+ # scope :with_states, lambda {|*states| where(:state => {'$in' => states})}
138
+ # # with_states also aliased to with_state
139
+ #
140
+ # scope :without_states, lambda {|*states| where(:state => {'$nin' => states})}
141
+ # # without_states also aliased to without_state
142
+ # end
143
+ #
144
+ # *Note*, however, that the states are converted to their stored values
145
+ # before being passed into the query.
146
+ #
147
+ # Because of the way named scopes work in Mongoid, they *cannot* be
148
+ # chained.
149
+ #
150
+ # == Callbacks
151
+ #
152
+ # All before/after transition callbacks defined for Mongoid models
153
+ # behave in the same way that other Mongoid callbacks behave. The
154
+ # object involved in the transition is passed in as an argument.
155
+ #
156
+ # For example,
157
+ #
158
+ # class Vehicle
159
+ # include Mongoid::Document
160
+ #
161
+ # state_machine :initial => :parked do
162
+ # before_transition any => :idling do |vehicle|
163
+ # vehicle.put_on_seatbelt
164
+ # end
165
+ #
166
+ # before_transition do |vehicle, transition|
167
+ # # log message
168
+ # end
169
+ #
170
+ # event :ignite do
171
+ # transition :parked => :idling
172
+ # end
173
+ # end
174
+ #
175
+ # def put_on_seatbelt
176
+ # ...
177
+ # end
178
+ # end
179
+ #
180
+ # Note, also, that the transition can be accessed by simply defining
181
+ # additional arguments in the callback block.
182
+ #
183
+ # == Observers
184
+ #
185
+ # In addition to support for Mongoid-like hooks, there is additional support
186
+ # for Mongoid observers. Because of the way Mongoid observers are designed,
187
+ # there is less flexibility around the specific transitions that can be
188
+ # hooked in. However, a large number of hooks *are* supported. For
189
+ # example, if a transition for a record's +state+ attribute changes the
190
+ # state from +parked+ to +idling+ via the +ignite+ event, the following
191
+ # observer methods are supported:
192
+ # * before/after/after_failure_to-_ignite_from_parked_to_idling
193
+ # * before/after/after_failure_to-_ignite_from_parked
194
+ # * before/after/after_failure_to-_ignite_to_idling
195
+ # * before/after/after_failure_to-_ignite
196
+ # * before/after/after_failure_to-_transition_state_from_parked_to_idling
197
+ # * before/after/after_failure_to-_transition_state_from_parked
198
+ # * before/after/after_failure_to-_transition_state_to_idling
199
+ # * before/after/after_failure_to-_transition_state
200
+ # * before/after/after_failure_to-_transition
201
+ #
202
+ # The following class shows an example of some of these hooks:
203
+ #
204
+ # class VehicleObserver < Mongoid::Observer
205
+ # def before_save(vehicle)
206
+ # # log message
207
+ # end
208
+ #
209
+ # # Callback for :ignite event *before* the transition is performed
210
+ # def before_ignite(vehicle, transition)
211
+ # # log message
212
+ # end
213
+ #
214
+ # # Callback for :ignite event *after* the transition has been performed
215
+ # def after_ignite(vehicle, transition)
216
+ # # put on seatbelt
217
+ # end
218
+ #
219
+ # # Generic transition callback *before* the transition is performed
220
+ # def after_transition(vehicle, transition)
221
+ # Audit.log(vehicle, transition)
222
+ # end
223
+ # end
224
+ #
225
+ # More flexible transition callbacks can be defined directly within the
226
+ # model as described in StateMachine::Machine#before_transition
227
+ # and StateMachine::Machine#after_transition.
228
+ #
229
+ # To define a single observer for multiple state machines:
230
+ #
231
+ # class StateMachineObserver < Mongoid::Observer
232
+ # observe Vehicle, Switch, Project
233
+ #
234
+ # def after_transition(record, transition)
235
+ # Audit.log(record, transition)
236
+ # end
237
+ # end
238
+ module Mongoid
239
+ include Base
240
+ include ActiveModel
241
+
242
+ require 'state_machine/integrations/mongoid/versions'
243
+
244
+ # The default options to use for state machines using this integration
245
+ @defaults = {:action => :save}
246
+
247
+ # Whether this integration is available. Only true if Mongoid::Document
248
+ # is defined.
249
+ def self.available?
250
+ defined?(::Mongoid::Document)
251
+ end
252
+
253
+ # Should this integration be used for state machines in the given class?
254
+ # Classes that include Mongoid::Document will automatically use the
255
+ # Mongoid integration.
256
+ def self.matches?(klass)
257
+ klass <= ::Mongoid::Document
258
+ end
259
+
260
+ def self.extended(base) #:nodoc:
261
+ require 'mongoid/version'
262
+ super
263
+ end
264
+
265
+ # Forces the change in state to be recognized regardless of whether the
266
+ # state value actually changed
267
+ def write(object, attribute, value, *args)
268
+ result = super
269
+
270
+ if (attribute == :state || attribute == :event && value) && !object.send("#{self.attribute}_changed?")
271
+ current = read(object, :state)
272
+ object.changes[self.attribute.to_s] = [attribute == :event ? current : value, current]
273
+ end
274
+
275
+ result
276
+ end
277
+
278
+ protected
279
+ # Mongoid uses its own implementation of dirty tracking instead of
280
+ # ActiveModel's and doesn't support the #{attribute}_will_change! APIs
281
+ def supports_dirty_tracking?(object)
282
+ false
283
+ end
284
+
285
+ # Only runs validations on the action if using <tt>:save</tt>
286
+ def runs_validations_on_action?
287
+ action == :save
288
+ end
289
+
290
+ # Defines an initialization hook into the owner class for setting the
291
+ # initial state of the machine *before* any attributes are set on the
292
+ # object
293
+ def define_state_initializer
294
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
295
+ # Initializes dynamic states
296
+ def initialize(*)
297
+ super do |*args|
298
+ self.class.state_machines.initialize_states(self, :static => false)
299
+ yield(*args) if block_given?
300
+ end
301
+ end
302
+
303
+ # Initializes static states
304
+ def apply_default_attributes(*)
305
+ result = super
306
+ self.class.state_machines.initialize_states(self, :dynamic => false, :to => result) if new_record?
307
+ result
308
+ end
309
+ end_eval
310
+ end
311
+
312
+ # Skips defining reader/writer methods since this is done automatically
313
+ def define_state_accessor
314
+ owner_class.field(attribute, :type => String) unless owner_class.fields.include?(attribute)
315
+ super
316
+ end
317
+
318
+ # Uses around callbacks to run state events if using the :save hook
319
+ def define_action_hook
320
+ if action_hook == :save
321
+ owner_class.set_callback(:save, :around, self, :prepend => true)
322
+ else
323
+ super
324
+ end
325
+ end
326
+
327
+ # Runs state events around the machine's :save action
328
+ def around_save(object)
329
+ object.class.state_machines.transitions(object, action).perform { yield }
330
+ end
331
+
332
+ # Creates a scope for finding records *with* a particular state or
333
+ # states for the attribute
334
+ def create_with_scope(name)
335
+ define_scope(name, lambda {|values| {attribute => {'$in' => values}}})
336
+ end
337
+
338
+ # Creates a scope for finding records *without* a particular state or
339
+ # states for the attribute
340
+ def create_without_scope(name)
341
+ define_scope(name, lambda {|values| {attribute => {'$nin' => values}}})
342
+ end
343
+
344
+ # Defines a new scope with the given name
345
+ def define_scope(name, scope)
346
+ lambda {|model, values| model.criteria.where(scope.call(values))}
347
+ end
348
+
349
+ # ActiveModel's use of method_missing / respond_to for attribute methods
350
+ # breaks both ancestor lookups and defined?(super). Need to special-case
351
+ # the existence of query attribute methods.
352
+ def owner_class_ancestor_has_method?(scope, method)
353
+ scope == :instance && method == "#{name}?" || super
354
+ end
355
+ end
356
+ end
357
+ end