pluginaweek-state_machine 0.7.6 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,6 +1,26 @@
1
1
  == master
2
2
 
3
- * Add support for customizing generated methods like #{attribute}_name using :as instead of always prefixing with the attribute name
3
+ == 0.8.0 / 2009-08-15
4
+
5
+ * Add support for DataMapper 0.10.0
6
+ * Always interpet nil return values from actions as failed attempts
7
+ * Fix loopbacks not causing records to save in ORM integrations if no other fields were changed
8
+ * Fix events not failing with useful errors when an object's state is invalid
9
+ * Use more friendly NoMethodError messages for state-driven behaviors
10
+ * Fix before_transition callbacks getting run twice when using event attributes in ORM integrations
11
+ * Add the ability to query for the availability of specific transitions on an object
12
+ * Allow after_transition callbacks to be explicitly run on failed attempts
13
+ * By default, don't run after_transition callbacks on failed attempts
14
+ * Fix not allowing multiple methods to be specified as arguments in callbacks
15
+ * Fix initial states being set when loading records from the database in Sequel integration
16
+ * Allow static initial states to be set earlier in the initialization of an object
17
+ * Use friendly validation errors for nil states
18
+ * Fix states not being validated properly when using custom names in ActiveRecord / DataMapper integrations
19
+
20
+ == 0.7.6 / 2009-06-17
21
+
22
+ * Allow multiple state machines on the same class to target the same attribute
23
+ * Add support for :attribute to customize the attribute target, assuming the name is the first argument of #state_machine
4
24
  * Simplify reading from / writing to machine-related attributes on objects
5
25
  * Fix locale for ActiveRecord getting added to the i18n load path multiple times [Reiner Dieterich]
6
26
  * Fix callbacks, guards, and state-driven behaviors not always working on tainted classes [Brandon Dimcheff]
data/README.rdoc CHANGED
@@ -412,7 +412,7 @@ To generate multiple state machine graphs:
412
412
 
413
413
  *Note* that this will generate a different file for every state machine defined
414
414
  in the class. The generated files will use an output filename of the format
415
- #{class_name}_#{attribute}.#{format}.
415
+ #{class_name}_#{machine_name}.#{format}.
416
416
 
417
417
  For examples of actual images generated using this task, see those under the
418
418
  examples folder.
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'rake/contrib/sshpublisher'
5
5
 
6
6
  spec = Gem::Specification.new do |s|
7
7
  s.name = 'state_machine'
8
- s.version = '0.7.5'
8
+ s.version = '0.8.0'
9
9
  s.platform = Gem::Platform::RUBY
10
10
  s.summary = 'Adds support for creating state machines for attributes on any Ruby class'
11
11
  s.description = s.summary
data/lib/state_machine.rb CHANGED
@@ -5,10 +5,12 @@ require 'state_machine/machine'
5
5
  # functionality on any Ruby class.
6
6
  module StateMachine
7
7
  module MacroMethods
8
- # Creates a new state machine for the given attribute. The default
9
- # attribute, if not specified, is <tt>:state</tt>.
8
+ # Creates a new state machine with the given name. The default name, if not
9
+ # specified, is <tt>:state</tt>.
10
10
  #
11
11
  # Configuration options:
12
+ # * <tt>:attribute</tt> - The name of the attribute to store the state value
13
+ # in. By default, this is the same as the name of the machine.
12
14
  # * <tt>:initial</tt> - The initial state of the attribute. This can be a
13
15
  # static state or a lambda block which will be evaluated at runtime
14
16
  # (e.g. lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling}).
@@ -16,10 +18,6 @@ module StateMachine
16
18
  # * <tt>:action</tt> - The instance method to invoke when an object
17
19
  # transitions. Default is nil unless otherwise specified by the
18
20
  # configured integration.
