state_machine 0.7.5 → 0.7.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,15 @@
1
1
  == master
2
2
 
3
+ == 0.7.6 / 2009-06-17
4
+
5
+ * Allow multiple state machines on the same class to target the same attribute
6
+ * Add support for :attribute to customize the attribute target, assuming the name is the first argument of #state_machine
7
+ * Simplify reading from / writing to machine-related attributes on objects
8
+ * Fix locale for ActiveRecord getting added to the i18n load path multiple times [Reiner Dieterich]
9
+ * Fix callbacks, guards, and state-driven behaviors not always working on tainted classes [Brandon Dimcheff]
10
+ * Use Ruby 1.9's built-in Object#instance_exec for bound callbacks when it's available
11
+ * Improve performance of cached dynamic state lookups by 25%
12
+
3
13
  == 0.7.5 / 2009-05-25
4
14
 
5
15
  * Add built-in caching for dynamic state values when the value only needs to be generated once
@@ -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.7.6'
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
@@ -53,7 +53,14 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
53
53
  rdoc.options << '--line-numbers' << '--inline-source'
54
54
  rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG.rdoc', 'LICENSE', 'lib/**/*.rb')
55
55
  end
56
-
56
+
57
+ desc 'Generate a gemspec file.'
58
+ task :gemspec do
59
+ File.open("#{spec.name}.gemspec", 'w') do |f|
60
+ f.write spec.to_ruby
61
+ end
62
+ end
63
+
57
64
  Rake::GemPackageTask.new(spec) do |p|
58
65
  p.gem_spec = spec
59
66
  p.need_tar = true
@@ -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}).
@@ -17,8 +19,9 @@ module StateMachine
17
19
  # transitions. Default is nil unless otherwise specified by the
18
20
  # configured integration.
19
21
  # * <tt>:namespace</tt> - The name to use for namespacing all generated
20
- # instance methods (e.g. "heater" would generate :turn_on_heater and
21
- # :turn_off_heater for the :turn_on/:turn_off events). Default is nil.
22
+ # state / event instance methods (e.g. "heater" would generate
23
+ # :turn_on_heater and :turn_off_heater for the :turn_on/:turn_off events).
24
+ # Default is nil.
22
25
  # * <tt>:integration</tt> - The name of the integration to use for adding
23
26
  # library-specific behavior to the machine. Built-in integrations
24
27
  # include :data_mapper, :active_record, and :sequel. By default, this
@@ -49,7 +52,7 @@ module StateMachine
49
52
  #
50
53
  # == Examples
51
54
  #
52
- # With the default attribute and no configuration:
55
+ # With the default name/attribute and no configuration:
53
56
  #
54
57
  # class Vehicle
55
58
  # state_machine do
@@ -59,13 +62,14 @@ module StateMachine
59
62
  # end
60
63
  # end
61
64
  #
62
- # The above example will define a state machine for the +state+ attribute
63
- # 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.
64
68
  #
65
- # With a custom attribute:
69
+ # With a custom name / attribute:
66
70
  #
67
71
  # class Vehicle
68
- # state_machine :status do
72
+ # state_machine :status, :attribute => :status_value do
69
73
  # ...
70
74
  # end
71
75
  # end
@@ -89,7 +93,8 @@ module StateMachine
89
93
  # == Instance Methods
90
94
  #
91
95
  # The following instance methods will be automatically generated by the
92
- # 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.
93
98
  # * <tt>state</tt> - Gets the current value for the attribute
94
99
  # * <tt>state=(value)</tt> - Sets the current value for the attribute
95
100
  # * <tt>state?(name)</tt> - Checks the given state name against the current
@@ -294,14 +299,14 @@ module StateMachine
294
299
  #
295
300
  # When a namespace is configured for a state machine, the name provided
296
301
  # will be used in generating the instance methods for interacting with
