state_machine 0.4.0 → 0.4.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.
data/CHANGELOG.rdoc CHANGED
@@ -1,5 +1,14 @@
1
1
  == master
2
2
 
3
+ == 0.4.1 / 2008-12-16
4
+
5
+ * Fix nil states not being handled properly in guards, known states, or visualizations
6
+ * Fix the same node being used for different dynamic states in GraphViz output
7
+ * Always include initial state in the list of known states even if it's dynamic
8
+ * Use consistent naming scheme for dynamic states in GraphViz output
9
+ * Allow blocks to be directly passed into machine class
10
+ * Fix attribute predicates not working on attributes that represent columns in ActiveRecord
11
+
3
12
  == 0.4.0 / 2008-12-14
4
13
 
5
14
  * Remove the PluginAWeek namespace
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2006 Scott Barron, 2006-2008 Aaron Pfefier
1
+ Copyright (c) 2006-2008 Aaron Pfefier
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -335,10 +335,11 @@ To generate multiple state machine graphs:
335
335
  rake state_machine:draw FILE=vehicle.rb,car.rb CLASS=Vehicle,Car
336
336
 
337
337
  *Note* that this will generate a different file for every state machine defined
338
- in the class. The generates files will an output filename of the format #{class_name}_#{attribute}.#{format}.
338
+ in the class. The generated files will use an output filename of the format
339
+ #{class_name}_#{attribute}.#{format}.
339
340
 
340
341
  For examples of actual images generated using this task, see those under the
341
- test/examples folder.
342
+ examples folder.
342
343
 
343
344
  ==== Ruby on Rails Integration
344
345
 
@@ -371,7 +372,7 @@ events for your models. It is cross-platform, written in Java.
371
372
  == Testing
372
373
 
373
374
  To run the entire test suite (will test ActiveRecord, DataMapper, and Sequel
374
- integrations if available):
375
+ integrations if the proper dependencies are available):
375
376
 
376
377
  rake test
377
378
 
@@ -386,5 +387,4 @@ dependencies are listed below.
386
387
 
387
388
  == References
388
389
 
