pluginaweek-state_machine 0.7.6
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.
- data/CHANGELOG.rdoc +273 -0
- data/LICENSE +20 -0
- data/README.rdoc +466 -0
- data/Rakefile +98 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/TrafficLight_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/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 +11 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view_edit.html.erb +25 -0
- data/examples/rails-rest/view_index.html.erb +23 -0
- data/examples/rails-rest/view_new.html.erb +14 -0
- data/examples/rails-rest/view_show.html.erb +17 -0
- data/examples/traffic_light.rb +7 -0
- data/examples/vehicle.rb +31 -0
- data/init.rb +1 -0
- data/lib/state_machine.rb +429 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/callback.rb +189 -0
- data/lib/state_machine/condition_proxy.rb +94 -0
- data/lib/state_machine/eval_helpers.rb +67 -0
- data/lib/state_machine/event.rb +251 -0
- data/lib/state_machine/event_collection.rb +113 -0
- data/lib/state_machine/extensions.rb +158 -0
- data/lib/state_machine/guard.rb +219 -0
- data/lib/state_machine/integrations.rb +68 -0
- data/lib/state_machine/integrations/active_record.rb +444 -0
- data/lib/state_machine/integrations/active_record/locale.rb +10 -0
- data/lib/state_machine/integrations/active_record/observer.rb +41 -0
- data/lib/state_machine/integrations/data_mapper.rb +325 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
- data/lib/state_machine/integrations/sequel.rb +292 -0
- data/lib/state_machine/machine.rb +1431 -0
- data/lib/state_machine/machine_collection.rb +146 -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 +152 -0
- data/lib/state_machine/state.rb +249 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/transition.rb +367 -0
- data/tasks/state_machine.rake +1 -0
- data/tasks/state_machine.rb +30 -0
- data/test/classes/switch.rb +11 -0
- data/test/functional/state_machine_test.rb +941 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/callback_test.rb +455 -0
- data/test/unit/condition_proxy_test.rb +328 -0
- data/test/unit/eval_helpers_test.rb +129 -0
- data/test/unit/event_collection_test.rb +293 -0
- data/test/unit/event_test.rb +605 -0
- data/test/unit/guard_test.rb +862 -0
- data/test/unit/integrations/active_record_test.rb +1001 -0
- data/test/unit/integrations/data_mapper_test.rb +694 -0
- data/test/unit/integrations/sequel_test.rb +486 -0
- data/test/unit/integrations_test.rb +42 -0
- data/test/unit/invalid_event_test.rb +7 -0
- data/test/unit/invalid_transition_test.rb +7 -0
- data/test/unit/machine_collection_test.rb +710 -0
- data/test/unit/machine_test.rb +1910 -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 +207 -0
- data/test/unit/state_collection_test.rb +280 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +795 -0
- data/test/unit/transition_test.rb +1113 -0
- metadata +161 -0
@@ -0,0 +1,146 @@
|
|
1
|
+
module StateMachine
|
2
|
+
# Represents a collection of state machines for a class
|
3
|
+
class MachineCollection < Hash
|
4
|
+
# Initializes the state of each machine in the given object. Initial
|
5
|
+
# values are only set if the machine's attribute doesn't already exist
|
6
|
+
# (which must mean the defaults are being skipped)
|
7
|
+
def initialize_states(object)
|
8
|
+
each do |attribute, machine|
|
9
|
+
value = machine.read(object, :state)
|
10
|
+
machine.write(object, :state, machine.initial_state(object).value) if value.nil? || value.respond_to?(:empty?) && value.empty?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Runs one or more events in parallel on the given object. See
|
15
|
+
# StateMachine::InstanceMethods#fire_events for more information.
|
16
|
+
def fire_events(object, *events)
|
17
|
+
run_action = [true, false].include?(events.last) ? events.pop : true
|
18
|
+
|
19
|
+
# Generate the transitions to run for each event
|
20
|
+
transitions = events.collect do |name|
|
21
|
+
# Find the actual event being run
|
22
|
+
event = nil
|
23
|
+
detect do |attribute, machine|
|
24
|
+
event = machine.events[name, :qualified_name]
|
25
|
+
end
|
26
|
+
|
27
|
+
raise InvalidEvent, "#{name.inspect} is an unknown state machine event" unless event
|
28
|
+
|
29
|
+
# Get the transition that will be performed for the event
|
30
|
+
unless transition = event.transition_for(object)
|
31
|
+
machine = event.machine
|
32
|
+
machine.invalidate(object, machine.attribute, :invalid_transition, [[:event, name]])
|
33
|
+
end
|
34
|
+
|
35
|
+
transition
|
36
|
+
end.compact
|
37
|
+
|
38
|
+
# Run the events in parallel only if valid transitions were found for
|
39
|
+
# all of them
|
40
|
+
if events.length == transitions.length
|
41
|
+
Transition.perform_within_transaction(transitions, :action => run_action)
|
42
|
+
else
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Runs one or more event attributes in parallel during the invocation of
|
48
|
+
# an action on the given object. After transition callbacks can be
|
49
|
+
# optionally disabled if the events are being only partially fired (for
|
50
|
+
# example, when validating records in ORM integrations).
|
51
|
+
#
|
52
|
+
# The event attributes that will be fired are based on which machines
|
53
|
+
# match the action that is being invoked.
|
54
|
+
#
|
55
|
+
# == Examples
|
56
|
+
#
|
57
|
+
# class Vehicle
|
58
|
+
# include DataMapper::Resource
|
59
|
+
# property :id, Integer, :serial => true
|
60
|
+
#
|
61
|
+
# state_machine :initial => :parked do
|
62
|
+
# event :ignite do
|
63
|
+
# transition :parked => :idling
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# state_machine :alarm_state, :namespace => 'alarm', :initial => :active do
|
68
|
+
# event :disable do
|
69
|
+
# transition all => :off
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# With valid events:
|
75
|
+
#
|
76
|
+
# vehicle = Vehicle.create # => #<Vehicle id=1 state="parked" alarm_state="active">
|
77
|
+
# vehicle.state_event = 'ignite'
|
78
|
+
# vehicle.alarm_state_event = 'disable'
|
79
|
+
#
|
80
|
+
# Vehicle.state_machines.fire_event_attributes(vehicle, :save) { true }
|
81
|
+
# vehicle.state # => "idling"
|
82
|
+
# vehicle.state_event # => nil
|
83
|
+
# vehicle.alarm_state # => "off"
|
84
|
+
# vehicle.alarm_state_event # => nil
|
85
|
+
#
|
86
|
+
# With invalid events:
|
87
|
+
#
|
88
|
+
# vehicle = Vehicle.create # => #<Vehicle id=1 state="parked" alarm_state="active">
|
89
|
+
# vehicle.state_event = 'park'
|
90
|
+
# vehicle.alarm_state_event = 'disable'
|
91
|
+
#
|
92
|
+
# Vehicle.state_machines.fire_event_attributes(vehicle, :save) { true }
|
93
|
+
# vehicle.state # => "parked"
|
94
|
+
# vehicle.state_event # => nil
|
95
|
+
# vehicle.alarm_state # => "active"
|
96
|
+
# vehicle.alarm_state_event # => nil
|
97
|
+
# vehicle.errors # => #<DataMapper::Validate::ValidationErrors:0xb7af9abc @errors={"state_event"=>["is invalid"]}>
|
98
|
+
#
|
99
|
+
# With partial firing:
|
100
|
+
#
|
101
|
+
# vehicle = Vehicle.create # => #<Vehicle id=1 state="parked" alarm_state="active">
|
102
|
+
# vehicle.state_event = 'ignite'
|
103
|
+
#
|
104
|
+
# Vehicle.state_machines.fire_event_attributes(vehicle, :save, false) { true }
|
105
|
+
# vehicle.state # => "idling"
|
106
|
+
# vehicle.state_event # => "ignite"
|
107
|
+
# vehicle.state_event_transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
|
108
|
+
def fire_event_attributes(object, action, complete = true)
|
109
|
+
# Get the transitions to fire for each applicable machine
|
110
|
+
transitions = map {|attribute, machine| machine.action == action ? machine.events.attribute_transition_for(object, true) : nil}.compact
|
111
|
+
return yield if transitions.empty?
|
112
|
+
|
113
|
+
# The value generated by the yielded block (the actual action)
|
114
|
+
action_value = nil
|
115
|
+
|
116
|
+
# Make sure all events were valid
|
117
|
+
if result = transitions.all? {|transition| transition != false}
|
118
|
+
begin
|
119
|
+
result = Transition.perform(transitions, :after => complete) do
|
120
|
+
# Prevent events from being evaluated multiple times if actions are nested
|
121
|
+
transitions.each {|transition| transition.machine.write(object, :event, nil)}
|
122
|
+
action_value = yield
|
123
|
+
end
|
124
|
+
rescue Exception
|
125
|
+
# Revert attribute modifications
|
126
|
+
transitions.each do |transition|
|
127
|
+
transition.machine.write(object, :event, transition.event)
|
128
|
+
transition.machine.write(object, :event_transition, nil) if complete
|
129
|
+
end
|
130
|
+
|
131
|
+
raise
|
132
|
+
end
|
133
|
+
|
134
|
+
transitions.each do |transition|
|
135
|
+
# Revert event unless transition was successful
|
136
|
+
transition.machine.write(object, :event, transition.event) unless complete && result
|
137
|
+
|
138
|
+
# Track transition if partial transition completed successfully
|
139
|
+
transition.machine.write(object, :event_transition, !complete && result ? transition : nil)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
action_value.nil? ? result : action_value
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -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,152 @@
|
|
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
|
+
# The machine associated with the nodes
|
10
|
+
attr_reader :machine
|
11
|
+
|
12
|
+
# Creates a new collection of nodes for the given state machine. By default,
|
13
|
+
# the collection is empty.
|
14
|
+
#
|
15
|
+
# Configuration options:
|
16
|
+
# * <tt>:index</tt> - One or more attributes to automatically generate
|
17
|
+
# hashed indices for in order to perform quick lookups. Default is to
|
18
|
+
# index by the :name attribute
|
19
|
+
def initialize(machine, options = {})
|
20
|
+
assert_valid_keys(options, :index)
|
21
|
+
options = {:index => :name}.merge(options)
|
22
|
+
|
23
|
+
@machine = machine
|
24
|
+
@nodes = []
|
25
|
+
@indices = Array(options[:index]).inject({}) {|indices, attribute| indices[attribute] = {}; indices}
|
26
|
+
@default_index = Array(options[:index]).first
|
27
|
+
end
|
28
|
+
|
29
|
+
# Creates a copy of this collection such that modifications don't affect
|
30
|
+
# the original collection
|
31
|
+
def initialize_copy(orig) #:nodoc:
|
32
|
+
super
|
33
|
+
|
34
|
+
nodes = @nodes
|
35
|
+
@nodes = []
|
36
|
+
@indices = @indices.inject({}) {|indices, (name, index)| indices[name] = {}; indices}
|
37
|
+
nodes.each {|node| self << node.dup}
|
38
|
+
end
|
39
|
+
|
40
|
+
# Changes the current machine associated with the collection. In turn, this
|
41
|
+
# will change the state machine associated with each node in the collection.
|
42
|
+
def machine=(new_machine)
|
43
|
+
@machine = new_machine
|
44
|
+
each {|node| node.machine = new_machine}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Gets the number of nodes in this collection
|
48
|
+
def length
|
49
|
+
@nodes.length
|
50
|
+
end
|
51
|
+
|
52
|
+
# Gets the set of unique keys for the given index
|
53
|
+
def keys(index_name = @default_index)
|
54
|
+
index(index_name).keys
|
55
|
+
end
|
56
|
+
|
57
|
+
# Adds a new node to the collection. By doing so, this will also add it to
|
58
|
+
# the configured indices.
|
59
|
+
def <<(node)
|
60
|
+
@nodes << node
|
61
|
+
@indices.each {|attribute, index| index[value(node, attribute)] = node}
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
# Updates the indexed keys for the given node. If the node's attribute
|
66
|
+
# has changed since it was added to the collection, the old indexed keys
|
67
|
+
# will be replaced with the updated ones.
|
68
|
+
def update(node)
|
69
|
+
@indices.each do |attribute, index|
|
70
|
+
old_key = RUBY_VERSION < '1.9' ? index.index(node) : index.key(node)
|
71
|
+
new_key = value(node, attribute)
|
72
|
+
|
73
|
+
# Only replace the key if it's changed
|
74
|
+
if old_key != new_key
|
75
|
+
index.delete(old_key)
|
76
|
+
index[new_key] = node
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Calls the block once for each element in self, passing that element as a
|
82
|
+
# parameter.
|
83
|
+
#
|
84
|
+
# states = StateMachine::NodeCollection.new
|
85
|
+
# states << StateMachine::State.new(machine, :parked)
|
86
|
+
# states << StateMachine::State.new(machine, :idling)
|
87
|
+
# states.each {|state| puts state.name, ' -- '}
|
88
|
+
#
|
89
|
+
# ...produces:
|
90
|
+
#
|
91
|
+
# parked -- idling --
|
92
|
+
def each
|
93
|
+
@nodes.each {|node| yield node}
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
# Gets the node at the given index.
|
98
|
+
#
|
99
|
+
# states = StateMachine::NodeCollection.new
|
100
|
+
# states << StateMachine::State.new(machine, :parked)
|
101
|
+
# states << StateMachine::State.new(machine, :idling)
|
102
|
+
#
|
103
|
+
# states.at(0).name # => :parked
|
104
|
+
# states.at(1).name # => :idling
|
105
|
+
def at(index)
|
106
|
+
@nodes[index]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Gets the node indexed by the given key. By default, this will look up the
|
110
|
+
# key in the first index configured for the collection. A custom index can
|
111
|
+
# be specified like so:
|
112
|
+
#
|
113
|
+
# collection['parked', :value]
|
114
|
+
#
|
115
|
+
# The above will look up the "parked" key in a hash indexed by each node's
|
116
|
+
# +value+ attribute.
|
117
|
+
#
|
118
|
+
# If the key cannot be found, then nil will be returned.
|
119
|
+
def [](key, index_name = @default_index)
|
120
|
+
index(index_name)[key]
|
121
|
+
end
|
122
|
+
|
123
|
+
# Gets the node indexed by the given key. By default, this will look up the
|
124
|
+
# key in the first index configured for the collection. A custom index can
|
125
|
+
# be specified like so:
|
126
|
+
#
|
127
|
+
# collection['parked', :value]
|
128
|
+
#
|
129
|
+
# The above will look up the "parked" key in a hash indexed by each node's
|
130
|
+
# +value+ attribute.
|
131
|
+
#
|
132
|
+
# If the key cannot be found, then an IndexError exception will be raised:
|
133
|
+
#
|
134
|
+
# collection['invalid', :value] # => IndexError: "invalid" is an invalid value
|
135
|
+
def fetch(key, index_name = @default_index)
|
136
|
+
self[key, index_name] || raise(IndexError, "#{key.inspect} is an invalid #{index_name}")
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
# Gets the given index. If the index does not exist, then an ArgumentError
|
141
|
+
# is raised.
|
142
|
+
def index(name)
|
143
|
+
raise ArgumentError, 'No indices configured' unless @indices.any?
|
144
|
+
@indices[name] || raise(ArgumentError, "Invalid index: #{name.inspect}")
|
145
|
+
end
|
146
|
+
|
147
|
+
# Gets the value for the given attribute on the node
|
148
|
+
def value(node, attribute)
|
149
|
+
node.send(attribute)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|