297
- # events/states in the machine. This is particularly useful when a class
302
+ # states/events in the machine. This is particularly useful when a class
298
303
  # has multiple state machines and it would be difficult to differentiate
299
304
  # between the various states / events.
300
305
  #
301
306
  # For example,
302
307
  #
303
308
  # class Vehicle
304
- # state_machine :heater_state, :initial => :off :namespace => 'heater' do
309
+ # state_machine :heater_state, :initial => :off, :namespace => 'heater' do
305
310
  # event :turn_on do
306
311
  # transition all => :on
307
312
  # end
@@ -2,9 +2,8 @@ module StateMachine
2
2
  # Provides a set of helper methods for making assertions about the content
3
3
  # of various objects
4
4
  module Assertions
5
- # Validates that all keys in the given hash *only* includes the specified
6
- # valid keys. If any invalid keys are found, an ArgumentError will be
7
- # raised.
5
+ # Validates that the given hash *only* includes the specified valid keys.
6
+ # If any invalid keys are found, an ArgumentError will be raised.
8
7
  #
9
8
  # == Examples
10
9
  #
@@ -18,8 +17,8 @@ module StateMachine
18
17
  raise ArgumentError, "Invalid key(s): #{invalid_keys.join(', ')}" unless invalid_keys.empty?
19
18
  end
20
19
 
21
- # Validates that at *most* one of a set of exclusive keys is included in
22
- # the given hash. If more than one key is found, an ArgumentError will be
20
+ # Validates that the given hash only includes at *most* one of a set of
21
+ # exclusive keys. If more than one key is found, an ArgumentError will be
23
22
  # raised.
24
23
  #
25
24
  # == Examples
@@ -106,9 +106,9 @@ module StateMachine
106
106
  # * <tt>:bind_to_object</tt> - Whether to bind the callback to the object involved.
107
107
  # If set to false, the object will be passed as a parameter instead.
108
108
  # Default is integration-specific or set to the application default.
109
- # * <tt>:terminator</tt> - A block/proc that determines what callback results
110
- # should cause the callback chain to halt (if not using the default
111
- # <tt>throw :halt</tt> technique).
109
+ # * <tt>:terminator</tt> - A block/proc that determines what callback
110
+ # results should cause the callback chain to halt (if not using the
111
+ # default <tt>throw :halt</tt> technique).
112
112
  #
113
113
  # More information about how those options affect the behavior of the
114
114
  # callback can be found in their attribute definitions.
@@ -129,7 +129,6 @@ module StateMachine
129
129
  end
130
130
 
131
131
  @terminator = options.delete(:terminator)
132
-
133
132
  @guard = Guard.new(options)
134
133
  end
135
134
 
@@ -142,8 +141,8 @@ module StateMachine
142
141
  # Runs the callback as long as the transition context matches the guard
143
142
  # requirements configured for this callback.
144
143
  #
145
- # If a terminator has been configured and it matches the result from
146
- # the evaluated method, then the callback chain should be halted
144
+ # If a terminator has been configured and it matches the result from the
145
+ # evaluated method, then the callback chain should be halted
147
146
  def call(object, context = {}, *args)
148
147
  if @guard.matches?(object, context)
149
148
  @methods.each do |method|
@@ -161,22 +160,29 @@ module StateMachine
161
160
  # Generates a method that can be bound to the object being transitioned
162
161
  # when the callback is invoked
163
162
  def bound_method(block)
164
- # Generate a thread-safe unbound method that can be used on any object
165
- # This is essentially a workaround for not having Ruby 1.9's instance_exec
166
- unbound_method = Object.class_eval do
167
- time = Time.now
168
- method_name = "__bind_#{time.to_i}_#{time.usec}"
169
- define_method(method_name, &block)
170
- method = instance_method(method_name)
171
- remove_method(method_name)
172
- method
173
- end
174
- arity = unbound_method.arity
163
+ arity = block.arity
175
164
 
