state_machine 0.4.3 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +17 -0
- data/LICENSE +1 -1
- data/README.rdoc +54 -84
- data/Rakefile +1 -1
- data/examples/Car_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +11 -0
- data/examples/car.rb +19 -0
- data/examples/traffic_light.rb +9 -0
- data/examples/vehicle.rb +35 -0
- data/lib/state_machine.rb +65 -52
- data/lib/state_machine/assertions.rb +1 -1
- data/lib/state_machine/callback.rb +13 -9
- data/lib/state_machine/eval_helpers.rb +4 -3
- data/lib/state_machine/event.rb +51 -33
- data/lib/state_machine/extensions.rb +2 -2
- data/lib/state_machine/guard.rb +47 -41
- data/lib/state_machine/integrations.rb +67 -0
- data/lib/state_machine/integrations/active_record.rb +62 -36
- data/lib/state_machine/integrations/active_record/observer.rb +41 -0
- data/lib/state_machine/integrations/data_mapper.rb +23 -37
- data/lib/state_machine/integrations/data_mapper/observer.rb +23 -9
- data/lib/state_machine/integrations/sequel.rb +23 -24
- data/lib/state_machine/machine.rb +380 -277
- data/lib/state_machine/node_collection.rb +142 -0
- data/lib/state_machine/state.rb +114 -69
- data/lib/state_machine/state_collection.rb +38 -0
- data/lib/state_machine/transition.rb +36 -17
- data/test/active_record.log +2940 -85664
- data/test/functional/state_machine_test.rb +49 -53
- data/test/sequel.log +747 -11990
- data/test/unit/assertions_test.rb +2 -1
- data/test/unit/callback_test.rb +14 -12
- data/test/unit/eval_helpers_test.rb +25 -6
- data/test/unit/event_test.rb +144 -124
- data/test/unit/guard_test.rb +118 -140
- data/test/unit/integrations/active_record_test.rb +102 -68
- data/test/unit/integrations/data_mapper_test.rb +48 -37
- data/test/unit/integrations/sequel_test.rb +34 -25
- data/test/unit/integrations_test.rb +42 -0
- data/test/unit/machine_test.rb +460 -531
- data/test/unit/node_collection_test.rb +208 -0
- data/test/unit/state_collection_test.rb +167 -0
- data/test/unit/state_machine_test.rb +1 -1
- data/test/unit/state_test.rb +223 -200
- data/test/unit/transition_test.rb +81 -46
- metadata +17 -3
- data/test/data_mapper.log +0 -30860
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'state_machine/assertions'
|
2
|
+
|
3
|
+
module StateMachine
|
4
|
+
# Represents a collection of nodes in a state machine, be it events or states.
|
5
|
+
class NodeCollection
|
6
|
+
include Enumerable
|
7
|
+
include Assertions
|
8
|
+
|
9
|
+
# Creates a new collection of nodes for the given state machine. By default,
|
10
|
+
# the collection is empty.
|
11
|
+
#
|
12
|
+
# Configuration options:
|
13
|
+
# * <tt>:index</tt> - One or more attributes to automatically generate
|
14
|
+
# hashed indices for in order to perform quick lookups. Default is to
|
15
|
+
# index by the :name attribute
|
16
|
+
def initialize(options = {})
|
17
|
+
assert_valid_keys(options, :index)
|
18
|
+
options = {:index => :name}.merge(options)
|
19
|
+
|
20
|
+
@nodes = []
|
21
|
+
@indices = Array(options[:index]).inject({}) {|indices, attribute| indices[attribute] = {}; indices}
|
22
|
+
@default_index = Array(options[:index]).first
|
23
|
+
end
|
24
|
+
|
25
|
+
# Creates a copy of this collection such that modifications don't affect
|
26
|
+
# the original collection
|
27
|
+
def initialize_copy(orig) #:nodoc:
|
28
|
+
super
|
29
|
+
|
30
|
+
nodes = @nodes
|
31
|
+
@nodes = []
|
32
|
+
@indices = @indices.inject({}) {|indices, (name, index)| indices[name] = {}; indices}
|
33
|
+
nodes.each {|node| self << node.dup}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Changes the current machine associated with the collection. In turn, this
|
37
|
+
# will change the state machine associated with each node in the collection.
|
38
|
+
def machine=(new_machine)
|
39
|
+
each {|node| node.machine = new_machine}
|
40
|
+
end
|
41
|
+
|
42
|
+
# Gets the number of nodes in this collection
|
43
|
+
def length
|
44
|
+
@nodes.length
|
45
|
+
end
|
46
|
+
|
47
|
+
# Gets the set of unique keys for the given index
|
48
|
+
def keys(index_name = @default_index)
|
49
|
+
index(index_name).keys
|
50
|
+
end
|
51
|
+
|
52
|
+
# Adds a new node to the collection. By doing so, this will also add it to
|
53
|
+
# the configured indices.
|
54
|
+
def <<(node)
|
55
|
+
@nodes << node
|
56
|
+
@indices.each {|attribute, index| index[node.send(attribute)] = node}
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
# Updates the indexed keys for the given node. If the node's attribute
|
61
|
+
# has changed since it was added to the collection, the old indexed keys
|
62
|
+
# will be replaced with the updated ones.
|
63
|
+
def update(node)
|
64
|
+
@indices.each do |attribute, index|
|
65
|
+
old_key = index.respond_to?(:key) ? index.key(node) : index.index(node)
|
66
|
+
new_key = node.send(attribute)
|
67
|
+
|
68
|
+
# Only replace the key if it's changed
|
69
|
+
if old_key != new_key
|
70
|
+
index.delete(old_key)
|
71
|
+
index[new_key] = node
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Calls the block once for each element in self, passing that element as a
|
77
|
+
# parameters.
|
78
|
+
#
|
79
|
+
# states = StateMachine::NodeCollection.new
|
80
|
+
# states << StateMachine::State.new(machine, :parked)
|
81
|
+
# states << StateMachine::State.new(machine, :idling)
|
82
|
+
# states.each {|state| puts state.name, ' -- '}
|
83
|
+
#
|
84
|
+
# ...produces:
|
85
|
+
#
|
86
|
+
# parked -- idling --
|
87
|
+
def each
|
88
|
+
@nodes.each {|node| yield node}
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
# Gets the node at the given index.
|
93
|
+
#
|
94
|
+
# states = StateMachine::NodeCollection.new
|
95
|
+
# states << StateMachine::State.new(machine, :parked)
|
96
|
+
# states << StateMachine::State.new(machine, :idling)
|
97
|
+
#
|
98
|
+
# states.at(0).name # => :parked
|
99
|
+
# states.at(1).name # => :idling
|
100
|
+
def at(index)
|
101
|
+
@nodes[index]
|
102
|
+
end
|
103
|
+
|
104
|
+
# Gets the node indexed by the given key. By default, this will look up the
|
105
|
+
# key in the first index configured for the collection. A custom index can
|
106
|
+
# be specified like so:
|
107
|
+
#
|
108
|
+
# collection['parked', :value]
|
109
|
+
#
|
110
|
+
# The above will look up the "parked" key in a hash indexed by each node's
|
111
|
+
# +value+ attribute.
|
112
|
+
#
|
113
|
+
# If the key cannot be found, then nil will be returned.
|
114
|
+
def [](key, index_name = @default_index)
|
115
|
+
index(index_name)[key]
|
116
|
+
end
|
117
|
+
|
118
|
+
# Gets the node indexed by the given key. By default, this will look up the
|
119
|
+
# key in the first index configured for the collection. A custom index can
|
120
|
+
# be specified like so:
|
121
|
+
#
|
122
|
+
# collection['parked', :value]
|
123
|
+
#
|
124
|
+
# The above will look up the "parked" key in a hash indexed by each node's
|
125
|
+
# +value+ attribute.
|
126
|
+
#
|
127
|
+
# If the key cannot be found, then an IndexError exception will be raised:
|
128
|
+
#
|
129
|
+
# collection['invalid', :value] # => IndexError: "invalid" is an invalid value
|
130
|
+
def fetch(key, index_name = @default_index)
|
131
|
+
self[key, index_name] || raise(ArgumentError, "#{key.inspect} is an invalid #{index_name}")
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
# Gets the given index. If the index does not exist, then an ArgumentError
|
136
|
+
# is raised.
|
137
|
+
def index(name)
|
138
|
+
raise ArgumentError, 'No indices configured' unless @indices.any?
|
139
|
+
@indices[name] || raise(ArgumentError, "Invalid index: #{name.inspect}")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
data/lib/state_machine/state.rb
CHANGED
@@ -3,54 +3,55 @@ require 'state_machine/assertions'
|
|
3
3
|
module StateMachine
|
4
4
|
# A state defines a value that an attribute can be in after being transitioned
|
5
5
|
# 0 or more times. States can represent a value of any type in Ruby, though
|
6
|
-
# the most common type is String.
|
6
|
+
# the most common (and default) type is String.
|
7
7
|
#
|
8
|
-
# In addition to defining the machine's value,
|
8
|
+
# In addition to defining the machine's value, a state can also define a
|
9
9
|
# behavioral context for an object when that object is in the state. See
|
10
10
|
# StateMachine::Machine#state for more information about how state-driven
|
11
11
|
# behavior can be utilized.
|
12
12
|
class State
|
13
13
|
include Assertions
|
14
14
|
|
15
|
-
class << self
|
16
|
-
# Generates a unique identifier for the given state value. Ids are based
|
17
|
-
# on the object type:
|
18
|
-
# * Proc - "lambda#{object_id}"
|
19
|
-
# * NilClass - "nil"
|
20
|
-
# * Everything else - Stringified version of the object
|
21
|
-
def id_for(value)
|
22
|
-
case value
|
23
|
-
when Proc
|
24
|
-
"lambda#{value.object_id.abs}"
|
25
|
-
when nil
|
26
|
-
'nil'
|
27
|
-
else
|
28
|
-
value.to_s
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
15
|
# The state machine for which this state is defined
|
34
16
|
attr_accessor :machine
|
35
17
|
|
18
|
+
# The unique identifier for the state used in event and callback definitions
|
19
|
+
attr_reader :name
|
20
|
+
|
21
|
+
# The value that is written to a machine's attribute when an object
|
22
|
+
# transitions into this state
|
23
|
+
attr_writer :value
|
24
|
+
|
25
|
+
# Whether or not this state is the initial state to use for new objects
|
26
|
+
attr_accessor :initial
|
27
|
+
|
28
|
+
# A custom lambda block for determining whether a given value matches this
|
29
|
+
# state
|
30
|
+
attr_accessor :matcher
|
31
|
+
|
36
32
|
# Tracks all of the methods that have been defined for the machine's owner
|
37
33
|
# class when objects are in this state.
|
38
34
|
#
|
39
|
-
# Maps
|
35
|
+
# Maps :method_name => UnboundMethod
|
40
36
|
attr_reader :methods
|
41
37
|
|
42
|
-
#
|
43
|
-
attr_accessor :initial
|
44
|
-
|
45
|
-
# Creates a new state within the context of the given machine
|
38
|
+
# Creates a new state within the context of the given machine.
|
46
39
|
#
|
47
40
|
# Configuration options:
|
48
|
-
# *
|
49
|
-
|
50
|
-
|
41
|
+
# * <tt>:initial</tt> - Whether this state is the beginning state for the
|
42
|
+
# machine. Default is false.
|
43
|
+
# * <tt>:value</tt> - The value to store when an object transitions to this
|
44
|
+
# state. Default is the name (stringified).
|
45
|
+
# * <tt>:if</tt> - Determines whether a value matches this state
|
46
|
+
# (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
|
47
|
+
# By default, the configured value is matched.
|
48
|
+
def initialize(machine, name, options = {}) #:nodoc:
|
49
|
+
assert_valid_keys(options, :initial, :value, :if)
|
51
50
|
|
52
51
|
@machine = machine
|
53
|
-
@
|
52
|
+
@name = name
|
53
|
+
@value = options.include?(:value) ? options[:value] : name && name.to_s
|
54
|
+
@matcher = options[:if]
|
54
55
|
@methods = {}
|
55
56
|
@initial = options.include?(:initial) && options[:initial]
|
56
57
|
|
@@ -61,14 +62,53 @@ module StateMachine
|
|
61
62
|
# methods to prevent conflicts across different states.
|
62
63
|
def initialize_copy(orig) #:nodoc:
|
63
64
|
super
|
64
|
-
@methods =
|
65
|
+
@methods = methods.dup
|
65
66
|
end
|
66
67
|
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
|
71
|
-
|
68
|
+
# Generates a human-readable description of this state's name / value:
|
69
|
+
#
|
70
|
+
# For example,
|
71
|
+
#
|
72
|
+
# State.new(machine, :parked).description # => "parked"
|
73
|
+
# State.new(machine, :parked, :value => :parked).description # => "parked"
|
74
|
+
# State.new(machine, :parked, :value => nil).description # => "parked (nil)"
|
75
|
+
# State.new(machine, :parked, :value => 1).description # => "parked (1)"
|
76
|
+
# State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
|
77
|
+
def description
|
78
|
+
description = name ? name.to_s : name.inspect
|
79
|
+
description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
|
80
|
+
description
|
81
|
+
end
|
82
|
+
|
83
|
+
# The value that represents this state. If the value is a lambda block,
|
84
|
+
# then it will be evaluated at this time. Otherwise, the static value is
|
85
|
+
# returned.
|
86
|
+
#
|
87
|
+
# For example,
|
88
|
+
#
|
89
|
+
# State.new(machine, :parked, :value => 1).value # => 1
|
90
|
+
# State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
|
91
|
+
def value
|
92
|
+
@value.is_a?(Proc) ? @value.call : @value
|
93
|
+
end
|
94
|
+
|
95
|
+
# Determines whether this state matches the given value. If no matcher is
|
96
|
+
# configured, then this will check whether the values are equivalent.
|
97
|
+
# Otherwise, the matcher will determine the result.
|
98
|
+
#
|
99
|
+
# For example,
|
100
|
+
#
|
101
|
+
# # Without a matcher
|
102
|
+
# state = State.new(machine, :parked, :value => 1)
|
103
|
+
# state.matches?(1) # => true
|
104
|
+
# state.matches?(2) # => false
|
105
|
+
#
|
106
|
+
# # With a matcher
|
107
|
+
# state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
|
108
|
+
# state.matches?(nil) # => false
|
109
|
+
# state.matches?(Time.now) # => true
|
110
|
+
def matches?(other_value)
|
111
|
+
matcher ? matcher.call(other_value) : other_value == value
|
72
112
|
end
|
73
113
|
|
74
114
|
# Defines a context for the state which will be enabled on instances of the
|
@@ -77,7 +117,6 @@ module StateMachine
|
|
77
117
|
# This can be called multiple times. Each time a new context is created, a
|
78
118
|
# new module will be included in the owner class.
|
79
119
|
def context(&block)
|
80
|
-
value = self.value
|
81
120
|
owner_class = machine.owner_class
|
82
121
|
attribute = machine.attribute
|
83
122
|
|
@@ -95,15 +134,14 @@ module StateMachine
|
|
95
134
|
# not possible with lambdas in Ruby 1.8.6.
|
96
135
|
owner_class.class_eval <<-end_eval, __FILE__, __LINE__
|
97
136
|
def #{method}(*args, &block)
|
98
|
-
attribute
|
99
|
-
self.class.state_machines[attribute].state(send(attribute)).call(self, #{method.dump}, *args, &block)
|
137
|
+
self.class.state_machines[#{attribute.inspect}].state_for(self).call(self, #{method.inspect}, *args, &block)
|
100
138
|
end
|
101
139
|
end_eval
|
102
140
|
end
|
103
141
|
|
104
142
|
# Track the method defined for the context so that it can be invoked
|
105
143
|
# at a later point in time
|
106
|
-
methods[method] = context.instance_method(method)
|
144
|
+
methods[method.to_sym] = context.instance_method(method)
|
107
145
|
end
|
108
146
|
|
109
147
|
# Include the context so that it can be bound to the owner class (the
|
@@ -112,7 +150,7 @@ module StateMachine
|
|
112
150
|
include context
|
113
151
|
end
|
114
152
|
|
115
|
-
|
153
|
+
context
|
116
154
|
end
|
117
155
|
|
118
156
|
# Calls a method defined in this state's context on the given object. All
|
@@ -121,53 +159,60 @@ module StateMachine
|
|
121
159
|
# If the method has never been defined for this state, then a NoMethodError
|
122
160
|
# will be raised.
|
123
161
|
def call(object, method, *args, &block)
|
124
|
-
if context_method = methods[method.
|
162
|
+
if context_method = methods[method.to_sym]
|
125
163
|
# Method is defined by the state: proxy it through
|
126
164
|
context_method.bind(object).call(*args, &block)
|
127
165
|
else
|
128
166
|
# Raise exception as if the method never existed on the original object
|
129
|
-
raise NoMethodError, "undefined method '#{method}' for #{object} in state #{
|
167
|
+
raise NoMethodError, "undefined method '#{method}' for #{object} in state #{machine.state_for(object).name.inspect}"
|
130
168
|
end
|
131
169
|
end
|
132
170
|
|
133
171
|
# Draws a representation of this state on the given machine. This will
|
134
172
|
# create a new node on the graph with the following properties:
|
135
|
-
# * +label+ -
|
173
|
+
# * +label+ - The human-friendly description of the state.
|
136
174
|
# * +width+ - The width of the node. Always 1.
|
137
175
|
# * +height+ - The height of the node. Always 1.
|
138
|
-
# * +fixedsize+ - Whether the size of the node stays the same regardless of
|
139
|
-
#
|
176
|
+
# * +fixedsize+ - Whether the size of the node stays the same regardless of
|
177
|
+
# its contents. Always true.
|
178
|
+
# * +shape+ - The actual shape of the node. If the state is the initial
|
179
|
+
# state, then "doublecircle", otherwise "circle".
|
140
180
|
#
|
141
181
|
# The actual node generated on the graph will be returned.
|
142
182
|
def draw(graph)
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
183
|
+
graph.add_node(name ? name.to_s : 'nil',
|
184
|
+
:label => description,
|
185
|
+
:width => '1',
|
186
|
+
:height => '1',
|
187
|
+
:fixedsize => 'true',
|
188
|
+
:shape => initial ? 'doublecircle' : 'circle'
|
189
|
+
)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Generates a nicely formatted description of this state's contents.
|
193
|
+
#
|
194
|
+
# For example,
|
195
|
+
#
|
196
|
+
# state = StateMachine::State.new(machine, :parked, :value => 1, :initial => true)
|
197
|
+
# state # => #<StateMachine::State name=:parked value=1 initial=true>
|
198
|
+
def inspect
|
199
|
+
"#<#{self.class} #{%w(name value initial).map {|attr| "#{attr}=#{instance_variable_get("@#{attr}").inspect}"} * ' '}>"
|
154
200
|
end
|
155
201
|
|
156
202
|
private
|
157
|
-
# Adds a predicate method to the owner class
|
158
|
-
#
|
203
|
+
# Adds a predicate method to the owner class so long as a name has
|
204
|
+
# actually been configured for the state
|
159
205
|
def add_predicate
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
end unless method_defined?(name) || private_method_defined?(name)
|
206
|
+
return unless name
|
207
|
+
|
208
|
+
attribute = machine.attribute
|
209
|
+
qualified_name = name = self.name
|
210
|
+
qualified_name = "#{machine.namespace}_#{name}" if machine.namespace
|
211
|
+
|
212
|
+
machine.owner_class.class_eval do
|
213
|
+
# Checks whether the current value matches this state
|
214
|
+
define_method("#{qualified_name}?") do
|
215
|
+
self.class.state_machines[attribute].state(name).matches?(send(attribute))
|
171
216
|
end
|
172
217
|
end
|
173
218
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'state_machine/node_collection'
|
2
|
+
|
3
|
+
module StateMachine
|
4
|
+
# Represents a collection of states in a state machine
|
5
|
+
class StateCollection < NodeCollection
|
6
|
+
def initialize #:nodoc:
|
7
|
+
super(:index => [:name, :value])
|
8
|
+
end
|
9
|
+
|
10
|
+
# Gets the order in which states should be displayed based on where they
|
11
|
+
# were first referenced. This will order states in the following priority:
|
12
|
+
#
|
13
|
+
# 1. Initial state
|
14
|
+
# 2. Event transitions (:from, :except_from, :to, :except_to options)
|
15
|
+
# 3. States with behaviors
|
16
|
+
# 4. States referenced via +state+ or +other_states+
|
17
|
+
# 5. States referenced in callbacks
|
18
|
+
#
|
19
|
+
# This order will determine how the GraphViz visualizations are rendered.
|
20
|
+
def by_priority
|
21
|
+
if first = @nodes.first
|
22
|
+
machine = first.machine
|
23
|
+
order = select {|state| state.initial}.map {|state| state.name}
|
24
|
+
|
25
|
+
machine.events.each {|event| order += event.known_states}
|
26
|
+
order += select {|state| state.methods.any?}.map {|state| state.name}
|
27
|
+
order += keys(:name) - machine.callbacks.values.flatten.map {|callback| callback.known_states}.flatten
|
28
|
+
order += keys(:name)
|
29
|
+
|
30
|
+
order.uniq!
|
31
|
+
order.map! {|name| self[name]}
|
32
|
+
order
|
33
|
+
else
|
34
|
+
[]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -4,6 +4,7 @@ module StateMachine
|
|
4
4
|
end
|
5
5
|
|
6
6
|
# A transition represents a state change for a specific attribute.
|
7
|
+
#
|
7
8
|
# Transitions consist of:
|
8
9
|
# * An event
|
9
10
|
# * A starting state
|
@@ -18,19 +19,27 @@ module StateMachine
|
|
18
19
|
# The event that caused the transition
|
19
20
|
attr_reader :event
|
20
21
|
|
21
|
-
# The original state *before* the transition
|
22
|
+
# The original state value *before* the transition
|
22
23
|
attr_reader :from
|
23
24
|
|
24
|
-
# The
|
25
|
+
# The original state name *before* the transition
|
26
|
+
attr_reader :from_name
|
27
|
+
|
28
|
+
# The new state value *after* the transition
|
25
29
|
attr_reader :to
|
26
30
|
|
31
|
+
# The new state name *after* the transition
|
32
|
+
attr_reader :to_name
|
33
|
+
|
27
34
|
# Creates a new, specific transition
|
28
|
-
def initialize(object, machine, event,
|
35
|
+
def initialize(object, machine, event, from_name, to_name) #:nodoc:
|
29
36
|
@object = object
|
30
37
|
@machine = machine
|
31
38
|
@event = event
|
32
|
-
@from =
|
33
|
-
@
|
39
|
+
@from = object.send(machine.attribute)
|
40
|
+
@from_name = from_name
|
41
|
+
@to = machine.states[to_name].value
|
42
|
+
@to_name = to_name
|
34
43
|
end
|
35
44
|
|
36
45
|
# Gets the attribute which this transition's machine is defined for
|
@@ -38,14 +47,14 @@ module StateMachine
|
|
38
47
|
machine.attribute
|
39
48
|
end
|
40
49
|
|
41
|
-
# Gets a hash of all the attributes defined for this transition with
|
42
|
-
# names as keys and values of the attributes as values.
|
50
|
+
# Gets a hash of all the core attributes defined for this transition with
|
51
|
+
# their names as keys and values of the attributes as values.
|
43
52
|
#
|
44
53
|
# == Example
|
45
54
|
#
|
46
|
-
# machine = StateMachine.new(
|
47
|
-
# transition = StateMachine::Transition.new(
|
48
|
-
# transition.attributes # => {:object => #<
|
55
|
+
# machine = StateMachine.new(Vehicle)
|
56
|
+
# transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
|
57
|
+
# transition.attributes # => {:object => #<Vehicle:0xb7d60ea4>, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}
|
49
58
|
def attributes
|
50
59
|
@attributes ||= {:object => object, :attribute => attribute, :event => event, :from => from, :to => to}
|
51
60
|
end
|
@@ -63,9 +72,9 @@ module StateMachine
|
|
63
72
|
# end
|
64
73
|
#
|
65
74
|
# vehicle = Vehicle.new
|
66
|
-
# transition = StateMachine::Transition.new(vehicle, machine, :
|
67
|
-
# transition.perform # => Runs the +save+ action after
|
68
|
-
# transition.perform(false) # => Only
|
75
|
+
# transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
|
76
|
+
# transition.perform # => Runs the +save+ action after setting the state attribute
|
77
|
+
# transition.perform(false) # => Only sets the state attribute
|
69
78
|
def perform(run_action = true)
|
70
79
|
result = false
|
71
80
|
|
@@ -91,17 +100,27 @@ module StateMachine
|
|
91
100
|
result
|
92
101
|
end
|
93
102
|
|
103
|
+
# Generates a nicely formatted description of this transitions's contents.
|
104
|
+
#
|
105
|
+
# For example,
|
106
|
+
#
|
107
|
+
# transition = StateMachine::Transition.new(object, machine, :ignite, :parked, :idling)
|
108
|
+
# transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
|
109
|
+
def inspect
|
110
|
+
"#<#{self.class} #{%w(attribute event from from_name to to_name).map {|attr| "#{attr}=#{send(attr).inspect}"} * ' '}>"
|
111
|
+
end
|
112
|
+
|
94
113
|
protected
|
95
114
|
# Gets a hash of the context defining this unique transition (including
|
96
115
|
# event, from state, and to state).
|
97
116
|
#
|
98
117
|
# == Example
|
99
118
|
#
|
100
|
-
# machine = StateMachine.new(
|
101
|
-
# transition = StateMachine::Transition.new(
|
102
|
-
# transition.context # => {:on =>
|
119
|
+
# machine = StateMachine.new(Vehicle)
|
120
|
+
# transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
|
121
|
+
# transition.context # => {:on => :ignite, :from => :parked, :to => :idling}
|
103
122
|
def context
|
104
|
-
@context ||= {:on => event, :from =>
|
123
|
+
@context ||= {:on => event, :from => from_name, :to => to_name}
|
105
124
|
end
|
106
125
|
|
107
126
|
# Runs the callbacks of the given type for this transition. This will
|