19
- # * <tt>:as</tt> - The name to use for prefixing all generated machine
20
- # instance / class methods (e.g. if the attribute is +state_id+, then
21
- # "state" would generate :state_name, :state_transitions, etc. instead of
22
- # :state_id_name and :state_id_transitions)
23
21
  # * <tt>:namespace</tt> - The name to use for namespacing all generated
24
22
  # state / event instance methods (e.g. "heater" would generate
25
23
  # :turn_on_heater and :turn_off_heater for the :turn_on/:turn_off events).
@@ -49,12 +47,12 @@ module StateMachine
49
47
  # result, you will not be able to access any class methods unless you refer
50
48
  # to them directly (i.e. specifying the class name).
51
49
  #
52
- # For examples on the types of configured state machines and blocks, see
50
+ # For examples on the types of state machine configurations and blocks, see
53
51
  # the section below.
54
52
  #
55
53
  # == Examples
56
54
  #
57
- # With the default attribute and no configuration:
55
+ # With the default name/attribute and no configuration:
58
56
  #
59
57
  # class Vehicle
60
58
  # state_machine do
@@ -64,13 +62,14 @@ module StateMachine
64
62
  # end
65
63
  # end
66
64
  #
67
- # The above example will define a state machine for the +state+ attribute
68
- # on the class. Every vehicle will start without an initial state.
65
+ # The above example will define a state machine named "state" that will
66
+ # store the value in the +state+ attribute. Every vehicle will start
67
+ # without an initial state.
69
68
  #
70
- # With a custom attribute:
69
+ # With a custom name / attribute:
71
70
  #
72
71
  # class Vehicle
73
- # state_machine :status do
72
+ # state_machine :status, :attribute => :status_value do
74
73
  # ...
75
74
  # end
76
75
  # end
@@ -94,7 +93,8 @@ module StateMachine
94
93
  # == Instance Methods
95
94
  #
96
95
  # The following instance methods will be automatically generated by the
97
- # state machine. Any existing methods will not be overwritten.
96
+ # state machine based on the *name* of the machine. Any existing methods
97
+ # will not be overwritten.
98
98
  # * <tt>state</tt> - Gets the current value for the attribute
99
99
  # * <tt>state=(value)</tt> - Sets the current value for the attribute
100
100
  # * <tt>state?(name)</tt> - Checks the given state name against the current
@@ -102,8 +102,11 @@ module StateMachine
102
102
  # * <tt>state_name</tt> - Gets the name of the state for the current value
103
103
  # * <tt>state_events</tt> - Gets the list of events that can be fired on
104
104
  # the current object's state (uses the *unqualified* event names)
105
- # * <tt>state_transitions</tt> - Gets the list of possible transitions
106
- # that can be made on the current object's state
105
+ # * <tt>state_transitions(requirements = {})</tt> - Gets the list of possible
106
+ # transitions that can be made on the current object's state. Additional
107
+ # requirements, such as the :from / :to state and :on event can be specified
108
+ # to restrict the transitions to select. By default, the current state
109
+ # will be used for the :from state.
107
110
  #
108
111
  # For example,
109
112
  #
@@ -253,7 +256,7 @@ module StateMachine
253
256
  #
254
257
  # class Vehicle
255
258
  # include DataMapper::Resource
256
- # property :id, Integer, :serial => true
259
+ # property :id, Serial
257
260
  #
258
261
  # state_machine :initial => :parked do
259
262
  # event :ignite do
@@ -295,55 +298,11 @@ module StateMachine
295
298
  # see StateMachine::Machine#before_transition and
296
299
  # StateMachine::Machine#after_transition.
297
300
  #
