hsume2-state_machine 1.0.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.
Files changed (110) hide show
  1. data/CHANGELOG.rdoc +413 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +717 -0
  4. data/Rakefile +77 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine.rb +448 -0
  28. data/lib/state_machine/alternate_machine.rb +79 -0
  29. data/lib/state_machine/assertions.rb +36 -0
  30. data/lib/state_machine/branch.rb +224 -0
  31. data/lib/state_machine/callback.rb +236 -0
  32. data/lib/state_machine/condition_proxy.rb +94 -0
  33. data/lib/state_machine/error.rb +13 -0
  34. data/lib/state_machine/eval_helpers.rb +86 -0
  35. data/lib/state_machine/event.rb +304 -0
  36. data/lib/state_machine/event_collection.rb +139 -0
  37. data/lib/state_machine/extensions.rb +149 -0
  38. data/lib/state_machine/initializers.rb +4 -0
  39. data/lib/state_machine/initializers/merb.rb +1 -0
  40. data/lib/state_machine/initializers/rails.rb +25 -0
  41. data/lib/state_machine/integrations.rb +110 -0
  42. data/lib/state_machine/integrations/active_model.rb +502 -0
  43. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  44. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  45. data/lib/state_machine/integrations/active_model/versions.rb +31 -0
  46. data/lib/state_machine/integrations/active_record.rb +424 -0
  47. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  48. data/lib/state_machine/integrations/active_record/versions.rb +143 -0
  49. data/lib/state_machine/integrations/base.rb +91 -0
  50. data/lib/state_machine/integrations/data_mapper.rb +392 -0
  51. data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
  52. data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
  53. data/lib/state_machine/integrations/mongo_mapper.rb +272 -0
  54. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  55. data/lib/state_machine/integrations/mongo_mapper/versions.rb +110 -0
  56. data/lib/state_machine/integrations/mongoid.rb +357 -0
  57. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  58. data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
  59. data/lib/state_machine/integrations/sequel.rb +428 -0
  60. data/lib/state_machine/integrations/sequel/versions.rb +36 -0
  61. data/lib/state_machine/machine.rb +1873 -0
  62. data/lib/state_machine/machine_collection.rb +87 -0
  63. data/lib/state_machine/matcher.rb +123 -0
  64. data/lib/state_machine/matcher_helpers.rb +54 -0
  65. data/lib/state_machine/node_collection.rb +157 -0
  66. data/lib/state_machine/path.rb +120 -0
  67. data/lib/state_machine/path_collection.rb +90 -0
  68. data/lib/state_machine/state.rb +271 -0
  69. data/lib/state_machine/state_collection.rb +112 -0
  70. data/lib/state_machine/transition.rb +458 -0
  71. data/lib/state_machine/transition_collection.rb +244 -0
  72. data/lib/tasks/state_machine.rake +1 -0
  73. data/lib/tasks/state_machine.rb +27 -0
  74. data/test/files/en.yml +17 -0
  75. data/test/files/switch.rb +11 -0
  76. data/test/functional/alternate_state_machine_test.rb +122 -0
  77. data/test/functional/state_machine_test.rb +993 -0
  78. data/test/test_helper.rb +4 -0
  79. data/test/unit/assertions_test.rb +40 -0
  80. data/test/unit/branch_test.rb +890 -0
  81. data/test/unit/callback_test.rb +701 -0
  82. data/test/unit/condition_proxy_test.rb +328 -0
  83. data/test/unit/error_test.rb +43 -0
  84. data/test/unit/eval_helpers_test.rb +222 -0
  85. data/test/unit/event_collection_test.rb +358 -0
  86. data/test/unit/event_test.rb +985 -0
  87. data/test/unit/integrations/active_model_test.rb +1097 -0
  88. data/test/unit/integrations/active_record_test.rb +2021 -0
  89. data/test/unit/integrations/base_test.rb +99 -0
  90. data/test/unit/integrations/data_mapper_test.rb +1909 -0
  91. data/test/unit/integrations/mongo_mapper_test.rb +1611 -0
  92. data/test/unit/integrations/mongoid_test.rb +1591 -0
  93. data/test/unit/integrations/sequel_test.rb +1523 -0
  94. data/test/unit/integrations_test.rb +61 -0
  95. data/test/unit/invalid_event_test.rb +20 -0
  96. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  97. data/test/unit/invalid_transition_test.rb +77 -0
  98. data/test/unit/machine_collection_test.rb +599 -0
  99. data/test/unit/machine_test.rb +3043 -0
  100. data/test/unit/matcher_helpers_test.rb +37 -0
  101. data/test/unit/matcher_test.rb +155 -0
  102. data/test/unit/node_collection_test.rb +217 -0
  103. data/test/unit/path_collection_test.rb +266 -0
  104. data/test/unit/path_test.rb +485 -0
  105. data/test/unit/state_collection_test.rb +310 -0
  106. data/test/unit/state_machine_test.rb +31 -0
  107. data/test/unit/state_test.rb +924 -0
  108. data/test/unit/transition_collection_test.rb +2102 -0
  109. data/test/unit/transition_test.rb +1541 -0
  110. metadata +207 -0
