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 +9 -0
- data/LICENSE +1 -1
- data/README.rdoc +4 -4
- data/Rakefile +1 -1
- data/lib/state_machine/event.rb +5 -3
- data/lib/state_machine/extensions.rb +2 -1
- data/lib/state_machine/guard.rb +24 -17
- data/lib/state_machine/integrations/active_record.rb +23 -1
- data/lib/state_machine/machine.rb +68 -27
- data/lib/state_machine.rb +1 -3
- data/test/active_record.log +17542 -0
- data/test/data_mapper.log +4814 -0
- data/test/sequel.log +2697 -0
- data/test/unit/event_test.rb +33 -0
- data/test/unit/guard_test.rb +64 -1
- data/test/unit/integrations/active_record_test.rb +58 -2
- data/test/unit/machine_test.rb +82 -11
- metadata +2 -2
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
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
|
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
|
-
|
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.
|
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
|
|
data/lib/state_machine/event.rb
CHANGED
@@ -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]
|
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
|
-
# +
|
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)
|
data/lib/state_machine/guard.rb
CHANGED
@@ -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 = [
|
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
|
-
|
72
|
-
|
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
|
91
|
-
#
|
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) # =>
|
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
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
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
|
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.
|
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
|
-
|
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
|
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
|
385
|
-
#
|
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 =>
|
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
|
-
|
692
|
-
|
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 =
|
700
|
-
to_state = guard.requirements[:to]
|
701
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|