298
- # == Attribute aliases
299
- #
300
- # When a state machine is defined, several methods are generated scoped by
301
- # the name of the attribute, such as (if the attribute were "state"):
302
- # * <tt>state_name</tt>
303
- # * <tt>state_event</tt>
304
- # * <tt>state_transitions</tt>
305
- # * etc.
306
- #
307
- # If the attribute for the machine were something less common, such as
308
- # "state_id" or "state_value", this makes for more awkward scoped methods.
309
- #
310
- # Rather than scope based on the attribute, these methods can be customized
311
- # using the <tt>:as</tt> option as essentially an alias.
312
- #
313
- # For example,
314
- #
315
- # class Vehicle
316
- # state_machine :state_id, :as => :state do
317
- # event :turn_on do
318
- # transition all => :on
319
- # end
320
- #
321
- # event :turn_off do
322
- # transition all => :off
323
- # end
324
- #
325
- # state :on, :value => 1
326
- # state :off, :value => 2
327
- # end
328
- # end
329
- #
330
- # ...will generate the following methods:
331
- # * <tt>state_name</tt>
332
- # * <tt>state_event</tt>
333
- # * <tt>state_transitions</tt>
334
- #
335
- # ...instead of:
336
- # * <tt>state_id_name</tt>
337
- # * <tt>state_id_event</tt>
338
- # * <tt>state_id_transitions</tt>
339
- #
340
- # However, it will continue to read and write to the +state_id+ attribute.
341
- #
342
301
  # == Namespaces
343
302
  #
344
303
  # When a namespace is configured for a state machine, the name provided
345
304
  # will be used in generating the instance methods for interacting with
346
- # events/states in the machine. This is particularly useful when a class
305
+ # states/events in the machine. This is particularly useful when a class
347
306
  # has multiple state machines and it would be difficult to differentiate
348
307
  # between the various states / events.
349
308
  #
@@ -398,7 +357,7 @@ module StateMachine
398
357
  #
399
358
  # For integrations that support it, a group of default scope filters will
400
359
  # be automatically created for assisting in finding objects that have the
401
- # attribute set to the value for a given set of states.
360
+ # attribute set to one of a given set of states.
402
361
  #
403
362
  # For example,
404
363
  #
@@ -412,9 +371,9 @@ module StateMachine
412
371
  # :with_state, :with_states, :without_state, or :without_states), then a
413
372
  # scope will not be defined for that name.
414
373
  #
415
- # See StateMachine::Machine for more information about using
416
- # integrations and the individual integration docs for information about
417
- # the actual scopes that are generated.
374
+ # See StateMachine::Machine for more information about using integrations
375
+ # and the individual integration docs for information about the actual
376
+ # scopes that are generated.
418
377
  def state_machine(*args, &block)
419
378
  StateMachine::Machine.find_or_create(self, *args, &block)
420
379
  end
@@ -142,7 +142,7 @@ module StateMachine
142
142
  # requirements configured for this callback.
143
143
  #
144
144
  # If a terminator has been configured and it matches the result from the
145
- # evaluated method, then the callback chain should be halted
145
+ # evaluated method, then the callback chain should be halted.
146
146
  def call(object, context = {}, *args)
147
147
  if @guard.matches?(object, context)
148
148
  @methods.each do |method|
@@ -65,8 +65,8 @@ module StateMachine
65
65
  # state to be +idling+ if it's current state is +parked+ or +first_gear+
66
66
  # if it's current state is +idling+.
67
67
  #
68
- # To help defining these implicit transitions, a set of helpers are available
69
- # for defining slightly more complex matching:
68
+ # To help define these implicit transitions, a set of helpers are available
69
+ # for slightly more complex matching:
70
70
  # * <tt>all</tt> - Matches every state in the machine
71
71
  # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
72
72
  # * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
@@ -147,7 +147,7 @@ module StateMachine
147
147
  # requirements
