pluginaweek-state_machine 0.7.6

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 (78) hide show
  1. data/CHANGELOG.rdoc +273 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +466 -0
  4. data/Rakefile +98 -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 +429 -0
  28. data/lib/state_machine/assertions.rb +36 -0
  29. data/lib/state_machine/callback.rb +189 -0
  30. data/lib/state_machine/condition_proxy.rb +94 -0
  31. data/lib/state_machine/eval_helpers.rb +67 -0
  32. data/lib/state_machine/event.rb +251 -0
  33. data/lib/state_machine/event_collection.rb +113 -0
  34. data/lib/state_machine/extensions.rb +158 -0
  35. data/lib/state_machine/guard.rb +219 -0
  36. data/lib/state_machine/integrations.rb +68 -0
  37. data/lib/state_machine/integrations/active_record.rb +444 -0
  38. data/lib/state_machine/integrations/active_record/locale.rb +10 -0
  39. data/lib/state_machine/integrations/active_record/observer.rb +41 -0
  40. data/lib/state_machine/integrations/data_mapper.rb +325 -0
  41. data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
  42. data/lib/state_machine/integrations/sequel.rb +292 -0
  43. data/lib/state_machine/machine.rb +1431 -0
  44. data/lib/state_machine/machine_collection.rb +146 -0
  45. data/lib/state_machine/matcher.rb +123 -0
  46. data/lib/state_machine/matcher_helpers.rb +54 -0
  47. data/lib/state_machine/node_collection.rb +152 -0
  48. data/lib/state_machine/state.rb +249 -0
  49. data/lib/state_machine/state_collection.rb +112 -0
  50. data/lib/state_machine/transition.rb +367 -0
  51. data/tasks/state_machine.rake +1 -0
  52. data/tasks/state_machine.rb +30 -0
  53. data/test/classes/switch.rb +11 -0
  54. data/test/functional/state_machine_test.rb +941 -0
  55. data/test/test_helper.rb +4 -0
  56. data/test/unit/assertions_test.rb +40 -0
  57. data/test/unit/callback_test.rb +455 -0
  58. data/test/unit/condition_proxy_test.rb +328 -0
  59. data/test/unit/eval_helpers_test.rb +129 -0
  60. data/test/unit/event_collection_test.rb +293 -0
  61. data/test/unit/event_test.rb +605 -0
  62. data/test/unit/guard_test.rb +862 -0
  63. data/test/unit/integrations/active_record_test.rb +1001 -0
  64. data/test/unit/integrations/data_mapper_test.rb +694 -0
  65. data/test/unit/integrations/sequel_test.rb +486 -0
  66. data/test/unit/integrations_test.rb +42 -0
  67. data/test/unit/invalid_event_test.rb +7 -0
  68. data/test/unit/invalid_transition_test.rb +7 -0
  69. data/test/unit/machine_collection_test.rb +710 -0
  70. data/test/unit/machine_test.rb +1910 -0
  71. data/test/unit/matcher_helpers_test.rb +37 -0
  72. data/test/unit/matcher_test.rb +155 -0
  73. data/test/unit/node_collection_test.rb +207 -0
  74. data/test/unit/state_collection_test.rb +280 -0
  75. data/test/unit/state_machine_test.rb +31 -0
  76. data/test/unit/state_test.rb +795 -0
  77. data/test/unit/transition_test.rb +1113 -0
  78. metadata +161 -0
