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 +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
|