148
148
  assert_valid_keys(options, :from, :to, :except_from, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
149
149
 
150
- guards << guard = Guard.new(options)
150
+ guards << guard = Guard.new(options.merge(:on => name))
151
151
  @known_states |= guard.known_states
152
152
  guard
153
153
  end
@@ -162,15 +162,16 @@ module StateMachine
162
162
 
163
163
  # Finds and builds the next transition that can be performed on the given
164
164
  # object. If no transitions can be made, then this will return nil.
165
- def transition_for(object)
166
- from = machine.states.match(object).name
165
+ def transition_for(object, requirements = {})
166
+ requirements[:from] = machine.states.match!(object).name unless custom_from_state = requirements.include?(:from)
167
167
 
168
168
  guards.each do |guard|
169
- if match = guard.match(object, :from => from)
169
+ if match = guard.match(object, requirements)
170
170
  # Guard allows for the transition to occur
171
+ from = requirements[:from]
171
172
  to = match[:to].values.empty? ? from : match[:to].values.first
172
173
 
173
- return Transition.new(object, machine, name, from, to)
174
+ return Transition.new(object, machine, name, from, to, !custom_from_state)
174
175
  end
175
176
  end
176
177
 
@@ -190,7 +191,7 @@ module StateMachine
190
191
  if transition = transition_for(object)
191
192
  transition.perform(*args)
192
193
  else
193
- machine.invalidate(object, machine.attribute, :invalid_transition, [[:event, name]])
194
+ machine.invalidate(object, :state, :invalid_transition, [[:event, name]])
194
195
  false
195
196
  end
196
197
  end
@@ -244,7 +245,7 @@ module StateMachine
244
245
 
245
246
  # Fires the event, raising an exception if it fails
246
247
  machine.define_instance_method("#{qualified_name}!") do |machine, object, *args|
247
- object.send(qualified_name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{machine.name} via :#{name} from #{machine.states.match(object).name.inspect}")
248
+ object.send(qualified_name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{machine.name} via :#{name} from #{machine.states.match!(object).name.inspect}")
248
249
  end
249
250
  end
250
251
  end
@@ -34,6 +34,14 @@ module StateMachine
34
34
 
35
35
  # Gets the list of transitions that can be run on the given object.
36
36
  #
37
+ # Valid requirement options:
38
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
39
+ # are specified, then this will be the object's current state.
40
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
41
+ # specified, then this will match any to state.
42
+ # * <tt>:on</tt> - One or more events that fire the transition. If none
43
+ # are specified, then this will match any event.
44
+ #
37
45
  # == Examples
38
46
  #
39
47
  # class Vehicle
@@ -50,22 +58,25 @@ module StateMachine
50
58
  #
51
59
  # events = Vehicle.state_machine.events
52
60
  #
53
- # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
54
- # events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
61
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
62
+ # events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
55
63
  #
56
64
  # vehicle.state = 'idling'
57
- # events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
58
- def transitions_for(object)
59
- map {|event| event.transition_for(object)}.compact
65
+ # events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
66
+ #
67
+ # # Search for explicit transitions regardless of the current state
68
+ # events.transitions_for(vehicle, :from => :parked) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
69
+ def transitions_for(object, requirements = {})
70
+ map {|event| event.transition_for(object, requirements)}.compact
60
71
  end
61
72
 
62
73
  # Gets the transition that should be performed for the event stored in the
63
74
  # given object's event attribute. This also takes an additional parameter
64
- # for automatically invalidating the object if the event or transition
65
- # are invalid. By default, this is turned off.
75
+ # for automatically invalidating the object if the event or transition are
76
+ # invalid. By default, this is turned off.
66
77
  #
67
- # *Note* that if a transition has already been generated for the event,
68
- # then that transition will be used.
78
+ # *Note* that if a transition has already been generated for the event, then
79
+ # that transition will be used.
69
80
  #
70
81
  # == Examples
71
82
  #
@@ -97,7 +108,7 @@ module StateMachine
97
108
  if event = self[event_name.to_sym, :name]
98
109
  unless result = machine.read(object, :event_transition) || event.transition_for(object)
99
110
  # No valid transition: invalidate
100
- machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).name]]) if invalidate
111
+ machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).name || 'nil']]) if invalidate
101
112
  result = false
102
113
  end
103
114
  else
@@ -22,15 +22,6 @@ module StateMachine
22
22
  end
23
23
 
24
24
  module InstanceMethods
