enum_state_machine 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +12 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. metadata +83 -130
  6. data/.rvmrc +0 -1
  7. data/enum_state_machine.gemspec +0 -25
  8. data/lib/enum_state_machine.rb +0 -9
  9. data/lib/enum_state_machine/assertions.rb +0 -36
  10. data/lib/enum_state_machine/branch.rb +0 -225
  11. data/lib/enum_state_machine/callback.rb +0 -232
  12. data/lib/enum_state_machine/core.rb +0 -12
  13. data/lib/enum_state_machine/core_ext.rb +0 -2
  14. data/lib/enum_state_machine/core_ext/class/state_machine.rb +0 -5
  15. data/lib/enum_state_machine/error.rb +0 -13
  16. data/lib/enum_state_machine/eval_helpers.rb +0 -87
  17. data/lib/enum_state_machine/event.rb +0 -257
  18. data/lib/enum_state_machine/event_collection.rb +0 -141
  19. data/lib/enum_state_machine/extensions.rb +0 -149
  20. data/lib/enum_state_machine/graph.rb +0 -92
  21. data/lib/enum_state_machine/helper_module.rb +0 -17
  22. data/lib/enum_state_machine/initializers.rb +0 -4
  23. data/lib/enum_state_machine/initializers/rails.rb +0 -22
  24. data/lib/enum_state_machine/integrations.rb +0 -97
  25. data/lib/enum_state_machine/integrations/active_model.rb +0 -585
  26. data/lib/enum_state_machine/integrations/active_model/locale.rb +0 -11
  27. data/lib/enum_state_machine/integrations/active_model/observer.rb +0 -33
  28. data/lib/enum_state_machine/integrations/active_model/observer_update.rb +0 -42
  29. data/lib/enum_state_machine/integrations/active_model/versions.rb +0 -31
  30. data/lib/enum_state_machine/integrations/active_record.rb +0 -548
  31. data/lib/enum_state_machine/integrations/active_record/locale.rb +0 -20
  32. data/lib/enum_state_machine/integrations/active_record/versions.rb +0 -123
  33. data/lib/enum_state_machine/integrations/base.rb +0 -100
  34. data/lib/enum_state_machine/machine.rb +0 -2292
  35. data/lib/enum_state_machine/machine_collection.rb +0 -86
  36. data/lib/enum_state_machine/macro_methods.rb +0 -518
  37. data/lib/enum_state_machine/matcher.rb +0 -123
  38. data/lib/enum_state_machine/matcher_helpers.rb +0 -54
  39. data/lib/enum_state_machine/node_collection.rb +0 -222
  40. data/lib/enum_state_machine/path.rb +0 -120
  41. data/lib/enum_state_machine/path_collection.rb +0 -90
  42. data/lib/enum_state_machine/state.rb +0 -297
  43. data/lib/enum_state_machine/state_collection.rb +0 -112
  44. data/lib/enum_state_machine/state_context.rb +0 -138
  45. data/lib/enum_state_machine/state_enum.rb +0 -23
  46. data/lib/enum_state_machine/transition.rb +0 -470
  47. data/lib/enum_state_machine/transition_collection.rb +0 -245
  48. data/lib/enum_state_machine/version.rb +0 -3
  49. data/lib/enum_state_machine/yard.rb +0 -8
  50. data/lib/enum_state_machine/yard/handlers.rb +0 -12
  51. data/lib/enum_state_machine/yard/handlers/base.rb +0 -32
  52. data/lib/enum_state_machine/yard/handlers/event.rb +0 -25
  53. data/lib/enum_state_machine/yard/handlers/machine.rb +0 -344
  54. data/lib/enum_state_machine/yard/handlers/state.rb +0 -25
  55. data/lib/enum_state_machine/yard/handlers/transition.rb +0 -47
  56. data/lib/enum_state_machine/yard/templates.rb +0 -3
  57. data/lib/enum_state_machine/yard/templates/default/class/html/setup.rb +0 -30
  58. data/lib/enum_state_machine/yard/templates/default/class/html/state_machines.erb +0 -12
  59. data/lib/tasks/enum_state_machine.rake +0 -1
  60. data/lib/tasks/enum_state_machine.rb +0 -24
  61. data/lib/yard-enum_state_machine.rb +0 -2
  62. data/test/functional/state_machine_test.rb +0 -1066
  63. data/test/unit/integrations/active_model_test.rb +0 -1245
  64. data/test/unit/integrations/active_record_test.rb +0 -2551
  65. data/test/unit/integrations/base_test.rb +0 -104
  66. data/test/unit/integrations_test.rb +0 -71
  67. data/test/unit/invalid_event_test.rb +0 -20
  68. data/test/unit/invalid_parallel_transition_test.rb +0 -18
  69. data/test/unit/invalid_transition_test.rb +0 -115
  70. data/test/unit/machine_collection_test.rb +0 -603
  71. data/test/unit/machine_test.rb +0 -3395
  72. data/test/unit/state_machine_test.rb +0 -31
