state_machine 0.7.5 → 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.
@@ -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