levels 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rbenv-version +1 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +12 -0
- data/Guardfile +14 -0
- data/LICENSE +22 -0
- data/README.md +315 -0
- data/Rakefile +28 -0
- data/bin/levels +130 -0
- data/examples/01_base.rb +6 -0
- data/examples/01_merge_to_json.sh +27 -0
- data/examples/01_prod.json +8 -0
- data/examples/02_base.rb +4 -0
- data/examples/02_merge_with_file.sh +20 -0
- data/examples/02_value +1 -0
- data/levels.gemspec +20 -0
- data/lib/levels.rb +77 -0
- data/lib/levels/audit.rb +24 -0
- data/lib/levels/audit/group_observer.rb +26 -0
- data/lib/levels/audit/nested_group_observer.rb +37 -0
- data/lib/levels/audit/root_observer.rb +63 -0
- data/lib/levels/audit/value.rb +64 -0
- data/lib/levels/audit/value_observer.rb +46 -0
- data/lib/levels/audit/values.rb +66 -0
- data/lib/levels/configuration.rb +98 -0
- data/lib/levels/configured_group.rb +62 -0
- data/lib/levels/event_handler.rb +127 -0
- data/lib/levels/group.rb +61 -0
- data/lib/levels/input/json.rb +17 -0
- data/lib/levels/input/ruby.rb +120 -0
- data/lib/levels/input/system.rb +63 -0
- data/lib/levels/input/yaml.rb +17 -0
- data/lib/levels/key.rb +28 -0
- data/lib/levels/key_values.rb +54 -0
- data/lib/levels/lazy_evaluator.rb +54 -0
- data/lib/levels/level.rb +80 -0
- data/lib/levels/method_missing.rb +14 -0
- data/lib/levels/output/json.rb +33 -0
- data/lib/levels/output/system.rb +29 -0
- data/lib/levels/output/yaml.rb +19 -0
- data/lib/levels/runtime.rb +30 -0
- data/lib/levels/setup.rb +132 -0
- data/lib/levels/system/constants.rb +8 -0
- data/lib/levels/system/key_formatter.rb +15 -0
- data/lib/levels/system/key_generator.rb +50 -0
- data/lib/levels/system/key_parser.rb +67 -0
- data/lib/levels/version.rb +3 -0
- data/test/acceptance/audit_test.rb +105 -0
- data/test/acceptance/event_handler_test.rb +43 -0
- data/test/acceptance/read_json_test.rb +35 -0
- data/test/acceptance/read_ruby_test.rb +117 -0
- data/test/acceptance/read_system_test.rb +121 -0
- data/test/acceptance/read_yaml_test.rb +38 -0
- data/test/acceptance/setup_test.rb +115 -0
- data/test/acceptance/write_json_test.rb +39 -0
- data/test/acceptance/write_system_test.rb +68 -0
- data/test/acceptance/write_yaml_test.rb +33 -0
- data/test/bin/merge_test.rb +194 -0
- data/test/bin/options_test.rb +41 -0
- data/test/helper.rb +12 -0
- data/test/support/acceptance_spec.rb +58 -0
- data/test/support/base_spec.rb +14 -0
- data/test/support/bin_spec.rb +65 -0
- data/test/support/tempfile_helper.rb +35 -0
- data/test/unit/audit/group_observer_test.rb +24 -0
- data/test/unit/audit/nested_group_observer_test.rb +28 -0
- data/test/unit/audit/root_observer_test.rb +54 -0
- data/test/unit/audit/value_observer_test.rb +63 -0
- data/test/unit/audit/value_test.rb +41 -0
- data/test/unit/audit/values_test.rb +86 -0
- data/test/unit/configuration_test.rb +72 -0
- data/test/unit/configured_group_test.rb +75 -0
- data/test/unit/group_test.rb +105 -0
- data/test/unit/input/json_test.rb +32 -0
- data/test/unit/input/ruby_test.rb +140 -0
- data/test/unit/input/system_test.rb +59 -0
- data/test/unit/input/yaml_test.rb +33 -0
- data/test/unit/key_test.rb +45 -0
- data/test/unit/key_values_test.rb +106 -0
- data/test/unit/lazy_evaluator_test.rb +38 -0
- data/test/unit/level_test.rb +89 -0
- data/test/unit/levels_test.rb +23 -0
- data/test/unit/output/json_test.rb +55 -0
- data/test/unit/output/system_test.rb +32 -0
- data/test/unit/output/yaml_test.rb +38 -0
- data/test/unit/runtime_test.rb +40 -0
- data/test/unit/system/key_formatter_test.rb +43 -0
- data/test/unit/system/key_generator_test.rb +21 -0
- data/test/unit/system/key_parser_test.rb +207 -0
- metadata +215 -0
data/examples/01_base.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
#
|
3
|
+
# This example sets up three levels:
|
4
|
+
#
|
5
|
+
# * "Base" from examples/01_base.rb
|
6
|
+
# * "Prod" from examples/01_prod.json
|
7
|
+
# * "System Environment" with no prefix.
|
8
|
+
#
|
9
|
+
# Base defines the possible keys with default vaules, Prod changes a few of
|
10
|
+
# those values, and System changes one more.
|
11
|
+
#
|
12
|
+
# A log of the levels used, and where each value came from is written to
|
13
|
+
# STDERR. The merged output as JSON is written to STDOUT.
|
14
|
+
|
15
|
+
examples="$(cd "$(dirname "$BASH_SOURCE")" && pwd)"
|
16
|
+
|
17
|
+
# Alter a value via the system environment.
|
18
|
+
export TASK_QUEUE_WORKERS='10'
|
19
|
+
|
20
|
+
bundle exec levels \
|
21
|
+
--output json \
|
22
|
+
--level "Base" \
|
23
|
+
--level "Prod" \
|
24
|
+
--system \
|
25
|
+
$examples/01_base.rb \
|
26
|
+
$examples/01_prod.json
|
27
|
+
|
data/examples/02_base.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
#
|
3
|
+
# This example shows how to store and read a value from a file.
|
4
|
+
#
|
5
|
+
# * examples/02_base.rb reads a file twice. Once where the file name
|
6
|
+
# is known, and once where it is unknown until set via an
|
7
|
+
# environment variable.
|
8
|
+
|
9
|
+
examples="$(cd "$(dirname "$BASH_SOURCE")" && pwd)"
|
10
|
+
|
11
|
+
# Alter a value via the system environment.
|
12
|
+
export EXAMPLES_FILE_NAME='02_value'
|
13
|
+
|
14
|
+
bundle exec levels \
|
15
|
+
--output json \
|
16
|
+
--level "Base" \
|
17
|
+
--system \
|
18
|
+
$examples/02_base.rb
|
19
|
+
|
20
|
+
|
data/examples/02_value
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Hello from the file!
|
data/levels.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/levels/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Ryan Carver"]
|
6
|
+
gem.email = ["ryan@ryancarver.com"]
|
7
|
+
gem.description = %q{A tool for reading and writing configuration data.}
|
8
|
+
gem.summary = %q{A tool for reading and writing configuration data.}
|
9
|
+
gem.homepage = "https://github.com/rcarver/levels"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "levels"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Levels::VERSION
|
17
|
+
|
18
|
+
gem.add_development_dependency "rake"
|
19
|
+
gem.add_development_dependency "minitest", "~>4.0"
|
20
|
+
end
|
data/lib/levels.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require "json"
|
2
|
+
require "yaml"
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
require "levels/version"
|
6
|
+
|
7
|
+
require "levels/method_missing"
|
8
|
+
require "levels/runtime"
|
9
|
+
|
10
|
+
require "levels/audit"
|
11
|
+
require "levels/configuration"
|
12
|
+
require "levels/configured_group"
|
13
|
+
require "levels/event_handler"
|
14
|
+
require "levels/group"
|
15
|
+
require "levels/key"
|
16
|
+
require "levels/key_values"
|
17
|
+
require "levels/lazy_evaluator"
|
18
|
+
require "levels/level"
|
19
|
+
require "levels/setup"
|
20
|
+
|
21
|
+
require "levels/input/json"
|
22
|
+
require "levels/input/ruby"
|
23
|
+
require "levels/input/system"
|
24
|
+
require "levels/input/yaml"
|
25
|
+
|
26
|
+
require "levels/output/json"
|
27
|
+
require "levels/output/system"
|
28
|
+
require "levels/output/yaml"
|
29
|
+
|
30
|
+
require "levels/system/constants"
|
31
|
+
require "levels/system/key_formatter"
|
32
|
+
require "levels/system/key_generator"
|
33
|
+
require "levels/system/key_parser"
|
34
|
+
|
35
|
+
module Levels
|
36
|
+
|
37
|
+
# Error thrown if a group is defined more than once.
|
38
|
+
DuplicateGroup = Class.new(StandardError)
|
39
|
+
|
40
|
+
# Error thrown when attempting to access a group that has not been defined.
|
41
|
+
UnknownGroup = Class.new(StandardError)
|
42
|
+
|
43
|
+
# Error thrown when attempting to read a key that has not been defined.
|
44
|
+
UnknownKey = Class.new(StandardError)
|
45
|
+
|
46
|
+
# Public: Begin a new setup. The setup is used to add one or more
|
47
|
+
# levels, then merged into a configuration.
|
48
|
+
#
|
49
|
+
# Examples
|
50
|
+
#
|
51
|
+
# setup = Levels.setup
|
52
|
+
# setup.add "Base", "file.rb"
|
53
|
+
# setup.add_system
|
54
|
+
# my_config = setup.merge
|
55
|
+
#
|
56
|
+
# Returns a Levels::Setup.
|
57
|
+
def self.setup
|
58
|
+
Levels::Setup.new
|
59
|
+
end
|
60
|
+
|
61
|
+
# Public: Get a merged configuration by using the setup.
|
62
|
+
#
|
63
|
+
# Examples
|
64
|
+
#
|
65
|
+
# my_config = Levels.merge do |setup|
|
66
|
+
# setup.add "Base", "file.rb"
|
67
|
+
# setup.add_system
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# Returns a Levels::Configuration.
|
71
|
+
def self.merge
|
72
|
+
setup = self.setup
|
73
|
+
yield setup if block_given?
|
74
|
+
setup.merge
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
data/lib/levels/audit.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Levels
|
2
|
+
# In order to understand which of many possible values is actually used at
|
3
|
+
# runtime, Levels provides an audit trail for each value that's accessed.
|
4
|
+
# The audit trail is reported via the Levels::EventHandler interface.
|
5
|
+
module Audit
|
6
|
+
|
7
|
+
# Internal: Begin an audit.
|
8
|
+
#
|
9
|
+
# evaluator - Ducktype #call used to interpret raw values.
|
10
|
+
#
|
11
|
+
# Returns a Levels::Audit::RootObserver.
|
12
|
+
def self.start(evaluator)
|
13
|
+
Levels::Audit::RootObserver.new(evaluator)
|
14
|
+
end
|
15
|
+
|
16
|
+
autoload :GroupObserver, 'levels/audit/group_observer'
|
17
|
+
autoload :NestedGroupObserver, 'levels/audit/nested_group_observer'
|
18
|
+
autoload :RootObserver, 'levels/audit/root_observer'
|
19
|
+
autoload :Value, 'levels/audit/value'
|
20
|
+
autoload :ValueObserver, 'levels/audit/value_observer'
|
21
|
+
autoload :Values, 'levels/audit/values'
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Levels
|
2
|
+
module Audit
|
3
|
+
# The GroupObserver notifies when a value is accessed.
|
4
|
+
class GroupObserver
|
5
|
+
|
6
|
+
# Initialize a new GroupObserver.
|
7
|
+
#
|
8
|
+
# value_observer - Levels::Audit::ValueObserver.
|
9
|
+
# event_handler - Levels::EventHandler.
|
10
|
+
#
|
11
|
+
def initialize(value_observer, event_handler)
|
12
|
+
@value_observer = value_observer
|
13
|
+
@event_handler = event_handler
|
14
|
+
end
|
15
|
+
|
16
|
+
# Retrieve the value at a group+value key and notify that it was read.
|
17
|
+
#
|
18
|
+
# Returns a Levels::Audit::Values.
|
19
|
+
def observe_values(levels, group_key, value_key)
|
20
|
+
values = @value_observer.observe_values(levels, group_key, value_key)
|
21
|
+
@event_handler.on_values(values)
|
22
|
+
values
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Levels
|
2
|
+
module Audit
|
3
|
+
# The NestedGroupObserver is like a GroupObserver, but used to
|
4
|
+
# observe what happens during recursive value evaluation.
|
5
|
+
class NestedGroupObserver
|
6
|
+
|
7
|
+
# Initialize a new NestedGroupObserver.
|
8
|
+
#
|
9
|
+
# value_observer - Levels::Audit::ValueObserver.
|
10
|
+
#
|
11
|
+
def initialize(value_observer)
|
12
|
+
@value_observer = value_observer
|
13
|
+
@values = []
|
14
|
+
end
|
15
|
+
|
16
|
+
# Retrieve the value at a group+value key.
|
17
|
+
#
|
18
|
+
# Returns a Levels::Audit::Values.
|
19
|
+
def observe_values(levels, group_key, value_key)
|
20
|
+
values = @value_observer.observe_values(levels, group_key, value_key)
|
21
|
+
@values << values
|
22
|
+
values
|
23
|
+
end
|
24
|
+
|
25
|
+
# Private: Notify that the observed values were seen. After notifying of
|
26
|
+
# the observed values, the set of observed values is reset.
|
27
|
+
#
|
28
|
+
# event_handler - Levels::EventHandler.
|
29
|
+
#
|
30
|
+
# Returns nothing.
|
31
|
+
def notify_nested(event_handler)
|
32
|
+
@values.each { |v| event_handler.on_nested_values(v) }
|
33
|
+
@values.clear
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Levels
|
2
|
+
module Audit
|
3
|
+
# The RootObserver observes all accesses to group and value data.
|
4
|
+
class RootObserver
|
5
|
+
|
6
|
+
# Initialize a new RootObserver
|
7
|
+
#
|
8
|
+
# evaluator - Ducktype #call used to interpret raw values.
|
9
|
+
#
|
10
|
+
def initialize(evaluator)
|
11
|
+
@evaluator = evaluator
|
12
|
+
@current_value_stack = []
|
13
|
+
end
|
14
|
+
|
15
|
+
# Get an observer to watch when values are accessed from a group.
|
16
|
+
#
|
17
|
+
# event_handler - Levels::EventHandler to receive observations.
|
18
|
+
#
|
19
|
+
# Returns a Levels::Audit::GroupObserver.
|
20
|
+
def observe_group(user_observer)
|
21
|
+
if current_value
|
22
|
+
observer = NestedGroupObserver.new(value_observer)
|
23
|
+
current_value.add_nested_group_observer(observer)
|
24
|
+
observer
|
25
|
+
else
|
26
|
+
GroupObserver.new(value_observer, user_observer)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Private: Set the current value context. This is used to capture
|
31
|
+
# recursive values.
|
32
|
+
#
|
33
|
+
# value - Levels::Audit::Value.
|
34
|
+
#
|
35
|
+
# Yields.
|
36
|
+
#
|
37
|
+
# Returns nothing.
|
38
|
+
def with_current_value(value)
|
39
|
+
begin
|
40
|
+
@current_value_stack << value
|
41
|
+
yield
|
42
|
+
ensure
|
43
|
+
@current_value_stack.pop
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Private: Get the current value.
|
48
|
+
#
|
49
|
+
# Returns a Levels::Audit::Value or nil.
|
50
|
+
def current_value
|
51
|
+
@current_value_stack.last
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def value_observer
|
57
|
+
ValueObserver.new(self, @evaluator)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Levels
|
2
|
+
module Audit
|
3
|
+
# The Value represents one piece of configuration data that was originally
|
4
|
+
# stored in a Level.
|
5
|
+
class Value
|
6
|
+
|
7
|
+
# Initialize a new Value.
|
8
|
+
#
|
9
|
+
# level_name - String the name of the level this value was in.
|
10
|
+
# final - Boolean true if this is the "final" value for a set of
|
11
|
+
# levels.
|
12
|
+
#
|
13
|
+
def initialize(level_name, final, value = :__no_value__)
|
14
|
+
@level_name = level_name
|
15
|
+
@final = final
|
16
|
+
@nested_group_observers = []
|
17
|
+
if value == :__no_value__
|
18
|
+
@value = yield self if block_given?
|
19
|
+
else
|
20
|
+
@value = value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns a String the name of the level.
|
25
|
+
attr_reader :level_name
|
26
|
+
|
27
|
+
# Returns the actual value.
|
28
|
+
attr_reader :value
|
29
|
+
|
30
|
+
# Returns the actual value.
|
31
|
+
alias raw value
|
32
|
+
|
33
|
+
# Returns a Boolean true if this is the final value.
|
34
|
+
def final?
|
35
|
+
!!@final
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns a Boolean true if this value is recursive.
|
39
|
+
def recursive?
|
40
|
+
!@nested_group_observers.empty?
|
41
|
+
end
|
42
|
+
|
43
|
+
# Public: Trigger a notification of the nested values. For any nested
|
44
|
+
# values, the event handler will receive the `#on_nested_values` message.
|
45
|
+
#
|
46
|
+
# event_handler - Levels::EventHandler.
|
47
|
+
def notify(event_handler)
|
48
|
+
@nested_group_observers.each do |ngo|
|
49
|
+
ngo.notify_nested(event_handler)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Private: This is used to accumulate recursive group access.
|
54
|
+
def add_nested_group_observer(nested_group_observer)
|
55
|
+
@nested_group_observers << nested_group_observer
|
56
|
+
end
|
57
|
+
|
58
|
+
def inspect
|
59
|
+
value.inspect
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Levels
|
2
|
+
module Audit
|
3
|
+
# The ValueObserver iterates over levels to find the appriate values. It
|
4
|
+
# accumulates all possible values and indicates which is the "final" value.
|
5
|
+
class ValueObserver
|
6
|
+
|
7
|
+
# Initialize a new ValueObserver.
|
8
|
+
#
|
9
|
+
# root_observer - Levels::Audit::RootObserver.
|
10
|
+
# evaluator - Ducktype #call used to interpret raw values.
|
11
|
+
#
|
12
|
+
def initialize(root_observer, evaluator)
|
13
|
+
@root_observer = root_observer
|
14
|
+
@evaluator = evaluator
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get the possible values for a group+value key. The values are retrieved
|
18
|
+
# and evaluated in context of the RootObserver so that recursive values
|
19
|
+
# are tracked.
|
20
|
+
#
|
21
|
+
# levels - Array of Levels::Level.
|
22
|
+
# group_key - Levels::Key.
|
23
|
+
# value_key - Levels::Key.
|
24
|
+
#
|
25
|
+
# Returns a Levels::Audit::Values.
|
26
|
+
def observe_values(levels, group_key, value_key)
|
27
|
+
valid_levels = levels.find_all do |level|
|
28
|
+
level.defined?(group_key) &&
|
29
|
+
level[group_key].defined?(value_key)
|
30
|
+
end
|
31
|
+
|
32
|
+
values = valid_levels.map.with_index do |level, index|
|
33
|
+
group = level[group_key]
|
34
|
+
|
35
|
+
Value.new(level._level_name, index == valid_levels.size - 1) do |value|
|
36
|
+
@root_observer.with_current_value(value) do
|
37
|
+
@evaluator.call(group[value_key])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
Values.new(group_key, value_key, values)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Levels
|
2
|
+
module Audit
|
3
|
+
# The Values is a set of possible values for a group+value key.
|
4
|
+
class Values
|
5
|
+
|
6
|
+
# Initialize a new Values.
|
7
|
+
#
|
8
|
+
# group_key - Levels::Key.
|
9
|
+
# value_key - Levels::Key.
|
10
|
+
# values - Array of Levels::Audit::Value.
|
11
|
+
#
|
12
|
+
def initialize(group_key, value_key, values)
|
13
|
+
@group_key = group_key
|
14
|
+
@value_key = value_key
|
15
|
+
@values = values
|
16
|
+
end
|
17
|
+
|
18
|
+
# Public: Returns a Levels::Key.
|
19
|
+
attr_reader :group_key
|
20
|
+
|
21
|
+
# Public: Returns a Levels::Key.
|
22
|
+
attr_reader :value_key
|
23
|
+
|
24
|
+
# Public: Returns the Levels::Audit::Value marked final.
|
25
|
+
def final
|
26
|
+
@values.find { |v| v.final? }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Public: Returns the actual user-defined final value.
|
30
|
+
def final_value
|
31
|
+
@values.find { |v| v.final? }.value
|
32
|
+
end
|
33
|
+
|
34
|
+
include Enumerable
|
35
|
+
|
36
|
+
# Public: Iterate over all potential values.
|
37
|
+
def each(&block)
|
38
|
+
@values.each(&block)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Public: Returns true if there are no potential values.
|
42
|
+
def empty?
|
43
|
+
@values.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
# Public: Returns true if there is only a final value.
|
47
|
+
def only_final?
|
48
|
+
size == 1 && final
|
49
|
+
end
|
50
|
+
|
51
|
+
# Public: Returns true if any of the values are recursive.
|
52
|
+
def recursive?
|
53
|
+
@values.any? { |v| v.recursive? }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public: Returns the number of potential values.
|
57
|
+
def size
|
58
|
+
@values.size
|
59
|
+
end
|
60
|
+
|
61
|
+
def inspect
|
62
|
+
"<Values #{group_key.inspect} #{value_key.inspect} #{@values.inspect}>"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|