pluginaweek-state_machine 0.7.6 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +21 -1
- data/README.rdoc +1 -1
- data/Rakefile +1 -1
- data/lib/state_machine.rb +24 -65
- data/lib/state_machine/callback.rb +1 -1
- data/lib/state_machine/event.rb +10 -9
- data/lib/state_machine/event_collection.rb +21 -10
- data/lib/state_machine/extensions.rb +2 -11
- data/lib/state_machine/guard.rb +12 -1
- data/lib/state_machine/integrations/active_record.rb +51 -13
- data/lib/state_machine/integrations/active_record/locale.rb +1 -0
- data/lib/state_machine/integrations/data_mapper.rb +22 -10
- data/lib/state_machine/integrations/data_mapper/observer.rb +5 -5
- data/lib/state_machine/integrations/sequel.rb +36 -7
- data/lib/state_machine/machine.rb +91 -55
- data/lib/state_machine/machine_collection.rb +15 -13
- data/lib/state_machine/state.rb +4 -4
- data/lib/state_machine/transition.rb +41 -14
- data/test/unit/assertions_test.rb +3 -3
- data/test/unit/eval_helpers_test.rb +13 -22
- data/test/unit/event_collection_test.rb +26 -2
- data/test/unit/event_test.rb +94 -0
- data/test/unit/guard_test.rb +46 -0
- data/test/unit/integrations/active_record_test.rb +294 -11
- data/test/unit/integrations/data_mapper_test.rb +214 -1
- data/test/unit/integrations/sequel_test.rb +321 -7
- data/test/unit/machine_collection_test.rb +42 -19
- data/test/unit/machine_test.rb +104 -10
- data/test/unit/state_test.rb +1 -1
- data/test/unit/transition_test.rb +108 -9
- metadata +4 -3
data/CHANGELOG.rdoc
CHANGED
@@ -1,6 +1,26 @@
|
|
1
1
|
== master
|
2
2
|
|
3
|
-
|
3
|
+
== 0.8.0 / 2009-08-15
|
4
|
+
|
5
|
+
* Add support for DataMapper 0.10.0
|
6
|
+
* Always interpet nil return values from actions as failed attempts
|
7
|
+
* Fix loopbacks not causing records to save in ORM integrations if no other fields were changed
|
8
|
+
* Fix events not failing with useful errors when an object's state is invalid
|
9
|
+
* Use more friendly NoMethodError messages for state-driven behaviors
|
10
|
+
* Fix before_transition callbacks getting run twice when using event attributes in ORM integrations
|
11
|
+
* Add the ability to query for the availability of specific transitions on an object
|
12
|
+
* Allow after_transition callbacks to be explicitly run on failed attempts
|
13
|
+
* By default, don't run after_transition callbacks on failed attempts
|
14
|
+
* Fix not allowing multiple methods to be specified as arguments in callbacks
|
15
|
+
* Fix initial states being set when loading records from the database in Sequel integration
|
16
|
+
* Allow static initial states to be set earlier in the initialization of an object
|
17
|
+
* Use friendly validation errors for nil states
|
18
|
+
* Fix states not being validated properly when using custom names in ActiveRecord / DataMapper integrations
|
19
|
+
|
20
|
+
== 0.7.6 / 2009-06-17
|
21
|
+
|
22
|
+
* Allow multiple state machines on the same class to target the same attribute
|
23
|
+
* Add support for :attribute to customize the attribute target, assuming the name is the first argument of #state_machine
|
4
24
|
* Simplify reading from / writing to machine-related attributes on objects
|
5
25
|
* Fix locale for ActiveRecord getting added to the i18n load path multiple times [Reiner Dieterich]
|
6
26
|
* Fix callbacks, guards, and state-driven behaviors not always working on tainted classes [Brandon Dimcheff]
|
data/README.rdoc
CHANGED
@@ -412,7 +412,7 @@ To generate multiple state machine graphs:
|
|
412
412
|
|
413
413
|
*Note* that this will generate a different file for every state machine defined
|
414
414
|
in the class. The generated files will use an output filename of the format
|
415
|
-
#{class_name}_#{
|
415
|
+
#{class_name}_#{machine_name}.#{format}.
|
416
416
|
|
417
417
|
For examples of actual images generated using this task, see those under the
|
418
418
|
examples folder.
|
data/Rakefile
CHANGED
@@ -5,7 +5,7 @@ require 'rake/contrib/sshpublisher'
|
|
5
5
|
|
6
6
|
spec = Gem::Specification.new do |s|
|
7
7
|
s.name = 'state_machine'
|
8
|
-
s.version = '0.
|
8
|
+
s.version = '0.8.0'
|
9
9
|
s.platform = Gem::Platform::RUBY
|
10
10
|
s.summary = 'Adds support for creating state machines for attributes on any Ruby class'
|
11
11
|
s.description = s.summary
|
data/lib/state_machine.rb
CHANGED
@@ -5,10 +5,12 @@ require 'state_machine/machine'
|
|
5
5
|
# functionality on any Ruby class.
|
6
6
|
module StateMachine
|
7
7
|
module MacroMethods
|
8
|
-
# Creates a new state machine
|
9
|
-
#
|
8
|
+
# Creates a new state machine with the given name. The default name, if not
|
9
|
+
# specified, is <tt>:state</tt>.
|
10
10
|
#
|
11
11
|
# Configuration options:
|
12
|
+
# * <tt>:attribute</tt> - The name of the attribute to store the state value
|
13
|
+
# in. By default, this is the same as the name of the machine.
|
12
14
|
# * <tt>:initial</tt> - The initial state of the attribute. This can be a
|
13
15
|
# static state or a lambda block which will be evaluated at runtime
|
14
16
|
# (e.g. lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling}).
|
@@ -16,10 +18,6 @@ module StateMachine
|
|
16
18
|
# * <tt>:action</tt> - The instance method to invoke when an object
|
17
19
|
# transitions. Default is nil unless otherwise specified by the
|
18
20
|
# configured integration.
|
19
|
-
# * <tt>:as</tt> - The name to use for prefixing all generated machine
|
20
|
-
# instance / class methods (e.g. if the attribute is +state_id+, then
|
21
|
-
# "state" would generate :state_name, :state_transitions, etc. instead of
|
22
|
-
# :state_id_name and :state_id_transitions)
|
23
21
|
# * <tt>:namespace</tt> - The name to use for namespacing all generated
|
24
22
|
# state / event instance methods (e.g. "heater" would generate
|
25
23
|
# :turn_on_heater and :turn_off_heater for the :turn_on/:turn_off events).
|
@@ -49,12 +47,12 @@ module StateMachine
|
|
49
47
|
# result, you will not be able to access any class methods unless you refer
|
50
48
|
# to them directly (i.e. specifying the class name).
|
51
49
|
#
|
52
|
-
# For examples on the types of
|
50
|
+
# For examples on the types of state machine configurations and blocks, see
|
53
51
|
# the section below.
|
54
52
|
#
|
55
53
|
# == Examples
|
56
54
|
#
|
57
|
-
# With the default attribute and no configuration:
|
55
|
+
# With the default name/attribute and no configuration:
|
58
56
|
#
|
59
57
|
# class Vehicle
|
60
58
|
# state_machine do
|
@@ -64,13 +62,14 @@ module StateMachine
|
|
64
62
|
# end
|
65
63
|
# end
|
66
64
|
#
|
67
|
-
# The above example will define a state machine
|
68
|
-
#
|
65
|
+
# The above example will define a state machine named "state" that will
|
66
|
+
# store the value in the +state+ attribute. Every vehicle will start
|
67
|
+
# without an initial state.
|
69
68
|
#
|
70
|
-
# With a custom attribute:
|
69
|
+
# With a custom name / attribute:
|
71
70
|
#
|
72
71
|
# class Vehicle
|
73
|
-
# state_machine :status do
|
72
|
+
# state_machine :status, :attribute => :status_value do
|
74
73
|
# ...
|
75
74
|
# end
|
76
75
|
# end
|
@@ -94,7 +93,8 @@ module StateMachine
|
|
94
93
|
# == Instance Methods
|
95
94
|
#
|
96
95
|
# The following instance methods will be automatically generated by the
|
97
|
-
# state machine. Any existing methods
|
96
|
+
# state machine based on the *name* of the machine. Any existing methods
|
97
|
+
# will not be overwritten.
|
98
98
|
# * <tt>state</tt> - Gets the current value for the attribute
|
99
99
|
# * <tt>state=(value)</tt> - Sets the current value for the attribute
|
100
100
|
# * <tt>state?(name)</tt> - Checks the given state name against the current
|
@@ -102,8 +102,11 @@ module StateMachine
|
|
102
102
|
# * <tt>state_name</tt> - Gets the name of the state for the current value
|
103
103
|
# * <tt>state_events</tt> - Gets the list of events that can be fired on
|
104
104
|
# the current object's state (uses the *unqualified* event names)
|
105
|
-
# * <tt>state_transitions</tt> - Gets the list of possible
|
106
|
-
# that can be made on the current object's state
|
105
|
+
# * <tt>state_transitions(requirements = {})</tt> - Gets the list of possible
|
106
|
+
# transitions that can be made on the current object's state. Additional
|
107
|
+
# requirements, such as the :from / :to state and :on event can be specified
|
108
|
+
# to restrict the transitions to select. By default, the current state
|
109
|
+
# will be used for the :from state.
|
107
110
|
#
|
108
111
|
# For example,
|
109
112
|
#
|
@@ -253,7 +256,7 @@ module StateMachine
|
|
253
256
|
#
|
254
257
|
# class Vehicle
|
255
258
|
# include DataMapper::Resource
|
256
|
-
# property :id,
|
259
|
+
# property :id, Serial
|
257
260
|
#
|
258
261
|
# state_machine :initial => :parked do
|
259
262
|
# event :ignite do
|
@@ -295,55 +298,11 @@ module StateMachine
|
|
295
298
|
# see StateMachine::Machine#before_transition and
|
296
299
|
# StateMachine::Machine#after_transition.
|
297
300
|
#
|
298
|
-
# == Attribute aliases
|
299
|
-
#
|
300
|
-
# When a state machine is defined, several methods are generated scoped by
|
301
|
-
# the name of the attribute, such as (if the attribute were "state"):
|
302
|
-
# * <tt>state_name</tt>
|
303
|
-
# * <tt>state_event</tt>
|
304
|
-
# * <tt>state_transitions</tt>
|
305
|
-
# * etc.
|
306
|
-
#
|
307
|
-
# If the attribute for the machine were something less common, such as
|
308
|
-
# "state_id" or "state_value", this makes for more awkward scoped methods.
|
309
|
-
#
|
310
|
-
# Rather than scope based on the attribute, these methods can be customized
|
311
|
-
# using the <tt>:as</tt> option as essentially an alias.
|
312
|
-
#
|
313
|
-
# For example,
|
314
|
-
#
|
315
|
-
# class Vehicle
|
316
|
-
# state_machine :state_id, :as => :state do
|
317
|
-
# event :turn_on do
|
318
|
-
# transition all => :on
|
319
|
-
# end
|
320
|
-
#
|
321
|
-
# event :turn_off do
|
322
|
-
# transition all => :off
|
323
|
-
# end
|
324
|
-
#
|
325
|
-
# state :on, :value => 1
|
326
|
-
# state :off, :value => 2
|
327
|
-
# end
|
328
|
-
# end
|
329
|
-
#
|
330
|
-
# ...will generate the following methods:
|
331
|
-
# * <tt>state_name</tt>
|
332
|
-
# * <tt>state_event</tt>
|
333
|
-
# * <tt>state_transitions</tt>
|
334
|
-
#
|
335
|
-
# ...instead of:
|
336
|
-
# * <tt>state_id_name</tt>
|
337
|
-
# * <tt>state_id_event</tt>
|
338
|
-
# * <tt>state_id_transitions</tt>
|
339
|
-
#
|
340
|
-
# However, it will continue to read and write to the +state_id+ attribute.
|
341
|
-
#
|
342
301
|
# == Namespaces
|
343
302
|
#
|
344
303
|
# When a namespace is configured for a state machine, the name provided
|
345
304
|
# will be used in generating the instance methods for interacting with
|
346
|
-
# events
|
305
|
+
# states/events in the machine. This is particularly useful when a class
|
347
306
|
# has multiple state machines and it would be difficult to differentiate
|
348
307
|
# between the various states / events.
|
349
308
|
#
|
@@ -398,7 +357,7 @@ module StateMachine
|
|
398
357
|
#
|
399
358
|
# For integrations that support it, a group of default scope filters will
|
400
359
|
# be automatically created for assisting in finding objects that have the
|
401
|
-
# attribute set to
|
360
|
+
# attribute set to one of a given set of states.
|
402
361
|
#
|
403
362
|
# For example,
|
404
363
|
#
|
@@ -412,9 +371,9 @@ module StateMachine
|
|
412
371
|
# :with_state, :with_states, :without_state, or :without_states), then a
|
413
372
|
# scope will not be defined for that name.
|
414
373
|
#
|
415
|
-
# See StateMachine::Machine for more information about using
|
416
|
-
#
|
417
|
-
#
|
374
|
+
# See StateMachine::Machine for more information about using integrations
|
375
|
+
# and the individual integration docs for information about the actual
|
376
|
+
# scopes that are generated.
|
418
377
|
def state_machine(*args, &block)
|
419
378
|
StateMachine::Machine.find_or_create(self, *args, &block)
|
420
379
|
end
|
@@ -142,7 +142,7 @@ module StateMachine
|
|
142
142
|
# requirements configured for this callback.
|
143
143
|
#
|
144
144
|
# If a terminator has been configured and it matches the result from the
|
145
|
-
# evaluated method, then the callback chain should be halted
|
145
|
+
# evaluated method, then the callback chain should be halted.
|
146
146
|
def call(object, context = {}, *args)
|
147
147
|
if @guard.matches?(object, context)
|
148
148
|
@methods.each do |method|
|
data/lib/state_machine/event.rb
CHANGED
@@ -65,8 +65,8 @@ module StateMachine
|
|
65
65
|
# state to be +idling+ if it's current state is +parked+ or +first_gear+
|
66
66
|
# if it's current state is +idling+.
|
67
67
|
#
|
68
|
-
# To help
|
69
|
-
# for
|
68
|
+
# To help define these implicit transitions, a set of helpers are available
|
69
|
+
# for slightly more complex matching:
|
70
70
|
# * <tt>all</tt> - Matches every state in the machine
|
71
71
|
# * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
|
72
72
|
# * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
|
@@ -147,7 +147,7 @@ module StateMachine
|
|
147
147
|
# requirements
|
148
148
|
assert_valid_keys(options, :from, :to, :except_from, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
|
149
149
|
|
150
|
-
guards << guard = Guard.new(options)
|
150
|
+
guards << guard = Guard.new(options.merge(:on => name))
|
151
151
|
@known_states |= guard.known_states
|
152
152
|
guard
|
153
153
|
end
|
@@ -162,15 +162,16 @@ module StateMachine
|
|
162
162
|
|
163
163
|
# Finds and builds the next transition that can be performed on the given
|
164
164
|
# object. If no transitions can be made, then this will return nil.
|
165
|
-
def transition_for(object)
|
166
|
-
from = machine.states.match(object).name
|
165
|
+
def transition_for(object, requirements = {})
|
166
|
+
requirements[:from] = machine.states.match!(object).name unless custom_from_state = requirements.include?(:from)
|
167
167
|
|
168
168
|
guards.each do |guard|
|
169
|
-
if match = guard.match(object,
|
169
|
+
if match = guard.match(object, requirements)
|
170
170
|
# Guard allows for the transition to occur
|
171
|
+
from = requirements[:from]
|
171
172
|
to = match[:to].values.empty? ? from : match[:to].values.first
|
172
173
|
|
173
|
-
return Transition.new(object, machine, name, from, to)
|
174
|
+
return Transition.new(object, machine, name, from, to, !custom_from_state)
|
174
175
|
end
|
175
176
|
end
|
176
177
|
|
@@ -190,7 +191,7 @@ module StateMachine
|
|
190
191
|
if transition = transition_for(object)
|
191
192
|
transition.perform(*args)
|
192
193
|
else
|
193
|
-
machine.invalidate(object,
|
194
|
+
machine.invalidate(object, :state, :invalid_transition, [[:event, name]])
|
194
195
|
false
|
195
196
|
end
|
196
197
|
end
|
@@ -244,7 +245,7 @@ module StateMachine
|
|
244
245
|
|
245
246
|
# Fires the event, raising an exception if it fails
|
246
247
|
machine.define_instance_method("#{qualified_name}!") do |machine, object, *args|
|
247
|
-
object.send(qualified_name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{machine.name} via :#{name} from #{machine.states.match(object).name.inspect}")
|
248
|
+
object.send(qualified_name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{machine.name} via :#{name} from #{machine.states.match!(object).name.inspect}")
|
248
249
|
end
|
249
250
|
end
|
250
251
|
end
|
@@ -34,6 +34,14 @@ module StateMachine
|
|
34
34
|
|
35
35
|
# Gets the list of transitions that can be run on the given object.
|
36
36
|
#
|
37
|
+
# Valid requirement options:
|
38
|
+
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
39
|
+
# are specified, then this will be the object's current state.
|
40
|
+
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
41
|
+
# specified, then this will match any to state.
|
42
|
+
# * <tt>:on</tt> - One or more events that fire the transition. If none
|
43
|
+
# are specified, then this will match any event.
|
44
|
+
#
|
37
45
|
# == Examples
|
38
46
|
#
|
39
47
|
# class Vehicle
|
@@ -50,22 +58,25 @@ module StateMachine
|
|
50
58
|
#
|
51
59
|
# events = Vehicle.state_machine.events
|
52
60
|
#
|
53
|
-
# vehicle = Vehicle.new
|
54
|
-
# events.transitions_for(vehicle)
|
61
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
62
|
+
# events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
|
55
63
|
#
|
56
64
|
# vehicle.state = 'idling'
|
57
|
-
# events.transitions_for(vehicle)
|
58
|
-
|
59
|
-
|
65
|
+
# events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
|
66
|
+
#
|
67
|
+
# # Search for explicit transitions regardless of the current state
|
68
|
+
# events.transitions_for(vehicle, :from => :parked) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
|
69
|
+
def transitions_for(object, requirements = {})
|
70
|
+
map {|event| event.transition_for(object, requirements)}.compact
|
60
71
|
end
|
61
72
|
|
62
73
|
# Gets the transition that should be performed for the event stored in the
|
63
74
|
# given object's event attribute. This also takes an additional parameter
|
64
|
-
# for automatically invalidating the object if the event or transition
|
65
|
-
#
|
75
|
+
# for automatically invalidating the object if the event or transition are
|
76
|
+
# invalid. By default, this is turned off.
|
66
77
|
#
|
67
|
-
# *Note* that if a transition has already been generated for the event,
|
68
|
-
#
|
78
|
+
# *Note* that if a transition has already been generated for the event, then
|
79
|
+
# that transition will be used.
|
69
80
|
#
|
70
81
|
# == Examples
|
71
82
|
#
|
@@ -97,7 +108,7 @@ module StateMachine
|
|
97
108
|
if event = self[event_name.to_sym, :name]
|
98
109
|
unless result = machine.read(object, :event_transition) || event.transition_for(object)
|
99
110
|
# No valid transition: invalidate
|
100
|
-
machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).name]]) if invalidate
|
111
|
+
machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).name || 'nil']]) if invalidate
|
101
112
|
result = false
|
102
113
|
end
|
103
114
|
else
|
@@ -22,15 +22,6 @@ module StateMachine
|
|
22
22
|
end
|
23
23
|
|
24
24
|
module InstanceMethods
|
25
|
-
# Defines the initial values for state machine attributes. The values
|
26
|
-
# will be set *after* the original initialize method is invoked. This is
|
27
|
-
# necessary in order to ensure that the object is initialized before
|
28
|
-
# dynamic initial attributes are evaluated.
|
29
|
-
def initialize(*args, &block)
|
30
|
-
super
|
31
|
-
initialize_state_machines
|
32
|
-
end
|
33
|
-
|
34
25
|
# Runs one or more events in parallel. All events will run through the
|
35
26
|
# following steps:
|
36
27
|
# * Before callbacks
|
@@ -151,8 +142,8 @@ module StateMachine
|
|
151
142
|
end
|
152
143
|
|
153
144
|
protected
|
154
|
-
def initialize_state_machines #:nodoc:
|
155
|
-
self.class.state_machines.initialize_states(self)
|
145
|
+
def initialize_state_machines(options = {}) #:nodoc:
|
146
|
+
self.class.state_machines.initialize_states(self, options)
|
156
147
|
end
|
157
148
|
end
|
158
149
|
end
|
data/lib/state_machine/guard.rb
CHANGED
@@ -24,6 +24,9 @@ module StateMachine
|
|
24
24
|
# requirements contain a mapping of {:from => matcher, :to => matcher}.
|
25
25
|
attr_reader :state_requirements
|
26
26
|
|
27
|
+
# The requirement for verifying the success of the event
|
28
|
+
attr_reader :success_requirement
|
29
|
+
|
27
30
|
# A list of all of the states known to this guard. This will pull states
|
28
31
|
# from the following options (in the same order):
|
29
32
|
# * +from+ / +except_from+
|
@@ -39,6 +42,9 @@ module StateMachine
|
|
39
42
|
# Build event requirement
|
40
43
|
@event_requirement = build_matcher(options, :on, :except_on)
|
41
44
|
|
45
|
+
# Build success requirement
|
46
|
+
@success_requirement = options.delete(:include_failures) ? AllMatcher.instance : WhitelistMatcher.new([true])
|
47
|
+
|
42
48
|
if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on]).empty?
|
43
49
|
# Explicit from/to requirements specified
|
44
50
|
@state_requirements = [{:from => build_matcher(options, :from, :except_from), :to => build_matcher(options, :to, :except_to)}]
|
@@ -185,11 +191,16 @@ module StateMachine
|
|
185
191
|
def match_query(query)
|
186
192
|
query ||= {}
|
187
193
|
|
188
|
-
if match_event(query) && (state_requirement = match_states(query))
|
194
|
+
if match_success(query) && match_event(query) && (state_requirement = match_states(query))
|
189
195
|
state_requirement.merge(:on => event_requirement)
|
190
196
|
end
|
191
197
|
end
|
192
198
|
|
199
|
+
# Verifies that the success requirement matches the given query
|
200
|
+
def match_success(query)
|
201
|
+
matches_requirement?(query, :success, success_requirement)
|
202
|
+
end
|
203
|
+
|
193
204
|
# Verifies that the event requirement matches the given query
|
194
205
|
def match_event(query)
|
195
206
|
matches_requirement?(query, :on, event_requirement)
|
@@ -84,14 +84,14 @@ module StateMachine
|
|
84
84
|
# you can build two state machines (one public and one protected) like so:
|
85
85
|
#
|
86
86
|
# class Vehicle < ActiveRecord::Base
|
87
|
-
# alias_attribute :public_state # Allow both machines to share the same state
|
88
87
|
# attr_protected :state_event # Prevent access to events in the first machine
|
89
88
|
#
|
90
89
|
# state_machine do
|
91
90
|
# # Define private events here
|
92
91
|
# end
|
93
92
|
#
|
94
|
-
#
|
93
|
+
# # Public machine targets the same state as the private machine
|
94
|
+
# state_machine :public_state, :attribute => :state do
|
95
95
|
# # Define public events here
|
96
96
|
# end
|
97
97
|
# end
|
@@ -281,6 +281,14 @@ module StateMachine
|
|
281
281
|
end
|
282
282
|
end
|
283
283
|
|
284
|
+
# Forces the change in state to be recognized regardless of whether the
|
285
|
+
# state value actually changed
|
286
|
+
def write(object, attribute, value)
|
287
|
+
result = super
|
288
|
+
object.send("#{self.attribute}_will_change!") if attribute == :state && object.respond_to?("#{self.attribute}_will_change!")
|
289
|
+
result
|
290
|
+
end
|
291
|
+
|
284
292
|
# Adds a validation error to the given object
|
285
293
|
def invalidate(object, attribute, message, values = [])
|
286
294
|
attribute = self.attribute(attribute)
|
@@ -308,11 +316,41 @@ module StateMachine
|
|
308
316
|
callbacks[:after] << Callback.new {|object, transition| notify(:after, object, transition)}
|
309
317
|
end
|
310
318
|
|
319
|
+
# Defines an initialization hook into the owner class for setting the
|
320
|
+
# initial state of the machine *before* any attributes are set on the
|
321
|
+
# object
|
322
|
+
def define_state_initializer
|
323
|
+
@instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
|
324
|
+
# Ensure that the attributes setter gets used to force initialization
|
325
|
+
# of the state machines
|
326
|
+
def initialize(attributes = nil, *args)
|
327
|
+
attributes ||= {}
|
328
|
+
super
|
329
|
+
end
|
330
|
+
|
331
|
+
# Hooks in to attribute initialization to set the states *prior*
|
332
|
+
# to the attributes being set
|
333
|
+
def attributes=(*args)
|
334
|
+
if new_record? && !@initialized_state_machines
|
335
|
+
@initialized_state_machines = true
|
336
|
+
|
337
|
+
initialize_state_machines(:dynamic => false)
|
338
|
+
super
|
339
|
+
initialize_state_machines(:dynamic => true)
|
340
|
+
else
|
341
|
+
super
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end_eval
|
345
|
+
end
|
346
|
+
|
311
347
|
# Skips defining reader/writer methods since this is done automatically
|
312
348
|
def define_state_accessor
|
349
|
+
name = self.name
|
350
|
+
|
313
351
|
owner_class.validates_each(attribute) do |record, attr, value|
|
314
|
-
machine = record.class.state_machine(
|
315
|
-
machine.invalidate(record,
|
352
|
+
machine = record.class.state_machine(name)
|
353
|
+
machine.invalidate(record, :state, :invalid) unless machine.states.match(record)
|
316
354
|
end
|
317
355
|
end
|
318
356
|
|
@@ -321,13 +359,12 @@ module StateMachine
|
|
321
359
|
# *anything* is set for the attribute's value
|
322
360
|
def define_state_predicate
|
323
361
|
name = self.name
|
324
|
-
attribute = self.attribute
|
325
362
|
|
326
363
|
# Still use class_eval here instance of define_instance_method since
|
327
364
|
# we need to be able to call +super+
|
328
365
|
@instance_helper_module.class_eval do
|
329
366
|
define_method("#{name}?") do |*args|
|
330
|
-
args.empty? ? super(*args) : self.class.state_machine(
|
367
|
+
args.empty? ? super(*args) : self.class.state_machine(name).states.matches?(self, *args)
|
331
368
|
end
|
332
369
|
end
|
333
370
|
end
|
@@ -388,17 +425,18 @@ module StateMachine
|
|
388
425
|
# inheritance is respected properly.
|
389
426
|
def define_scope(name, scope)
|
390
427
|
name = name.to_sym
|
391
|
-
|
428
|
+
machine_name = self.name
|
392
429
|
|
393
|
-
#
|
430
|
+
# Create the scope and then override it with state translation
|
394
431
|
owner_class.named_scope(name)
|
395
432
|
owner_class.scopes[name] = lambda do |klass, *states|
|
396
|
-
machine_states = klass.state_machine(
|
433
|
+
machine_states = klass.state_machine(machine_name).states
|
397
434
|
values = states.flatten.map {|state| machine_states.fetch(state).value}
|
398
435
|
|
399
436
|
::ActiveRecord::NamedScope::Scope.new(klass, scope.call(values))
|
400
437
|
end
|
401
438
|
|
439
|
+
# Prevent the Machine class from wrapping the scope
|
402
440
|
false
|
403
441
|
end
|
404
442
|
|
@@ -409,10 +447,10 @@ module StateMachine
|
|
409
447
|
# * #{type}_#{qualified_event}_from_#{from}
|
410
448
|
# * #{type}_#{qualified_event}_to_#{to}
|
411
449
|
# * #{type}_#{qualified_event}
|
412
|
-
# * #{type}_transition_#{
|
413
|
-
# * #{type}_transition_#{
|
414
|
-
# * #{type}_transition_#{
|
415
|
-
# * #{type}_transition_#{
|
450
|
+
# * #{type}_transition_#{machine_name}_from_#{from}_to_#{to}
|
451
|
+
# * #{type}_transition_#{machine_name}_from_#{from}
|
452
|
+
# * #{type}_transition_#{machine_name}_to_#{to}
|
453
|
+
# * #{type}_transition_#{machine_name}
|
416
454
|
# * #{type}_transition
|
417
455
|
#
|
418
456
|
# This will always return true regardless of the results of the
|