@@ -0,0 +1,90 @@
1
+ require 'state_machine/path'
2
+
3
+ module StateMachine
4
+ # Represents a collection of paths that are generated based on a set of
5
+ # requirements regarding what states to start and end on
6
+ class PathCollection < Array
7
+ include Assertions
8
+
9
+ # The object whose state machine is being walked
10
+ attr_reader :object
11
+
12
+ # The state machine these path are walking
13
+ attr_reader :machine
14
+
15
+ # The initial state to start each path from
16
+ attr_reader :from_name
17
+
18
+ # The target state for each path
19
+ attr_reader :to_name
20
+
21
+ # Creates a new collection of paths with the given requirements.
22
+ #
23
+ # Configuration options:
24
+ # * <tt>:from</tt> - The initial state to start from
25
+ # * <tt>:to</tt> - The target end state
26
+ # * <tt>:deep</tt> - Whether to enable deep searches for the target state.
27
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
28
+ # conditionals defined for each one
29
+ def initialize(object, machine, options = {})
30
+ options = {:deep => false, :from => machine.states.match!(object).name}.merge(options)
31
+ assert_valid_keys(options, :from, :to, :deep, :guard)
32
+
33
+ @object = object
34
+ @machine = machine
35
+ @from_name = machine.states.fetch(options[:from]).name
36
+ @to_name = options[:to] && machine.states.fetch(options[:to]).name
37
+ @guard = options[:guard]
38
+ @deep = options[:deep]
39
+
40
+ initial_paths.each {|path| walk(path)}
41
+ end
42
+
43
+ # Lists all of the states that can be transitioned from through the paths in
44
+ # this collection.
45
+ #
46
+ # For example,
47
+ #
48
+ # paths.from_states # => [:parked, :idling, :first_gear, ...]
49
+ def from_states
50
+ map {|path| path.from_states}.flatten.uniq
51
+ end
52
+
53
+ # Lists all of the states that can be transitioned to through the paths in
54
+ # this collection.
55
+ #
56
+ # For example,
57
+ #
58
+ # paths.to_states # => [:idling, :first_gear, :second_gear, ...]
59
+ def to_states
60
+ map {|path| path.to_states}.flatten.uniq
61
+ end
62
+
63
+ # Lists all of the events that can be fired through the paths in this
64
+ # collection.
65
+ #
66
+ # For example,
67
+ #
68
+ # paths.events # => [:park, :ignite, :shift_up, ...]
69
+ def events
70
+ map {|path| path.events}.flatten.uniq
71
+ end
72
+
73
+ private
74
+ # Gets the initial set of paths to walk
75
+ def initial_paths
76
+ machine.events.transitions_for(object, :from => from_name, :guard => @guard).map do |transition|
77
+ path = Path.new(object, machine, :target => to_name, :guard => @guard)
78
+ path << transition
79
+ path
80
+ end
81
+ end
82
+
83
+ # Walks down the given path. Each new path that matches the configured
84
+ # requirements will be added to this collection.
85
+ def walk(path)
86
+ self << path if path.complete?
87
+ path.walk {|next_path| walk(next_path)} unless to_name && path.complete? && !@deep
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,271 @@
1
+ require 'state_machine/assertions'
2
+ require 'state_machine/condition_proxy'
3
+
4
+ module StateMachine
5
+ # A state defines a value that an attribute can be in after being transitioned
6
+ # 0 or more times. States can represent a value of any type in Ruby, though
7
+ # the most common (and default) type is String.
8
+ #
9
+ # In addition to defining the machine's value, a state can also define a
10
+ # behavioral context for an object when that object is in the state. See
11
+ # StateMachine::Machine#state for more information about how state-driven
12
+ # behavior can be utilized.
13
+ class State
14
+ include Assertions
15
+
16
+ # The state machine for which this state is defined
17
+ attr_accessor :machine
18
+
19
+ # The unique identifier for the state used in event and callback definitions
20
+ attr_reader :name
21
+
22
+ # The fully-qualified identifier for the state, scoped by the machine's
23
+ # namespace
24
+ attr_reader :qualified_name
25
+
26
+ # The human-readable name for the state
27
+ attr_writer :human_name
28
+
29
+ # The value that is written to a machine's attribute when an object
30
+ # transitions into this state
31
+ attr_writer :value
32
+
33
+ # Whether this state's value should be cached after being evaluated
34
+ attr_accessor :cache
35
+
36
+ # Whether or not this state is the initial state to use for new objects
37
+ attr_accessor :initial
38
+ alias_method :initial?, :initial
39
+
40
+ # A custom lambda block for determining whether a given value matches this
41
+ # state
42
+ attr_accessor :matcher
43
+
44
+ # Tracks all of the methods that have been defined for the machine's owner
45
+ # class when objects are in this state.
46
+ #
47
+ # Maps :method_name => UnboundMethod
48
+ attr_reader :methods
49
+
50
+ # Creates a new state within the context of the given machine.
51
+ #
52
+ # Configuration options:
53
+ # * <tt>:initial</tt> - Whether this state is the beginning state for the
54
+ # machine. Default is false.
55
+ # * <tt>:value</tt> - The value to store when an object transitions to this
56
+ # state. Default is the name (stringified).
57
+ # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
58
+ # then setting this to true will cache the evaluated result
59
+ # * <tt>:if</tt> - Determines whether a value matches this state
60
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
61
+ # By default, the configured value is matched.
62
+ # * <tt>:human_name</tt> - The human-readable version of this state's name
63
+ def initialize(machine, name, options = {}) #:nodoc:
64
+ assert_valid_keys(options, :initial, :value, :cache, :if, :human_name)
65
+
66
+ @machine = machine
67
+ @name = name
68
+ @qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name
69
+ @human_name = options[:human_name] || (@name ? @name.to_s.tr('_', ' ') : 'nil')
70
+ @value = options.include?(:value) ? options[:value] : name && name.to_s
71
+ @cache = options[:cache]
72
+ @matcher = options[:if]
73
+ @methods = {}
74
+ @initial = options[:initial] == true
75
+
76
+ if name
77
+ conflicting_machines = machine.owner_class.state_machines.select {|name, other_machine| other_machine != machine && other_machine.states[qualified_name, :qualified_name]}
78
+
79
+ # Output a warning if another machine has a conflicting qualified name
80
+ # for a different attribute
81
+ if conflict = conflicting_machines.detect {|name, other_machine| other_machine.attribute != machine.attribute}
82
+ name, other_machine = conflict
83
+ warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
84
+ elsif conflicting_machines.empty?
85
+ # Only bother adding predicates when another machine for the same
86
+ # attribute hasn't already done so
87
+ add_predicate
88
+ end
89
+ end
90
+ end
91
+
92
+ # Creates a copy of this state in addition to the list of associated
93
+ # methods to prevent conflicts across different states.
94
+ def initialize_copy(orig) #:nodoc:
95
+ super
96
+ @methods = methods.dup
97
+ end
98
+
99
+ # Determines whether there are any states that can be transitioned to from
100
+ # this state. If there are none, then this state is considered *final*.
101
+ # Any objects in a final state will remain so forever given the current
102
+ # machine's definition.
103
+ def final?
104
+ !machine.events.any? do |event|
105
+ event.branches.any? do |branch|
106
+ branch.state_requirements.any? do |requirement|
107
+ requirement[:from].matches?(name) && !requirement[:to].matches?(name, :from => name)
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ # Transforms the state name into a more human-readable format, such as
114
+ # "first gear" instead of "first_gear"
115
+ def human_name(klass = @machine.owner_class)
116
+ @human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name
117
+ end
118
+
119
+ # Generates a human-readable description of this state's name / value:
120
+ #
121
+ # For example,
122
+ #
123
+ # State.new(machine, :parked).description # => "parked"
124
+ # State.new(machine, :parked, :value => :parked).description # => "parked"
125
+ # State.new(machine, :parked, :value => nil).description # => "parked (nil)"
126
+ # State.new(machine, :parked, :value => 1).description # => "parked (1)"
127
+ # State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
128
+ def description
129
+ description = name ? name.to_s : name.inspect
130
+ description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
131
+ description
132
+ end
133
+
134
+ # The value that represents this state. This will optionally evaluate the
135
+ # original block if it's a lambda block. Otherwise, the static value is
136
+ # returned.
137
+ #
138
+ # For example,
139
+ #
140
+ # State.new(machine, :parked, :value => 1).value # => 1
141
+ # State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
142
+ # State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => <Proc:0xb6ea7ca0@...>
143
+ def value(eval = true)
144
+ if @value.is_a?(Proc) && eval
145
+ if cache_value?
146
+ @value = @value.call
147
+ machine.states.update(self)
148
+ @value
149
+ else
150
+ @value.call
151
+ end
152
+ else
153
+ @value
154
+ end
155
+ end
156
+
157
+ # Determines whether this state matches the given value. If no matcher is
158
+ # configured, then this will check whether the values are equivalent.
159
+ # Otherwise, the matcher will determine the result.
160
+ #
161
+ # For example,
162
+ #
163
+ # # Without a matcher
164
+ # state = State.new(machine, :parked, :value => 1)
165
+ # state.matches?(1) # => true
166
+ # state.matches?(2) # => false
167
+ #
168
+ # # With a matcher
169
+ # state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
170
+ # state.matches?(nil) # => false
171
+ # state.matches?(Time.now) # => true
172
+ def matches?(other_value)
173
+ matcher ? matcher.call(other_value) : other_value == value
174
+ end
175
+
176
+ # Defines a context for the state which will be enabled on instances of
177
+ # the owner class when the machine is in this state.
178
+ #
179
+ # This can be called multiple times. Each time a new context is created,
180
+ # a new module will be included in the owner class.
181
+ def context(&block)
182
+ owner_class = machine.owner_class
183
+ machine_name = machine.name
184
+ name = self.name
185
+
186
+ # Evaluate the method definitions
187
+ context = ConditionProxy.new(owner_class, lambda {|object| object.class.state_machine(machine_name).states.matches?(object, name)})
188
+ context.class_eval(&block)
189
+ context.instance_methods.each do |method|
190
+ methods[method.to_sym] = context.instance_method(method)
191
+
192
+ # Calls the method defined by the current state of the machine
193
+ context.class_eval <<-end_eval, __FILE__, __LINE__ + 1
194
+ def #{method}(*args, &block)
195
+ self.class.state_machine(#{machine_name.inspect}).states.match!(self).call(self, #{method.inspect}, lambda {super}, *args, &block)
196
+ end
197
+ end_eval
198
+ end
199
+
200
+ # Include the context so that it can be bound to the owner class (the
201
+ # context is considered an ancestor, so it's allowed to be bound)
202
+ owner_class.class_eval { include context }
203
+
204
+ context
205
+ end
206
+
207
+ # Calls a method defined in this state's context on the given object. All
208
+ # arguments and any block will be passed into the method defined.
209
+ #
210
+ # If the method has never been defined for this state, then a NoMethodError
211
+ # will be raised.
212
+ def call(object, method, method_missing = nil, *args, &block)
213
+ if context_method = methods[method.to_sym]
214
+ # Method is defined by the state: proxy it through
215
+ context_method.bind(object).call(*args, &block)
216
+ else
217
+ # Dispatch to the superclass since this state doesn't handle the method
218
+ method_missing.call if method_missing
219
+ end
220
+ end
221
+
222
+ # Draws a representation of this state on the given machine. This will
223
+ # create a new node on the graph with the following properties:
224
+ # * +label+ - The human-friendly description of the state.
225
+ # * +width+ - The width of the node. Always 1.
226
+ # * +height+ - The height of the node. Always 1.
227
+ # * +shape+ - The actual shape of the node. If the state is a final
228
+ # state, then "doublecircle", otherwise "ellipse".
229
+ #
230
+ # The actual node generated on the graph will be returned.
231
+ def draw(graph)
232
+ node = graph.add_node(name ? name.to_s : 'nil',
233
+ :label => description,
234
+ :width => '1',
235
+ :height => '1',
236
+ :shape => final? ? 'doublecircle' : 'ellipse'
237
+ )
238
+
239
+ # Add open arrow for initial state
240
+ graph.add_edge(graph.add_node('starting_state', :shape => 'point'), node) if initial?
241
+
242
+ node
243
+ end
244
+
245
+ # Generates a nicely formatted description of this state's contents.
246
+ #
247
+ # For example,
248
+ #
249
+ # state = StateMachine::State.new(machine, :parked, :value => 1, :initial => true)
250
+ # state # => #<StateMachine::State name=:parked value=1 initial=true context=[]>
251
+ def inspect
252
+ attributes = [[:name, name], [:value, @value], [:initial, initial?], [:context, methods.keys]]
253
+ "#<#{self.class} #{attributes.map {|attr, value| "#{attr}=#{value.inspect}"} * ' '}>"
254
+ end
255
+
256
+ private
257
+ # Should the value be cached after it's evaluated for the first time?
258
+ def cache_value?
259
+ @cache
260
+ end
261
+
262
+ # Adds a predicate method to the owner class so long as a name has
263
+ # actually been configured for the state
264
+ def add_predicate
265
+ # Checks whether the current value matches this state
266
+ machine.define_helper(:instance, "#{qualified_name}?") do |machine, object|
267
+ machine.states.matches?(object, name)
268
+ end
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,112 @@
1
+ require 'state_machine/node_collection'
2
+
3
+ module StateMachine
4
+ # Represents a collection of states in a state machine
5
+ class StateCollection < NodeCollection
6
+ def initialize(machine) #:nodoc:
7
+ super(machine, :index => [:name, :qualified_name, :value])
8
+ end
9
+
10
+ # Determines whether the given object is in a specific state. If the
11
+ # object's current value doesn't match the state, then this will return
12
+ # false, otherwise true. If the given state is unknown, then an IndexError
13
+ # will be raised.
14
+ #
15
+ # == Examples
16
+ #
17
+ # class Vehicle
18
+ # state_machine :initial => :parked do
19
+ # other_states :idling
20
+ # end
21
+ # end
22
+ #
23
+ # states = Vehicle.state_machine.states
24
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
25
+ #
26
+ # states.matches?(vehicle, :parked) # => true
27
+ # states.matches?(vehicle, :idling) # => false
28
+ # states.matches?(vehicle, :invalid) # => IndexError: :invalid is an invalid key for :name index
29
+ def matches?(object, name)
30
+ fetch(name).matches?(machine.read(object, :state))
31
+ end
32
+
33
+ # Determines the current state of the given object as configured by this
34
+ # state machine. This will attempt to find a known state that matches
35
+ # the value of the attribute on the object.
36
+ #
37
+ # == Examples
38
+ #
39
+ # class Vehicle
40
+ # state_machine :initial => :parked do
41
+ # other_states :idling
42
+ # end
43
+ # end
44
+ #
45
+ # states = Vehicle.state_machine.states
46
+ #
47
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
48
+ # states.match(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
49
+ #
50
+ # vehicle.state = 'idling'
51
+ # states.match(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=true>
52
+ #
53
+ # vehicle.state = 'invalid'
54
+ # states.match(vehicle) # => nil
55
+ def match(object)
56
+ value = machine.read(object, :state)
57
+ self[value, :value] || detect {|state| state.matches?(value)}
58
+ end
59
+
60
+ # Determines the current state of the given object as configured by this
61
+ # state machine. If no state is found, then an ArgumentError will be
62
+ # raised.
63
+ #
64
+ # == Examples
65
+ #
66
+ # class Vehicle
67
+ # state_machine :initial => :parked do
68
+ # other_states :idling
69
+ # end
70
+ # end
71
+ #
72
+ # states = Vehicle.state_machine.states
73
+ #
74
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
75
+ # states.match!(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
76
+ #
77
+ # vehicle.state = 'invalid'
78
+ # states.match!(vehicle) # => ArgumentError: "invalid" is not a known state value
79
+ def match!(object)
80
+ match(object) || raise(ArgumentError, "#{machine.read(object, :state).inspect} is not a known #{machine.name} value")
81
+ end
82
+
83
+ # Gets the order in which states should be displayed based on where they
84
+ # were first referenced. This will order states in the following priority:
85
+ #
86
+ # 1. Initial state
87
+ # 2. Event transitions (:from, :except_from, :to, :except_to options)
88
+ # 3. States with behaviors
89
+ # 4. States referenced via +state+ or +other_states+
90
+ # 5. States referenced in callbacks
91
+ #
92
+ # This order will determine how the GraphViz visualizations are rendered.
93
+ def by_priority
94
+ order = select {|state| state.initial}.map {|state| state.name}
95
+
96
+ machine.events.each {|event| order += event.known_states}
97
+ order += select {|state| state.methods.any?}.map {|state| state.name}
98
+ order += keys(:name) - machine.callbacks.values.flatten.map {|callback| callback.known_states}.flatten
99
+ order += keys(:name)
100
+
101
+ order.uniq!
102
+ order.map! {|name| self[name]}
103
+ order
104
+ end
105
+
106
+ private
107
+ # Gets the value for the given attribute on the node
108
+ def value(node, attribute)
109
+ attribute == :value ? node.value(false) : super
110
+ end
111
+ end
112
+ end