176
- # Proxy calls to the method so that the method can be bound *and*
177
- # the arguments are adjusted
178
- lambda do |object, *args|
179
- unbound_method.bind(object).call(*(arity == 0 ? [] : args))
165
+ if RUBY_VERSION >= '1.9'
166
+ lambda do |object, *args|
167
+ object.instance_exec(*(arity == 0 ? [] : args), &block)
168
+ end
169
+ else
170
+ # Generate a thread-safe unbound method that can be used on any object.
171
+ # This is a workaround for not having Ruby 1.9's instance_exec
172
+ unbound_method = Object.class_eval do
173
+ time = Time.now
174
+ method_name = "__bind_#{time.to_i}_#{time.usec}"
175
+ define_method(method_name, &block)
176
+ method = instance_method(method_name)
177
+ remove_method(method_name)
178
+ method
179
+ end
180
+
181
+ # Proxy calls to the method so that the method can be bound *and*
182
+ # the arguments are adjusted
183
+ lambda do |object, *args|
184
+ unbound_method.bind(object).call(*(arity == 0 ? [] : args))
185
+ end
180
186
  end
181
187
  end
182
188
  end
@@ -2,7 +2,7 @@ require 'state_machine/eval_helpers'
2
2
 
3
3
  module StateMachine
4
4
  # Represents a type of module in which class-level methods are proxied to
5
- # another class, injecting a custom :if condition along with method.
5
+ # another class, injecting a custom <tt>:if</tt> condition along with method.
6
6
  #
7
7
  # This is used for being able to automatically include conditionals which
8
8
  # check the current state in class-level methods that have configuration
@@ -53,8 +53,7 @@ module StateMachine
53
53
  def evaluate_method(object, method, *args)
54
54
  case method
55
55
  when Symbol
56
- method = object.method(method)
57
- method.arity == 0 ? method.call : method.call(*args)
56
+ object.method(method).arity == 0 ? object.send(method) : object.send(method, *args)
58
57
  when Proc, Method
59
58
  args.unshift(object)
60
59
  [0, 1].include?(method.arity) ? method.call(*args.slice(0, method.arity)) : method.call(*args)
@@ -190,7 +190,7 @@ module StateMachine
190
190
  if transition = transition_for(object)
191
191
  transition.perform(*args)
192
192
  else
193
- machine.invalidate(object, machine.attribute, :invalid_transition, [[:event, name]])
193
+ machine.invalidate(object, :state, :invalid_transition, [[:event, name]])
194
194
  false
195
195
  end
196
196
  end
@@ -205,7 +205,7 @@ module StateMachine
205
205
  guards.collect {|guard| guard.draw(graph, name, valid_states)}.flatten
206
206
  end
207
207
 
208
- # Generates a nicely formatted description of this events's contents.
208
+ # Generates a nicely formatted description of this event's contents.
209
209
  #
210
210
  # For example,
211
211
  #
@@ -244,7 +244,7 @@ module StateMachine
244
244
 
245
245
  # Fires the event, raising an exception if it fails
246
246
  machine.define_instance_method("#{qualified_name}!") do |machine, object, *args|
247
- object.send(qualified_name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{machine.attribute} via :#{name} from #{machine.states.match(object).name.inspect}")
247
+ object.send(qualified_name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{machine.name} via :#{name} from #{machine.states.match(object).name.inspect}")
248
248
  end
249
249
  end
250
250
  end
@@ -92,18 +92,17 @@ module StateMachine
92
92
  return unless machine.action
93
93
 
94
94
  result = nil
95
- attribute = machine.attribute
96
95
 
97
- if name = object.send("#{attribute}_event")
98
- if event = self[name.to_sym, :name]
99
- unless result = object.send("#{attribute}_event_transition") || event.transition_for(object)
96
+ if event_name = machine.read(object, :event)
97
+ if event = self[event_name.to_sym, :name]
98
+ unless result = machine.read(object, :event_transition) || event.transition_for(object)
100
99
  # No valid transition: invalidate
101
- machine.invalidate(object, "#{attribute}_event", :invalid_event, [[:state, machine.states.match!(object).name]]) if invalidate
100
+ machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).name]]) if invalidate
102
101
  result = false