25
- # Defines the initial values for state machine attributes. The values
26
- # will be set *after* the original initialize method is invoked. This is
27
- # necessary in order to ensure that the object is initialized before
28
- # dynamic initial attributes are evaluated.
29
- def initialize(*args, &block)
30
- super
31
- initialize_state_machines
32
- end
33
-
34
25
  # Runs one or more events in parallel. All events will run through the
35
26
  # following steps:
36
27
  # * Before callbacks
@@ -151,8 +142,8 @@ module StateMachine
151
142
  end
152
143
 
153
144
  protected
154
- def initialize_state_machines #:nodoc:
155
- self.class.state_machines.initialize_states(self)
145
+ def initialize_state_machines(options = {}) #:nodoc:
146
+ self.class.state_machines.initialize_states(self, options)
156
147
  end
157
148
  end
158
149
  end
@@ -24,6 +24,9 @@ module StateMachine
24
24
  # requirements contain a mapping of {:from => matcher, :to => matcher}.
25
25
  attr_reader :state_requirements
26
26
 
27
+ # The requirement for verifying the success of the event
28
+ attr_reader :success_requirement
29
+
27
30
  # A list of all of the states known to this guard. This will pull states
28
31
  # from the following options (in the same order):
29
32
  # * +from+ / +except_from+
@@ -39,6 +42,9 @@ module StateMachine
39
42
  # Build event requirement
40
43
  @event_requirement = build_matcher(options, :on, :except_on)
41
44
 
45
+ # Build success requirement
46
+ @success_requirement = options.delete(:include_failures) ? AllMatcher.instance : WhitelistMatcher.new([true])
47
+
42
48
  if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on]).empty?
43
49
  # Explicit from/to requirements specified
44
50
  @state_requirements = [{:from => build_matcher(options, :from, :except_from), :to => build_matcher(options, :to, :except_to)}]
@@ -185,11 +191,16 @@ module StateMachine
185
191
  def match_query(query)
186
192
  query ||= {}
187
193
 
188
- if match_event(query) && (state_requirement = match_states(query))
194
+ if match_success(query) && match_event(query) && (state_requirement = match_states(query))
189
195
  state_requirement.merge(:on => event_requirement)
190
196
  end
191
197
  end
192
198
 
199
+ # Verifies that the success requirement matches the given query
200
+ def match_success(query)
201
+ matches_requirement?(query, :success, success_requirement)
202
+ end
203
+
193
204
  # Verifies that the event requirement matches the given query
194
205
  def match_event(query)
195
206
  matches_requirement?(query, :on, event_requirement)
@@ -84,14 +84,14 @@ module StateMachine
84
84
  # you can build two state machines (one public and one protected) like so:
85
85
  #
86
86
  # class Vehicle < ActiveRecord::Base
87
- # alias_attribute :public_state # Allow both machines to share the same state
88
87
  # attr_protected :state_event # Prevent access to events in the first machine
89
88
  #
90
89
  # state_machine do
91
90
  # # Define private events here
92
91
  # end
93
92
  #
94
- # state_machine :public_state do
93
+ # # Public machine targets the same state as the private machine
94
+ # state_machine :public_state, :attribute => :state do
95
95
  # # Define public events here
96
96
  # end
97
97
  # end
@@ -281,6 +281,14 @@ module StateMachine
281
281
  end
282
282
  end
283
283
 
284
+ # Forces the change in state to be recognized regardless of whether the
285
+ # state value actually changed
286
+ def write(object, attribute, value)
287
+ result = super
288
+ object.send("#{self.attribute}_will_change!") if attribute == :state && object.respond_to?("#{self.attribute}_will_change!")
289
+ result
290
+ end
291
+
284
292
  # Adds a validation error to the given object
285
293
  def invalidate(object, attribute, message, values = [])
286
294
  attribute = self.attribute(attribute)
@@ -308,11 +316,41 @@ module StateMachine
308
316
  callbacks[:after] << Callback.new {|object, transition| notify(:after, object, transition)}
