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
@@ -0,0 +1,98 @@
|
|
1
|
+
module Levels
|
2
|
+
# A Configuration is the merging of one or more levels. This is the top level
|
3
|
+
# object that you will interact with most.
|
4
|
+
#
|
5
|
+
# Examples
|
6
|
+
#
|
7
|
+
# # In this example we'll represent a Level as a Hash.
|
8
|
+
# level1 = { group1: { key1: 1 } }
|
9
|
+
# level2 = { group1: { key1: 2, key2: 2 }, group2: { x: 9 } }
|
10
|
+
#
|
11
|
+
# # The configuration exposes each group that exists in a Level.
|
12
|
+
# config = Levels::Configuration.new([level1, level2])
|
13
|
+
#
|
14
|
+
# # A Group may be accessed via hash syntax.
|
15
|
+
# config[:group1] # => { key1: 2, key2: 2 }
|
16
|
+
# # Or method syntax.
|
17
|
+
# config.group1 # => { key1: 2, key2: 2 }
|
18
|
+
#
|
19
|
+
# # You can check if a Group exists.
|
20
|
+
# config.defined?(:group1) # => true
|
21
|
+
# config.group1? # => true
|
22
|
+
#
|
23
|
+
class Configuration
|
24
|
+
include Levels::MethodMissing
|
25
|
+
|
26
|
+
# Internal: Initialze a new configuration.
|
27
|
+
#
|
28
|
+
# levels - Array of Levels::Level.
|
29
|
+
#
|
30
|
+
def initialize(levels, event_handler = nil)
|
31
|
+
@levels = levels
|
32
|
+
@event_handler = event_handler || NullEventHandler.new
|
33
|
+
@root_observer = Levels::Audit.start(LazyEvaluator.new(self))
|
34
|
+
end
|
35
|
+
|
36
|
+
# Public: Set the event handler. The event handler is notified whenever a
|
37
|
+
# key is read; allowing you to track exactly what is and isn't used at
|
38
|
+
# runtime.
|
39
|
+
#
|
40
|
+
# event_handler - Levels::EventHandler.
|
41
|
+
#
|
42
|
+
# Returns nothing.
|
43
|
+
def event_handler=(event_handler)
|
44
|
+
@event_handler = event_handler
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public: Retrieve a group. The resulting group is the union of the named
|
48
|
+
# group from each Level that defines that group.
|
49
|
+
#
|
50
|
+
# group_key - Symbol name of the group.
|
51
|
+
#
|
52
|
+
# Examples
|
53
|
+
#
|
54
|
+
# # In this example we'll represent a Level as a Hash.
|
55
|
+
# level1 = { group: { key1: 1 } }
|
56
|
+
# level2 = { group: { key1: 2, key2: 2 } }
|
57
|
+
#
|
58
|
+
# config = Levels::Configuration.new([level1, level2])
|
59
|
+
# config[:group] # => { key1: 2, key2: 2 }
|
60
|
+
#
|
61
|
+
# Returns a Levels::ConfiguredGroup.
|
62
|
+
# Raises Levels::UnknownGroup if the group is not defined.
|
63
|
+
def [](group_key)
|
64
|
+
raise UnknownGroup unless self.defined?(group_key)
|
65
|
+
group_observer = @root_observer.observe_group(@event_handler)
|
66
|
+
Levels::ConfiguredGroup.new(@levels, group_key, group_observer)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Public: Determine if a group is defined. A group is defined if it exists
|
70
|
+
# in any Level.
|
71
|
+
#
|
72
|
+
# group_key - Symbol name of the group.
|
73
|
+
#
|
74
|
+
# Returns a Boolean.
|
75
|
+
def defined?(group_key)
|
76
|
+
@levels.any? { |level| level.defined?(group_key) }
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
"<Levels::Configuration #{@levels.map { |l| l._level_name }.join(', ')}>"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns an Enumerator which yields [gruop_name, Group#to_enum].
|
84
|
+
def to_enum
|
85
|
+
Enumerator.new do |y|
|
86
|
+
group_keys = Set.new
|
87
|
+
@levels.each do |level|
|
88
|
+
level.to_enum.each do |name, group|
|
89
|
+
group_keys << name
|
90
|
+
end
|
91
|
+
end
|
92
|
+
group_keys.each do |name|
|
93
|
+
y << [name, self[name].to_enum]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Levels
|
2
|
+
# A ConfiguredGroup is the union of one or more Groups. When a key is read,
|
3
|
+
# the value returned is the value from the last Level that defines that key.
|
4
|
+
class ConfiguredGroup
|
5
|
+
include Levels::MethodMissing
|
6
|
+
|
7
|
+
# Internal: Initialize a configured group.
|
8
|
+
#
|
9
|
+
def initialize(levels, group_key, group_observer)
|
10
|
+
@levels = levels
|
11
|
+
@group_key = group_key
|
12
|
+
@group_observer = group_observer
|
13
|
+
end
|
14
|
+
|
15
|
+
# Public: Retrieve a value.
|
16
|
+
#
|
17
|
+
# value_key - Symbol name of the value.
|
18
|
+
#
|
19
|
+
# Returns the value stored at that key.
|
20
|
+
# Raises Levels::UnknownKey if the key is not defined.
|
21
|
+
def [](value_key)
|
22
|
+
raise UnknownKey unless self.defined?(value_key)
|
23
|
+
values = @group_observer.observe_values(@levels, @group_key, value_key)
|
24
|
+
values.final_value
|
25
|
+
end
|
26
|
+
|
27
|
+
# Public: Determine if a key is defined.
|
28
|
+
#
|
29
|
+
# value_key - Symbol name of the key.
|
30
|
+
#
|
31
|
+
# Returns a Boolean.
|
32
|
+
def defined?(value_key)
|
33
|
+
groups.any? { |group| group.defined?(value_key) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
"<Levels::ConfiguredGroup #{@group_key}>"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns an Enumerator which yields [key, value].
|
41
|
+
def to_enum
|
42
|
+
Enumerator.new do |y|
|
43
|
+
value_keys = Set.new
|
44
|
+
groups.each do |group|
|
45
|
+
group.to_enum.each do |key, value|
|
46
|
+
value_keys << key
|
47
|
+
end
|
48
|
+
end
|
49
|
+
value_keys.each do |key|
|
50
|
+
y << [key, self[key]]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def groups
|
58
|
+
levels = @levels.find_all { |level| level.defined?(@group_key) }
|
59
|
+
levels.map { |level| level[@group_key] }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Levels
|
2
|
+
# This is the interface for capturing what happens when a value is read.
|
3
|
+
# Capturing events exposes you to the audit trail system implemented in
|
4
|
+
# Levels::Audit.
|
5
|
+
#
|
6
|
+
# class MyEventHandler
|
7
|
+
#
|
8
|
+
# def on_values(values)
|
9
|
+
# # This method is called any time a value is accessed.
|
10
|
+
# # The argument `values` is a Levels::Audit::Values representing the
|
11
|
+
# # set of all possible values.
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# def on_nested_values(values)
|
15
|
+
# # Similar to `on_values`, but called when the values were found
|
16
|
+
# # during the evaluation of another value.
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
module EventHandler
|
21
|
+
|
22
|
+
def on_values(values)
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_nested_values(values)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# A null implementation.
|
30
|
+
class NullEventHandler
|
31
|
+
include EventHandler
|
32
|
+
end
|
33
|
+
|
34
|
+
module Colorizer
|
35
|
+
|
36
|
+
RESET = "\033[0m"
|
37
|
+
FOREGROUND = {
|
38
|
+
black: "\033[30m",
|
39
|
+
red: "\033[31m",
|
40
|
+
green: "\033[32m",
|
41
|
+
brown: "\033[33m",
|
42
|
+
blue: "\033[34m",
|
43
|
+
magenta: "\033[35m",
|
44
|
+
cyan: "\033[36m",
|
45
|
+
white: "\033[37m"
|
46
|
+
}
|
47
|
+
|
48
|
+
def foreground_color(name, str)
|
49
|
+
code = FOREGROUND[name] or raise ArgumentError, "Unknown color #{name.inspect}"
|
50
|
+
"#{code}#{str}#{RESET}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class CliEventHandler
|
55
|
+
include Colorizer
|
56
|
+
|
57
|
+
def initialize(stream, color = false)
|
58
|
+
@stream = stream
|
59
|
+
@color = color
|
60
|
+
@indent = 0
|
61
|
+
end
|
62
|
+
|
63
|
+
def on_values(values)
|
64
|
+
write :white, "> #{values.group_key}.#{values.value_key}"
|
65
|
+
values.each do |value|
|
66
|
+
value.notify(self)
|
67
|
+
if value.final?
|
68
|
+
write :green, " + #{value.inspect} from #{value.level_name}"
|
69
|
+
else
|
70
|
+
write :red, " - #{value.inspect} from #{value.level_name}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def on_nested_values(values)
|
76
|
+
indent do
|
77
|
+
on_values(values)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
def indent
|
84
|
+
begin
|
85
|
+
@indent += 1
|
86
|
+
yield
|
87
|
+
ensure
|
88
|
+
@indent -= 1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def write(color, str)
|
93
|
+
prefix = " " * @indent
|
94
|
+
str = foreground_color(color, str) if @color
|
95
|
+
@stream.puts prefix + str
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# A temporary implementation that will be moved back to config to
|
100
|
+
# restore its original behavior.
|
101
|
+
class ConfigCoreEventHandler
|
102
|
+
|
103
|
+
def initialize(log)
|
104
|
+
@log = log
|
105
|
+
@base_color = :magenta
|
106
|
+
@alt_color = :cyan
|
107
|
+
end
|
108
|
+
|
109
|
+
def on_read_from_merged_group(group_name, key, levels)
|
110
|
+
final_level_name, final_value = levels.last
|
111
|
+
|
112
|
+
if levels.size == 1
|
113
|
+
@log << @log.colorize("Read #{group_name}.#{key} => #{final_value.inspect} from #{final_level_name}", @base_color)
|
114
|
+
else
|
115
|
+
skipped_levels = levels[0..-2]
|
116
|
+
|
117
|
+
@log << @log.colorize("Read #{group_name}.#{key}", @base_color)
|
118
|
+
@log.indent do
|
119
|
+
skipped_levels.each do |level_name, value|
|
120
|
+
@log << @log.colorize("Skip #{value.inspect} from #{level_name}", @alt_color)
|
121
|
+
end
|
122
|
+
@log << @log.colorize("Use #{final_value.inspect} from #{final_level_name}", @base_color)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/levels/group.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
module Levels
|
2
|
+
# A group is a set of key/value pairs containing your data.
|
3
|
+
#
|
4
|
+
# Examples
|
5
|
+
#
|
6
|
+
# group = Levels::Group.new(name: "Joe", age: 33)
|
7
|
+
#
|
8
|
+
# group.name # => "Joe"
|
9
|
+
# group.age # => 33
|
10
|
+
#
|
11
|
+
class Group
|
12
|
+
include Levels::MethodMissing
|
13
|
+
|
14
|
+
# Internal: Initialize a new group.
|
15
|
+
#
|
16
|
+
# data - Hash of key/values for the group.
|
17
|
+
# value_transformer - Proc that takes (key, value) and returns value.
|
18
|
+
#
|
19
|
+
def initialize(data = {}, value_transformer = nil)
|
20
|
+
@values = Levels::KeyValues.new(data)
|
21
|
+
@value_transformer = value_transformer || -> key, value { value }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Public: Get the value for a key.
|
25
|
+
#
|
26
|
+
# Returns the value.
|
27
|
+
# Raises Levels::UnknownKey if the key is not defined.
|
28
|
+
def [](value_key)
|
29
|
+
if @values.key?(value_key)
|
30
|
+
key, value = @values.pair(value_key)
|
31
|
+
@value_transformer.call(key.to_sym, value)
|
32
|
+
else
|
33
|
+
raise UnknownKey, "#{value_key.inspect} is not defined in #{self}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Determine if a key is defined.
|
38
|
+
#
|
39
|
+
# Returns a Boolean.
|
40
|
+
def defined?(key)
|
41
|
+
@values.key?(key)
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
"<Levels::Group>"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns an Enumerator which yields [key, value].
|
49
|
+
def to_enum
|
50
|
+
Enumerator.new do |y|
|
51
|
+
@values.each do |key, value|
|
52
|
+
y << [key.to_sym, self[key]]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def eql_hash?(hash)
|
58
|
+
@values == Levels::KeyValues.new(hash)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Levels
|
2
|
+
module Input
|
3
|
+
# This input
|
4
|
+
class JSON
|
5
|
+
|
6
|
+
def initialize(json_string)
|
7
|
+
@json = ::JSON.parse(json_string)
|
8
|
+
end
|
9
|
+
|
10
|
+
def read(level)
|
11
|
+
@json.each do |group_name, group|
|
12
|
+
level.set_group(group_name, group)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Levels
|
2
|
+
module Input
|
3
|
+
# This input provides a DSL for writing levels in ruby.
|
4
|
+
class Ruby
|
5
|
+
|
6
|
+
def initialize(ruby_string, file = "Code in Memory", line = 1)
|
7
|
+
@ruby_string = ruby_string
|
8
|
+
@file = file
|
9
|
+
@line = line
|
10
|
+
end
|
11
|
+
|
12
|
+
def read(level)
|
13
|
+
dsl = DSL.new(level)
|
14
|
+
dsl.instance_eval(@ruby_string, @file, @line)
|
15
|
+
dsl.close_current_group
|
16
|
+
end
|
17
|
+
|
18
|
+
# The Ruby syntax DSL. This syntax aims to be very readable and writable.
|
19
|
+
# A stateful syntax and no trailing commas allows values to be easily
|
20
|
+
# moved around without excess text editing.
|
21
|
+
#
|
22
|
+
# Examples
|
23
|
+
#
|
24
|
+
# group :webserver
|
25
|
+
# set hostname: "localhost"
|
26
|
+
# group :task_queue
|
27
|
+
# set workers: 1
|
28
|
+
# set queues: -> { ["high", "low", webserver.hostname] }
|
29
|
+
#
|
30
|
+
class DSL
|
31
|
+
include Levels::Runtime
|
32
|
+
|
33
|
+
def initialize(level)
|
34
|
+
@level = level
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Start a group.
|
38
|
+
#
|
39
|
+
# name - Symbol or String name of the group.
|
40
|
+
#
|
41
|
+
# Returns nothing.
|
42
|
+
# Raises a RuntimeError if the group has already been defined.
|
43
|
+
def group(name)
|
44
|
+
close_current_group
|
45
|
+
if @level.defined?(name)
|
46
|
+
raise RuntimeError, "Group has already been created: #{name.inspect}"
|
47
|
+
end
|
48
|
+
@group = name
|
49
|
+
@hash = {}
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
# Public: Set a key/value pair in the current group.
|
54
|
+
#
|
55
|
+
# Arguments
|
56
|
+
#
|
57
|
+
# hash - Hash of key/value pairs. The keys may be a Symbol or String.
|
58
|
+
#
|
59
|
+
# key - Symbol or String key.
|
60
|
+
# value - Any value.
|
61
|
+
#
|
62
|
+
# Examples
|
63
|
+
#
|
64
|
+
# # Ruby 1.9 Hash.
|
65
|
+
# set key: "value"
|
66
|
+
#
|
67
|
+
# # Ruby 1.9 Hash.
|
68
|
+
# set :key => "value"
|
69
|
+
#
|
70
|
+
# # Key can be a String.
|
71
|
+
# set "key" => "value"
|
72
|
+
#
|
73
|
+
# # Multiple key/values.
|
74
|
+
# set :key => "value", :more => "values"
|
75
|
+
#
|
76
|
+
# # Key, value.
|
77
|
+
# set :key, "value"
|
78
|
+
#
|
79
|
+
# Returns nothing.
|
80
|
+
# Raises SyntaxError if no current group is defined.
|
81
|
+
# Raises ArgumentError if other forms of arguments are given
|
82
|
+
# Raises RuntimeError if a key has already been set.
|
83
|
+
def set(*args)
|
84
|
+
raise SyntaxError, "No group is defined" if @hash.nil?
|
85
|
+
case
|
86
|
+
when args.size == 1 && Hash === args.first
|
87
|
+
hash = args.first
|
88
|
+
when args.size == 2 && !(args.any? { |a| Hash === a })
|
89
|
+
hash = { args.first => args.last }
|
90
|
+
else
|
91
|
+
raise ArgumentError, "Set must be given a Hash or two arguments. Got #{args.inspect}"
|
92
|
+
end
|
93
|
+
key_values = Levels::KeyValues.new(@hash)
|
94
|
+
hash.keys.each do |key|
|
95
|
+
if key_values.key?(key)
|
96
|
+
raise RuntimeError, "Key has already been set: #{key.inspect}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
@hash.update(hash)
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
|
103
|
+
def close_current_group
|
104
|
+
@level.set_group(@group, @hash) if @group
|
105
|
+
@group = nil
|
106
|
+
@hash = nil
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_s
|
110
|
+
"<Levels>"
|
111
|
+
end
|
112
|
+
|
113
|
+
def inspect
|
114
|
+
"<Levels>"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|