state_machine 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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