@@ -1,20 +0,0 @@
1
- filename = "#{File.dirname(__FILE__)}/../active_model/locale.rb"
2
- translations = eval(IO.read(File.expand_path(filename)), binding, filename)
3
- translations[:en][:activerecord] = translations[:en].delete(:activemodel)
4
-
5
- # Only ActiveRecord 2.3.5+ can pull i18n >= 0.1.3 from system-wide gems (and
6
- # therefore possibly have I18n::VERSION available)
7
- begin
8
- require 'i18n/version'
9
- rescue Exception => ex
10
- end unless ::ActiveRecord::VERSION::MAJOR == 2 && (::ActiveRecord::VERSION::MINOR < 3 || ::ActiveRecord::VERSION::TINY < 5)
11
-
12
- # Only i18n 0.4.0+ has the new %{key} syntax
13
- if !defined?(I18n::VERSION) || I18n::VERSION < '0.4.0'
14
- translations[:en][:activerecord][:errors][:messages].each do |key, message|
15
- message.gsub!('%{', '{{')
16
- message.gsub!('}', '}}')
17
- end
18
- end
19
-
20
- translations
@@ -1,123 +0,0 @@
1
- module EnumStateMachine
2
- module Integrations #:nodoc:
3
- module ActiveRecord
4
- version '2.x - 3.0.x' do
5
- def self.active?
6
- ::ActiveRecord::VERSION::MAJOR == 2 || ::ActiveRecord::VERSION::MAJOR == 3 && ::ActiveRecord::VERSION::MINOR == 0
7
- end
8
-
9
- def define_static_state_initializer
10
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
11
- def attributes_from_column_definition(*)
12
- result = super
13
- self.class.state_machines.initialize_states(self, :static => :force, :dynamic => false, :to => result)
14
- result
15
- end
16
- end_eval
17
- end
18
- end
19
-
20
- version '2.x' do
21
- def self.active?
22
- ::ActiveRecord::VERSION::MAJOR == 2
23
- end
24
-
25
- def load_locale
26
- super if defined?(I18n)
27
- end
28
-
29
- def create_scope(name, scope)
30
- if owner_class.respond_to?(:named_scope)
31
- name = name.to_sym
32
- machine_name = self.name
33
-
34
- # Since ActiveRecord does not allow direct access to the model
35
- # being used within the evaluation of a dynamic named scope, the
36
- # scope must be generated manually. It's necessary to have access
37
- # to the model so that the state names can be translated to their
38
- # associated values and so that inheritance is respected properly.
39
- owner_class.named_scope(name)
40
- owner_class.scopes[name] = lambda do |model, *states|
41
- machine_states = model.state_machine(machine_name).states
42
- values = states.flatten.map {|state| machine_states.fetch(state).value}
43
-
44
- ::ActiveRecord::NamedScope::Scope.new(model, :conditions => scope.call(values))
45
- end
46
- end
47
-
48
- # Prevent the Machine class from wrapping the scope
49
- false
50
- end
51
-
52
- def invalidate(object, attribute, message, values = [])
53
- if defined?(I18n)
54
- super
55
- else
56
- object.errors.add(self.attribute(attribute), generate_message(message, values))
57
- end
58
- end
59
-
60
- def translate(klass, key, value)
61
- if defined?(I18n)
62
- super
63
- else
64
- value ? value.to_s.humanize.downcase : 'nil'
65
- end
66
- end
67
-
68
- def supports_observers?
69
- true
70
- end
71
-
72
- def supports_validations?
73
- true
74
- end
75
-
76
- def supports_mass_assignment_security?
77
- true
78
- end
79
-
80
- def i18n_scope(klass)
81
- :activerecord
82
- end
83
-
84
- def load_observer_extensions
85
- super
86
- ::ActiveRecord::Observer.class_eval do
87
- include EnumStateMachine::Integrations::ActiveModel::Observer
88
- end unless ::ActiveRecord::Observer < EnumStateMachine::Integrations::ActiveModel::Observer
89
- end
90
- end
91
-
92
- version '2.0 - 2.2.x' do
93
- def self.active?
94
- ::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR < 3
95
- end
96
-
97
- def default_error_message_options(object, attribute, message)
98
- {:default => @messages[message]}
99
- end
100
- end
101
-
102
- version '2.0 - 2.3.1' do
103
- def self.active?
104
- ::ActiveRecord::VERSION::MAJOR == 2 && (::ActiveRecord::VERSION::MINOR < 3 || ::ActiveRecord::VERSION::TINY < 2)
105
- end
106
-
107
- def ancestors_for(klass)
108
- klass.self_and_descendents_from_active_record
109
- end
110
- end
111
-
112
- version '2.3.2 - 2.3.x' do
113
- def self.active?
114
- ::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR == 3 && ::ActiveRecord::VERSION::TINY >= 2
115
- end
116
-
117
- def ancestors_for(klass)
118
- klass.self_and_descendants_from_active_record
119
- end
120
- end
121
- end
122
- end
123
- end
@@ -1,100 +0,0 @@
1
- module EnumStateMachine
2
- module Integrations
3
- # Provides a set of base helpers for managing individual integrations
4
- module Base
5
- module ClassMethods
6
- # The default options to use for state machines using this integration
7
- attr_reader :defaults
8
-
9
- # The name of the integration
10
- def integration_name
11
- @integration_name ||= begin
12
- name = self.name.split('::').last
13
- name.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
14
- name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
15
- name.downcase!
16
- name.to_sym
17
- end
18
- end
19
-
20
- # Whether this integration is available for the current library. This
21
- # is only true if the ORM that the integration is for is currently
22
- # defined.
23
- def available?
24
- matching_ancestors.any? && Object.const_defined?(matching_ancestors[0].split('::')[0])
25
- end
26
-
27
- # The list of ancestor names that cause this integration to matched.
28
- def matching_ancestors
29
- []
30
- end
31
-
32
- # Whether the integration should be used for the given class.
33
- def matches?(klass)
34
- matches_ancestors?(klass.ancestors.map {|ancestor| ancestor.name})
35
- end
36
-
37
- # Whether the integration should be used for the given list of ancestors.
38
- def matches_ancestors?(ancestors)
39
- (ancestors & matching_ancestors).any?
40
- end
41
-
42
- # Tracks the various version overrides for an integration
43
- def versions
44
- @versions ||= []
45
- end
46
-
47
- # Creates a new version override for an integration. When this
48
- # integration is activated, each version that is marked as active will
49
- # also extend the integration.
50
- #
51
- # == Example
52
- #
53
- # module EnumStateMachine
54
- # module Integrations
55
- # module ORMLibrary
56
- # version '0.2.x - 0.3.x' do
57
- # def self.active?
58
- # ::ORMLibrary::VERSION >= '0.2.0' && ::ORMLibrary::VERSION < '0.4.0'
59
- # end
60
- #
61
- # def invalidate(object, attribute, message, values = [])
62
- # # Override here...
63
- # end
64
- # end
65
- # end
66
- # end
67
- # end
68
- #
69
- # In the above example, a version override is defined for the ORMLibrary
70
- # integration when the version is between 0.2.x and 0.3.x.
71
- def version(name, &block)
72
- versions << mod = Module.new(&block)
73
- mod
74
- end
75
-
76
- # The path to the locale file containing translations for this
77
- # integration. This file will only exist for integrations that actually
78
- # support i18n.
79
- def locale_path
80
- path = "#{File.dirname(__FILE__)}/#{integration_name}/locale.rb"
81
- path if File.exists?(path)
82
- end
83
-
84
- # Extends the given object with any version overrides that are currently
85
- # active
86
- def extended(base)
87
- versions.each do |version|
88
- base.extend(version) if version.active?
89
- end
90
- end
91
- end
92
-
93
- extend ClassMethods
94
-
95
- def self.included(base) #:nodoc:
96
- base.class_eval { extend ClassMethods }
97
- end
98
- end
99
- end
100
- end
@@ -1,2292 +0,0 @@
1
- require 'enum_state_machine/extensions'
2
- require 'enum_state_machine/assertions'
3
- require 'enum_state_machine/integrations'
4
-
5
- require 'enum_state_machine/helper_module'
6
- require 'enum_state_machine/state'
7
- require 'enum_state_machine/event'
8
- require 'enum_state_machine/callback'
9
- require 'enum_state_machine/node_collection'
10
- require 'enum_state_machine/state_collection'
11
- require 'enum_state_machine/event_collection'
12
- require 'enum_state_machine/path_collection'
13
- require 'enum_state_machine/matcher_helpers'
14
-
15
- module EnumStateMachine
16
- # Represents a state machine for a particular attribute. State machines
17
- # consist of states, events and a set of transitions that define how the
18
- # state changes after a particular event is fired.
19
- #
20
- # A state machine will not know all of the possible states for an object
21
- # unless they are referenced *somewhere* in the state machine definition.
22
- # As a result, any unused states should be defined with the +other_states+
23
- # or +state+ helper.
24
- #
25
- # == Actions
26
- #
27
- # When an action is configured for a state machine, it is invoked when an
28
- # object transitions via an event. The success of the event becomes
29
- # dependent on the success of the action. If the action is successful, then
30
- # the transitioned state remains persisted. However, if the action fails
31
- # (by returning false), the transitioned state will be rolled back.
32
- #
33
- # For example,
34
- #
35
- # class Vehicle
36
- # attr_accessor :fail, :saving_state
37
- #
38
- # state_machine :initial => :parked, :action => :save do
39
- # event :ignite do
40
- # transition :parked => :idling
41
- # end
42
- #
43
- # event :park do
44
- # transition :idling => :parked
45
- # end
46
- # end
47
- #
48
- # def save
49
- # @saving_state = state
50
- # fail != true
51
- # end
52
- # end
53
- #
54
- # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
55
- # vehicle.save # => true
56
- # vehicle.saving_state # => "parked" # The state was "parked" was save was called
57
- #
58
- # # Successful event
59
- # vehicle.ignite # => true
60
- # vehicle.saving_state # => "idling" # The state was "idling" when save was called
61
- # vehicle.state # => "idling"
62
- #
63
- # # Failed event
64
- # vehicle.fail = true
65
- # vehicle.park # => false
66
- # vehicle.saving_state # => "parked"
67
- # vehicle.state # => "idling"
68
- #
69
- # As shown, even though the state is set prior to calling the +save+ action
70
- # on the object, it will be rolled back to the original state if the action
71
- # fails. *Note* that this will also be the case if an exception is raised
72
- # while calling the action.
73
- #
74
- # === Indirect transitions
75
- #
76
- # In addition to the action being run as the _result_ of an event, the action
77
- # can also be used to run events itself. For example, using the above as an
78
- # example:
79
- #
80
- # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
81
- #
82
- # vehicle.state_event = 'ignite'
83
- # vehicle.save # => true
84
- # vehicle.state # => "idling"
85
- # vehicle.state_event # => nil
86
- #
87
- # As can be seen, the +save+ action automatically invokes the event stored in
88
- # the +state_event+ attribute (<tt>:ignite</tt> in this case).
89
- #
90
- # One important note about using this technique for running transitions is
91
- # that if the class in which the state machine is defined *also* defines the
92
- # action being invoked (and not a superclass), then it must manually run the
93
- # EnumStateMachine hook that checks for event attributes.
94
- #
95
- # For example, in ActiveRecord
96
- # the default action (+save+) is already defined in a base class. As a result,
97
- # when a state machine is defined in a model / resource, EnumStateMachine can
98
- # automatically hook into the +save+ action.
99
- #
100
- # On the other hand, the Vehicle class from above defined its own +save+
101
- # method (and there is no +save+ method in its superclass). As a result, it
102
- # must be modified like so:
103
- #
104
- # def save
105
- # self.class.state_machines.transitions(self, :save).perform do
106
- # @saving_state = state
107
- # fail != true
108
- # end
109
- # end
110
- #
111
- # This will add in the functionality for firing the event stored in the
112
- # +state_event+ attribute.
113
- #
114
- # == Callbacks
115
- #
116
- # Callbacks are supported for hooking before and after every possible
117
- # transition in the machine. Each callback is invoked in the order in which
118
- # it was defined. See EnumStateMachine::Machine#before_transition and
119
- # EnumStateMachine::Machine#after_transition for documentation on how to define
120
- # new callbacks.
121
- #
122
- # *Note* that callbacks only get executed within the context of an event. As
123
- # a result, if a class has an initial state when it's created, any callbacks
124
- # that would normally get executed when the object enters that state will
125
- # *not* get triggered.
126
- #
127
- # For example,
128
- #
129
- # class Vehicle
130
- # state_machine :initial => :parked do
131
- # after_transition all => :parked do
132
- # raise ArgumentError
133
- # end
134
- # ...
135
- # end
136
- # end
137
- #
138
- # vehicle = Vehicle.new # => #<Vehicle id: 1, state: "parked">
139
- # vehicle.save # => true (no exception raised)
140
- #
141
- # If you need callbacks to get triggered when an object is created, this
142
- # should be done by one of the following techniques:
143
- # * Use a <tt>before :create</tt> or equivalent hook:
144
- #
145
- # class Vehicle
146
- # before :create, :track_initial_transition
147
- #
148
- # state_machine do
149
- # ...
150
- # end
151
- # end
152
- #
153
- # * Set an initial state and use the correct event to create the
154
- # object with the proper state, resulting in callbacks being triggered and
155
- # the object getting persisted (note that the <tt>:pending</tt> state is
156
- # actually stored as nil):
157
- #
158
- # class Vehicle
159
- # state_machine :initial => :pending
160
- # after_transition :pending => :parked, :do => :track_initial_transition
161
- #
162
- # event :park do
163
- # transition :pending => :parked
164
- # end
165
- #
166
- # state :pending, :value => nil
167
- # end
168
- # end
169
- #
170
- # vehicle = Vehicle.new
171
- # vehicle.park
172
- #
173
- # * Use a default event attribute that will automatically trigger when the
174
- # configured action gets run (note that the <tt>:pending</tt> state is
175
- # actually stored as nil):
176
- #
177
- # class Vehicle < ActiveRecord::Base
178
- # state_machine :initial => :pending
179
- # after_transition :pending => :parked, :do => :track_initial_transition
180
- #
181
- # event :park do
182
- # transition :pending => :parked
183
- # end
184
- #
185
- # state :pending, :value => nil
186
- # end
187
- #
188
- # def initialize(*)
189
- # super
190
- # self.state_event = 'park'
191
- # end
192
- # end
193
- #
194
- # vehicle = Vehicle.new
195
- # vehicle.save
196
- #
197
- # === Canceling callbacks
198
- #
199
- # Callbacks can be canceled by throwing :halt at any point during the
200
- # callback. For example,
201
- #
202
- # ...
203
- # throw :halt
204
- # ...
205
- #
206
- # If a +before+ callback halts the chain, the associated transition and all
207
- # later callbacks are canceled. If an +after+ callback halts the chain,
208
- # the later callbacks are canceled, but the transition is still successful.
209
- #
210
- # These same rules apply to +around+ callbacks with the exception that any
211
- # +around+ callback that doesn't yield will essentially result in :halt being
212
- # thrown. Any code executed after the yield will behave in the same way as
213
- # +after+ callbacks.
214
- #
215
- # *Note* that if a +before+ callback fails and the bang version of an event
216
- # was invoked, an exception will be raised instead of returning false. For
217
- # example,
218
- #
219
- # class Vehicle
220
- # state_machine :initial => :parked do
221
- # before_transition any => :idling, :do => lambda {|vehicle| throw :halt}
222
- # ...
223
- # end
224
- # end
225
- #
226
- # vehicle = Vehicle.new
227
- # vehicle.park # => false
228
- # vehicle.park! # => EnumStateMachine::InvalidTransition: Cannot transition state via :park from "idling"
229
- #
230
- # == Observers
231
- #
232
- # Observers, in the sense of external classes and *not* Ruby's Observable
233
- # mechanism, can hook into state machines as well. Such observers use the
234
- # same callback api that's used internally.
235
- #
236
- # Below are examples of defining observers for the following state machine:
237
- #
238
- # class Vehicle
239
- # state_machine do
240
- # event :park do
241
- # transition :idling => :parked
242
- # end
243
- # ...
244
- # end
245
- # ...
246
- # end
247
- #
248
- # Event/Transition behaviors:
249
- #
250
- # class VehicleObserver
251
- # def self.before_park(vehicle, transition)
252
- # logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}"
253
- # end
254
- #
255
- # def self.after_park(vehicle, transition, result)
256
- # logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}"
257
- # end
258
- #
259
- # def self.before_transition(vehicle, transition)
260
- # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}"
261
- # end
262
- #
263
- # def self.after_transition(vehicle, transition)
264
- # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
265
- # end
266
- #
267
- # def self.around_transition(vehicle, transition)
268
- # logger.info Benchmark.measure { yield }
269
- # end
270
- # end
271
- #
272
- # Vehicle.state_machine do
273
- # before_transition :on => :park, :do => VehicleObserver.method(:before_park)
274
- # before_transition VehicleObserver.method(:before_transition)
275
- #
276
- # after_transition :on => :park, :do => VehicleObserver.method(:after_park)
277
- # after_transition VehicleObserver.method(:after_transition)
278
- #
279
- # around_transition VehicleObserver.method(:around_transition)
280
- # end
281
- #
282
- # One common callback is to record transitions for all models in the system
283
- # for auditing/debugging purposes. Below is an example of an observer that
284
- # can easily automate this process for all models:
285
- #
286
- # class EnumStateMachineObserver
287
- # def self.before_transition(object, transition)
288
- # Audit.log_transition(object.attributes)
289
- # end
290
- # end
291
- #
292
- # [Vehicle, Switch, Project].each do |klass|
293
- # klass.state_machines.each do |attribute, machine|
294
- # machine.before_transition EnumStateMachineObserver.method(:before_transition)
295
- # end
296
- # end
297
- #
298
- # Additional observer-like behavior may be exposed by the various integrations
299
- # available. See below for more information on integrations.
300
- #
301
- # == Overriding instance / class methods
302
- #
303
- # Hooking in behavior to the generated instance / class methods from the
304
- # state machine, events, and states is very simple because of the way these
305
- # methods are generated on the class. Using the class's ancestors, the
306
- # original generated method can be referred to via +super+. For example,
307
- #
308
- # class Vehicle
309
- # state_machine do
310
- # event :park do
311
- # ...
312
- # end
313
- # end
314
- #
315
- # def park(*args)
316
- # logger.info "..."
317
- # super
318
- # end
319
- # end
320
- #
321
- # In the above example, the +park+ instance method that's generated on the
322
- # Vehicle class (by the associated event) is overridden with custom behavior.
323
- # Once this behavior is complete, the original method from the state machine
324
- # is invoked by simply calling +super+.
325
- #
326
- # The same technique can be used for +state+, +state_name+, and all other
327
- # instance *and* class methods on the Vehicle class.
328
- #
329
- # == Method conflicts
330
- #
331
- # By default state_machine does not redefine methods that exist on
332
- # superclasses (*including* Object) or any modules (*including* Kernel) that
333
- # were included before it was defined. This is in order to ensure that
334
- # existing behavior on the class is not broken by the inclusion of
335
- # state_machine.
336
- #
337
- # If a conflicting method is detected, state_machine will generate a warning.
338
- # For example, consider the following class:
339
- #
340
- # class Vehicle
341
- # state_machine do
342
- # event :open do
343
- # ...
344
- # end
345
- # end
346
- # end
347
- #
348
- # In the above class, an event named "open" is defined for its state machine.
349
- # However, "open" is already defined as an instance method in Ruby's Kernel
350
- # module that gets included in every Object. As a result, state_machine will
351
- # generate the following warning:
352
- #
353
- # Instance method "open" is already defined in Object, use generic helper instead or set EnumStateMachine::Machine.ignore_method_conflicts = true.
354
- #
355
- # Even though you may not be using Kernel's implementation of the "open"
356
- # instance method, state_machine isn't aware of this and, as a result, stays
357
- # safe and just skips redefining the method.
358
- #
359
- # As with almost all helpers methods defined by state_machine in your class,
360
- # there are generic methods available for working around this method conflict.
361
- # In the example above, you can invoke the "open" event like so:
362
- #
363
- # vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
364
- # vehicle.fire_events(:open) # => true
365
- #
366
- # # This will not work
367
- # vehicle.open # => NoMethodError: private method `open' called for #<Vehicle:0xb72686b4 @state=nil>
368
- #
369
- # If you want to take on the risk of overriding existing methods and just
370
- # ignore method conflicts altogether, you can do so by setting the following
371
- # configuration:
372
- #
373
- # EnumStateMachine::Machine.ignore_method_conflicts = true
374
- #
375
- # This will allow you to define events like "open" as described above and
376
- # still generate the "open" instance helper method. For example:
377
- #
378
- # EnumStateMachine::Machine.ignore_method_conflicts = true
379
- #
380
- # class Vehicle
381
- # state_machine do
382
- # event :open do
383
- # ...
384
- # end
385
- # end
386
- #
387
- # vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
388
- # vehicle.open # => true
389
- #
390
- # By default, state_machine helps prevent you from making mistakes and
391
- # accidentally overriding methods that you didn't intend to. Once you
392
- # understand this and what the consequences are, setting the
393
- # +ignore_method_conflicts+ option is a perfectly reasonable workaround.
394
- #
395
- # == Integrations
396
- #
397
- # By default, state machines are library-agnostic, meaning that they work
398
- # on any Ruby class and have no external dependencies. However, there are
399
- # certain libraries which expose additional behavior that can be taken
400
- # advantage of by state machines.
401
- #
402
- # This library is built to work out of the box with a few popular Ruby
403
- # libraries that allow for additional behavior to provide a cleaner and
404
- # smoother experience. This is especially the case for objects backed by a
405
- # database that may allow for transactions, persistent storage,
406
- # search/filters, callbacks, etc.
407
- #
408
- # When a state machine is defined for classes using any of the above libraries,
409
- # it will try to automatically determine the integration to use (Agnostic,
410
- # ActiveModel, ActiveRecord)
411
- # based on the class definition. To see how each integration affects the
412
- # machine's behavior, refer to all constants defined under the
413
- # EnumStateMachine::Integrations namespace.
414
- class Machine
415
- include Assertions
416
- include EvalHelpers
417
- include MatcherHelpers
418
-
419
- class << self
420
- # Attempts to find or create a state machine for the given class. For
421
- # example,
422
- #
423
- # EnumStateMachine::Machine.find_or_create(Vehicle)
424
- # EnumStateMachine::Machine.find_or_create(Vehicle, :initial => :parked)
425
- # EnumStateMachine::Machine.find_or_create(Vehicle, :status)
426
- # EnumStateMachine::Machine.find_or_create(Vehicle, :status, :initial => :parked)
427
- #
428
- # If a machine of the given name already exists in one of the class's
429
- # superclasses, then a copy of that machine will be created and stored
430
- # in the new owner class (the original will remain unchanged).
431
- def find_or_create(owner_class, *args, &block)
432
- options = args.last.is_a?(Hash) ? args.pop : {}
433
- name = args.first || :state
434
-
435
- # Find an existing machine
436
- if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[name]
437
- # Only create a new copy if changes are being made to the machine in
438
- # a subclass
439
- if machine.owner_class != owner_class && (options.any? || block_given?)
440
- machine = machine.clone
441
- machine.initial_state = options[:initial] if options.include?(:initial)
442
- machine.owner_class = owner_class
443
- end
444
-
445
- # Evaluate DSL
446
- machine.instance_eval(&block) if block_given?
447
- else
448
- # No existing machine: create a new one
449
- machine = new(owner_class, name, options, &block)
450
- end
451
-
452
- machine
453
- end
454
-
455
- # Draws the state machines defined in the given classes using GraphViz.
456
- # The given classes must be a comma-delimited string of class names.
457
- #
458
- # Configuration options:
459
- # * <tt>:file</tt> - A comma-delimited string of files to load that
460
- # contain the state machine definitions to draw
461
- # * <tt>:path</tt> - The path to write the graph file to
462
- # * <tt>:format</tt> - The image format to generate the graph in
463
- # * <tt>:font</tt> - The name of the font to draw state names in
464
- def draw(class_names, options = {})
465
- raise ArgumentError, 'At least one class must be specified' unless class_names && class_names.split(',').any?
466
-
467
- # Load any files
468
- if files = options.delete(:file)
469
- files.split(',').each {|file| require file}
470
- end
471
-
472
- class_names.split(',').each do |class_name|
473
- # Navigate through the namespace structure to get to the class
474
- klass = Object
475
- class_name.split('::').each do |name|
476
- klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name)
477
- end
478
-
479
- # Draw each of the class's state machines
480
- klass.state_machines.each_value do |machine|
481
- machine.draw(options)
482
- end
483
- end
484
- end
485
- end
486
-
487
- # Default messages to use for validation errors in ORM integrations
488
- class << self; attr_accessor :default_messages; end
489
- @default_messages = {
490
- :invalid => 'is invalid',
491
- :invalid_event => 'cannot transition when %s',
492
- :invalid_transition => 'cannot transition via "%1$s"'
493
- }
494
-
495
- # Whether to ignore any conflicts that are detected for helper methods that
496
- # get generated for a machine's owner class. Default is false.
497
- class << self; attr_accessor :ignore_method_conflicts; end
498
- @ignore_method_conflicts = false
499
-
500
- # The class that the machine is defined in
501
- attr_reader :owner_class
502
-
503
- # The name of the machine, used for scoping methods generated for the
504
- # machine as a whole (not states or events)
505
- attr_reader :name
506
-
507
- # The events that trigger transitions. These are sorted, by default, in
508
- # the order in which they were defined.
509
- attr_reader :events
510
-
511
- # A list of all of the states known to this state machine. This will pull
512
- # states from the following sources:
513
- # * Initial state
514
- # * State behaviors
515
- # * Event transitions (:to, :from, and :except_from options)
516
- # * Transition callbacks (:to, :from, :except_to, and :except_from options)
517
- # * Unreferenced states (using +other_states+ helper)
518
- #
519
- # These are sorted, by default, in the order in which they were referenced.
520
- attr_reader :states
521
-
522
- # The callbacks to invoke before/after a transition is performed
523
- #
524
- # Maps :before => callbacks and :after => callbacks
525
- attr_reader :callbacks
526
-
527
- # The action to invoke when an object transitions
528
- attr_reader :action
529
-
530
- # An identifier that forces all methods (including state predicates and
531
- # event methods) to be generated with the value prefixed or suffixed,
532
- # depending on the context.
533
- attr_reader :namespace
534
-
535
- # Whether the machine will use transactions when firing events
536
- attr_reader :use_transactions
537
-
538
- # Creates a new state machine for the given attribute
539
- def initialize(owner_class, *args, &block)
540
- options = args.last.is_a?(Hash) ? args.pop : {}
541
- assert_valid_keys(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
542
-
543
- # Find an integration that matches this machine's owner class
544
- if options.include?(:integration)
545
- @integration = options[:integration] && EnumStateMachine::Integrations.find_by_name(options[:integration])
546
- else
547
- @integration = EnumStateMachine::Integrations.match(owner_class)
548
- end
549
-
550
- if @integration
551
- extend @integration
552
- options = (@integration.defaults || {}).merge(options)
553
- end
554
-
555
- # Add machine-wide defaults
556
- options = {:use_transactions => true, :initialize => true}.merge(options)
557
-
558
- # Set machine configuration
559
- @name = args.first || :state
560
- @attribute = options[:attribute] || @name
561
- @events = EventCollection.new(self)
562
- @states = StateCollection.new(self)
563
- @callbacks = {:before => [], :after => [], :failure => []}
564
- @namespace = options[:namespace]
565
- @messages = options[:messages] || {}
566
- @action = options[:action]
567
- @use_transactions = options[:use_transactions]
568
- @initialize_state = options[:initialize]
569
- @action_hook_defined = false
570
- self.owner_class = owner_class
571
- self.initial_state = options[:initial] unless sibling_machines.any?
572
-
573
- # Merge with sibling machine configurations
574
- add_sibling_machine_configs
575
-
576
- # Define class integration
577
- define_helpers
578
- define_scopes(options[:plural])
579
- after_initialize
580
-
581
- # Evaluate DSL
582
- instance_eval(&block) if block_given?
583
- end
584
-
585
- # Creates a copy of this machine in addition to copies of each associated
586
- # event/states/callback, so that the modifications to those collections do
587
- # not affect the original machine.
588
- def initialize_copy(orig) #:nodoc:
589
- super
590
-
591
- @events = @events.dup
592
- @events.machine = self
593
- @states = @states.dup
594
- @states.machine = self
595
- @callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup, :failure => @callbacks[:failure].dup}
596
- end
597
-
598
- # Sets the class which is the owner of this state machine. Any methods
599
- # generated by states, events, or other parts of the machine will be defined
600
- # on the given owner class.
601
- def owner_class=(klass)
602
- @owner_class = klass
603
-
604
- # Create modules for extending the class with state/event-specific methods
605
- @helper_modules = helper_modules = {:instance => HelperModule.new(self, :instance), :class => HelperModule.new(self, :class)}
606
- owner_class.class_eval do
607
- extend helper_modules[:class]
608
- include helper_modules[:instance]
609
- end
610
-
611
- # Add class-/instance-level methods to the owner class for state initialization
612
- unless owner_class < EnumStateMachine::InstanceMethods
613
- owner_class.class_eval do
614
- extend EnumStateMachine::ClassMethods
615
- include EnumStateMachine::InstanceMethods
616
- end
617
-
618
- define_state_initializer if @initialize_state
619
- end
620
-
621
- # Record this machine as matched to the name in the current owner class.
622
- # This will override any machines mapped to the same name in any superclasses.
623
- owner_class.state_machines[name] = self
624
- end
625
-
626
- # Sets the initial state of the machine. This can be either the static name
627
- # of a state or a lambda block which determines the initial state at
628
- # creation time.
629
- def initial_state=(new_initial_state)
630
- @initial_state = new_initial_state
631
- add_states([@initial_state]) unless dynamic_initial_state?
632
-
633
- # Update all states to reflect the new initial state
634
- states.each {|state| state.initial = (state.name == @initial_state)}
635
-
636
- # Output a warning if there are conflicting initial states for the machine's
637
- # attribute
638
- initial_state = states.detect {|state| state.initial}
639
- if !owner_class_attribute_default.nil? && (dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state))
640
- warn(
641
- "Both #{owner_class.name} and its #{name.inspect} machine have defined "\
642
- "a different default for \"#{attribute}\". Use only one or the other for "\
643
- "defining defaults to avoid unexpected behaviors."
644
- )
645
- end
646
- end
647
-
648
- # Gets the initial state of the machine for the given object. If a dynamic
649
- # initial state was configured for this machine, then the object will be
650
- # passed into the lambda block to help determine the actual state.
651
- #
652
- # == Examples
653
- #
654
- # With a static initial state:
655
- #
656
- # class Vehicle
657
- # state_machine :initial => :parked do
658
- # ...
659
- # end
660
- # end
661
- #
662
- # vehicle = Vehicle.new
663
- # Vehicle.state_machine.initial_state(vehicle) # => #<EnumStateMachine::State name=:parked value="parked" initial=true>
664
- #
665
- # With a dynamic initial state:
666
- #
667
- # class Vehicle
668
- # attr_accessor :force_idle
669
- #
670
- # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do
671
- # ...
672
- # end
673
- # end
674
- #
675
- # vehicle = Vehicle.new
676
- #
677
- # vehicle.force_idle = true
678
- # Vehicle.state_machine.initial_state(vehicle) # => #<EnumStateMachine::State name=:idling value="idling" initial=false>
679
- #
680
- # vehicle.force_idle = false
681
- # Vehicle.state_machine.initial_state(vehicle) # => #<EnumStateMachine::State name=:parked value="parked" initial=false>
682
- def initial_state(object)
683
- states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?('@initial_state')
684
- end
685
-
686
- # Whether a dynamic initial state is being used in the machine
687
- def dynamic_initial_state?
688
- instance_variable_defined?('@initial_state') && @initial_state.is_a?(Proc)
689
- end
690
-
691
- # Initializes the state on the given object. Initial values are only set if
692
- # the machine's attribute hasn't been previously initialized.
693
- #
694
- # Configuration options:
695
- # * <tt>:force</tt> - Whether to initialize the state regardless of its
696
- # current value
697
- # * <tt>:to</tt> - A hash to set the initial value in instead of writing
698
- # directly to the object
699
- def initialize_state(object, options = {})
700
- state = initial_state(object)
701
- if state && (options[:force] || initialize_state?(object))
702
- value = state.value
703
-
704
- if hash = options[:to]
705
- hash[attribute.to_s] = value
706
- else
707
- write(object, :state, value)
708
- end
709
- end
710
- end
711
-
712
- # Gets the actual name of the attribute on the machine's owner class that
713
- # stores data with the given name.
714
- def attribute(name = :state)
715
- name == :state ? @attribute : :"#{self.name}_#{name}"
716
- end
717
-
718
- # Defines a new helper method in an instance or class scope with the given
719
- # name. If the method is already defined in the scope, then this will not
720
- # override it.
721
- #
722
- # If passing in a block, there are two side effects to be aware of
723
- # 1. The method cannot be chained, meaning that the block cannot call +super+
724
- # 2. If the method is already defined in an ancestor, then it will not get
725
- # overridden and a warning will be output.
726
- #
727
- # Example:
728
- #
729
- # # Instance helper
730
- # machine.define_helper(:instance, :state_name) do |machine, object|
731
- # machine.states.match(object).name
732
- # end
733
- #
734
- # # Class helper
735
- # machine.define_helper(:class, :state_machine_name) do |machine, klass|
736
- # "State"
737
- # end
738
- #
739
- # You can also define helpers using string evaluation like so:
740
- #
741
- # # Instance helper
742
- # machine.define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
743
- # def state_name
744
- # self.class.state_machine(:state).states.match(self).name
745
- # end
746
- # end_eval
747
- #
748
- # # Class helper
749
- # machine.define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1
750
- # def state_machine_name
751
- # "State"
752
- # end
753
- # end_eval
754
- def define_helper(scope, method, *args, &block)
755
- helper_module = @helper_modules.fetch(scope)
756
-
757
- if block_given?
758
- if !self.class.ignore_method_conflicts && conflicting_ancestor = owner_class_ancestor_has_method?(scope, method)
759
- ancestor_name = conflicting_ancestor.name && !conflicting_ancestor.name.empty? ? conflicting_ancestor.name : conflicting_ancestor.to_s
760
- warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead or set EnumStateMachine::Machine.ignore_method_conflicts = true."
761
- else
762
- name = self.name
763
- helper_module.class_eval do
764
- define_method(method) do |*block_args|
765
- block.call((scope == :instance ? self.class : self).state_machine(name), self, *block_args)
766
- end
767
- end
768
- end
769
- else
770
- helper_module.class_eval(method, *args)
771
- end
772
- end
773
-
774
- # Customizes the definition of one or more states in the machine.
775
- #
776
- # Configuration options:
777
- # * <tt>:value</tt> - The actual value to store when an object transitions
778
- # to the state. Default is the name (stringified).
779
- # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
780
- # then setting this to true will cache the evaluated result
781
- # * <tt>:if</tt> - Determines whether an object's value matches the state
782
- # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
783
- # By default, the configured value is matched.
784
- # * <tt>:human_name</tt> - The human-readable version of this state's name.
785
- # By default, this is either defined by the integration or stringifies the
786
- # name and converts underscores to spaces.
787
- #
788
- # == Customizing the stored value
789
- #
790
- # Whenever a state is automatically discovered in the state machine, its
791
- # default value is assumed to be the stringified version of the name. For
792
- # example,
793
- #
794
- # class Vehicle
795
- # state_machine :initial => :parked do
796
- # event :ignite do
797
- # transition :parked => :idling
798
- # end
799
- # end
800
- # end
801
- #
802
- # In the above state machine, there are two states automatically discovered:
803
- # :parked and :idling. These states, by default, will store their stringified
804
- # equivalents when an object moves into that state (e.g. "parked" / "idling").
805
- #
806
- # For legacy systems or when tying state machines into existing frameworks,
807
- # it's oftentimes necessary to need to store a different value for a state
808
- # than the default. In order to continue taking advantage of an expressive
809
- # state machine and helper methods, every defined state can be re-configured
810
- # with a custom stored value. For example,
811
- #
812
- # class Vehicle
813
- # state_machine :initial => :parked do
814
- # event :ignite do
815
- # transition :parked => :idling
816
- # end
817
- #
818
- # state :idling, :value => 'IDLING'
819
- # state :parked, :value => 'PARKED
820
- # end
821
- # end
822
- #
823
- # This is also useful if being used in association with a database and,
824
- # instead of storing the state name in a column, you want to store the
825
- # state's foreign key:
826
- #
827
- # class VehicleState < ActiveRecord::Base
828
- # end
829
- #
830
- # class Vehicle < ActiveRecord::Base
831
- # state_machine :attribute => :state_id, :initial => :parked do
832
- # event :ignite do
833
- # transition :parked => :idling
834
- # end
835
- #
836
- # states.each do |state|
837
- # self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
838
- # end
839
- # end
840
- # end
841
- #
842
- # In the above example, each known state is configured to store it's
843
- # associated database id in the +state_id+ attribute. Also, notice that a
844
- # lambda block is used to define the state's value. This is required in
845
- # situations (like testing) where the model is loaded without any existing
846
- # data (i.e. no VehicleState records available).
847
- #
848
- # One caveat to the above example is to keep performance in mind. To avoid
849
- # constant db hits for looking up the VehicleState ids, the value is cached
850
- # by specifying the <tt>:cache</tt> option. Alternatively, a custom
851
- # caching strategy can be used like so:
852
- #
853
- # class VehicleState < ActiveRecord::Base
854
- # cattr_accessor :cache_store
855
- # self.cache_store = ActiveSupport::Cache::MemoryStore.new
856
- #
857
- # def self.find_by_name(name)
858
- # cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
859
- # end
860
- # end
861
- #
862
- # === Dynamic values
863
- #
864
- # In addition to customizing states with other value types, lambda blocks
865
- # can also be specified to allow for a state's value to be determined
866
- # dynamically at runtime. For example,
867
- #
868
- # class Vehicle
869
- # state_machine :purchased_at, :initial => :available do
870
- # event :purchase do
871
- # transition all => :purchased
872
- # end
873
- #
874
- # event :restock do
875
- # transition all => :available
876
- # end
877
- #
878
- # state :available, :value => nil
879
- # state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
880
- # end
881
- # end
882
- #
883
- # In the above definition, the <tt>:purchased</tt> state is customized with
884
- # both a dynamic value *and* a value matcher.
885
- #
886
- # When an object transitions to the purchased state, the value's lambda
887
- # block will be called. This will get the current time and store it in the
888
- # object's +purchased_at+ attribute.
889
- #
890
- # *Note* that the custom matcher is very important here. Since there's no
891
- # way for the state machine to figure out an object's state when it's set to
892
- # a runtime value, it must be explicitly defined. If the <tt>:if</tt> option
893
- # were not configured for the state, then an ArgumentError exception would
894
- # be raised at runtime, indicating that the state machine could not figure
895
- # out what the current state of the object was.
896
- #
897
- # == Behaviors
898
- #
899
- # Behaviors define a series of methods to mixin with objects when the current
900
- # state matches the given one(s). This allows instance methods to behave
901
- # a specific way depending on what the value of the object's state is.
902
- #
903
- # For example,
904
- #
905
- # class Vehicle
906
- # attr_accessor :driver
907
- # attr_accessor :passenger
908
- #
909
- # state_machine :initial => :parked do
910
- # event :ignite do
911
- # transition :parked => :idling
912
- # end
913
- #
914
- # state :parked do
915
- # def speed
916
- # 0
917
- # end
918
- #
919
- # def rotate_driver
920
- # driver = self.driver
921
- # self.driver = passenger
922
- # self.passenger = driver
923
- # true
924
- # end
925
- # end
926
- #
927
- # state :idling, :first_gear do
928
- # def speed
929
- # 20
930
- # end
931
- #
932
- # def rotate_driver
933
- # self.state = 'parked'
934
- # rotate_driver
935
- # end
936
- # end
937
- #
938
- # other_states :backing_up
939
- # end
940
- # end
941
- #
942
- # In the above example, there are two dynamic behaviors defined for the
943
- # class:
944
- # * +speed+
945
- # * +rotate_driver+
946
- #
947
- # Each of these behaviors are instance methods on the Vehicle class. However,
948
- # which method actually gets invoked is based on the current state of the
949
- # object. Using the above class as the example:
950
- #
951
- # vehicle = Vehicle.new
952
- # vehicle.driver = 'John'
953
- # vehicle.passenger = 'Jane'
954
- #
955
- # # Behaviors in the "parked" state
956
- # vehicle.state # => "parked"
957
- # vehicle.speed # => 0
958
- # vehicle.rotate_driver # => true
959
- # vehicle.driver # => "Jane"
960
- # vehicle.passenger # => "John"
961
- #
962
- # vehicle.ignite # => true
963
- #
964
- # # Behaviors in the "idling" state
965
- # vehicle.state # => "idling"
966
- # vehicle.speed # => 20
967
- # vehicle.rotate_driver # => true
968
- # vehicle.driver # => "John"
969
- # vehicle.passenger # => "Jane"
970
- #
971
- # As can be seen, both the +speed+ and +rotate_driver+ instance method
972
- # implementations changed how they behave based on what the current state
973
- # of the vehicle was.
974
- #
975
- # === Invalid behaviors
976
- #
977
- # If a specific behavior has not been defined for a state, then a
978
- # NoMethodError exception will be raised, indicating that that method would
979
- # not normally exist for an object with that state.
980
- #
981
- # Using the example from before:
982
- #
983
- # vehicle = Vehicle.new
984
- # vehicle.state = 'backing_up'
985
- # vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
986
- #
987
- # === Using matchers
988
- #
989
- # The +all+ / +any+ matchers can be used to easily define behaviors for a
990
- # group of states. Note, however, that you cannot use these matchers to
991
- # set configurations for states. Behaviors using these matchers can be
992
- # defined at any point in the state machine and will always get applied to
993
- # the proper states.
994
- #
995
- # For example:
996
- #
997
- # state_machine :initial => :parked do
998
- # ...
999
- #
1000
- # state all - [:parked, :idling, :stalled] do
1001
- # validates_presence_of :speed
1002
- #
1003
- # def speed
1004
- # gear * 10
1005
- # end
1006
- # end
1007
- # end
1008
- #
1009
- # == State-aware class methods
1010
- #
1011
- # In addition to defining scopes for instance methods that are state-aware,
1012
- # the same can be done for certain types of class methods.
1013
- #
1014
- # Some libraries have support for class-level methods that only run certain
1015
- # behaviors based on a conditions hash passed in. For example:
1016
- #
1017
- # class Vehicle < ActiveRecord::Base
1018
- # state_machine do
1019
- # ...
1020
- # state :first_gear, :second_gear, :third_gear do
1021
- # validates_presence_of :speed
1022
- # validates_inclusion_of :speed, :in => 0..25, :if => :in_school_zone?
1023
- # end
1024
- # end
1025
- # end
1026
- #
1027
- # In the above ActiveRecord model, two validations have been defined which
1028
- # will *only* run when the Vehicle object is in one of the three states:
1029
- # +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless
1030
- # conditions can continue to be used.
1031
- #
1032
- # This functionality is not library-specific and can work for any class-level
1033
- # method that is defined like so:
1034
- #
1035
- # def validates_presence_of(attribute, options = {})
1036
- # ...
1037
- # end
1038
- #
1039
- # The minimum requirement is that the last argument in the method be an
1040
- # options hash which contains at least <tt>:if</tt> condition support.
1041
- def state(*names, &block)
1042
- options = names.last.is_a?(Hash) ? names.pop : {}
1043
- assert_valid_keys(options, :value, :cache, :if, :human_name)
1044
-
1045
- # Store the context so that it can be used for / matched against any state
1046
- # that gets added
1047
- @states.context(names, &block) if block_given?
1048
-
1049
- if names.first.is_a?(Matcher)
1050
- # Add any states referenced in the matcher. When matchers are used,
1051
- # states are not allowed to be configured.
1052
- raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any?
1053
- states = add_states(names.first.values)
1054
- else
1055
- states = add_states(names)
1056
-
1057
- # Update the configuration for the state(s)
1058
- states.each do |state|
1059
- if options.include?(:value)
1060
- state.value = options[:value]
1061
- self.states.update(state)
1062
- end
1063
-
1064
- state.human_name = options[:human_name] if options.include?(:human_name)
1065
- state.cache = options[:cache] if options.include?(:cache)
1066
- state.matcher = options[:if] if options.include?(:if)
1067
- end
1068
- end
1069
-
1070
- states.length == 1 ? states.first : states
1071
- end
1072
- alias_method :other_states, :state
1073
-
1074
- # Gets the current value stored in the given object's attribute.
1075
- #
1076
- # For example,
1077
- #
1078
- # class Vehicle
1079
- # state_machine :initial => :parked do
1080
- # ...
1081
- # end
1082
- # end
1083
- #
1084
- # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
1085
- # Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state
1086
- # Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
1087
- def read(object, attribute, ivar = false)
1088
- attribute = self.attribute(attribute)
1089
- if ivar
1090
- object.instance_variable_defined?("@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
1091
- else
1092
- object.send(attribute)
1093
- end
1094
- end
1095
-
1096
- # Sets a new value in the given object's attribute.
1097
- #
1098
- # For example,
1099
- #
1100
- # class Vehicle
1101
- # state_machine :initial => :parked do
1102
- # ...
1103
- # end
1104
- # end
1105
- #
1106
- # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
1107
- # Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling'
1108
- # Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
1109
- # vehicle.state # => "idling"
1110
- # vehicle.event # => "park"
1111
- def write(object, attribute, value, ivar = false)
1112
- attribute = self.attribute(attribute)
1113
- ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value)
1114
- end
1115
-
1116
- # Defines one or more events for the machine and the transitions that can
1117
- # be performed when those events are run.
1118
- #
1119
- # This method is also aliased as +on+ for improved compatibility with
1120
- # using a domain-specific language.
1121
- #
1122
- # Configuration options:
1123
- # * <tt>:human_name</tt> - The human-readable version of this event's name.
1124
- # By default, this is either defined by the integration or stringifies the
1125
- # name and converts underscores to spaces.
1126
- #
1127
- # == Instance methods
1128
- #
1129
- # The following instance methods are generated when a new event is defined
1130
- # (the "park" event is used as an example):
1131
- # * <tt>park(..., run_action = true)</tt> - Fires the "park" event,
1132
- # transitioning from the current state to the next valid state. If the
1133
- # last argument is a boolean, it will control whether the machine's action
1134
- # gets run.
1135
- # * <tt>park!(..., run_action = true)</tt> - Fires the "park" event,
1136
- # transitioning from the current state to the next valid state. If the
1137
- # transition fails, then a EnumStateMachine::InvalidTransition error will be
1138
- # raised. If the last argument is a boolean, it will control whether the
1139
- # machine's action gets run.
1140
- # * <tt>can_park?(requirements = {})</tt> - Checks whether the "park" event
1141
- # can be fired given the current state of the object. This will *not* run
1142
- # validations or callbacks in ORM integrations. It will only determine if
1143
- # the state machine defines a valid transition for the event. To check
1144
- # whether an event can fire *and* passes validations, use event attributes
1145
- # (e.g. state_event) as described in the "Events" documentation of each
1146
- # ORM integration.
1147
- # * <tt>park_transition(requirements = {})</tt> - Gets the next transition
1148
- # that would be performed if the "park" event were to be fired now on the
1149
- # object or nil if no transitions can be performed. Like <tt>can_park?</tt>
1150
- # this will also *not* run validations or callbacks. It will only
1151
- # determine if the state machine defines a valid transition for the event.
1152
- #
1153
- # With a namespace of "car", the above names map to the following methods:
1154
- # * <tt>can_park_car?</tt>
1155
- # * <tt>park_car_transition</tt>
1156
- # * <tt>park_car</tt>
1157
- # * <tt>park_car!</tt>
1158
- #
1159
- # The <tt>can_park?</tt> and <tt>park_transition</tt> helpers both take an
1160
- # optional set of requirements for determining what transitions are available
1161
- # for the current object. These requirements include:
1162
- # * <tt>:from</tt> - One or more states to transition from. If none are
1163
- # specified, then this will be the object's current state.
1164
- # * <tt>:to</tt> - One or more states to transition to. If none are
1165
- # specified, then this will match any to state.
1166
- # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
1167
- # conditionals defined for each one. Default is true.
1168
- #
1169
- # == Defining transitions
1170
- #
1171
- # +event+ requires a block which allows you to define the possible
1172
- # transitions that can happen as a result of that event. For example,
1173
- #
1174
- # event :park, :stop do
1175
- # transition :idling => :parked
1176
- # end
1177
- #
1178
- # event :first_gear do
1179
- # transition :parked => :first_gear, :if => :seatbelt_on?
1180
- # transition :parked => same # Allow to loopback if seatbelt is off
1181
- # end
1182
- #
1183
- # See EnumStateMachine::Event#transition for more information on
1184
- # the possible options that can be passed in.
1185
- #
1186
- # *Note* that this block is executed within the context of the actual event
1187
- # object. As a result, you will not be able to reference any class methods
1188
- # on the model without referencing the class itself. For example,
1189
- #
1190
- # class Vehicle
1191
- # def self.safe_states
1192
- # [:parked, :idling, :stalled]
1193
- # end
1194
- #
1195
- # state_machine do
1196
- # event :park do
1197
- # transition Vehicle.safe_states => :parked
1198
- # end
1199
- # end
1200
- # end
1201
- #
1202
- # == Overriding the event method
1203
- #
1204
- # By default, this will define an instance method (with the same name as the
1205
- # event) that will fire the next possible transition for that. Although the
1206
- # +before_transition+, +after_transition+, and +around_transition+ hooks
1207
- # allow you to define behavior that gets executed as a result of the event's
1208
- # transition, you can also override the event method in order to have a
1209
- # little more fine-grained control.
1210
- #
1211
- # For example:
1212
- #
1213
- # class Vehicle
1214
- # state_machine do
1215
- # event :park do
1216
- # ...
1217
- # end
1218
- # end
1219
- #
1220
- # def park(*)
1221
- # take_deep_breath # Executes before the transition (and before_transition hooks) even if no transition is possible
1222
- # if result = super # Runs the transition and all before/after/around hooks
1223
- # applaud # Executes after the transition (and after_transition hooks)
1224
- # end
1225
- # result
1226
- # end
1227
- # end
1228
- #
1229
- # There are a few important things to note here. First, the method
1230
- # signature is defined with an unlimited argument list in order to allow
1231
- # callers to continue passing arguments that are expected by state_machine.
1232
- # For example, it will still allow calls to +park+ with a single parameter
1233
- # for skipping the configured action.
1234
- #
1235
- # Second, the overridden event method must call +super+ in order to run the
1236
- # logic for running the next possible transition. In order to remain
1237
- # consistent with other events, the result of +super+ is returned.
1238
- #
1239
- # Third, any behavior defined in this method will *not* get executed if
1240
- # you're taking advantage of attribute-based event transitions. For example:
1241
- #
1242
- # vehicle = Vehicle.new
1243
- # vehicle.state_event = 'park'
1244
- # vehicle.save
1245
- #
1246
- # In this case, the +park+ event will run the before/after/around transition
1247
- # hooks and transition the state, but the behavior defined in the overriden
1248
- # +park+ method will *not* be executed.
1249
- #
1250
- # == Defining additional arguments
1251
- #
1252
- # Additional arguments can be passed into events and accessed by transition
1253
- # hooks like so:
1254
- #
1255
- # class Vehicle
1256
- # state_machine do
1257
- # after_transition :on => :park do |vehicle, transition|
1258
- # kind = *transition.args # :parallel
1259
- # ...
1260
- # end
1261
- # after_transition :on => :park, :do => :take_deep_breath
1262
- #
1263
- # event :park do
1264
- # ...
1265
- # end
1266
- #
1267
- # def take_deep_breath(transition)
1268
- # kind = *transition.args # :parallel
1269
- # ...
1270
- # end
1271
- # end
1272
- # end
1273
- #
1274
- # vehicle = Vehicle.new
1275
- # vehicle.park(:parallel)
1276
- #
1277
- # *Remember* that if the last argument is a boolean, it will be used as the
1278
- # +run_action+ parameter to the event action. Using the +park+ action
1279
- # example from above, you can might call it like so:
1280
- #
1281
- # vehicle.park # => Uses default args and runs machine action
1282
- # vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action
1283
- # vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action
1284
- #
1285
- # If you decide to override the +park+ event method *and* define additional
1286
- # arguments, you can do so as shown below:
1287
- #
1288
- # class Vehicle
1289
- # state_machine do
1290
- # event :park do
1291
- # ...
1292
- # end
1293
- # end
1294
- #
1295
- # def park(kind = :parallel, *args)
1296
- # take_deep_breath if kind == :parallel
1297
- # super
1298
- # end
1299
- # end
1300
- #
1301
- # Note that +super+ is called instead of <tt>super(*args)</tt>. This allow
1302
- # the entire arguments list to be accessed by transition callbacks through
1303
- # EnumStateMachine::Transition#args.
1304
- #
1305
- # === Using matchers
1306
- #
1307
- # The +all+ / +any+ matchers can be used to easily execute blocks for a
1308
- # group of events. Note, however, that you cannot use these matchers to
1309
- # set configurations for events. Blocks using these matchers can be
1310
- # defined at any point in the state machine and will always get applied to
1311
- # the proper events.
1312
- #
1313
- # For example:
1314
- #
1315
- # state_machine :initial => :parked do
1316
- # ...
1317
- #
1318
- # event all - [:crash] do
1319
- # transition :stalled => :parked
1320
- # end
1321
- # end
1322
- #
1323
- # == Example
1324
- #
1325
- # class Vehicle
1326
- # state_machine do
1327
- # # The park, stop, and halt events will all share the given transitions
1328
- # event :park, :stop, :halt do
1329
- # transition [:idling, :backing_up] => :parked
1330
- # end
1331
- #
1332
- # event :stop do
1333
- # transition :first_gear => :idling
1334
- # end
1335
- #
1336
- # event :ignite do
1337
- # transition :parked => :idling
1338
- # transition :idling => same # Allow ignite while still idling
1339
- # end
1340
- # end
1341
- # end
1342
- def event(*names, &block)
1343
- options = names.last.is_a?(Hash) ? names.pop : {}
1344
- assert_valid_keys(options, :human_name)
1345
-
1346
- # Store the context so that it can be used for / matched against any event
1347
- # that gets added
1348
- @events.context(names, &block) if block_given?
1349
-
1350
- if names.first.is_a?(Matcher)
1351
- # Add any events referenced in the matcher. When matchers are used,
1352
- # events are not allowed to be configured.
1353
- raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
1354
- events = add_events(names.first.values)
1355
- else
1356
- events = add_events(names)
1357
-
1358
- # Update the configuration for the event(s)
1359
- events.each do |event|
1360
- event.human_name = options[:human_name] if options.include?(:human_name)
1361
-
1362
- # Add any states that may have been referenced within the event
1363
- add_states(event.known_states)
1364
- end
1365
- end
1366
-
1367
- events.length == 1 ? events.first : events
1368
- end
1369
- alias_method :on, :event
1370
-
1371
- # Creates a new transition that determines what to change the current state
1372
- # to when an event fires.
1373
- #
1374
- # == Defining transitions
1375
- #
1376
- # The options for a new transition uses the Hash syntax to map beginning
1377
- # states to ending states. For example,
1378
- #
1379
- # transition :parked => :idling, :idling => :first_gear, :on => :ignite
1380
- #
1381
- # In this case, when the +ignite+ event is fired, this transition will cause
1382
- # the state to be +idling+ if it's current state is +parked+ or +first_gear+
1383
- # if it's current state is +idling+.
1384
- #
1385
- # To help define these implicit transitions, a set of helpers are available
1386
- # for slightly more complex matching:
1387
- # * <tt>all</tt> - Matches every state in the machine
1388
- # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
1389
- # * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
1390
- # * <tt>same</tt> - Matches the same state being transitioned from
1391
- #
1392
- # See EnumStateMachine::MatcherHelpers for more information.
1393
- #
1394
- # Examples:
1395
- #
1396
- # transition all => nil, :on => :ignite # Transitions to nil regardless of the current state
1397
- # transition all => :idling, :on => :ignite # Transitions to :idling regardless of the current state
1398
- # transition all - [:idling, :first_gear] => :idling, :on => :ignite # Transitions every state but :idling and :first_gear to :idling
1399
- # transition nil => :idling, :on => :ignite # Transitions to :idling from the nil state
1400
- # transition :parked => :idling, :on => :ignite # Transitions to :idling if :parked
1401
- # transition [:parked, :stalled] => :idling, :on => :ignite # Transitions to :idling if :parked or :stalled
1402
- #
1403
- # transition :parked => same, :on => :park # Loops :parked back to :parked
1404
- # transition [:parked, :stalled] => same, :on => [:park, :stall] # Loops either :parked or :stalled back to the same state on the park and stall events
1405
- # transition all - :parked => same, :on => :noop # Loops every state but :parked back to the same state
1406
- #
1407
- # # Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear
1408
- # transition :parked => :idling, :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up
1409
- #
1410
- # == Verbose transitions
1411
- #
1412
- # Transitions can also be defined use an explicit set of configuration
1413
- # options:
1414
- # * <tt>:from</tt> - A state or array of states that can be transitioned from.
1415
- # If not specified, then the transition can occur for *any* state.
1416
- # * <tt>:to</tt> - The state that's being transitioned to. If not specified,
1417
- # then the transition will simply loop back (i.e. the state will not change).
1418
- # * <tt>:except_from</tt> - A state or array of states that *cannot* be
1419
- # transitioned from.
1420
- #
1421
- # These options must be used when defining transitions within the context
1422
- # of a state.
1423
- #
1424
- # Examples:
1425
- #
1426
- # transition :to => nil, :on => :park
1427
- # transition :to => :idling, :on => :ignite
1428
- # transition :except_from => [:idling, :first_gear], :to => :idling, :on => :ignite
1429
- # transition :from => nil, :to => :idling, :on => :ignite
1430
- # transition :from => [:parked, :stalled], :to => :idling, :on => :ignite
1431
- #
1432
- # == Conditions
1433
- #
1434
- # In addition to the state requirements for each transition, a condition
1435
- # can also be defined to help determine whether that transition is
1436
- # available. These options will work on both the normal and verbose syntax.
1437
- #
1438
- # Configuration options:
1439
- # * <tt>:if</tt> - A method, proc or string to call to determine if the
1440
- # transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
1441
- # The condition should return or evaluate to true or false.
1442
- # * <tt>:unless</tt> - A method, proc or string to call to determine if the
1443
- # transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
1444
- # The condition should return or evaluate to true or false.
1445
- #
1446
- # Examples:
1447
- #
1448
- # transition :parked => :idling, :on => :ignite, :if => :moving?
1449
- # transition :parked => :idling, :on => :ignite, :unless => :stopped?
1450
- # transition :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up, :if => :seatbelt_on?
1451
- #
1452
- # transition :from => :parked, :to => :idling, :on => ignite, :if => :moving?
1453
- # transition :from => :parked, :to => :idling, :on => ignite, :unless => :stopped?
1454
- #
1455
- # == Order of operations
1456
- #
1457
- # Transitions are evaluated in the order in which they're defined. As a
1458
- # result, if more than one transition applies to a given object, then the
1459
- # first transition that matches will be performed.
1460
- def transition(options)
1461
- raise ArgumentError, 'Must specify :on event' unless options[:on]
1462
-
1463
- branches = []
1464
- options = options.dup
1465
- event(*Array(options.delete(:on))) { branches << transition(options) }
1466
-
1467
- branches.length == 1 ? branches.first : branches
1468
- end
1469
-
1470
- # Creates a callback that will be invoked *before* a transition is
1471
- # performed so long as the given requirements match the transition.
1472
- #
1473
- # == The callback
1474
- #
1475
- # Callbacks must be defined as either an argument, in the :do option, or
1476
- # as a block. For example,
1477
- #
1478
- # class Vehicle
1479
- # state_machine do
1480
- # before_transition :set_alarm
1481
- # before_transition :set_alarm, all => :parked
1482
- # before_transition all => :parked, :do => :set_alarm
1483
- # before_transition all => :parked do |vehicle, transition|
1484
- # vehicle.set_alarm
1485
- # end
1486
- # ...
1487
- # end
1488
- # end
1489
- #
1490
- # Notice that the first three callbacks are the same in terms of how the
1491
- # methods to invoke are defined. However, using the <tt>:do</tt> can
1492
- # provide for a more fluid DSL.
1493
- #
1494
- # In addition, multiple callbacks can be defined like so:
1495
- #
1496
- # class Vehicle
1497
- # state_machine do
1498
- # before_transition :set_alarm, :lock_doors, all => :parked
1499
- # before_transition all => :parked, :do => [:set_alarm, :lock_doors]
1500
- # before_transition :set_alarm do |vehicle, transition|
1501
- # vehicle.lock_doors
1502
- # end
1503
- # end
1504
- # end
1505
- #
1506
- # Notice that the different ways of configuring methods can be mixed.
1507
- #
1508
- # == State requirements
1509
- #
1510
- # Callbacks can require that the machine be transitioning from and to
1511
- # specific states. These requirements use a Hash syntax to map beginning
1512
- # states to ending states. For example,
1513
- #
1514
- # before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
1515
- #
1516
- # In this case, the +set_alarm+ callback will only be called if the machine
1517
- # is transitioning from +parked+ to +idling+ or from +idling+ to +parked+.
1518
- #
1519
- # To help define state requirements, a set of helpers are available for
1520
- # slightly more complex matching:
1521
- # * <tt>all</tt> - Matches every state/event in the machine
1522
- # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state/event except those specified
1523
- # * <tt>any</tt> - An alias for +all+ (matches every state/event in the machine)
1524
- # * <tt>same</tt> - Matches the same state being transitioned from
1525
- #
1526
- # See EnumStateMachine::MatcherHelpers for more information.
1527
- #
1528
- # Examples:
1529
- #
1530
- # before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
1531
- # before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
1532
- # before_transition all => :parked, :do => ... # Matches all states to parked
1533
- # before_transition any => same, :do => ... # Matches every loopback
1534
- #
1535
- # == Event requirements
1536
- #
1537
- # In addition to state requirements, an event requirement can be defined so
1538
- # that the callback is only invoked on specific events using the +on+
1539
- # option. This can also use the same matcher helpers as the state
1540
- # requirements.
1541
- #
1542
- # Examples:
1543
- #
1544
- # before_transition :on => :ignite, :do => ... # Matches only on ignite
1545
- # before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
1546
- # before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
1547
- #
1548
- # == Verbose Requirements
1549
- #
1550
- # Requirements can also be defined using verbose options rather than the
1551
- # implicit Hash syntax and helper methods described above.
1552
- #
1553
- # Configuration options:
1554
- # * <tt>:from</tt> - One or more states being transitioned from. If none
1555
- # are specified, then all states will match.
1556
- # * <tt>:to</tt> - One or more states being transitioned to. If none are
1557
- # specified, then all states will match.
1558
- # * <tt>:on</tt> - One or more events that fired the transition. If none
1559
- # are specified, then all events will match.
1560
- # * <tt>:except_from</tt> - One or more states *not* being transitioned from
1561
- # * <tt>:except_to</tt> - One more states *not* being transitioned to
1562
- # * <tt>:except_on</tt> - One or more events that *did not* fire the transition
1563
- #
1564
- # Examples:
1565
- #
1566
- # before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
1567
- # before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
1568
- #
1569
- # == Conditions
1570
- #
1571
- # In addition to the state/event requirements, a condition can also be
1572
- # defined to help determine whether the callback should be invoked.
1573
- #
1574
- # Configuration options:
1575
- # * <tt>:if</tt> - A method, proc or string to call to determine if the
1576
- # callback should occur (e.g. :if => :allow_callbacks, or
1577
- # :if => lambda {|user| user.signup_step > 2}). The method, proc or string
1578
- # should return or evaluate to a true or false value.
1579
- # * <tt>:unless</tt> - A method, proc or string to call to determine if the
1580
- # callback should not occur (e.g. :unless => :skip_callbacks, or
1581
- # :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
1582
- # string should return or evaluate to a true or false value.
1583
- #
1584
- # Examples:
1585
- #
1586
- # before_transition :parked => :idling, :if => :moving?, :do => ...
1587
- # before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
1588
- #
1589
- # == Accessing the transition
1590
- #
1591
- # In addition to passing the object being transitioned, the actual
1592
- # transition describing the context (e.g. event, from, to) can be accessed
1593
- # as well. This additional argument is only passed if the callback allows
1594
- # for it.
1595
- #
1596
- # For example,
1597
- #
1598
- # class Vehicle
1599
- # # Only specifies one parameter (the object being transitioned)
1600
- # before_transition all => :parked do |vehicle|
1601
- # vehicle.set_alarm
1602
- # end
1603
- #
1604
- # # Specifies 2 parameters (object being transitioned and actual transition)
1605
- # before_transition all => :parked do |vehicle, transition|
1606
- # vehicle.set_alarm(transition)
1607
- # end
1608
- # end
1609
- #
1610
- # *Note* that the object in the callback will only be passed in as an
1611
- # argument if callbacks are configured to *not* be bound to the object
1612
- # involved. This is the default and may change on a per-integration basis.
1613
- #
1614
- # See EnumStateMachine::Transition for more information about the
1615
- # attributes available on the transition.
1616
- #
1617
- # == Usage with delegates
1618
- #
1619
- # As noted above, state_machine uses the callback method's argument list
1620
- # arity to determine whether to include the transition in the method call.
1621
- # If you're using delegates, such as those defined in ActiveSupport or
1622
- # Forwardable, the actual arity of the delegated method gets masked. This
1623
- # means that callbacks which reference delegates will always get passed the
1624
- # transition as an argument. For example:
1625
- #
1626
- # class Vehicle
1627
- # extend Forwardable
1628
- # delegate :refresh => :dashboard
1629
- #
1630
- # state_machine do
1631
- # before_transition :refresh
1632
- # ...
1633
- # end
1634
- #
1635
- # def dashboard
1636
- # @dashboard ||= Dashboard.new
1637
- # end
1638
- # end
1639
- #
1640
- # class Dashboard
1641
- # def refresh(transition)
1642
- # # ...
1643
- # end
1644
- # end
1645
- #
1646
- # In the above example, <tt>Dashboard#refresh</tt> *must* defined a
1647
- # +transition+ argument. Otherwise, an +ArgumentError+ exception will get
1648
- # raised. The only way around this is to avoid the use of delegates and
1649
- # manually define the delegate method so that the correct arity is used.
1650
- #
1651
- # == Examples
1652
- #
1653
- # Below is an example of a class with one state machine and various types
1654
- # of +before+ transitions defined for it:
1655
- #
1656
- # class Vehicle
1657
- # state_machine do
1658
- # # Before all transitions
1659
- # before_transition :update_dashboard
1660
- #
1661
- # # Before specific transition:
1662
- # before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
1663
- #
1664
- # # With conditional callback:
1665
- # before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
1666
- #
1667
- # # Using helpers:
1668
- # before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
1669
- # ...
1670
- # end
1671
- # end
1672
- #
1673
- # As can be seen, any number of transitions can be created using various
1674
- # combinations of configuration options.
1675
- def before_transition(*args, &block)
1676
- options = (args.last.is_a?(Hash) ? args.pop : {})
1677
- options[:do] = args if args.any?
1678
- add_callback(:before, options, &block)
1679
- end
1680
-
1681
- # Creates a callback that will be invoked *after* a transition is
1682
- # performed so long as the given requirements match the transition.
1683
- #
1684
- # See +before_transition+ for a description of the possible configurations
1685
- # for defining callbacks.
1686
- def after_transition(*args, &block)
1687
- options = (args.last.is_a?(Hash) ? args.pop : {})
1688
- options[:do] = args if args.any?
1689
- add_callback(:after, options, &block)
1690
- end
1691
-
1692
- # Creates a callback that will be invoked *around* a transition so long as
1693
- # the given requirements match the transition.
1694
- #
1695
- # == The callback
1696
- #
1697
- # Around callbacks wrap transitions, executing code both before and after.
1698
- # These callbacks are defined in the exact same manner as before / after
1699
- # callbacks with the exception that the transition must be yielded to in
1700
- # order to finish running it.
1701
- #
1702
- # If defining +around+ callbacks using blocks, you must yield within the
1703
- # transition by directly calling the block (since yielding is not allowed
1704
- # within blocks).
1705
- #
1706
- # For example,
1707
- #
1708
- # class Vehicle
1709
- # state_machine do
1710
- # around_transition do |block|
1711
- # Benchmark.measure { block.call }
1712
- # end
1713
- #
1714
- # around_transition do |vehicle, block|
1715
- # logger.info "vehicle was #{state}..."
1716
- # block.call
1717
- # logger.info "...and is now #{state}"
1718
- # end
1719
- #
1720
- # around_transition do |vehicle, transition, block|
1721
- # logger.info "before #{transition.event}: #{vehicle.state}"
1722
- # block.call
1723
- # logger.info "after #{transition.event}: #{vehicle.state}"
1724
- # end
1725
- # end
1726
- # end
1727
- #
1728
- # Notice that referencing the block is similar to doing so within an
1729
- # actual method definition in that it is always the last argument.
1730
- #
1731
- # On the other hand, if you're defining +around+ callbacks using method
1732
- # references, you can yield like normal:
1733
- #
1734
- # class Vehicle
1735
- # state_machine do
1736
- # around_transition :benchmark
1737
- # ...
1738
- # end
1739
- #
1740
- # def benchmark
1741
- # Benchmark.measure { yield }
1742
- # end
1743
- # end
1744
- #
1745
- # See +before_transition+ for a description of the possible configurations
1746
- # for defining callbacks.
1747
- def around_transition(*args, &block)
1748
- options = (args.last.is_a?(Hash) ? args.pop : {})
1749
- options[:do] = args if args.any?
1750
- add_callback(:around, options, &block)
1751
- end
1752
-
1753
- # Creates a callback that will be invoked *after* a transition failures to
1754
- # be performed so long as the given requirements match the transition.
1755
- #
1756
- # See +before_transition+ for a description of the possible configurations
1757
- # for defining callbacks. *Note* however that you cannot define the state
1758
- # requirements in these callbacks. You may only define event requirements.
1759
- #
1760
- # = The callback
1761
- #
1762
- # Failure callbacks get invoked whenever an event fails to execute. This
1763
- # can happen when no transition is available, a +before+ callback halts
1764
- # execution, or the action associated with this machine fails to succeed.
1765
- # In any of these cases, any failure callback that matches the attempted
1766
- # transition will be run.
1767
- #
1768
- # For example,
1769
- #
1770
- # class Vehicle
1771
- # state_machine do
1772
- # after_failure do |vehicle, transition|
1773
- # logger.error "vehicle #{vehicle} failed to transition on #{transition.event}"
1774
- # end
1775
- #
1776
- # after_failure :on => :ignite, :do => :log_ignition_failure
1777
- #
1778
- # ...
1779
- # end
1780
- # end
1781
- def after_failure(*args, &block)
1782
- options = (args.last.is_a?(Hash) ? args.pop : {})
1783
- options[:do] = args if args.any?
1784
- assert_valid_keys(options, :on, :do, :if, :unless)
1785
-
1786
- add_callback(:failure, options, &block)
1787
- end
1788
-
1789
- # Generates a list of the possible transition sequences that can be run on
1790
- # the given object. These paths can reveal all of the possible states and
1791
- # events that can be encountered in the object's state machine based on the
1792
- # object's current state.
1793
- #
1794
- # Configuration options:
1795
- # * +from+ - The initial state to start all paths from. By default, this
1796
- # is the object's current state.
1797
- # * +to+ - The target state to end all paths on. By default, paths will
1798
- # end when they loop back to the first transition on the path.
1799
- # * +deep+ - Whether to allow the target state to be crossed more than once
1800
- # in a path. By default, paths will immediately stop when the target
1801
- # state (if specified) is reached. If this is enabled, then paths can
1802
- # continue even after reaching the target state; they will stop when
1803
- # reaching the target state a second time.
1804
- #
1805
- # *Note* that the object is never modified when the list of paths is
1806
- # generated.
1807
- #
1808
- # == Examples
1809
- #
1810
- # class Vehicle
1811
- # state_machine :initial => :parked do
1812
- # event :ignite do
1813
- # transition :parked => :idling
1814
- # end
1815
- #
1816
- # event :shift_up do
1817
- # transition :idling => :first_gear, :first_gear => :second_gear
1818
- # end
1819
- #
1820
- # event :shift_down do
1821
- # transition :second_gear => :first_gear, :first_gear => :idling
1822
- # end
1823
- # end
1824
- # end
1825
- #
1826
- # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
1827
- # vehicle.state # => "parked"
1828
- #
1829
- # vehicle.state_paths
1830
- # # => [
1831
- # # [#<EnumStateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
1832
- # # #<EnumStateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
1833
- # # #<EnumStateMachine::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>,
1834
- # # #<EnumStateMachine::Transition attribute=:state event=:shift_down from="second_gear" from_name=:second_gear to="first_gear" to_name=:first_gear>,
1835
- # # #<EnumStateMachine::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>],
1836
- # #
1837
- # # [#<EnumStateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
1838
- # # #<EnumStateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
1839
- # # #<EnumStateMachine::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>]
1840
- # # ]
1841
- #
1842
- # vehicle.state_paths(:from => :parked, :to => :second_gear)
1843
- # # => [
1844
- # # [#<EnumStateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
1845
- # # #<EnumStateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
1846
- # # #<EnumStateMachine::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>]
1847
- # # ]
1848
- #
1849
- # In addition to getting the possible paths that can be accessed, you can
1850
- # also get summary information about the states / events that can be
1851
- # accessed at some point along one of the paths. For example:
1852
- #
1853
- # # Get the list of states that can be accessed from the current state
1854
- # vehicle.state_paths.to_states # => [:idling, :first_gear, :second_gear]
1855
- #
1856
- # # Get the list of events that can be accessed from the current state
1857
- # vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down]
1858
- def paths_for(object, requirements = {})
1859
- PathCollection.new(object, self, requirements)
1860
- end
1861
-
1862
- # Marks the given object as invalid with the given message.
1863
- #
1864
- # By default, this is a no-op.
1865
- def invalidate(object, attribute, message, values = [])
1866
- end
1867
-
1868
- # Gets a description of the errors for the given object. This is used to
1869
- # provide more detailed information when an InvalidTransition exception is
1870
- # raised.
1871
- def errors_for(object)
1872
- ''
1873
- end
1874
-
1875
- # Resets any errors previously added when invalidating the given object.
1876
- #
1877
- # By default, this is a no-op.
1878
- def reset(object)
1879
- end
1880
-
1881
- # Generates the message to use when invalidating the given object after
1882
- # failing to transition on a specific event
1883
- def generate_message(name, values = [])
1884
- message = (@messages[name] || self.class.default_messages[name])
1885
-
1886
- # Check whether there are actually any values to interpolate to avoid
1887
- # any warnings
1888
- if message.scan(/%./).any? {|match| match != '%%'}
1889
- message % values.map {|value| value.last}
1890
- else
1891
- message
1892
- end
1893
- end
1894
-
1895
- # Runs a transaction, rolling back any changes if the yielded block fails.
1896
- #
1897
- # This is only applicable to integrations that involve databases. By
1898
- # default, this will not run any transactions since the changes aren't
1899
- # taking place within the context of a database.
1900
- def within_transaction(object)
1901
- if use_transactions
1902
- transaction(object) { yield }
1903
- else
1904
- yield
1905
- end
1906
- end
1907
-
1908
- # Draws a directed graph of the machine for visualizing the various events,
1909
- # states, and their transitions.
1910
- #
1911
- # This requires both the Ruby graphviz gem and the graphviz library be
1912
- # installed on the system.
1913
- #
1914
- # Configuration options:
1915
- # * <tt>:name</tt> - The name of the file to write to (without the file extension).
1916
- # Default is "#{owner_class.name}_#{name}"
1917
- # * <tt>:path</tt> - The path to write the graph file to. Default is the
1918
- # current directory (".").
1919
- # * <tt>:format</tt> - The image format to generate the graph in.
1920
- # Default is "png'.
1921
- # * <tt>:font</tt> - The name of the font to draw state names in.
1922
- # Default is "Arial".
1923
- # * <tt>:orientation</tt> - The direction of the graph ("portrait" or
1924
- # "landscape"). Default is "portrait".
1925
- # * <tt>:human_names</tt> - Whether to use human state / event names for
1926
- # node labels on the graph instead of the internal name. Default is false.
1927
- def draw(graph_options = {})
1928
- name = graph_options.delete(:name) || "#{owner_class.name}_#{self.name}"
1929
- draw_options = {:human_name => false}
1930
- draw_options[:human_name] = graph_options.delete(:human_names) if graph_options.include?(:human_names)
1931
-
1932
- graph = Graph.new(name, graph_options)
1933
-
1934
- # Add nodes / edges
1935
- states.by_priority.each {|state| state.draw(graph, draw_options)}
1936
- events.each {|event| event.draw(graph, draw_options)}
1937
-
1938
- # Output result
1939
- graph.output
1940
- graph
1941
- end
1942
-
1943
- # Determines whether an action hook was defined for firing attribute-based
1944
- # event transitions when the configured action gets called.
1945
- def action_hook?(self_only = false)
1946
- @action_hook_defined || !self_only && owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self && machine.action_hook?(true)}
1947
- end
1948
-
1949
- protected
1950
- # Runs additional initialization hooks. By default, this is a no-op.
1951
- def after_initialize
1952
- end
1953
-
1954
- # Looks up other machines that have been defined in the owner class and
1955
- # are targeting the same attribute as this machine. When accessing
1956
- # sibling machines, they will be automatically copied for the current
1957
- # class if they haven't been already. This ensures that any configuration
1958
- # changes made to the sibling machines only affect this class and not any
1959
- # base class that may have originally defined the machine.
1960
- def sibling_machines
1961
- owner_class.state_machines.inject([]) do |machines, (name, machine)|
1962
- if machine.attribute == attribute && machine != self
1963
- machines << (owner_class.state_machine(name) {})
1964
- end
1965
- machines
1966
- end
1967
- end
1968
-
1969
- # Determines if the machine's attribute needs to be initialized. This
1970
- # will only be true if the machine's attribute is blank.
1971
- def initialize_state?(object)
1972
- value = read(object, :state)
1973
- (value.nil? || value.respond_to?(:empty?) && value.empty?) && !states[value, :value]
1974
- end
1975
-
1976
- # Adds helper methods for interacting with the state machine, including
1977
- # for states, events, and transitions
1978
- def define_helpers
1979
- define_state_accessor
1980
- define_state_predicate
1981
- define_event_helpers
1982
- define_path_helpers
1983
- define_action_helpers if define_action_helpers?
1984
- define_name_helpers
1985
- end
1986
-
1987
- # Defines the initial values for state machine attributes. Static values
1988
- # are set prior to the original initialize method and dynamic values are
1989
- # set *after* the initialize method in case it is dependent on it.
1990
- def define_state_initializer
1991
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
1992
- def initialize(*)
1993
- self.class.state_machines.initialize_states(self) { super }
1994
- end
1995
- end_eval
1996
- end
1997
-
1998
- # Adds reader/writer methods for accessing the state attribute
1999
- def define_state_accessor
2000
- attribute = self.attribute
2001
-
2002
- @helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute)
2003
- @helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=")
2004
- end
2005
-
2006
- # Adds predicate method to the owner class for determining the name of the
2007
- # current state
2008
- def define_state_predicate
2009
- call_super = !!owner_class_ancestor_has_method?(:instance, "#{name}?")
2010
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
2011
- def #{name}?(*args)
2012
- args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
2013
- end
2014
- end_eval
2015
- end
2016
-
2017
- # Adds helper methods for getting information about this state machine's
2018
- # events
2019
- def define_event_helpers
2020
- # Gets the events that are allowed to fire on the current object
2021
- define_helper(:instance, attribute(:events)) do |machine, object, *args|
2022
- machine.events.valid_for(object, *args).map {|event| event.name}
2023
- end
2024
-
2025
- # Gets the next possible transitions that can be run on the current
2026
- # object
2027
- define_helper(:instance, attribute(:transitions)) do |machine, object, *args|
2028
- machine.events.transitions_for(object, *args)
2029
- end
2030
-
2031
- # Fire an arbitrary event for this machine
2032
- define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args|
2033
- machine.events.fetch(event).fire(object, *args)
2034
- end
2035
-
2036
- # Add helpers for tracking the event / transition to invoke when the
2037
- # action is called
2038
- if action
2039
- event_attribute = attribute(:event)
2040
- define_helper(:instance, event_attribute) do |machine, object|
2041
- # Interpret non-blank events as present
2042
- event = machine.read(object, :event, true)
2043
- event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
2044
- end
2045
-
2046
- # A roundabout way of writing the attribute is used here so that
2047
- # integrations can hook into this modification
2048
- define_helper(:instance, "#{event_attribute}=") do |machine, object, value|
2049
- machine.write(object, :event, value, true)
2050
- end
2051
-
2052
- event_transition_attribute = attribute(:event_transition)
2053
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
2054
- protected; attr_accessor #{event_transition_attribute.inspect}
2055
- end_eval
2056
- end
2057
- end
2058
-
2059
- # Adds helper methods for getting information about this state machine's
2060
- # available transition paths
2061
- def define_path_helpers
2062
- # Gets the paths of transitions available to the current object
2063
- define_helper(:instance, attribute(:paths)) do |machine, object, *args|
2064
- machine.paths_for(object, *args)
2065
- end
2066
- end
2067
-
2068
- # Determines whether action helpers should be defined for this machine.
2069
- # This is only true if there is an action configured and no other machines
2070
- # have process this same configuration already.
2071
- def define_action_helpers?
2072
- action && !owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self}
2073
- end
2074
-
2075
- # Adds helper methods for automatically firing events when an action
2076
- # is invoked
2077
- def define_action_helpers
2078
- if action_hook
2079
- @action_hook_defined = true
2080
- define_action_hook
2081
- end
2082
- end
2083
-
2084
- # Hooks directly into actions by defining the same method in an included
2085
- # module. As a result, when the action gets invoked, any state events
2086
- # defined for the object will get run. Method visibility is preserved.
2087
- def define_action_hook
2088
- action_hook = self.action_hook
2089
- action = self.action
2090
- private_action_hook = owner_class.private_method_defined?(action_hook)
2091
-
2092
- # Only define helper if it hasn't
2093
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
2094
- def #{action_hook}(*)
2095
- self.class.state_machines.transitions(self, #{action.inspect}).perform { super }
2096
- end
2097
-
2098
- private #{action_hook.inspect} if #{private_action_hook}
2099
- end_eval
2100
- end
2101
-
2102
- # The method to hook into for triggering transitions when invoked. By
2103
- # default, this is the action configured for the machine.
2104
- #
2105
- # Since the default hook technique relies on module inheritance, the
2106
- # action must be defined in an ancestor of the owner classs in order for
2107
- # it to be the action hook.
2108
- def action_hook
2109
- action && owner_class_ancestor_has_method?(:instance, action) ? action : nil
2110
- end
2111
-
2112
- # Determines whether there's already a helper method defined within the
2113
- # given scope. This is true only if one of the owner's ancestors defines
2114
- # the method and is further along in the ancestor chain than this
2115
- # machine's helper module.
2116
- def owner_class_ancestor_has_method?(scope, method)
2117
- superclasses = owner_class.ancestors[1..-1].select {|ancestor| ancestor.is_a?(Class)}
2118
-
2119
- if scope == :class
2120
- # Use singleton classes
2121
- current = (class << owner_class; self; end)
2122
- superclass = superclasses.first
2123
- else
2124
- current = owner_class
2125
- superclass = owner_class.superclass
2126
- end
2127
-
2128
- # Generate the list of modules that *only* occur in the owner class, but
2129
- # were included *prior* to the helper modules, in addition to the
2130
- # superclasses
2131
- ancestors = current.ancestors - superclass.ancestors + superclasses
2132
- ancestors = ancestors[ancestors.index(@helper_modules[scope])..-1].reverse
2133
-
2134
- # Search for for the first ancestor that defined this method
2135
- ancestors.detect do |ancestor|
2136
- ancestor = (class << ancestor; self; end) if scope == :class && ancestor.is_a?(Class)
2137
- ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
2138
- end
2139
- end
2140
-
2141
- # Adds helper methods for accessing naming information about states and
2142
- # events on the owner class
2143
- def define_name_helpers
2144
- # Gets the humanized version of a state
2145
- define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state|
2146
- machine.states.fetch(state).human_name(klass)
2147
- end
2148
-
2149
- # Gets the humanized version of an event
2150
- define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event|
2151
- machine.events.fetch(event).human_name(klass)
2152
- end
2153
-
2154
- # Gets the state name for the current value
2155
- define_helper(:instance, attribute(:name)) do |machine, object|
2156
- machine.states.match!(object).name
2157
- end
2158
-
2159
- # Gets the human state name for the current value
2160
- define_helper(:instance, "human_#{attribute(:name)}") do |machine, object|
2161
- machine.states.match!(object).human_name(object.class)
2162
- end
2163
- end
2164
-
2165
- # Defines the with/without scope helpers for this attribute. Both the
2166
- # singular and plural versions of the attribute are defined for each
2167
- # scope helper. A custom plural can be specified if it cannot be
2168
- # automatically determined by either calling +pluralize+ on the attribute
2169
- # name or adding an "s" to the end of the name.
2170
- def define_scopes(custom_plural = nil)
2171
- plural = custom_plural || pluralize(name)
2172
-
2173
- [:with, :without].each do |kind|
2174
- [name, plural].map {|s| s.to_s}.uniq.each do |suffix|
2175
- method = "#{kind}_#{suffix}"
2176
-
2177
- if scope = send("create_#{kind}_scope", method)
2178
- # Converts state names to their corresponding values so that they
2179
- # can be looked up properly
2180
- define_helper(:class, method) do |machine, klass, *states|
2181
- run_scope(scope, machine, klass, states)
2182
- end
2183
- end
2184
- end
2185
- end
2186
- end
2187
-
2188
- # Generates the results for the given scope based on one or more states to
2189
- # filter by
2190
- def run_scope(scope, machine, klass, states)
2191
- values = states.flatten.map {|state| machine.states.fetch(state).value}
2192
- scope.call(klass, values)
2193
- end
2194
-
2195
- # Pluralizes the given word using #pluralize (if available) or simply
2196
- # adding an "s" to the end of the word
2197
- def pluralize(word)
2198
- word = word.to_s
2199
- if word.respond_to?(:pluralize)
2200
- word.pluralize
2201
- else
2202
- "#{name}s"
2203
- end
2204
- end
2205
-
2206
- # Creates a scope for finding objects *with* a particular value or values
2207
- # for the attribute.
2208
- #
2209
- # By default, this is a no-op.
2210
- def create_with_scope(name)
2211
- end
2212
-
2213
- # Creates a scope for finding objects *without* a particular value or
2214
- # values for the attribute.
2215
- #
2216
- # By default, this is a no-op.
2217
- def create_without_scope(name)
2218
- end
2219
-
2220
- # Always yields
2221
- def transaction(object)
2222
- yield
2223
- end
2224
-
2225
- # Gets the initial attribute value defined by the owner class (outside of
2226
- # the machine's definition). By default, this is always nil.
2227
- def owner_class_attribute_default
2228
- nil
2229
- end
2230
-
2231
- # Checks whether the given state matches the attribute default specified
2232
- # by the owner class
2233
- def owner_class_attribute_default_matches?(state)
2234
- state.matches?(owner_class_attribute_default)
2235
- end
2236
-
2237
- # Updates this machine based on the configuration of other machines in the
2238
- # owner class that share the same target attribute.
2239
- def add_sibling_machine_configs
2240
- # Add existing states
2241
- sibling_machines.each do |machine|
2242
- machine.states.each {|state| states << state unless states[state.name]}
2243
- end
2244
- end
2245
-
2246
- # Adds a new transition callback of the given type.
2247
- def add_callback(type, options, &block)
2248
- callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block)
2249
- add_states(callback.known_states)
2250
- callback
2251
- end
2252
-
2253
- # Tracks the given set of states in the list of all known states for
2254
- # this machine
2255
- def add_states(new_states)
2256
- new_states.map do |new_state|
2257
- # Check for other states that use a different class type for their name.
2258
- # This typically prevents string / symbol misuse.
2259
- if new_state && conflict = states.detect {|state| state.name && state.name.class != new_state.class}
2260
- raise ArgumentError, "#{new_state.inspect} state defined as #{new_state.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all states must be consistent"
2261
- end
2262
-
2263
- unless state = states[new_state]
2264
- states << state = State.new(self, new_state)
2265
-
2266
- # Copy states over to sibling machines
2267
- sibling_machines.each {|machine| machine.states << state}
2268
- end
2269
-
2270
- state
2271
- end
2272
- end
2273
-
2274
- # Tracks the given set of events in the list of all known events for
2275
- # this machine
2276
- def add_events(new_events)
2277
- new_events.map do |new_event|
2278
- # Check for other states that use a different class type for their name.
2279
- # This typically prevents string / symbol misuse.
2280
- if conflict = events.detect {|event| event.name.class != new_event.class}
2281
- raise ArgumentError, "#{new_event.inspect} event defined as #{new_event.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all events must be consistent"
2282
- end
2283
-
2284
- unless event = events[new_event]
2285
- events << event = Event.new(self, new_event)
2286
- end
2287
-
2288
- event
2289
- end
2290
- end
2291
- end
2292
- end