spree-state_machine 2.0.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +12 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +502 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +1246 -0
- data/Rakefile +20 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/Gemfile +5 -0
- data/examples/Gemfile.lock +14 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +13 -0
- data/examples/car.rb +21 -0
- data/examples/doc/AutoShop.html +2856 -0
- data/examples/doc/AutoShop_state.png +0 -0
- data/examples/doc/Car.html +919 -0
- data/examples/doc/Car_state.png +0 -0
- data/examples/doc/TrafficLight.html +2230 -0
- data/examples/doc/TrafficLight_state.png +0 -0
- data/examples/doc/Vehicle.html +7921 -0
- data/examples/doc/Vehicle_state.png +0 -0
- data/examples/doc/_index.html +136 -0
- data/examples/doc/class_list.html +47 -0
- data/examples/doc/css/common.css +1 -0
- data/examples/doc/css/full_list.css +55 -0
- data/examples/doc/css/style.css +322 -0
- data/examples/doc/file_list.html +46 -0
- data/examples/doc/frames.html +13 -0
- data/examples/doc/index.html +136 -0
- data/examples/doc/js/app.js +205 -0
- data/examples/doc/js/full_list.js +173 -0
- data/examples/doc/js/jquery.js +16 -0
- data/examples/doc/method_list.html +734 -0
- data/examples/doc/top-level-namespace.html +105 -0
- data/examples/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +7 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view__form.html.erb +34 -0
- data/examples/rails-rest/view_edit.html.erb +6 -0
- data/examples/rails-rest/view_index.html.erb +25 -0
- data/examples/rails-rest/view_new.html.erb +5 -0
- data/examples/rails-rest/view_show.html.erb +19 -0
- data/examples/traffic_light.rb +9 -0
- data/examples/vehicle.rb +33 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/branch.rb +225 -0
- data/lib/state_machine/callback.rb +236 -0
- data/lib/state_machine/core.rb +7 -0
- data/lib/state_machine/core_ext/class/state_machine.rb +5 -0
- data/lib/state_machine/core_ext.rb +2 -0
- data/lib/state_machine/error.rb +13 -0
- data/lib/state_machine/eval_helpers.rb +87 -0
- data/lib/state_machine/event.rb +257 -0
- data/lib/state_machine/event_collection.rb +141 -0
- data/lib/state_machine/extensions.rb +149 -0
- data/lib/state_machine/graph.rb +92 -0
- data/lib/state_machine/helper_module.rb +17 -0
- data/lib/state_machine/initializers/rails.rb +25 -0
- data/lib/state_machine/initializers.rb +4 -0
- data/lib/state_machine/integrations/active_model/locale.rb +11 -0
- data/lib/state_machine/integrations/active_model/observer.rb +33 -0
- data/lib/state_machine/integrations/active_model/observer_update.rb +42 -0
- data/lib/state_machine/integrations/active_model/versions.rb +31 -0
- data/lib/state_machine/integrations/active_model.rb +585 -0
- data/lib/state_machine/integrations/active_record/locale.rb +20 -0
- data/lib/state_machine/integrations/active_record/versions.rb +123 -0
- data/lib/state_machine/integrations/active_record.rb +525 -0
- data/lib/state_machine/integrations/base.rb +100 -0
- data/lib/state_machine/integrations.rb +121 -0
- data/lib/state_machine/machine.rb +2287 -0
- data/lib/state_machine/machine_collection.rb +74 -0
- data/lib/state_machine/macro_methods.rb +522 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +222 -0
- data/lib/state_machine/path.rb +120 -0
- data/lib/state_machine/path_collection.rb +90 -0
- data/lib/state_machine/state.rb +297 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/state_context.rb +138 -0
- data/lib/state_machine/transition.rb +470 -0
- data/lib/state_machine/transition_collection.rb +245 -0
- data/lib/state_machine/version.rb +3 -0
- data/lib/state_machine/yard/handlers/base.rb +32 -0
- data/lib/state_machine/yard/handlers/event.rb +25 -0
- data/lib/state_machine/yard/handlers/machine.rb +344 -0
- data/lib/state_machine/yard/handlers/state.rb +25 -0
- data/lib/state_machine/yard/handlers/transition.rb +47 -0
- data/lib/state_machine/yard/handlers.rb +12 -0
- data/lib/state_machine/yard/templates/default/class/html/setup.rb +30 -0
- data/lib/state_machine/yard/templates/default/class/html/state_machines.erb +12 -0
- data/lib/state_machine/yard/templates.rb +3 -0
- data/lib/state_machine/yard.rb +8 -0
- data/lib/state_machine.rb +8 -0
- data/lib/yard-state_machine.rb +2 -0
- data/state_machine.gemspec +22 -0
- data/test/files/en.yml +17 -0
- data/test/files/switch.rb +15 -0
- data/test/functional/state_machine_test.rb +1066 -0
- data/test/test_helper.rb +7 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/branch_test.rb +969 -0
- data/test/unit/callback_test.rb +704 -0
- data/test/unit/error_test.rb +43 -0
- data/test/unit/eval_helpers_test.rb +270 -0
- data/test/unit/event_collection_test.rb +398 -0
- data/test/unit/event_test.rb +1196 -0
- data/test/unit/graph_test.rb +98 -0
- data/test/unit/helper_module_test.rb +17 -0
- data/test/unit/integrations/active_model_test.rb +1245 -0
- data/test/unit/integrations/active_record_test.rb +2551 -0
- data/test/unit/integrations/base_test.rb +104 -0
- data/test/unit/integrations_test.rb +71 -0
- data/test/unit/invalid_event_test.rb +20 -0
- data/test/unit/invalid_parallel_transition_test.rb +18 -0
- data/test/unit/invalid_transition_test.rb +115 -0
- data/test/unit/machine_collection_test.rb +603 -0
- data/test/unit/machine_test.rb +3395 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +362 -0
- data/test/unit/path_collection_test.rb +266 -0
- data/test/unit/path_test.rb +485 -0
- data/test/unit/state_collection_test.rb +352 -0
- data/test/unit/state_context_test.rb +441 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +1101 -0
- data/test/unit/transition_collection_test.rb +2168 -0
- data/test/unit/transition_test.rb +1558 -0
- metadata +264 -0
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module StateMachine
|
4
|
+
# Provides a general strategy pattern for determining whether a match is found
|
5
|
+
# for a value. The algorithm that actually determines the match depends on
|
6
|
+
# the matcher in use.
|
7
|
+
class Matcher
|
8
|
+
# The list of values against which queries are matched
|
9
|
+
attr_reader :values
|
10
|
+
|
11
|
+
# Creates a new matcher for querying against the given set of values
|
12
|
+
def initialize(values = [])
|
13
|
+
@values = values.is_a?(Array) ? values : [values]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Generates a subset of values that exists in both the set of values being
|
17
|
+
# filtered and the values configured for the matcher
|
18
|
+
def filter(values)
|
19
|
+
self.values & values
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Matches any given value. Since there is no configuration for this type of
|
24
|
+
# matcher, it must be used as a singleton.
|
25
|
+
class AllMatcher < Matcher
|
26
|
+
include Singleton
|
27
|
+
|
28
|
+
# Generates a blacklist matcher based on the given set of values
|
29
|
+
#
|
30
|
+
# == Examples
|
31
|
+
#
|
32
|
+
# matcher = StateMachine::AllMatcher.instance - [:parked, :idling]
|
33
|
+
# matcher.matches?(:parked) # => false
|
34
|
+
# matcher.matches?(:first_gear) # => true
|
35
|
+
def -(blacklist)
|
36
|
+
BlacklistMatcher.new(blacklist)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Always returns true
|
40
|
+
def matches?(value, context = {})
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
# Always returns the given set of values
|
45
|
+
def filter(values)
|
46
|
+
values
|
47
|
+
end
|
48
|
+
|
49
|
+
# A human-readable description of this matcher. Always "all".
|
50
|
+
def description
|
51
|
+
'all'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Matches a specific set of values
|
56
|
+
class WhitelistMatcher < Matcher
|
57
|
+
# Checks whether the given value exists within the whitelist configured
|
58
|
+
# for this matcher.
|
59
|
+
#
|
60
|
+
# == Examples
|
61
|
+
#
|
62
|
+
# matcher = StateMachine::WhitelistMatcher.new([:parked, :idling])
|
63
|
+
# matcher.matches?(:parked) # => true
|
64
|
+
# matcher.matches?(:first_gear) # => false
|
65
|
+
def matches?(value, context = {})
|
66
|
+
values.include?(value)
|
67
|
+
end
|
68
|
+
|
69
|
+
# A human-readable description of this matcher
|
70
|
+
def description
|
71
|
+
values.length == 1 ? values.first.inspect : values.inspect
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Matches everything but a specific set of values
|
76
|
+
class BlacklistMatcher < Matcher
|
77
|
+
# Checks whether the given value exists outside the blacklist configured
|
78
|
+
# for this matcher.
|
79
|
+
#
|
80
|
+
# == Examples
|
81
|
+
#
|
82
|
+
# matcher = StateMachine::BlacklistMatcher.new([:parked, :idling])
|
83
|
+
# matcher.matches?(:parked) # => false
|
84
|
+
# matcher.matches?(:first_gear) # => true
|
85
|
+
def matches?(value, context = {})
|
86
|
+
!values.include?(value)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Finds all values that are *not* within the blacklist configured for this
|
90
|
+
# matcher
|
91
|
+
def filter(values)
|
92
|
+
values - self.values
|
93
|
+
end
|
94
|
+
|
95
|
+
# A human-readable description of this matcher
|
96
|
+
def description
|
97
|
+
"all - #{values.length == 1 ? values.first.inspect : values.inspect}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Matches a loopback of two values within a context. Since there is no
|
102
|
+
# configuration for this type of matcher, it must be used as a singleton.
|
103
|
+
class LoopbackMatcher < Matcher
|
104
|
+
include Singleton
|
105
|
+
|
106
|
+
# Checks whether the given value matches what the value originally was.
|
107
|
+
# This value should be defined in the context.
|
108
|
+
#
|
109
|
+
# == Examples
|
110
|
+
#
|
111
|
+
# matcher = StateMachine::LoopbackMatcher.instance
|
112
|
+
# matcher.matches?(:parked, :from => :parked) # => true
|
113
|
+
# matcher.matches?(:parked, :from => :idling) # => false
|
114
|
+
def matches?(value, context)
|
115
|
+
context[:from] == value
|
116
|
+
end
|
117
|
+
|
118
|
+
# A human-readable description of this matcher. Always "same".
|
119
|
+
def description
|
120
|
+
'same'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module StateMachine
|
2
|
+
# Provides a set of helper methods for generating matchers
|
3
|
+
module MatcherHelpers
|
4
|
+
# Represents a state that matches all known states in a machine.
|
5
|
+
#
|
6
|
+
# == Examples
|
7
|
+
#
|
8
|
+
# class Vehicle
|
9
|
+
# state_machine do
|
10
|
+
# before_transition any => :parked, :do => lambda {...}
|
11
|
+
# before_transition all - :parked => all - :idling, :do => lambda {}
|
12
|
+
#
|
13
|
+
# event :park
|
14
|
+
# transition all => :parked
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# event :crash
|
18
|
+
# transition all - :parked => :stalled
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# In the above example, +all+ will match the following states since they
|
24
|
+
# are known:
|
25
|
+
# * +parked+
|
26
|
+
# * +stalled+
|
27
|
+
# * +idling+
|
28
|
+
def all
|
29
|
+
AllMatcher.instance
|
30
|
+
end
|
31
|
+
alias_method :any, :all
|
32
|
+
|
33
|
+
# Represents a state that matches the original +from+ state. This is useful
|
34
|
+
# for defining transitions which are loopbacks.
|
35
|
+
#
|
36
|
+
# == Examples
|
37
|
+
#
|
38
|
+
# class Vehicle
|
39
|
+
# state_machine do
|
40
|
+
# event :ignite
|
41
|
+
# transition [:idling, :first_gear] => same
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# In the above example, +same+ will match whichever the from state is. In
|
47
|
+
# the case of the +ignite+ event, it is essential the same as the following:
|
48
|
+
#
|
49
|
+
# transition :parked => :parked, :first_gear => :first_gear
|
50
|
+
def same
|
51
|
+
LoopbackMatcher.instance
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,222 @@
|
|
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
|
+
# Nodes will not differentiate between the String and Symbol versions of the
|
6
|
+
# values being indexed.
|
7
|
+
class NodeCollection
|
8
|
+
include Enumerable
|
9
|
+
include Assertions
|
10
|
+
|
11
|
+
# The machine associated with the nodes
|
12
|
+
attr_reader :machine
|
13
|
+
|
14
|
+
# Creates a new collection of nodes for the given state machine. By default,
|
15
|
+
# the collection is empty.
|
16
|
+
#
|
17
|
+
# Configuration options:
|
18
|
+
# * <tt>:index</tt> - One or more attributes to automatically generate
|
19
|
+
# hashed indices for in order to perform quick lookups. Default is to
|
20
|
+
# index by the :name attribute
|
21
|
+
def initialize(machine, options = {})
|
22
|
+
assert_valid_keys(options, :index)
|
23
|
+
options = {:index => :name}.merge(options)
|
24
|
+
|
25
|
+
@machine = machine
|
26
|
+
@nodes = []
|
27
|
+
@index_names = Array(options[:index])
|
28
|
+
@indices = @index_names.inject({}) do |indices, name|
|
29
|
+
indices[name] = {}
|
30
|
+
indices[:"#{name}_to_s"] = {}
|
31
|
+
indices[:"#{name}_to_sym"] = {}
|
32
|
+
indices
|
33
|
+
end
|
34
|
+
@default_index = Array(options[:index]).first
|
35
|
+
@contexts = []
|
36
|
+
end
|
37
|
+
|
38
|
+
# Creates a copy of this collection such that modifications don't affect
|
39
|
+
# the original collection
|
40
|
+
def initialize_copy(orig) #:nodoc:
|
41
|
+
super
|
42
|
+
|
43
|
+
nodes = @nodes
|
44
|
+
contexts = @contexts
|
45
|
+
@nodes = []
|
46
|
+
@contexts = []
|
47
|
+
@indices = @indices.inject({}) {|indices, (name, *)| indices[name] = {}; indices}
|
48
|
+
|
49
|
+
# Add nodes *prior* to copying over the contexts so that they don't get
|
50
|
+
# evaluated multiple times
|
51
|
+
concat(nodes.map {|n| n.dup})
|
52
|
+
@contexts = contexts.dup
|
53
|
+
end
|
54
|
+
|
55
|
+
# Changes the current machine associated with the collection. In turn, this
|
56
|
+
# will change the state machine associated with each node in the collection.
|
57
|
+
def machine=(new_machine)
|
58
|
+
@machine = new_machine
|
59
|
+
each {|node| node.machine = new_machine}
|
60
|
+
end
|
61
|
+
|
62
|
+
# Gets the number of nodes in this collection
|
63
|
+
def length
|
64
|
+
@nodes.length
|
65
|
+
end
|
66
|
+
|
67
|
+
# Gets the set of unique keys for the given index
|
68
|
+
def keys(index_name = @default_index)
|
69
|
+
index(index_name).keys
|
70
|
+
end
|
71
|
+
|
72
|
+
# Tracks a context that should be evaluated for any nodes that get added
|
73
|
+
# which match the given set of nodes. Matchers can be used so that the
|
74
|
+
# context can get added once and evaluated after multiple adds.
|
75
|
+
def context(nodes, &block)
|
76
|
+
nodes = nodes.first.is_a?(Matcher) ? nodes.first : WhitelistMatcher.new(nodes)
|
77
|
+
@contexts << context = {:nodes => nodes, :block => block}
|
78
|
+
|
79
|
+
# Evaluate the new context for existing nodes
|
80
|
+
each {|node| eval_context(context, node)}
|
81
|
+
|
82
|
+
context
|
83
|
+
end
|
84
|
+
|
85
|
+
# Adds a new node to the collection. By doing so, this will also add it to
|
86
|
+
# the configured indices. This will also evaluate any existings contexts
|
87
|
+
# that match the new node.
|
88
|
+
def <<(node)
|
89
|
+
@nodes << node
|
90
|
+
@index_names.each {|name| add_to_index(name, value(node, name), node)}
|
91
|
+
@contexts.each {|context| eval_context(context, node)}
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
# Appends a group of nodes to the collection
|
96
|
+
def concat(nodes)
|
97
|
+
nodes.each {|node| self << node}
|
98
|
+
end
|
99
|
+
|
100
|
+
# Updates the indexed keys for the given node. If the node's attribute
|
101
|
+
# has changed since it was added to the collection, the old indexed keys
|
102
|
+
# will be replaced with the updated ones.
|
103
|
+
def update(node)
|
104
|
+
@index_names.each {|name| update_index(name, node)}
|
105
|
+
end
|
106
|
+
|
107
|
+
# Calls the block once for each element in self, passing that element as a
|
108
|
+
# parameter.
|
109
|
+
#
|
110
|
+
# states = StateMachine::NodeCollection.new
|
111
|
+
# states << StateMachine::State.new(machine, :parked)
|
112
|
+
# states << StateMachine::State.new(machine, :idling)
|
113
|
+
# states.each {|state| puts state.name, ' -- '}
|
114
|
+
#
|
115
|
+
# ...produces:
|
116
|
+
#
|
117
|
+
# parked -- idling --
|
118
|
+
def each
|
119
|
+
@nodes.each {|node| yield node}
|
120
|
+
self
|
121
|
+
end
|
122
|
+
|
123
|
+
# Gets the node at the given index.
|
124
|
+
#
|
125
|
+
# states = StateMachine::NodeCollection.new
|
126
|
+
# states << StateMachine::State.new(machine, :parked)
|
127
|
+
# states << StateMachine::State.new(machine, :idling)
|
128
|
+
#
|
129
|
+
# states.at(0).name # => :parked
|
130
|
+
# states.at(1).name # => :idling
|
131
|
+
def at(index)
|
132
|
+
@nodes[index]
|
133
|
+
end
|
134
|
+
|
135
|
+
# Gets the node indexed by the given key. By default, this will look up the
|
136
|
+
# key in the first index configured for the collection. A custom index can
|
137
|
+
# be specified like so:
|
138
|
+
#
|
139
|
+
# collection['parked', :value]
|
140
|
+
#
|
141
|
+
# The above will look up the "parked" key in a hash indexed by each node's
|
142
|
+
# +value+ attribute.
|
143
|
+
#
|
144
|
+
# If the key cannot be found, then nil will be returned.
|
145
|
+
def [](key, index_name = @default_index)
|
146
|
+
self.index(index_name)[key] ||
|
147
|
+
self.index(:"#{index_name}_to_s")[key.to_s] ||
|
148
|
+
to_sym?(key) && self.index(:"#{index_name}_to_sym")[:"#{key}"] ||
|
149
|
+
nil
|
150
|
+
end
|
151
|
+
|
152
|
+
# Gets the node indexed by the given key. By default, this will look up the
|
153
|
+
# key in the first index configured for the collection. A custom index can
|
154
|
+
# be specified like so:
|
155
|
+
#
|
156
|
+
# collection['parked', :value]
|
157
|
+
#
|
158
|
+
# The above will look up the "parked" key in a hash indexed by each node's
|
159
|
+
# +value+ attribute.
|
160
|
+
#
|
161
|
+
# If the key cannot be found, then an IndexError exception will be raised:
|
162
|
+
#
|
163
|
+
# collection['invalid', :value] # => IndexError: "invalid" is an invalid value
|
164
|
+
def fetch(key, index_name = @default_index)
|
165
|
+
self[key, index_name] || raise(IndexError, "#{key.inspect} is an invalid #{index_name}")
|
166
|
+
end
|
167
|
+
|
168
|
+
protected
|
169
|
+
# Gets the given index. If the index does not exist, then an ArgumentError
|
170
|
+
# is raised.
|
171
|
+
def index(name)
|
172
|
+
raise ArgumentError, 'No indices configured' unless @indices.any?
|
173
|
+
@indices[name] || raise(ArgumentError, "Invalid index: #{name.inspect}")
|
174
|
+
end
|
175
|
+
|
176
|
+
# Gets the value for the given attribute on the node
|
177
|
+
def value(node, attribute)
|
178
|
+
node.send(attribute)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Adds the given key / node combination to an index, including the string
|
182
|
+
# and symbol versions of the index
|
183
|
+
def add_to_index(name, key, node)
|
184
|
+
index(name)[key] = node
|
185
|
+
index(:"#{name}_to_s")[key.to_s] = node
|
186
|
+
index(:"#{name}_to_sym")[:"#{key}"] = node if to_sym?(key)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Removes the given key from an index, including the string and symbol
|
190
|
+
# versions of the index
|
191
|
+
def remove_from_index(name, key)
|
192
|
+
index(name).delete(key)
|
193
|
+
index(:"#{name}_to_s").delete(key.to_s)
|
194
|
+
index(:"#{name}_to_sym").delete(:"#{key}") if to_sym?(key)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Updates the node for the given index, including the string and symbol
|
198
|
+
# versions of the index
|
199
|
+
def update_index(name, node)
|
200
|
+
index = self.index(name)
|
201
|
+
old_key = RUBY_VERSION < '1.9' ? index.index(node) : index.key(node)
|
202
|
+
new_key = value(node, name)
|
203
|
+
|
204
|
+
# Only replace the key if it's changed
|
205
|
+
if old_key != new_key
|
206
|
+
remove_from_index(name, old_key)
|
207
|
+
add_to_index(name, new_key, node)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Determines whether the given value can be converted to a symbol
|
212
|
+
def to_sym?(value)
|
213
|
+
"#{value}" != ''
|
214
|
+
end
|
215
|
+
|
216
|
+
# Evaluates the given context for a particular node. This will only
|
217
|
+
# evaluate the context if the node matches.
|
218
|
+
def eval_context(context, node)
|
219
|
+
node.context(&context[:block]) if context[:nodes].matches?(node.name)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module StateMachine
|
2
|
+
# A path represents a sequence of transitions that can be run for a particular
|
3
|
+
# object. Paths can walk to new transitions, revealing all of the possible
|
4
|
+
# branches that can be encountered in the object's state machine.
|
5
|
+
class Path < Array
|
6
|
+
include Assertions
|
7
|
+
|
8
|
+
# The object whose state machine is being walked
|
9
|
+
attr_reader :object
|
10
|
+
|
11
|
+
# The state machine this path is walking
|
12
|
+
attr_reader :machine
|
13
|
+
|
14
|
+
# Creates a new transition path for the given object. Initially this is an
|
15
|
+
# empty path. In order to start walking the path, it must be populated with
|
16
|
+
# an initial transition.
|
17
|
+
#
|
18
|
+
# Configuration options:
|
19
|
+
# * <tt>:target</tt> - The target state to end the path on
|
20
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
21
|
+
# conditionals defined for each one
|
22
|
+
def initialize(object, machine, options = {})
|
23
|
+
assert_valid_keys(options, :target, :guard)
|
24
|
+
|
25
|
+
@object = object
|
26
|
+
@machine = machine
|
27
|
+
@target = options[:target]
|
28
|
+
@guard = options[:guard]
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize_copy(orig) #:nodoc:
|
32
|
+
super
|
33
|
+
@transitions = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
# The initial state name for this path
|
37
|
+
def from_name
|
38
|
+
first && first.from_name
|
39
|
+
end
|
40
|
+
|
41
|
+
# Lists all of the from states that can be reached through this path.
|
42
|
+
#
|
43
|
+
# For example,
|
44
|
+
#
|
45
|
+
# path.to_states # => [:parked, :idling, :first_gear, ...]
|
46
|
+
def from_states
|
47
|
+
map {|transition| transition.from_name}.uniq
|
48
|
+
end
|
49
|
+
|
50
|
+
# The end state name for this path. If a target state was specified for
|
51
|
+
# the path, then that will be returned if the path is complete.
|
52
|
+
def to_name
|
53
|
+
last && last.to_name
|
54
|
+
end
|
55
|
+
|
56
|
+
# Lists all of the to states that can be reached through this path.
|
57
|
+
#
|
58
|
+
# For example,
|
59
|
+
#
|
60
|
+
# path.to_states # => [:parked, :idling, :first_gear, ...]
|
61
|
+
def to_states
|
62
|
+
map {|transition| transition.to_name}.uniq
|
63
|
+
end
|
64
|
+
|
65
|
+
# Lists all of the events that can be fired through this path.
|
66
|
+
#
|
67
|
+
# For example,
|
68
|
+
#
|
69
|
+
# path.events # => [:park, :ignite, :shift_up, ...]
|
70
|
+
def events
|
71
|
+
map {|transition| transition.event}.uniq
|
72
|
+
end
|
73
|
+
|
74
|
+
# Walks down the next transitions at the end of this path. This will only
|
75
|
+
# walk down paths that are considered valid.
|
76
|
+
def walk
|
77
|
+
transitions.each {|transition| yield dup.push(transition)}
|
78
|
+
end
|
79
|
+
|
80
|
+
# Determines whether or not this path has completed. A path is considered
|
81
|
+
# complete when one of the following conditions is met:
|
82
|
+
# * The last transition in the path ends on the target state
|
83
|
+
# * There are no more transitions remaining to walk and there is no target
|
84
|
+
# state
|
85
|
+
def complete?
|
86
|
+
!empty? && (@target ? to_name == @target : transitions.empty?)
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
# Calculates the number of times the given state has been walked to
|
91
|
+
def times_walked_to(state)
|
92
|
+
select {|transition| transition.to_name == state}.length
|
93
|
+
end
|
94
|
+
|
95
|
+
# Determines whether the given transition has been recently walked down in
|
96
|
+
# this path. If a target is configured for this path, then this will only
|
97
|
+
# look at transitions walked down since the target was last reached.
|
98
|
+
def recently_walked?(transition)
|
99
|
+
transitions = self
|
100
|
+
if @target && @target != to_name && target_transition = detect {|t| t.to_name == @target}
|
101
|
+
transitions = transitions[index(target_transition) + 1..-1]
|
102
|
+
end
|
103
|
+
transitions.include?(transition)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Determines whether it's possible to walk to the given transition from
|
107
|
+
# the current path. A transition can be walked to if:
|
108
|
+
# * It has not been recently walked and
|
109
|
+
# * If a target is specified, it has not been walked to twice yet
|
110
|
+
def can_walk_to?(transition)
|
111
|
+
!recently_walked?(transition) && (!@target || times_walked_to(@target) < 2)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Get the next set of transitions that can be walked to starting from the
|
115
|
+
# end of this path
|
116
|
+
def transitions
|
117
|
+
@transitions ||= empty? ? [] : machine.events.transitions_for(object, :from => to_name, :guard => @guard).select {|transition| can_walk_to?(transition)}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'state_machine/path'
|
2
|
+
|
3
|
+
module StateMachine
|
4
|
+
# Represents a collection of paths that are generated based on a set of
|
5
|
+
# requirements regarding what states to start and end on
|
6
|
+
class PathCollection < Array
|
7
|
+
include Assertions
|
8
|
+
|
9
|
+
# The object whose state machine is being walked
|
10
|
+
attr_reader :object
|
11
|
+
|
12
|
+
# The state machine these path are walking
|
13
|
+
attr_reader :machine
|
14
|
+
|
15
|
+
# The initial state to start each path from
|
16
|
+
attr_reader :from_name
|
17
|
+
|
18
|
+
# The target state for each path
|
19
|
+
attr_reader :to_name
|
20
|
+
|
21
|
+
# Creates a new collection of paths with the given requirements.
|
22
|
+
#
|
23
|
+
# Configuration options:
|
24
|
+
# * <tt>:from</tt> - The initial state to start from
|
25
|
+
# * <tt>:to</tt> - The target end state
|
26
|
+
# * <tt>:deep</tt> - Whether to enable deep searches for the target state.
|
27
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
28
|
+
# conditionals defined for each one
|
29
|
+
def initialize(object, machine, options = {})
|
30
|
+
options = {:deep => false, :from => machine.states.match!(object).name}.merge(options)
|
31
|
+
assert_valid_keys(options, :from, :to, :deep, :guard)
|
32
|
+
|
33
|
+
@object = object
|
34
|
+
@machine = machine
|
35
|
+
@from_name = machine.states.fetch(options[:from]).name
|
36
|
+
@to_name = options[:to] && machine.states.fetch(options[:to]).name
|
37
|
+
@guard = options[:guard]
|
38
|
+
@deep = options[:deep]
|
39
|
+
|
40
|
+
initial_paths.each {|path| walk(path)}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Lists all of the states that can be transitioned from through the paths in
|
44
|
+
# this collection.
|
45
|
+
#
|
46
|
+
# For example,
|
47
|
+
#
|
48
|
+
# paths.from_states # => [:parked, :idling, :first_gear, ...]
|
49
|
+
def from_states
|
50
|
+
map {|path| path.from_states}.flatten.uniq
|
51
|
+
end
|
52
|
+
|
53
|
+
# Lists all of the states that can be transitioned to through the paths in
|
54
|
+
# this collection.
|
55
|
+
#
|
56
|
+
# For example,
|
57
|
+
#
|
58
|
+
# paths.to_states # => [:idling, :first_gear, :second_gear, ...]
|
59
|
+
def to_states
|
60
|
+
map {|path| path.to_states}.flatten.uniq
|
61
|
+
end
|
62
|
+
|
63
|
+
# Lists all of the events that can be fired through the paths in this
|
64
|
+
# collection.
|
65
|
+
#
|
66
|
+
# For example,
|
67
|
+
#
|
68
|
+
# paths.events # => [:park, :ignite, :shift_up, ...]
|
69
|
+
def events
|
70
|
+
map {|path| path.events}.flatten.uniq
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
# Gets the initial set of paths to walk
|
75
|
+
def initial_paths
|
76
|
+
machine.events.transitions_for(object, :from => from_name, :guard => @guard).map do |transition|
|
77
|
+
path = Path.new(object, machine, :target => to_name, :guard => @guard)
|
78
|
+
path << transition
|
79
|
+
path
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Walks down the given path. Each new path that matches the configured
|
84
|
+
# requirements will be added to this collection.
|
85
|
+
def walk(path)
|
86
|
+
self << path if path.complete?
|
87
|
+
path.walk {|next_path| walk(next_path)} unless to_name && path.complete? && !@deep
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|