spree-state_machine 2.0.0.beta1

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 (140) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.travis.yml +12 -0
  4. data/.yardopts +5 -0
  5. data/CHANGELOG.md +502 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE +20 -0
  8. data/README.md +1246 -0
  9. data/Rakefile +20 -0
  10. data/examples/AutoShop_state.png +0 -0
  11. data/examples/Car_state.png +0 -0
  12. data/examples/Gemfile +5 -0
  13. data/examples/Gemfile.lock +14 -0
  14. data/examples/TrafficLight_state.png +0 -0
  15. data/examples/Vehicle_state.png +0 -0
  16. data/examples/auto_shop.rb +13 -0
  17. data/examples/car.rb +21 -0
  18. data/examples/doc/AutoShop.html +2856 -0
  19. data/examples/doc/AutoShop_state.png +0 -0
  20. data/examples/doc/Car.html +919 -0
  21. data/examples/doc/Car_state.png +0 -0
  22. data/examples/doc/TrafficLight.html +2230 -0
  23. data/examples/doc/TrafficLight_state.png +0 -0
  24. data/examples/doc/Vehicle.html +7921 -0
  25. data/examples/doc/Vehicle_state.png +0 -0
  26. data/examples/doc/_index.html +136 -0
  27. data/examples/doc/class_list.html +47 -0
  28. data/examples/doc/css/common.css +1 -0
  29. data/examples/doc/css/full_list.css +55 -0
  30. data/examples/doc/css/style.css +322 -0
  31. data/examples/doc/file_list.html +46 -0
  32. data/examples/doc/frames.html +13 -0
  33. data/examples/doc/index.html +136 -0
  34. data/examples/doc/js/app.js +205 -0
  35. data/examples/doc/js/full_list.js +173 -0
  36. data/examples/doc/js/jquery.js +16 -0
  37. data/examples/doc/method_list.html +734 -0
  38. data/examples/doc/top-level-namespace.html +105 -0
  39. data/examples/merb-rest/controller.rb +51 -0
  40. data/examples/merb-rest/model.rb +28 -0
  41. data/examples/merb-rest/view_edit.html.erb +24 -0
  42. data/examples/merb-rest/view_index.html.erb +23 -0
  43. data/examples/merb-rest/view_new.html.erb +13 -0
  44. data/examples/merb-rest/view_show.html.erb +17 -0
  45. data/examples/rails-rest/controller.rb +43 -0
  46. data/examples/rails-rest/migration.rb +7 -0
  47. data/examples/rails-rest/model.rb +23 -0
  48. data/examples/rails-rest/view__form.html.erb +34 -0
  49. data/examples/rails-rest/view_edit.html.erb +6 -0
  50. data/examples/rails-rest/view_index.html.erb +25 -0
  51. data/examples/rails-rest/view_new.html.erb +5 -0
  52. data/examples/rails-rest/view_show.html.erb +19 -0
  53. data/examples/traffic_light.rb +9 -0
  54. data/examples/vehicle.rb +33 -0
  55. data/lib/state_machine/assertions.rb +36 -0
  56. data/lib/state_machine/branch.rb +225 -0
  57. data/lib/state_machine/callback.rb +236 -0
  58. data/lib/state_machine/core.rb +7 -0
  59. data/lib/state_machine/core_ext/class/state_machine.rb +5 -0
  60. data/lib/state_machine/core_ext.rb +2 -0
  61. data/lib/state_machine/error.rb +13 -0
  62. data/lib/state_machine/eval_helpers.rb +87 -0
  63. data/lib/state_machine/event.rb +257 -0
  64. data/lib/state_machine/event_collection.rb +141 -0
  65. data/lib/state_machine/extensions.rb +149 -0
  66. data/lib/state_machine/graph.rb +92 -0
  67. data/lib/state_machine/helper_module.rb +17 -0
  68. data/lib/state_machine/initializers/rails.rb +25 -0
  69. data/lib/state_machine/initializers.rb +4 -0
  70. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  71. data/lib/state_machine/integrations/active_model/observer.rb +33 -0
  72. data/lib/state_machine/integrations/active_model/observer_update.rb +42 -0
  73. data/lib/state_machine/integrations/active_model/versions.rb +31 -0
  74. data/lib/state_machine/integrations/active_model.rb +585 -0
  75. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  76. data/lib/state_machine/integrations/active_record/versions.rb +123 -0
  77. data/lib/state_machine/integrations/active_record.rb +525 -0
  78. data/lib/state_machine/integrations/base.rb +100 -0
  79. data/lib/state_machine/integrations.rb +121 -0
  80. data/lib/state_machine/machine.rb +2287 -0
  81. data/lib/state_machine/machine_collection.rb +74 -0
  82. data/lib/state_machine/macro_methods.rb +522 -0
  83. data/lib/state_machine/matcher.rb +123 -0
  84. data/lib/state_machine/matcher_helpers.rb +54 -0
  85. data/lib/state_machine/node_collection.rb +222 -0
  86. data/lib/state_machine/path.rb +120 -0
  87. data/lib/state_machine/path_collection.rb +90 -0
  88. data/lib/state_machine/state.rb +297 -0
  89. data/lib/state_machine/state_collection.rb +112 -0
  90. data/lib/state_machine/state_context.rb +138 -0
  91. data/lib/state_machine/transition.rb +470 -0
  92. data/lib/state_machine/transition_collection.rb +245 -0
  93. data/lib/state_machine/version.rb +3 -0
  94. data/lib/state_machine/yard/handlers/base.rb +32 -0
  95. data/lib/state_machine/yard/handlers/event.rb +25 -0
  96. data/lib/state_machine/yard/handlers/machine.rb +344 -0
  97. data/lib/state_machine/yard/handlers/state.rb +25 -0
  98. data/lib/state_machine/yard/handlers/transition.rb +47 -0
  99. data/lib/state_machine/yard/handlers.rb +12 -0
  100. data/lib/state_machine/yard/templates/default/class/html/setup.rb +30 -0
  101. data/lib/state_machine/yard/templates/default/class/html/state_machines.erb +12 -0
  102. data/lib/state_machine/yard/templates.rb +3 -0
  103. data/lib/state_machine/yard.rb +8 -0
  104. data/lib/state_machine.rb +8 -0
  105. data/lib/yard-state_machine.rb +2 -0
  106. data/state_machine.gemspec +22 -0
  107. data/test/files/en.yml +17 -0
  108. data/test/files/switch.rb +15 -0
  109. data/test/functional/state_machine_test.rb +1066 -0
  110. data/test/test_helper.rb +7 -0
  111. data/test/unit/assertions_test.rb +40 -0
  112. data/test/unit/branch_test.rb +969 -0
  113. data/test/unit/callback_test.rb +704 -0
  114. data/test/unit/error_test.rb +43 -0
  115. data/test/unit/eval_helpers_test.rb +270 -0
  116. data/test/unit/event_collection_test.rb +398 -0
  117. data/test/unit/event_test.rb +1196 -0
  118. data/test/unit/graph_test.rb +98 -0
  119. data/test/unit/helper_module_test.rb +17 -0
  120. data/test/unit/integrations/active_model_test.rb +1245 -0
  121. data/test/unit/integrations/active_record_test.rb +2551 -0
  122. data/test/unit/integrations/base_test.rb +104 -0
  123. data/test/unit/integrations_test.rb +71 -0
  124. data/test/unit/invalid_event_test.rb +20 -0
  125. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  126. data/test/unit/invalid_transition_test.rb +115 -0
  127. data/test/unit/machine_collection_test.rb +603 -0
  128. data/test/unit/machine_test.rb +3395 -0
  129. data/test/unit/matcher_helpers_test.rb +37 -0
  130. data/test/unit/matcher_test.rb +155 -0
  131. data/test/unit/node_collection_test.rb +362 -0
  132. data/test/unit/path_collection_test.rb +266 -0
  133. data/test/unit/path_test.rb +485 -0
  134. data/test/unit/state_collection_test.rb +352 -0
  135. data/test/unit/state_context_test.rb +441 -0
  136. data/test/unit/state_machine_test.rb +31 -0
  137. data/test/unit/state_test.rb +1101 -0
  138. data/test/unit/transition_collection_test.rb +2168 -0
  139. data/test/unit/transition_test.rb +1558 -0
  140. metadata +264 -0
