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][:mongoid] = translations[:en].delete(:activemodel)
4
+ translations
@@ -0,0 +1,18 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module Mongoid
4
+ # Assumes Mongoid 2.1+ uses ActiveModel 3.1+
5
+ version '2.0.x' do
6
+ def self.active?
7
+ ::Mongoid::VERSION >= '2.0.0' && ::Mongoid::VERSION < '2.1.0'
8
+ end
9
+
10
+ def define_action_hook
11
+ # +around+ callbacks don't have direct access to results until AS 3.1
12
+ owner_class.set_callback(:save, :after, 'value', :prepend => true) if action_hook == :save
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,428 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ # Adds support for integrating state machines with Sequel models.
4
+ #
5
+ # == Examples
6
+ #
7
+ # Below is an example of a simple state machine defined within a
8
+ # Sequel model:
9
+ #
10
+ # class Vehicle < Sequel::Model
11
+ # state_machine :initial => :parked do
12
+ # event :ignite do
13
+ # transition :parked => :idling
14
+ # end
15
+ # end
16
+ # end
17
+ #
18
+ # The examples in the sections below will use the above class as a
19
+ # reference.
20
+ #
21
+ # == Actions
22
+ #
23
+ # By default, the action that will be invoked when a state is transitioned
24
+ # is the +save+ action. This will cause the resource to save the changes
25
+ # made to the state machine's attribute. *Note* that if any other changes
26
+ # were made to the resource prior to transition, then those changes will
27
+ # be made as well.
28
+ #
29
+ # For example,
30
+ #
31
+ # vehicle = Vehicle.create # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
32
+ # vehicle.name = 'Ford Explorer'
33
+ # vehicle.ignite # => true
34
+ # vehicle.refresh # => #<Vehicle @values={:state=>"idling", :name=>"Ford Explorer", :id=>1}>
35
+ #
36
+ # == Events
37
+ #
38
+ # As described in StateMachine::InstanceMethods#state_machine, event
39
+ # attributes are created for every machine that allow transitions to be
40
+ # performed automatically when the object's action (in this case, :save)
41
+ # is called.
42
+ #
43
+ # In Sequel, these automated events are run in the following order:
44
+ # * before validation - Run before callbacks and persist new states, then validate
45
+ # * before save - If validation was skipped, run before callbacks and persist new states, then save
46
+ # * after save - Run after callbacks
47
+ #
48
+ # For example,
49
+ #
50
+ # vehicle = Vehicle.create # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
51
+ # vehicle.state_event # => nil
52
+ # vehicle.state_event = 'invalid'
53
+ # vehicle.valid? # => false
54
+ # vehicle.errors.full_messages # => ["state_event is invalid"]
55
+ #
56
+ # vehicle.state_event = 'ignite'
57
+ # vehicle.valid? # => true
58
+ # vehicle.save # => #<Vehicle @values={:state=>"idling", :name=>nil, :id=>1}>
59
+ # vehicle.state # => "idling"
60
+ # vehicle.state_event # => nil
61
+ #
62
+ # Note that this can also be done on a mass-assignment basis:
63
+ #
64
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle @values={:state=>"idling", :name=>nil, :id=>1}>
65
+ # vehicle.state # => "idling"
66
+ #
67
+ # This technique is always used for transitioning states when the +save+
68
+ # action (which is the default) is configured for the machine.
69
+ #
70
+ # === Security implications
71
+ #
72
+ # Beware that public event attributes mean that events can be fired
73
+ # whenever mass-assignment is being used. If you want to prevent malicious
74
+ # users from tampering with events through URLs / forms, the attribute
75
+ # should be protected like so:
76
+ #
77
+ # class Vehicle < Sequel::Model
78
+ # set_restricted_columns :state_event
79
+ # # set_allowed_columns ... # Alternative technique
80
+ #
81
+ # state_machine do
82
+ # ...
83
+ # end
84
+ # end
85
+ #
86
+ # If you want to only have *some* events be able to fire via mass-assignment,
87
+ # you can build two state machines (one public and one protected) like so:
88
+ #
89
+ # class Vehicle < Sequel::Model
90
+ # set_restricted_columns :state_event # Prevent access to events in the first machine
91
+ #
92
+ # state_machine do
93
+ # # Define private events here
94
+ # end
95
+ #
96
+ # # Allow both machines to share the same state
97
+ # state_machine :public_state, :attribute => :state do
98
+ # # Define public events here
99
+ # end
100
+ # end
101
+ #
102
+ # == Transactions
103
+ #
104
+ # In order to ensure that any changes made during transition callbacks
105
+ # are rolled back during a failed attempt, every transition is wrapped
106
+ # within a transaction.
107
+ #
108
+ # For example,
109
+ #
110
+ # class Message < Sequel::Model
111
+ # end
112
+ #
113
+ # Vehicle.state_machine do
114
+ # before_transition do |transition|
115
+ # Message.create(:content => transition.inspect)
116
+ # false
117
+ # end
118
+ # end
119
+ #
120
+ # vehicle = Vehicle.create # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
121
+ # vehicle.ignite # => false
122
+ # Message.count # => 0
123
+ #
124
+ # *Note* that only before callbacks that halt the callback chain and
125
+ # failed attempts to save the record will result in the transaction being
126
+ # rolled back. If an after callback halts the chain, the previous result
127
+ # still applies and the transaction is *not* rolled back.
128
+ #
129
+ # To turn off transactions:
130
+ #
131
+ # class Vehicle < Sequel::Model
132
+ # state_machine :initial => :parked, :use_transactions => false do
133
+ # ...
134
+ # end
135
+ # end
136
+ #
137
+ # If using the +save+ action for the machine, this option will be ignored as
138
+ # the transaction will be created by Sequel within +save+.
139
+ #
140
+ # == Validation errors
141
+ #
142
+ # If an event fails to successfully fire because there are no matching
143
+ # transitions for the current record, a validation error is added to the
144
+ # record's state attribute to help in determining why it failed and for
145
+ # reporting via the UI.
146
+ #
147
+ # For example,
148
+ #
149
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
150
+ # vehicle.ignite # => false
151
+ # vehicle.errors.full_messages # => ["state cannot transition via \"ignite\""]
152
+ #
153
+ # If an event fails to fire because of a validation error on the record and
154
+ # *not* because a matching transition was not available, no error messages
155
+ # will be added to the state attribute.
156
+ #
157
+ # == Scopes
158
+ #
159
+ # To assist in filtering models with specific states, a series of class
160
+ # methods are defined on the model for finding records with or without a
161
+ # particular set of states.
162
+ #
163
+ # These named scopes are the functional equivalent of the following
164
+ # definitions:
165
+ #
166
+ # class Vehicle < Sequel::Model
167
+ # class << self
168
+ # def with_states(*states)
169
+ # filter(:state => states)
170
+ # end
171
+ # alias_method :with_state, :with_states
172
+ #
173
+ # def without_states(*states)
174
+ # filter(~{:state => states})
175
+ # end
176
+ # alias_method :without_state, :without_states
177
+ # end
178
+ # end
179
+ #
180
+ # *Note*, however, that the states are converted to their stored values
181
+ # before being passed into the query.
182
+ #
183
+ # Because of the way scopes work in Sequel, they can be chained like so:
184
+ #
185
+ # Vehicle.with_state(:parked).order(:id.desc)
186
+ #
187
+ # == Callbacks
188
+ #
189
+ # All before/after transition callbacks defined for Sequel resources
190
+ # behave in the same way that other Sequel hooks behave. Rather than
191
+ # passing in the record as an argument to the callback, the callback is
192
+ # instead bound to the object and evaluated within its context.
193
+ #
194
+ # For example,
195
+ #
196
+ # class Vehicle < Sequel::Model
197
+ # state_machine :initial => :parked do
198
+ # before_transition any => :idling do
199
+ # put_on_seatbelt
200
+ # end
201
+ #
202
+ # before_transition do |transition|
203
+ # # log message
204
+ # end
205
+ #
206
+ # event :ignite do
207
+ # transition :parked => :idling
208
+ # end
209
+ # end
210
+ #
211
+ # def put_on_seatbelt
212
+ # ...
213
+ # end
214
+ # end
215
+ #
216
+ # Note, also, that the transition can be accessed by simply defining
217
+ # additional arguments in the callback block.
218
+ module Sequel
219
+ include Base
220
+
221
+ require 'state_machine/integrations/sequel/versions'
222
+
223
+ # The default options to use for state machines using this integration
224
+ class << self; attr_reader :defaults; end
225
+ @defaults = {:action => :save}
226
+
227
+ # Whether this integration is available. Only true if Sequel::Model is
228
+ # defined.
229
+ def self.available?
230
+ defined?(::Sequel::Model)
231
+ end
232
+
233
+ # Should this integration be used for state machines in the given class?
234
+ # Classes that include Sequel::Model will automatically use the Sequel
235
+ # integration.
236
+ def self.matches?(klass)
237
+ klass <= ::Sequel::Model
238
+ end
239
+
240
+ # Forces the change in state to be recognized regardless of whether the
241
+ # state value actually changed
242
+ def write(object, attribute, value, *args)
243
+ result = super
244
+
245
+ column = self.attribute.to_sym
246
+ if (attribute == :state || attribute == :event && value) && owner_class.columns.include?(column) && !object.changed_columns.include?(column)
247
+ object.changed_columns << column
248
+ end
249
+
250
+ result
251
+ end
252
+
253
+ # Adds a validation error to the given object
254
+ def invalidate(object, attribute, message, values = [])
255
+ object.errors.add(self.attribute(attribute), generate_message(message, values))
256
+ end
257
+
258
+ # Resets any errors previously added when invalidating the given object
259
+ def reset(object)
260
+ object.errors.clear
261
+ end
262
+
263
+ # Pluralizes the name using the built-in inflector
264
+ def pluralize(word)
265
+ load_inflector
266
+ super
267
+ end
268
+
269
+ protected
270
+ # Loads the built-in inflector
271
+ def load_inflector
272
+ require 'sequel/extensions/inflector'
273
+ end
274
+
275
+ # Defines an initialization hook into the owner class for setting the
276
+ # initial state of the machine *before* any attributes are set on the
277
+ # object
278
+ def define_state_initializer
279
+ # Hooks in to attribute initialization to set the states *prior* to
280
+ # the attributes being set
281
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
282
+ # Initializes dynamic states
283
+ def initialize(*)
284
+ super do |*args|
285
+ self.class.state_machines.initialize_states(self, :static => false)
286
+ changed_columns.clear
287
+ yield(*args) if block_given?
288
+ end
289
+ end
290
+
291
+ # Initializes static states
292
+ def set(*)
293
+ self.class.state_machines.initialize_states(self, :dynamic => false) if values.empty?
294
+ super
295
+ end
296
+ end_eval
297
+ end
298
+
299
+ # Skips defining reader/writer methods since this is done automatically
300
+ def define_state_accessor
301
+ name = self.name
302
+ owner_class.validates_each(attribute) do |record, attr, value|
303
+ machine = record.class.state_machine(name)
304
+ machine.invalidate(record, :state, :invalid) unless machine.states.match(record)
305
+ end
306
+ end
307
+
308
+ # Adds hooks into validation for automatically firing events. This is
309
+ # a bit more complicated than other integrations since Sequel doesn't
310
+ # provide an easy way to hook around validation calls
311
+ def define_action_helpers
312
+ super
313
+
314
+ if action == :save
315
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
316
+ def valid?(*args)
317
+ yielded = false
318
+ result = self.class.state_machines.transitions(self, :save, :after => false).perform do
319
+ yielded = true
320
+ super
321
+ end
322
+
323
+ if yielded || result
324
+ result
325
+ else
326
+ #{handle_validation_failure}
327
+ end
328
+ end
329
+ end_eval
330
+ end
331
+ end
332
+
333
+ # Uses custom hooks for :save actions in order to preserve failure
334
+ # behavior within Sequel. This is a bit more complicated than other
335
+ # integrations since Sequel doesn't provide an easy way to hook around
336
+ # save calls.
337
+ def define_action_hook
338
+ if action == :save
339
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
340
+ def #{action_hook}(*)
341
+ yielded = false
342
+ result = self.class.state_machines.transitions(self, :save).perform do
343
+ yielded = true
344
+ super
345
+ end
346
+
347
+ if yielded || result
348
+ result
349
+ else
350
+ #{handle_save_failure}
351
+ end
352
+ end
353
+ end_eval
354
+ else
355
+ super
356
+ end
357
+ end
358
+
359
+ # Uses internal save hooks if using the :save action
360
+ def action_hook
361
+ action == :save ? :_save : super
362
+ end
363
+
364
+ # Handles whether validation errors should be raised
365
+ def handle_validation_failure
366
+ 'raise_on_failure?(args.first || {}) ? raise_hook_failure(:validation) : result'
367
+ end
368
+
369
+ # Handles how save failures are raised
370
+ def handle_save_failure
371
+ 'raise_hook_failure(:save)'
372
+ end
373
+
374
+ # Creates a scope for finding records *with* a particular state or
375
+ # states for the attribute
376
+ def create_with_scope(name)
377
+ create_scope(name, lambda {|dataset, values| dataset.filter(attribute_column => values)})
378
+ end
379
+
380
+ # Creates a scope for finding records *without* a particular state or
381
+ # states for the attribute
382
+ def create_without_scope(name)
383
+ create_scope(name, lambda {|dataset, values| dataset.exclude(attribute_column => values)})
384
+ end
385
+
386
+ # Creates a new named scope with the given name
387
+ def create_scope(name, scope)
388
+ machine = self
389
+ owner_class.def_dataset_method(name) do |*states|
390
+ machine.send(:run_scope, scope, self, states)
391
+ end
392
+
393
+ false
394
+ end
395
+
396
+ # Generates the results for the given scope based on one or more states to
397
+ # filter by
398
+ def run_scope(scope, dataset, states)
399
+ super(scope, model_from_dataset(dataset).state_machine(name), dataset, states)
400
+ end
401
+
402
+ # Determines the model associated with the given dataset
403
+ def model_from_dataset(dataset)
404
+ dataset.model
405
+ end
406
+
407
+ # Generates the fully-qualifed column name for this machine's attribute
408
+ def attribute_column
409
+ ::Sequel::SQL::QualifiedIdentifier.new(owner_class.table_name, attribute)
410
+ end
411
+
412
+ # Runs a new database transaction, rolling back any changes if the
413
+ # yielded block fails (i.e. returns false).
414
+ def transaction(object)
415
+ object.db.transaction {raise ::Sequel::Error::Rollback unless yield}
416
+ end
417
+
418
+ # Creates a new callback in the callback chain, always ensuring that
419
+ # it's configured to bind to the object as this is the convention for
420
+ # Sequel callbacks
421
+ def add_callback(type, options, &block)
422
+ options[:bind_to_object] = true
423
+ options[:terminator] = @terminator ||= lambda {|result| result == false}
424
+ super
425
+ end
426
+ end
427
+ end
428
+ end