389
- * Scott Barron - acts_as_state_machine[http://elitists.textdriven.com/svn/plugins/acts_as_state_machine]
390
390
  * acts_as_enumeration[http://github.com/pluginaweek/acts_as_enumeration]
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.4.0'
8
+ s.version = '0.4.1'
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
 
@@ -20,7 +20,7 @@ module StateMachine
20
20
  attr_reader :guards
21
21
 
22
22
  # A list of all of the states known to this event using the configured
23
- # guards/transitions as the source.
23
+ # guards/transitions as the source
24
24
  attr_reader :known_states
25
25
 
26
26
  # Creates a new event within the context of the given machine
@@ -45,7 +45,7 @@ module StateMachine
45
45
  #
46
46
  # Configuration options:
47
47
  # * +to+ - The state that being transitioned to. If not specified, then the transition will not change the state.
48
- # * +from+ - A state or array of states that can be transitioned from. If not specified, then the transition can occur for *any* from state
48
+ # * +from+ - A state or array of states that can be transitioned from. If not specified, then the transition can occur for *any* from state.
49
49
  # * +except_from+ - A state or array of states that *cannot* be transitioned from.
50
50
  # * +if+ - Specifies a method, proc or string to call to determine if the transition should occur (e.g. :if => :moving?, or :if => Proc.new {|car| car.speed > 60}). The method, proc or string should return or evaluate to a true or false value.
51
51
  # * +unless+ - Specifies a method, proc or string to call to determine if the transition should not occur (e.g. :unless => :stopped?, or :unless => Proc.new {|car| car.speed <= 60}). The method, proc or string should return or evaluate to a true or false value.
@@ -68,8 +68,10 @@ module StateMachine
68
68
  #
69
69
  # == Examples
70
70
  #
71
+ # transition :from => nil, :to => 'parked'
71
72
  # transition :from => %w(first_gear reverse)
72
73
  # transition :except_from => 'parked'
74
+ # transition :to => nil
73
75
  # transition :to => 'parked'
74
76
  # transition :to => lambda {Time.now}
75
77
  # transition :to => 'parked', :from => 'first_gear'
@@ -100,7 +102,7 @@ module StateMachine
100
102
 
101
103
  if guard = guards.find {|guard| guard.matches?(object, :from => from)}
102
104
  # Guard allows for the transition to occur
103
- to = guard.requirements[:to] || from
105
+ to = guard.requirements[:to] ? guard.requirements[:to].first : from
104
106
  to = to.call if to.is_a?(Proc)
105
107
  Transition.new(object, machine, name, from, to)
106
108
  end
@@ -33,7 +33,8 @@ module StateMachine
33
33
  if !@skip_initialize_hook && [:initialize, :initialize_with_state_machine].include?(method)
34
34
  @skip_initialize_hook = true
35
35
 
36
- # +define_method+ is used to prevent it from showing up in #instance_methods
36
+ # Re-defining +initialize+ instead of alias chaining is done in order to
37
+ # prevent +initialize+ from showing up in #instance_methods
37
38
  alias_method :initialize_without_state_machine, :initialize
38
39
  class_eval <<-end_eval, __FILE__, __LINE__
39
40
  def initialize(*args, &block)
@@ -27,7 +27,17 @@ module StateMachine
27
27
  assert_valid_keys(requirements, :to, :from, :on, :except_to, :except_from, :except_on, :if, :unless)
28
28
 
29
29
  @requirements = requirements
30
- @known_states = [:to, :from, :except_to, :except_from].inject([]) {|states, option| states |= Array(requirements[option])}
30
+ @known_states = []
31
+
32
+ # Normalize the requirements and track known states
33
+ [:to, :from, :on, :except_to, :except_from, :except_on].each do |option|
34
+ if @requirements.include?(option)
35
+ values = @requirements[option]
36
+
37
+ @requirements[option] = values = [values] unless values.is_a?(Array)
38
+ @known_states |= values if [:to, :from, :except_to, :except_from].include?(option)
39
+ end
40
+ end
31
41
  end
32
42
 
33
43
  # Determines whether the given object / query matches the requirements
@@ -45,10 +55,11 @@ module StateMachine
45
55
  #
46
56
  # == Examples
47
57
  #
48
- # guard = StateMachine::Guard.new(:on => 'ignite', :from => 'parked', :to => 'idling')
58
+ # guard = StateMachine::Guard.new(:on => 'ignite', :from => [nil, 'parked'], :to => 'idling')
49
59
  #
50
60
  # # Successful
51
61
  # guard.matches?(object, :on => 'ignite') # => true
62
+ # guard.matches?(object, :from => nil) # => true
52
63
  # guard.matches?(object, :from => 'parked') # => true
53
64
  # guard.matches?(object, :to => 'idling') # => true
54
65
  # guard.matches?(object, :from => 'parked', :to => 'idling') # => true
@@ -67,10 +78,9 @@ module StateMachine
67
78
  protected
68
79
  # Verify that the from state, to state, and event match the query
69
80
  def matches_query?(object, query)
70
- (!query || query.empty?) ||
71
- find_match(query[:from], requirements[:from], requirements[:except_from]) &&
72
- find_match(query[:to], requirements[:to], requirements[:except_to]) &&
73
- find_match(query[:on], requirements[:on], requirements[:except_on])
81
+ (!query || query.empty?) || [:from, :to, :on].all? do |option|
82
+ !query.include?(option) || find_match(query[option], requirements[option], requirements[:"except_#{option}"])
83
+ end
74
84
  end
75
85
 
76
86
  # Verify that the conditionals for this guard evaluate to true for the
@@ -87,26 +97,23 @@ module StateMachine
87
97
 
88
98
  # Attempts to find the given value in either a whitelist of values or
89
99
  # a blacklist of values. The whitelist will always be used first if it
90
- # is specified. If neither lists are specified or the value is blank,
91
- # then this will always find a match and return true.
100
+ # is specified. If neither lists are specified, then this will always
101
+ # find a match and return true.
92
102
  #
93
103
  # == Examples
94
104
  #
95
- # find_match(nil, %w(parked idling), nil) # => true
105
+ # find_match(nil, %w(parked idling), nil) # => false
106
+ # find_match(nil, [nil], nil) # => true
96
107
  # find_match('parked', nil, nil) # => true
97
108
  # find_match('parked', %w(parked idling), nil) # => true
98
109
  # find_match('first_gear', %w(parked idling, nil) # => false
99
110
  # find_match('parked', nil, %w(parked idling)) # => false
100
111
  # find_match('first_gear', nil, %w(parked idling)) # => true
101
112
  def find_match(value, whitelist, blacklist)
102
- if value
103
- if whitelist
104
- Array(whitelist).include?(value)
105
- elsif blacklist
106
- !Array(blacklist).include?(value)
107
- else
108
- true
109
- end
113
+ if whitelist
114
+ whitelist.include?(value)
115
+ elsif blacklist
116
+ !blacklist.include?(value)
110
117
  else
111
118
  true
112
119
  end
@@ -188,7 +188,29 @@ module StateMachine
188
188
  # Forces all attribute methods to be generated for the model so that
189
189
  # the reader/writer methods for the attribute are available
190
190
  def define_attribute_accessor
191
- owner_class.define_attribute_methods if owner_class.table_exists?
191
+ if owner_class.table_exists?
192
+ owner_class.define_attribute_methods
193
+
194
+ # Support attribute predicate for ActiveRecord columns
195
+ if owner_class.column_names.include?(attribute)
196
+ attribute = self.attribute
197
+
198
+ owner_class.class_eval do
199
+ define_method("#{attribute}?") do |*args|
200
+ if args.empty?
201
+ # No arguments: querying for presence of the attribute
202
+ super
203
+ else
204
+ # Arguments: querying for the attribute's current value
205
+ state = args.first
206
+ raise ArgumentError, "#{state.inspect} is not a known #{attribute} value" unless self.class.state_machines[attribute].states.include?(state)
207
+ send(attribute) == state
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+
192
214
  super
193
215
  end
194
216
 
@@ -17,11 +17,11 @@ module StateMachine
17
17
  # an object since they can be any arbitrary value. As a result, anything
18
18
  # that relies on a list of all possible states should keep in mind that if
19
19
  # a state has not been referenced *anywhere* in the state machine definition,
20
- # then it will *not* be a known state unless the +other_states+ is used.
20
+ # then it will *not* be a known state unless the +other_states+ helper is used.
21
21
  #
22
22
  # == State values
23
23
  #
24
- # While string are the most common object type used for setting values on
24
+ # While strings are the most common object type used for setting values on
25
25
  # the state of the machine, there are no restrictions on what can be used.
26
26
  # This means that symbols, integers, dates/times, etc. can all be used.
27
27
  #
@@ -49,6 +49,8 @@ module StateMachine
49
49
  #
50
50
  # class Switch
51
51
  # state_machine :activated_at
52
+ # before_transition :to => nil, :do => lambda {...}
53
+ #
52
54
  # event :activate do
53
55
  # transition :to => lambda {Time.now}
54
56
  # end
@@ -219,16 +221,19 @@ module StateMachine
219
221
  # If a machine of the given name already exists in one of the class's
220
222
  # superclasses, then a copy of that machine will be created and stored
221
223
  # in the new owner class (the original will remain unchanged).
222
- def find_or_create(owner_class, *args)
224
+ def find_or_create(owner_class, *args, &block)
223
225
  options = args.last.is_a?(Hash) ? args.pop : {}
224
- attribute = args.any? ? args.first.to_s : 'state'
226
+ attribute = (args.first || 'state').to_s
225
227
 
226
228
  # Attempts to find an existing machine
227
229
  if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[attribute]
228
230
  machine = machine.within_context(owner_class, options) unless machine.owner_class == owner_class
231
+
232
+ # Evaluate caller block for DSL
233
+ machine.instance_eval(&block) if block_given?
229
234
  else
230
235
  # No existing machine: create a new one
231
- machine = new(owner_class, attribute, options)
236
+ machine = new(owner_class, attribute, options, &block)
232
237
  end
233
238
 
234
239
  machine
@@ -283,7 +288,7 @@ module StateMachine
283
288
  include StateMachine::InstanceMethods
284
289
  end unless owner_class.included_modules.include?(StateMachine::InstanceMethods)
285
290
 
286
- # Initialize the context of the machine
291
+ # Initialize the class context of the machine
287
292
  set_context(owner_class, :initial => options[:initial], :integration => options[:integration], &block)
288
293
 
289
294
  # Set integration-specific configurations
@@ -293,6 +298,9 @@ module StateMachine
293
298
 
294
299
  # Call after hook for integration-specific extensions
295
300
  after_initialize
301
+
302
+ # Evaluate caller block for DSL
303
+ instance_eval(&block) if block_given?
296
304
  end
297
305
 
298
306
  # Creates a copy of this machine in addition to copies of each associated
@@ -313,7 +321,7 @@ module StateMachine
313
321
 
314
322
  # Creates a copy of this machine within the context of the given class.
315
323
  # This should be used for inheritance support of state machines.
316
- def within_context(owner_class, options = {}) #:nodoc:
324
+ def within_context(owner_class, options = {}, &block) #:nodoc:
317
325
  machine = dup
318
326
  machine.set_context(owner_class, {:integration => @integration}.merge(options))
319
327
  machine
@@ -332,10 +340,8 @@ module StateMachine
332
340
  assert_valid_keys(options, :initial, :integration)
333
341
 
334
342
  @owner_class = owner_class
335
- if options[:initial]
336
- @initial_state = options[:initial]
337
- add_states([@initial_state]) unless @initial_state.is_a?(Proc)
338
- end
343
+ @initial_state = options[:initial] if options[:initial]
344
+ add_states([@initial_state])
339
345
 
340
346
  # Find an integration that can be used for implementing various parts
341
347
  # of the state machine that may behave differently in different libraries
@@ -351,12 +357,12 @@ module StateMachine
351
357
 
352
358
  # Gets the initial state of the machine for the given object. If a dynamic
353
359
  # initial state was configured for this machine, then the object will be
354
- # passed into the proc to help determine the actual value of the initial
355
- # state.
360
+ # passed into the lambda block to help determine the actual value of the
361
+ # initial state.
356
362
  #
357
363
  # == Examples
358
364
  #
359
- # With static initial state:
365
+ # With a static initial state:
360
366
  #
361
367
  # class Vehicle
362
368
  # state_machine :initial => 'parked' do
@@ -364,25 +370,36 @@ module StateMachine
364
370
  # end
365
371
  # end
366
372
  #
373
+ # vehicle = Vehicle.new
367
374
  # Vehicle.state_machines['state'].initial_state(vehicle) # => "parked"
368
375
  #
369
- # With dynamic initial state:
376
+ # With a dynamic initial state:
370
377
  #
371
378
  # class Vehicle
379
+ # attr_accessor :force_idle
380
+ #
372
381
  # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? 'idling' : 'parked'} do
373
382
  # ...
374
383
  # end
375
384
  # end
376
385
  #
386
+ # vehicle = Vehicle.new
387
+ #
388
+ # vehicle.force_idle = true
377
389
  # Vehicle.state_machines['state'].initial_state(vehicle) # => "idling"
390
+ #
391
+ # vehicle.force_idle = false
392
+ # Vehicle.state_machines['state'].initial_state(vehicle) # => "parked"
378
393
  def initial_state(object)
379
394
  @initial_state.is_a?(Proc) ? @initial_state.call(object) : @initial_state
380
395
  end
381
396
 
382
397
  # Defines additional states that are possible in the state machine, but
383
398
  # which are derived outside of any events/transitions or possibly
384
- # dynamically via Proc. This allows the creation of state conditionals
385
- # which are not defined in the standard :to or :from structure.
399
+ # dynamically via a lambda block. This allows the given states to be:
400
+ # * Queried via instance-level predicates
401
+ # * Included in GraphViz visualizations
402
+ # * Used in :except_from and :except_to transition/callback conditionals
386
403
  #
387
404
  # == Example
388
405
  #
@@ -450,7 +467,7 @@ module StateMachine
450
467
  #
451
468
  # state_machine do
452
469
  # event :park do
453
- # transition :to => 'parked', :from => Car.safe_states
470
+ # transition :to => 'parked', :from => Vehicle.safe_states
454
471
  # end
455
472
  # end
456
473
  # end
@@ -685,23 +702,47 @@ module StateMachine
685
702
 
686
703
  graph = GraphViz.new('G', :output => options[:format], :file => File.join(options[:path], "#{options[:name]}.#{options[:format]}"))
687
704
 
705
+ # Tracks unique identifiers for dynamic states (via lambda blocks)
706
+ dynamic_states = {}
707
+
688
708
  # Add nodes
689
709
  states.each do |state|
690
710
  shape = state == @initial_state ? 'doublecircle' : 'circle'
691
- state = state.is_a?(Proc) ? 'lambda' : state.to_s
692
- graph.add_node(state, :width => '1', :height => '1', :fixedsize => 'true', :shape => shape, :fontname => options[:font])
711
+
712
+ # Use GraphViz-friendly name/label for dynamic/nil states
713
+ if state.is_a?(Proc)
714
+ name = "lambda#{dynamic_states.keys.length}"
715
+ label = '*'
716
+ dynamic_states[state] = name
717
+ else
718
+ name = label = state.nil? ? 'nil' : state.to_s
719
+ end
720
+
721
+ graph.add_node(name, :label => label, :width => '1', :height => '1', :fixedsize => 'true', :shape => shape, :fontname => options[:font])
693
722
  end
694
723
 
695
724
  # Add edges
696
725
  events.values.each do |event|
697
726
  event.guards.each do |guard|
698
727
  # From states: :from, everything but :except states, or all states
699
- from_states = Array(guard.requirements[:from]) || guard.requirements[:except_from] && (states - Array(guard.requirements[:except_from])) || states
700
- to_state = guard.requirements[:to]
701
- to_state = to_state.is_a?(Proc) ? 'lambda' : to_state.to_s if to_state
728
+ from_states = guard.requirements[:from] || guard.requirements[:except_from] && (states - guard.requirements[:except_from]) || states
729
+ if to_state = guard.requirements[:to]
730
+ to_state = to_state.first
731
+
732
+ # Convert to GraphViz-friendly name
733
+ to_state = case to_state
734
+ when Proc; dynamic_states[to_state]
735
+ when nil; 'nil'
736
+ else; to_state.to_s; end
737
+ end
702
738
 
703
739
  from_states.each do |from_state|
704
- from_state = from_state.to_s
740
+ # Convert to GraphViz-friendly name
741
+ from_state = case from_state
742
+ when Proc; dynamic_states[from_state]
743
+ when nil; 'nil'
744
+ else; from_state.to_s; end
745
+
705
746
  graph.add_edge(from_state, to_state || from_state, :label => event.name, :fontname => options[:font])
706
747
  end
707
748
  end
@@ -728,8 +769,8 @@ module StateMachine
728
769
  def default_action
729
770
  end
730
771
 
731
- # Adds reader/writer methods for accessing the attribute that this state
732
- # machine is defined for.
772
+ # Adds reader/writer/prediate methods for accessing the attribute that
773
+ # this state machine is defined for.
733
774
  def define_attribute_accessor
734
775
  attribute = self.attribute
735
776
 
@@ -790,7 +831,7 @@ module StateMachine
790
831
  # Add state predicates
791
832
  attribute = self.attribute
792
833
  new_states.each do |state|
793
- if state.is_a?(String) || state.is_a?(Symbol)
834
+ if state && (state.is_a?(String) || state.is_a?(Symbol))
794
835
  name = "#{state}?"
795
836
 
796
837
  owner_class.class_eval do
data/lib/state_machine.rb CHANGED
@@ -156,9 +156,7 @@ module StateMachine
156
156
  # integrations and the individual integration docs for information about
157
157
  # the actual scopes that are generated.
158
158
  def state_machine(*args, &block)
159
- machine = StateMachine::Machine.find_or_create(self, *args)
160
- machine.instance_eval(&block) if block
161
- machine
159
+ StateMachine::Machine.find_or_create(self, *args, &block)
162
160
  end
163
161
  end
164
162
  end