@@ -0,0 +1,297 @@
1
+ require 'state_machine/assertions'
2
+ require 'state_machine/state_context'
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
+ # Creates a new state within the context of the given machine.
45
+ #
46
+ # Configuration options:
47
+ # * <tt>:initial</tt> - Whether this state is the beginning state for the
48
+ # machine. Default is false.
49
+ # * <tt>:value</tt> - The value to store when an object transitions to this
50
+ # state. Default is the name (stringified).
51
+ # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
52
+ # then setting this to true will cache the evaluated result
53
+ # * <tt>:if</tt> - Determines whether a value matches this state
54
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
55
+ # By default, the configured value is matched.
56
+ # * <tt>:human_name</tt> - The human-readable version of this state's name
57
+ def initialize(machine, name, options = {}) #:nodoc:
58
+ assert_valid_keys(options, :initial, :value, :cache, :if, :human_name)
59
+
60
+ @machine = machine
61
+ @name = name
62
+ @qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name
63
+ @human_name = options[:human_name] || (@name ? @name.to_s.tr('_', ' ') : 'nil')
64
+ @value = options.include?(:value) ? options[:value] : name && name.to_s
65
+ @cache = options[:cache]
66
+ @matcher = options[:if]
67
+ @initial = options[:initial] == true
68
+ @context = StateContext.new(self)
69
+
70
+ if name
71
+ conflicting_machines = machine.owner_class.state_machines.select {|other_name, other_machine| other_machine != machine && other_machine.states[qualified_name, :qualified_name]}
72
+
73
+ # Output a warning if another machine has a conflicting qualified name
74
+ # for a different attribute
75
+ if conflict = conflicting_machines.detect {|other_name, other_machine| other_machine.attribute != machine.attribute}
76
+ name, other_machine = conflict
77
+ warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
78
+ elsif conflicting_machines.empty?
79
+ # Only bother adding predicates when another machine for the same
80
+ # attribute hasn't already done so
81
+ add_predicate
82
+ end
83
+ end
84
+ end
85
+
86
+ # Creates a copy of this state, excluding the context to prevent conflicts
87
+ # across different machines.
88
+ def initialize_copy(orig) #:nodoc:
89
+ super
90
+ @context = StateContext.new(self)
91
+ end
92
+
93
+ # Determines whether there are any states that can be transitioned to from
94
+ # this state. If there are none, then this state is considered *final*.
95
+ # Any objects in a final state will remain so forever given the current
96
+ # machine's definition.
97
+ def final?
98
+ !machine.events.any? do |event|
99
+ event.branches.any? do |branch|
100
+ branch.state_requirements.any? do |requirement|
101
+ requirement[:from].matches?(name) && !requirement[:to].matches?(name, :from => name)
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ # Transforms the state name into a more human-readable format, such as
108
+ # "first gear" instead of "first_gear"
109
+ def human_name(klass = @machine.owner_class)
110
+ @human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name
111
+ end
112
+
113
+ # Generates a human-readable description of this state's name / value:
114
+ #
115
+ # For example,
116
+ #
117
+ # State.new(machine, :parked).description # => "parked"
118
+ # State.new(machine, :parked, :value => :parked).description # => "parked"
119
+ # State.new(machine, :parked, :value => nil).description # => "parked (nil)"
120
+ # State.new(machine, :parked, :value => 1).description # => "parked (1)"
121
+ # State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
122
+ #
123
+ # Configuration options:
124
+ # * <tt>:human_name</tt> - Whether to use this state's human name in the
125
+ # description or just the internal name
126
+ def description(options = {})
127
+ label = options[:human_name] ? human_name : name
128
+ description = label ? label.to_s : label.inspect
129
+ description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
130
+ description
131
+ end
132
+
133
+ # The value that represents this state. This will optionally evaluate the
134
+ # original block if it's a lambda block. Otherwise, the static value is
135
+ # returned.
136
+ #
137
+ # For example,
138
+ #
139
+ # State.new(machine, :parked, :value => 1).value # => 1
140
+ # State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
141
+ # State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => <Proc:0xb6ea7ca0@...>
142
+ def value(eval = true)
143
+ if @value.is_a?(Proc) && eval
144
+ if cache_value?
145
+ @value = @value.call
146
+ machine.states.update(self)
147
+ @value
148
+ else
149
+ @value.call
150
+ end
151
+ else
152
+ @value
153
+ end
154
+ end
155
+
156
+ # Determines whether this state matches the given value. If no matcher is
157
+ # configured, then this will check whether the values are equivalent.
158
+ # Otherwise, the matcher will determine the result.
159
+ #
160
+ # For example,
161
+ #
162
+ # # Without a matcher
163
+ # state = State.new(machine, :parked, :value => 1)
164
+ # state.matches?(1) # => true
165
+ # state.matches?(2) # => false
166
+ #
167
+ # # With a matcher
168
+ # state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
169
+ # state.matches?(nil) # => false
170
+ # state.matches?(Time.now) # => true
171
+ def matches?(other_value)
172
+ matcher ? matcher.call(other_value) : other_value == value
173
+ end
174
+
175
+ # Defines a context for the state which will be enabled on instances of
176
+ # the owner class when the machine is in this state.
177
+ #
178
+ # This can be called multiple times. Each time a new context is created,
179
+ # a new module will be included in the owner class.
180
+ def context(&block)
181
+ # Include the context
182
+ context = @context
183
+ machine.owner_class.class_eval { include context }
184
+
185
+ # Evaluate the method definitions and track which ones were added
186
+ old_methods = context_methods
187
+ context.class_eval(&block)
188
+ new_methods = context_methods.to_a.select {|(name, method)| old_methods[name] != method}
189
+
190
+ # Alias new methods so that the only execute when the object is in this state
191
+ new_methods.each do |(method_name, method)|
192
+ context_name = context_name_for(method_name)
193
+ context.class_eval <<-end_eval, __FILE__, __LINE__ + 1
194
+ alias_method :"#{context_name}", :#{method_name}
195
+ def #{method_name}(*args, &block)
196
+ state = self.class.state_machine(#{machine.name.inspect}).states.fetch(#{name.inspect})
197
+ options = {:method_missing => lambda {super(*args, &block)}, :method_name => #{method_name.inspect}}
198
+ state.call(self, :"#{context_name}", *(args + [options]), &block)
199
+ end
200
+ end_eval
201
+ end
202
+
203
+ true
204
+ end
205
+
206
+ # The list of methods that have been defined in this state's context
207
+ def context_methods
208
+ @context.instance_methods.inject({}) do |methods, name|
209
+ methods.merge(name.to_sym => @context.instance_method(name))
210
+ end
211
+ end
212
+
213
+ # Calls a method defined in this state's context on the given object. All
214
+ # arguments and any block will be passed into the method defined.
215
+ #
216
+ # If the method has never been defined for this state, then a NoMethodError
217
+ # will be raised.
218
+ def call(object, method, *args, &block)
219
+ options = args.last.is_a?(Hash) ? args.pop : {}
220
+ options = {:method_name => method}.merge(options)
221
+ state = machine.states.match!(object)
222
+
223
+ if state == self && object.respond_to?(method)
224
+ object.send(method, *args, &block)
225
+ elsif method_missing = options[:method_missing]
226
+ # Dispatch to the superclass since the object either isn't in this state
227
+ # or this state doesn't handle the method
228
+ begin
229
+ method_missing.call
230
+ rescue NoMethodError => ex
231
+ if ex.name.to_s == options[:method_name].to_s && ex.args == args
232
+ # No valid context for this method
233
+ raise InvalidContext.new(object, "State #{state.name.inspect} for #{machine.name.inspect} is not a valid context for calling ##{options[:method_name]}")
234
+ else
235
+ raise
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ # Draws a representation of this state on the given machine. This will
242
+ # create a new node on the graph with the following properties:
243
+ # * +label+ - The human-friendly description of the state.
244
+ # * +width+ - The width of the node. Always 1.
245
+ # * +height+ - The height of the node. Always 1.
246
+ # * +shape+ - The actual shape of the node. If the state is a final
247
+ # state, then "doublecircle", otherwise "ellipse".
248
+ #
249
+ # Configuration options:
250
+ # * <tt>:human_name</tt> - Whether to use the state's human name for the
251
+ # node's label that gets drawn on the graph
252
+ def draw(graph, options = {})
253
+ node = graph.add_nodes(name ? name.to_s : 'nil',
254
+ :label => description(options),
255
+ :width => '1',
256
+ :height => '1',
257
+ :shape => final? ? 'doublecircle' : 'ellipse'
258
+ )
259
+
260
+ # Add open arrow for initial state
261
+ graph.add_edges(graph.add_nodes('starting_state', :shape => 'point'), node) if initial?
262
+
263
+ true
264
+ end
265
+
266
+ # Generates a nicely formatted description of this state's contents.
267
+ #
268
+ # For example,
269
+ #
270
+ # state = StateMachine::State.new(machine, :parked, :value => 1, :initial => true)
271
+ # state # => #<StateMachine::State name=:parked value=1 initial=true context=[]>
272
+ def inspect
273
+ attributes = [[:name, name], [:value, @value], [:initial, initial?]]
274
+ "#<#{self.class} #{attributes.map {|attr, value| "#{attr}=#{value.inspect}"} * ' '}>"
275
+ end
276
+
277
+ private
278
+ # Should the value be cached after it's evaluated for the first time?
279
+ def cache_value?
280
+ @cache
281
+ end
282
+
283
+ # Adds a predicate method to the owner class so long as a name has
284
+ # actually been configured for the state
285
+ def add_predicate
286
+ # Checks whether the current value matches this state
287
+ machine.define_helper(:instance, "#{qualified_name}?") do |machine, object|
288
+ machine.states.matches?(object, name)
289
+ end
290
+ end
291
+
292
+ # Generates the name of the method containing the actual implementation
293
+ def context_name_for(method)
294
+ :"__#{machine.name}_#{name}_#{method}_#{@context.object_id}__"
295
+ end
296
+ end
297
+ 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.context_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
@@ -0,0 +1,138 @@
1
+ require 'state_machine/assertions'
2
+ require 'state_machine/eval_helpers'
3
+
4
+ module StateMachine
5
+ # A method was called in an invalid state context
6
+ class InvalidContext < Error
7
+ end
8
+
9
+ # Represents a module which will get evaluated within the context of a state.
10
+ #
11
+ # Class-level methods are proxied to the owner class, injecting a custom
12
+ # <tt>:if</tt> condition along with method. This assumes that the method has
13
+ # support for a set of configuration options, including <tt>:if</tt>. This
14
+ # condition will check that the object's state matches this context's state.
15
+ #
16
+ # Instance-level methods are used to define state-driven behavior on the
17
+ # state's owner class.
18
+ #
19
+ # == Examples
20
+ #
21
+ # class Vehicle
22
+ # class << self
23
+ # attr_accessor :validations
24
+ #
25
+ # def validate(options, &block)
26
+ # validations << options
27
+ # end
28
+ # end
29
+ #
30
+ # self.validations = []
31
+ # attr_accessor :state, :simulate
32
+ #
33
+ # def moving?
34
+ # self.class.validations.all? {|validation| validation[:if].call(self)}
35
+ # end
36
+ # end
37
+ #
38
+ # In the above class, a simple set of validation behaviors have been defined.
39
+ # Each validation consists of a configuration like so:
40
+ #
41
+ # Vehicle.validate :unless => :simulate
42
+ # Vehicle.validate :if => lambda {|vehicle| ...}
43
+ #
44
+ # In order to scope validations to a particular state context, the class-level
45
+ # +validate+ method can be invoked like so:
46
+ #
47
+ # machine = StateMachine::Machine.new(Vehicle)
48
+ # context = StateMachine::StateContext.new(machine.state(:first_gear))
49
+ # context.validate(:unless => :simulate)
50
+ #
51
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7ce491c @simulate=nil, @state=nil>
52
+ # vehicle.moving? # => false
53
+ #
54
+ # vehicle.state = 'first_gear'
55
+ # vehicle.moving? # => true
56
+ #
57
+ # vehicle.simulate = true
58
+ # vehicle.moving? # => false
59
+ class StateContext < Module
60
+ include Assertions
61
+ include EvalHelpers
62
+
63
+ # The state machine for which this context's state is defined
64
+ attr_reader :machine
65
+
66
+ # The state that must be present in an object for this context to be active
67
+ attr_reader :state
68
+
69
+ # Creates a new context for the given state
70
+ def initialize(state)
71
+ @state = state
72
+ @machine = state.machine
73
+
74
+ state_name = state.name
75
+ machine_name = machine.name
76
+ @condition = lambda {|object| object.class.state_machine(machine_name).states.matches?(object, state_name)}
77
+ end
78
+
79
+ # Creates a new transition that determines what to change the current state
80
+ # to when an event fires from this state.
81
+ #
82
+ # Since this transition is being defined within a state context, you do
83
+ # *not* need to specify the <tt>:from</tt> option for the transition. For
84
+ # example:
85
+ #
86
+ # state_machine do
87
+ # state :parked do
88
+ # transition :to => :idling, :on => [:ignite, :shift_up] # Transitions to :idling
89
+ # transition :from => [:idling, :parked], :on => :park, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off
90
+ # end
91
+ # end
92
+ #
93
+ # See StateMachine::Machine#transition for a description of the possible
94
+ # configurations for defining transitions.
95
+ def transition(options)
96
+ assert_valid_keys(options, :from, :to, :on, :if, :unless)
97
+ raise ArgumentError, 'Must specify :on event' unless options[:on]
98
+ raise ArgumentError, 'Must specify either :to or :from state' unless !options[:to] ^ !options[:from]
99
+
100
+ machine.transition(options.merge(options[:to] ? {:from => state.name} : {:to => state.name}))
101
+ end
102
+
103
+ # Hooks in condition-merging to methods that don't exist in this module
104
+ def method_missing(*args, &block)
105
+ # Get the configuration
106
+ if args.last.is_a?(Hash)
107
+ options = args.last
108
+ else
109
+ args << options = {}
110
+ end
111
+
112
+ # Get any existing condition that may need to be merged
113
+ if_condition = options.delete(:if)
114
+ unless_condition = options.delete(:unless)
115
+
116
+ # Provide scope access to configuration in case the block is evaluated
117
+ # within the object instance
118
+ proxy = self
119
+ proxy_condition = @condition
120
+
121
+ # Replace the configuration condition with the one configured for this
122
+ # proxy, merging together any existing conditions
123
+ options[:if] = lambda do |*condition_args|
124
+ # Block may be executed within the context of the actual object, so
125
+ # it'll either be the first argument or the executing context
126
+ object = condition_args.first || self
127
+
128
+ proxy.evaluate_method(object, proxy_condition) &&
129
+ Array(if_condition).all? {|condition| proxy.evaluate_method(object, condition)} &&
130
+ !Array(unless_condition).any? {|condition| proxy.evaluate_method(object, condition)}
131
+ end
132
+
133
+ # Evaluate the method on the owner class with the condition proxied
134
+ # through
135
+ machine.owner_class.send(*args, &block)
136
+ end
137
+ end
138
+ end