103
102
  end
104
103
  else
105
104
  # Event is unknown: invalidate
106
- machine.invalidate(object, "#{attribute}_event", :invalid) if invalidate
105
+ machine.invalidate(object, :event, :invalid) if invalidate
107
106
  result = false
108
107
  end
109
108
  end
@@ -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
@@ -274,11 +274,17 @@ module StateMachine
274
274
  # Loads additional files specific to ActiveRecord
275
275
  def self.extended(base) #:nodoc:
276
276
  require 'state_machine/integrations/active_record/observer'
277
- I18n.load_path << "#{File.dirname(__FILE__)}/active_record/locale.rb" if Object.const_defined?(:I18n)
277
+
278
+ if Object.const_defined?(:I18n)
279
+ locale = "#{File.dirname(__FILE__)}/active_record/locale.rb"
280
+ I18n.load_path << locale unless I18n.load_path.include?(locale)
281
+ end
278
282
  end
279
283
 
280
284
  # Adds a validation error to the given object
281
285
  def invalidate(object, attribute, message, values = [])
286
+ attribute = self.attribute(attribute)
287
+
282
288
  if Object.const_defined?(:I18n)
283
289
  options = values.inject({}) {|options, (key, value)| options[key] = value; options}
284
290
  object.errors.add(attribute, message, options.merge(
@@ -304,8 +310,10 @@ module StateMachine
304
310
 
305
311
  # Skips defining reader/writer methods since this is done automatically
306
312
  def define_state_accessor
313
+ name = self.name
314
+
307
315
  owner_class.validates_each(attribute) do |record, attr, value|
308
- machine = record.class.state_machine(attr)
316
+ machine = record.class.state_machine(name)
309
317
  machine.invalidate(record, attr, :invalid) unless machine.states.match(record)
310
318
  end
311
319
  end
@@ -314,13 +322,13 @@ module StateMachine
314
322
  # compatibility with the default predicate which determines whether
315
323
  # *anything* is set for the attribute's value
316
324
  def define_state_predicate
317
- attribute = self.attribute
325
+ name = self.name
318
326
 
319
327
  # Still use class_eval here instance of define_instance_method since
320
328
  # we need to be able to call +super+
321
329
  @instance_helper_module.class_eval do
322
- define_method("#{attribute}?") do |*args|
323
- args.empty? ? super(*args) : self.class.state_machine(attribute).states.matches?(self, *args)
330
+ define_method("#{name}?") do |*args|
331
+ args.empty? ? super(*args) : self.class.state_machine(name).states.matches?(self, *args)
324
332
  end
325
333
  end
326
334
  end
@@ -381,17 +389,18 @@ module StateMachine
381
389
  # inheritance is respected properly.
382
390
  def define_scope(name, scope)
383
391
  name = name.to_sym
384
- attribute = self.attribute
392
+ machine_name = self.name
385
393
 
386
- # Created the scope and then override it with state translation
394
+ # Create the scope and then override it with state translation
387
395
  owner_class.named_scope(name)
388
396
  owner_class.scopes[name] = lambda do |klass, *states|
389
- machine_states = klass.state_machine(attribute).states
397
+ machine_states = klass.state_machine(machine_name).states
390
398
  values = states.flatten.map {|state| machine_states.fetch(state).value}
391
399
 
392
400
  ::ActiveRecord::NamedScope::Scope.new(klass, scope.call(values))
393
401
  end
394
402
 
403
+ # Prevent the Machine class from wrapping the scope
395
404
  false
396
405
  end
397
406
 
@@ -402,22 +411,22 @@ module StateMachine
402
411
  # * #{type}_#{qualified_event}_from_#{from}
403
412
  # * #{type}_#{qualified_event}_to_#{to}
404
413
  # * #{type}_#{qualified_event}
405
- # * #{type}_transition_#{attribute}_from_#{from}_to_#{to}
406
- # * #{type}_transition_#{attribute}_from_#{from}
407
- # * #{type}_transition_#{attribute}_to_#{to}
408
- # * #{type}_transition_#{attribute}
414
+ # * #{type}_transition_#{machine_name}_from_#{from}_to_#{to}
415
+ # * #{type}_transition_#{machine_name}_from_#{from}
416
+ # * #{type}_transition_#{machine_name}_to_#{to}
417
+ # * #{type}_transition_#{machine_name}
409
418
  # * #{type}_transition
410
419
  #
411
420
  # This will always return true regardless of the results of the
412
421
  # callbacks.
413
422
  def notify(type, object, transition)
414
- attribute = transition.attribute
423
+ name = self.name
415
424
  event = transition.qualified_event
416
425
  from = transition.from_name
417
426
  to = transition.to_name
418
427
 
419
428
  # Machine-specific updates
420
- ["#{type}_#{event}", "#{type}_transition_#{attribute}"].each do |event_segment|
429
+ ["#{type}_#{event}", "#{type}_transition_#{name}"].each do |event_segment|
421
430
  ["_from_#{from}", nil].each do |from_segment|
422
431
  ["_to_#{to}", nil].each do |to_segment|
423
432
  object.class.changed
@@ -2,6 +2,7 @@
2
2
  :activerecord => {
3
3
  :errors => {
4
4
  :messages => {
5
+ :invalid => StateMachine::Machine.default_messages[:invalid],
5
6
  :invalid_event => StateMachine::Machine.default_messages[:invalid_event] % ['{{state}}'],
6
7
  :invalid_transition => StateMachine::Machine.default_messages[:invalid_transition] % ['{{event}}']
7
8
  }
@@ -94,16 +94,13 @@ module StateMachine
94
94
  # include DataMapper::Resource
95
95
  # ...
96
96
  #
97
- # # Allow both machines to share the same state
98
- # alias_method :public_state, :state
99
- # alias_method :public_state=, :state=
100
- #
101
97
  # state_machine do
102
98
  # # Define private events here
103
99
  # end
104
100
  # protected :state_event= # Prevent access to events in the first machine
105
101
  #
106
- # state_machine :public_state do
102
+ # # Allow both machines to share the same state
103
+ # state_machine :public_state, :attribute => :state do
107
104
  # # Define public events here
108
105
  # end
109
106
  # end
@@ -112,7 +109,7 @@ module StateMachine
112
109
  #
113
110
  # By default, the use of transactions during an event transition is
114
111
  # 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
112
+ # changes are made to the database during a before callback, but the
116
113
  # transition fails to complete, those changes will *not* be rolled back.
117
114
  #
118
115
  # For example,
@@ -255,7 +252,7 @@ module StateMachine
255
252
 
256
253
  # Adds a validation error to the given object
257
254
  def invalidate(object, attribute, message, values = [])
258
- object.errors.add(attribute, generate_message(message, values)) if supports_validations?
255
+ object.errors.add(self.attribute(attribute), generate_message(message, values)) if supports_validations?
259
256
  end
260
257
 
261
258
  # Resets any errors previously added when invalidating the given object
@@ -274,9 +271,9 @@ module StateMachine
274
271
  owner_class.property(attribute, String) unless owner_class.properties.has_property?(attribute)
275
272
 
276
273
  if supports_validations?
277
- attribute = self.attribute
274
+ name = self.name
278
275
  owner_class.validates_with_block(attribute) do
279
- machine = self.class.state_machine(attribute)
276
+ machine = self.class.state_machine(name)
280
277
  machine.states.match(self) ? true : [false, machine.generate_message(:invalid)]
281
278
  end
282
279
  end