levels 0.1.0
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/.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
|