hsume2-state_machine 1.0.1

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