@@ -0,0 +1,10 @@
1
+ {:en => {
2
+ :activerecord => {
3
+ :errors => {
4
+ :messages => {
5
+ :invalid_event => StateMachine::Machine.default_messages[:invalid_event] % ['{{state}}'],
6
+ :invalid_transition => StateMachine::Machine.default_messages[:invalid_transition] % ['{{event}}']
7
+ }
8
+ }
9
+ }
10
+ }}
@@ -0,0 +1,41 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module ActiveRecord
4
+ # Adds support for invoking callbacks on ActiveRecord observers with more
5
+ # than one argument (e.g. the record *and* the state transition). By
6
+ # default, ActiveRecord only supports passing the record into the
7
+ # callbacks.
8
+ #
9
+ # For example:
10
+ #
11
+ # class VehicleObserver < ActiveRecord::Observer
12
+ # # The default behavior: only pass in the record
13
+ # def after_save(vehicle)
14
+ # end
15
+ #
16
+ # # Custom behavior: allow the transition to be passed in as well
17
+ # def after_transition(vehicle, transition)
18
+ # Audit.log(vehicle, transition)
19
+ # end
20
+ # end
21
+ module Observer
22
+ def self.included(base) #:nodoc:
23
+ base.class_eval do
24
+ alias_method :update_without_multiple_args, :update
25
+ alias_method :update, :update_with_multiple_args
26
+ end
27
+ end
28
+
29
+ # Allows additional arguments other than the object to be passed to the
30
+ # observed methods
31
+ def update_with_multiple_args(observed_method, object, *args) #:nodoc:
32
+ send(observed_method, object, *args) if respond_to?(observed_method)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ ActiveRecord::Observer.class_eval do
40
+ include StateMachine::Integrations::ActiveRecord::Observer
41
+ end
@@ -0,0 +1,325 @@
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
+ # === Security implications
74
+ #
75
+ # Beware that public event attributes mean that events can be fired
76
+ # whenever mass-assignment is being used. If you want to prevent malicious
77
+ # users from tampering with events through URLs / forms, the attribute
78
+ # should be protected like so:
79
+ #
80
+ # class Vehicle
81
+ # include DataMapper::Resource
82
+ # ...
83
+ #
84
+ # state_machine do
85
+ # ...
86
+ # end
87
+ # protected :state_event
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 DataMapper::Resource
95
+ # ...
96
+ #
97
+ # # Allow both machines to share the same state
98
+ # alias_method :public_state, :state
99
+ # alias_method :public_state=, :state=
100
+ #
101
+ # state_machine do
102
+ # # Define private events here
103
+ # end
104
+ # protected :state_event= # Prevent access to events in the first machine
105
+ #
106
+ # state_machine :public_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 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
+ # == Validation errors
147
+ #
148
+ # If an event fails to successfully fire because there are no matching
149
+ # transitions for the current record, a validation error is added to the
150
+ # record's state attribute to help in determining why it failed and for
151
+ # reporting via the UI.
152
+ #
153
+ # For example,
154
+ #
155
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id=1 name=nil state="idling">
156
+ # vehicle.ignite # => false
157
+ # vehicle.errors.full_messages # => ["cannot transition via \"ignite\""]
158
+ #
159
+ # If an event fails to fire because of a validation error on the record and
160
+ # *not* because a matching transition was not available, no error messages
161
+ # will be added to the state attribute.
162
+ #
163
+ # == Scopes
164
+ #
165
+ # To assist in filtering models with specific states, a series of class
166
+ # methods are defined on the model for finding records with or without a
167
+ # particular set of states.
168
+ #
169
+ # These named scopes are the functional equivalent of the following
170
+ # definitions:
171
+ #
172
+ # class Vehicle
173
+ # include DataMapper::Resource
174
+ #
175
+ # property :id, Serial
176
+ # property :state, String
177
+ #
178
+ # class << self
179
+ # def with_states(*states)
180
+ # all(:state => states.flatten)
181
+ # end
182
+ # alias_method :with_state, :with_states
183
+ #
184
+ # def without_states(*states)
185
+ # all(:state.not => states.flatten)
186
+ # end
187
+ # alias_method :without_state, :without_states
188
+ # end
189
+ # end
190
+ #
191
+ # *Note*, however, that the states are converted to their stored values
192
+ # before being passed into the query.
193
+ #
194
+ # Because of the way scopes work in DataMapper, they can be chained like
195
+ # so:
196
+ #
197
+ # Vehicle.with_state(:parked).all(:order => [:id.desc])
198
+ #
199
+ # == Callbacks / Observers
200
+ #
201
+ # All before/after transition callbacks defined for DataMapper resources
202
+ # behave in the same way that other DataMapper hooks behave. Rather than
203
+ # passing in the record as an argument to the callback, the callback is
204
+ # instead bound to the object and evaluated within its context.
205
+ #
206
+ # For example,
207
+ #
208
+ # class Vehicle
209
+ # include DataMapper::Resource
210
+ #
211
+ # property :id, Serial
212
+ # property :state, String
213
+ #
214
+ # state_machine :initial => :parked do
215
+ # before_transition any => :idling do
216
+ # put_on_seatbelt
217
+ # end
218
+ #
219
+ # before_transition do |transition|
220
+ # # log message
221
+ # end
222
+ #
223
+ # event :ignite do
224
+ # transition :parked => :idling
225
+ # end
226
+ # end
227
+ #
228
+ # def put_on_seatbelt
229
+ # ...
230
+ # end
231
+ # end
232
+ #
233
+ # Note, also, that the transition can be accessed by simply defining
234
+ # additional arguments in the callback block.
235
+ #
236
+ # In addition to support for DataMapper-like hooks, there is additional
237
+ # support for DataMapper observers. See StateMachine::Integrations::DataMapper::Observer
238
+ # for more information.
239
+ module DataMapper
240
+ # The default options to use for state machines using this integration
241
+ class << self; attr_reader :defaults; end
242
+ @defaults = {:action => :save, :use_transactions => false}
243
+
244
+ # Should this integration be used for state machines in the given class?
245
+ # Classes that include DataMapper::Resource will automatically use the
246
+ # DataMapper integration.
247
+ def self.matches?(klass)
248
+ defined?(::DataMapper::Resource) && klass <= ::DataMapper::Resource
249
+ end
250
+
251
+ # Loads additional files specific to DataMapper
252
+ def self.extended(base) #:nodoc:
253
+ require 'state_machine/integrations/data_mapper/observer' if ::DataMapper.const_defined?('Observer')
254
+ end
255
+
256
+ # Adds a validation error to the given object
257
+ def invalidate(object, attribute, message, values = [])
258
+ object.errors.add(self.attribute(attribute), generate_message(message, values)) if supports_validations?
259
+ end
260
+
261
+ # Resets any errors previously added when invalidating the given object
262
+ def reset(object)
263
+ object.errors.clear if object.respond_to?(:errors)
264
+ end
265
+
266
+ protected
267
+ # Is validation support currently loaded?
268
+ def supports_validations?
269
+ @supports_validations ||= ::DataMapper.const_defined?('Validate')
270
+ end
271
+
272
+ # Skips defining reader/writer methods since this is done automatically
273
+ def define_state_accessor
274
+ owner_class.property(attribute, String) unless owner_class.properties.has_property?(attribute)
275
+
276
+ if supports_validations?
277
+ attribute = self.attribute
278
+ owner_class.validates_with_block(attribute) do
279
+ machine = self.class.state_machine(attribute)
280
+ machine.states.match(self) ? true : [false, machine.generate_message(:invalid)]
281
+ end
282
+ end
283
+ end
284
+
285
+ # Adds hooks into validation for automatically firing events
286
+ def define_action_helpers
287
+ if super && action == :save && supports_validations?
288
+ @instance_helper_module.class_eval do
289
+ define_method(:valid?) do |*args|
290
+ self.class.state_machines.fire_event_attributes(self, :save, false) { super(*args) }
291
+ end
292
+ end
293
+ end
294
+ end
295
+
296
+ # Creates a scope for finding records *with* a particular state or
297
+ # states for the attribute
298
+ def create_with_scope(name)
299
+ attribute = self.attribute
300
+ lambda {|resource, values| resource.all(attribute => values)}
301
+ end
302
+
303
+ # Creates a scope for finding records *without* a particular state or
304
+ # states for the attribute
305
+ def create_without_scope(name)
306
+ attribute = self.attribute
307
+ lambda {|resource, values| resource.all(attribute.to_sym.not => values)}
308
+ end
309
+
310
+ # Runs a new database transaction, rolling back any changes if the
311
+ # yielded block fails (i.e. returns false).
312
+ def transaction(object)
313
+ object.class.transaction {|t| t.rollback unless yield}
314
+ end
315
+
316
+ # Creates a new callback in the callback chain, always ensuring that
317
+ # it's configured to bind to the object as this is the convention for
318
+ # DataMapper/Extlib callbacks
319
+ def add_callback(type, options, &block)
320
+ options[:bind_to_object] = true
321
+ super
322
+ end
323
+ end
324
+ end
325
+ end
@@ -0,0 +1,139 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module DataMapper
4
+ # Adds support for creating before/after transition callbacks within a
5
+ # DataMapper observer. These callbacks behave very similar to
6
+ # before/after hooks during save/update/destroy/etc., but with the
7
+ # following modifications:
8
+ # * Each callback can define a set of transition conditions (i.e. guards)
9
+ # that must be met in order for the callback to get invoked.
10
+ # * An additional transition parameter is available that provides
11
+ # contextual information about the event (see StateMachine::Transition
12
+ # for more information)
13
+ #
14
+ # To define a single observer for multiple state machines:
15
+ #
16
+ # class StateMachineObserver
17
+ # include DataMapper::Observer
18
+ #
19
+ # observe Vehicle, Switch, Project
20
+ #
21
+ # after_transition do |transition|
22
+ # Audit.log(self, transition)
23
+ # end
24
+ # end
25
+ #
26
+ # == Requirements
27
+ #
28
+ # To use this feature of the DataMapper integration, the dm-observer library
29
+ # must be available. This can be installed either directly or indirectly
30
+ # through dm-more. When loading DataMapper, be sure to load the dm-observer
31
+ # library as well like so:
32
+ #
33
+ # require 'rubygems'
34
+ # require 'dm-core'
35
+ # require 'dm-observer'
36
+ #
37
+ # If dm-observer is not available, then this feature will be skipped.
38
+ module Observer
39
+ include MatcherHelpers
40
+
41
+ # Creates a callback that will be invoked *before* a transition is
42
+ # performed, so long as the given configuration options match the
43
+ # transition. Each part of the transition (event, to state, from state)
44
+ # must match in order for the callback to get invoked.
45
+ #
46
+ # See StateMachine::Machine#before_transition for more
47
+ # information about the various configuration options available.
48
+ #
49
+ # == Examples
50
+ #
51
+ # class Vehicle
52
+ # include DataMapper::Resource
53
+ #
54
+ # property :id, Serial
55
+ # property :state, :String
56
+ #
57
+ # state_machine :initial => :parked do
58
+ # event :ignite do
59
+ # transition :parked => :idling
60
+ # end
61
+ # end
62
+ # end
63
+ #
64
+ # class VehicleObserver
65
+ # include DataMapper::Observer
66
+ #
67
+ # observe Vehicle
68
+ #
69
+ # before :save do
70
+ # # log message
71
+ # end
72
+ #
73
+ # # Target all state machines
74
+ # before_transition :parked => :idling, :on => :ignite do
75
+ # # put on seatbelt
76
+ # end
77
+ #
78
+ # # Target a specific state machine
79
+ # before_transition :state, any => :idling do
80
+ # # put on seatbelt
81
+ # end
82
+ #
83
+ # # Target all state machines without requirements
84
+ # before_transition do |transition|
85
+ # # log message
86
+ # end
87
+ # end
88
+ #
89
+ # *Note* that in each of the above +before_transition+ callbacks, the
90
+ # callback is executed within the context of the object (i.e. the
91
+ # Vehicle instance being transition). This means that +self+ refers
92
+ # to the vehicle record within each callback block.
93
+ def before_transition(*args, &block)
94
+ add_transition_callback(:before, *args, &block)
95
+ end
96
+
97
+ # Creates a callback that will be invoked *after* a transition is
98
+ # performed so long as the given configuration options match the
99
+ # transition.
100
+ #
101
+ # See +before_transition+ for a description of the possible configurations
102
+ # for defining callbacks.
103
+ def after_transition(*args, &block)
104
+ add_transition_callback(:after, *args, &block)
105
+ end
106
+
107
+ private
108
+ # Adds the transition callback to a specific machine or all of the
109
+ # state machines for each observed class.
110
+ def add_transition_callback(type, *args, &block)
111
+ if args.any? && !args.first.is_a?(Hash)
112
+ # Specific attribute(s) being targeted
113
+ attributes = args
114
+ args = args.last.is_a?(Hash) ? [args.pop] : []
115
+ else
116
+ # Target all state machines
117
+ attributes = nil
118
+ end
119
+
120
+ # Add the transition callback to each class being observed
121
+ observing.each do |klass|
122
+ state_machines =
123
+ if attributes
124
+ attributes.map {|attribute| klass.state_machines.fetch(attribute)}
125
+ else
126
+ klass.state_machines.values
127
+ end
128
+
129
+ state_machines.each {|machine| machine.send("#{type}_transition", *args, &block)}
130
+ end if observing
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ DataMapper::Observer::ClassMethods.class_eval do
138
+ include StateMachine::Integrations::DataMapper::Observer
139
+ end