309
317
  end
310
318
 
319
+ # Defines an initialization hook into the owner class for setting the
320
+ # initial state of the machine *before* any attributes are set on the
321
+ # object
322
+ def define_state_initializer
323
+ @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
324
+ # Ensure that the attributes setter gets used to force initialization
325
+ # of the state machines
326
+ def initialize(attributes = nil, *args)
327
+ attributes ||= {}
328
+ super
329
+ end
330
+
331
+ # Hooks in to attribute initialization to set the states *prior*
332
+ # to the attributes being set
333
+ def attributes=(*args)
334
+ if new_record? && !@initialized_state_machines
335
+ @initialized_state_machines = true
336
+
337
+ initialize_state_machines(:dynamic => false)
338
+ super
339
+ initialize_state_machines(:dynamic => true)
340
+ else
341
+ super
342
+ end
343
+ end
344
+ end_eval
345
+ end
346
+
311
347
  # Skips defining reader/writer methods since this is done automatically
312
348
  def define_state_accessor
349
+ name = self.name
350
+
313
351
  owner_class.validates_each(attribute) do |record, attr, value|
314
- machine = record.class.state_machine(attr)
315
- machine.invalidate(record, attr, :invalid) unless machine.states.match(record)
352
+ machine = record.class.state_machine(name)
353
+ machine.invalidate(record, :state, :invalid) unless machine.states.match(record)
316
354
  end
317
355
  end
318
356
 
@@ -321,13 +359,12 @@ module StateMachine
321
359
  # *anything* is set for the attribute's value
322
360
  def define_state_predicate
323
361
  name = self.name
324
- attribute = self.attribute
325
362
 
326
363
  # Still use class_eval here instance of define_instance_method since
327
364
  # we need to be able to call +super+
328
365
  @instance_helper_module.class_eval do
329
366
  define_method("#{name}?") do |*args|
330
- args.empty? ? super(*args) : self.class.state_machine(attribute).states.matches?(self, *args)
367
+ args.empty? ? super(*args) : self.class.state_machine(name).states.matches?(self, *args)
331
368
  end
332
369
  end
333
370
  end
@@ -388,17 +425,18 @@ module StateMachine
388
425
  # inheritance is respected properly.
389
426
  def define_scope(name, scope)
390
427
  name = name.to_sym
391
- attribute = self.attribute
428
+ machine_name = self.name
392
429
 
393
- # Created the scope and then override it with state translation
430
+ # Create the scope and then override it with state translation
394
431
  owner_class.named_scope(name)
395
432
  owner_class.scopes[name] = lambda do |klass, *states|
396
- machine_states = klass.state_machine(attribute).states
433
+ machine_states = klass.state_machine(machine_name).states
397
434
  values = states.flatten.map {|state| machine_states.fetch(state).value}
398
435
 
399
436
  ::ActiveRecord::NamedScope::Scope.new(klass, scope.call(values))
400
437
  end
401
438
 
439
+ # Prevent the Machine class from wrapping the scope
402
440
  false
403
441
  end
404
442
 
@@ -409,10 +447,10 @@ module StateMachine
409
447
  # * #{type}_#{qualified_event}_from_#{from}
410
448
  # * #{type}_#{qualified_event}_to_#{to}
411
449
  # * #{type}_#{qualified_event}
412
- # * #{type}_transition_#{attribute}_from_#{from}_to_#{to}
413
- # * #{type}_transition_#{attribute}_from_#{from}
414
- # * #{type}_transition_#{attribute}_to_#{to}
415
- # * #{type}_transition_#{attribute}
450
+ # * #{type}_transition_#{machine_name}_from_#{from}_to_#{to}
451
+ # * #{type}_transition_#{machine_name}_from_#{from}
452
+ # * #{type}_transition_#{machine_name}_to_#{to}
453
+ # * #{type}_transition_#{machine_name}
416
454
  # * #{type}_transition
417
455
  #
418
456
  # This will always return true regardless of the results of the