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,50 @@
|
|
1
|
+
module Levels
|
2
|
+
module System
|
3
|
+
class KeyGenerator
|
4
|
+
|
5
|
+
def initialize(key_formatter = nil)
|
6
|
+
@key_formatter = key_formatter || KeyFormatter.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def generate(enumerator)
|
10
|
+
env = {}
|
11
|
+
enumerator.each do |group, key, value|
|
12
|
+
add_keys(env, group, key, value)
|
13
|
+
end
|
14
|
+
env
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def k(*parts)
|
20
|
+
@key_formatter.create(*parts)
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_keys(env, group, key, value)
|
24
|
+
case value
|
25
|
+
when Array
|
26
|
+
env[k(group, key)] = value.join(":")
|
27
|
+
env[k(group, key, TYPE_SUFFIX)] = "array"
|
28
|
+
env[k(group, key, ARRAY_DELIMITER_SUFFIX)] = ":"
|
29
|
+
env[k(group, key, TYPE_SUFFIX, TYPE_SUFFIX)] = type_for(value.first)
|
30
|
+
else
|
31
|
+
env[k(group, key)] = value.to_s
|
32
|
+
env[k(group, key, TYPE_SUFFIX)] = type_for(value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def type_for(value)
|
37
|
+
case value
|
38
|
+
when String, NilClass then "string"
|
39
|
+
when Integer then "integer"
|
40
|
+
when Float then "float"
|
41
|
+
when TrueClass, FalseClass then "boolean"
|
42
|
+
when Array then "array"
|
43
|
+
else
|
44
|
+
raise ArgumentError, "Could not export #{value.class}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Levels
|
2
|
+
module System
|
3
|
+
class KeyParser
|
4
|
+
|
5
|
+
def initialize(key_formatter = nil)
|
6
|
+
@key_formatter = key_formatter || KeyFormatter.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def parse(env_hash, key, template_value)
|
10
|
+
value = env_hash[key]
|
11
|
+
typecast_info = env_hash[k(key, TYPE_SUFFIX)]
|
12
|
+
parse_any(env_hash, key, value, typecast_info, template_value)
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def k(*parts)
|
18
|
+
@key_formatter.create(*parts)
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_any(env_hash, key, value, typecast_info, template_value)
|
22
|
+
case
|
23
|
+
when value.empty? then nil
|
24
|
+
when !typecast_info.nil? then parse_from_typecast(env_hash, key, value, typecast_info)
|
25
|
+
when !template_value.nil? then parse_from_template(env_hash, key, value, template_value)
|
26
|
+
else value.to_s
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_from_typecast(env_hash, key, value, typecast_info)
|
31
|
+
case typecast_info
|
32
|
+
when "string", nil then value.to_s
|
33
|
+
when "integer" then value.to_i
|
34
|
+
when "float" then value.to_f
|
35
|
+
when "boolean" then boolean(value)
|
36
|
+
when "array" then array(env_hash, key, value, nil)
|
37
|
+
else raise ArgumentError, "Unknown typecast to #{typecast_info.inspect}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_from_template(env_hash, key, value, template_value)
|
42
|
+
case template_value
|
43
|
+
when String then value.to_s
|
44
|
+
when Integer then value.to_i
|
45
|
+
when Float then value.to_f
|
46
|
+
when TrueClass then boolean(value.to_s)
|
47
|
+
when FalseClass then boolean(value.to_s)
|
48
|
+
when Array then array(env_hash, key, value, template_value.first)
|
49
|
+
else raise ArgumentError, "Unknown template value of type #{template_value.class}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def array(env_hash, key, value, template_value)
|
54
|
+
value_typecast_info = env_hash[k(key, TYPE_SUFFIX, TYPE_SUFFIX)]
|
55
|
+
delimiter = env_hash[k(key, ARRAY_DELIMITER_SUFFIX)] || ":"
|
56
|
+
value.split(delimiter).map do |v|
|
57
|
+
parse_any(env_hash, key, v, value_typecast_info, template_value)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def boolean(value)
|
62
|
+
value.match(/^(true|1)$/i) != nil
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe "acceptance: the audit observers" do
|
4
|
+
|
5
|
+
# This test requires a lot of collaborators, but hopefully shows how the
|
6
|
+
# auditor works as a whole.
|
7
|
+
|
8
|
+
let(:event_handler_class) {
|
9
|
+
Class.new do
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@current_key = nil
|
13
|
+
@captured = Hash.new { |h, k| h[k] = [] }
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :captured
|
17
|
+
|
18
|
+
def on_values(values)
|
19
|
+
@current_key = values.value_key
|
20
|
+
values.each do |value|
|
21
|
+
@captured[@current_key] << value.raw
|
22
|
+
value.notify(self)
|
23
|
+
end
|
24
|
+
@current_key = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_nested_values(values)
|
28
|
+
values.each do |value|
|
29
|
+
@captured[@current_key] << [values.value_key, value.raw]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
}
|
34
|
+
|
35
|
+
# The levels that will be merged.
|
36
|
+
let(:levels) { [] }
|
37
|
+
|
38
|
+
# The merged view of the levels.
|
39
|
+
let(:configuration) { Levels::Configuration.new(levels) }
|
40
|
+
|
41
|
+
# Computed values are evaluated against the merged levels.
|
42
|
+
let(:lazy_evaluator) { Levels::LazyEvaluator.new(configuration) }
|
43
|
+
|
44
|
+
# The root observer performing the audit.
|
45
|
+
let(:root_observer) { Levels::Audit.start(lazy_evaluator) }
|
46
|
+
|
47
|
+
before do
|
48
|
+
level1 = Levels::Level.new("one")
|
49
|
+
level1.set_group(:group1, a: 1)
|
50
|
+
level1.set_group(:group2, x: -> { "a = #{group1.a}, b = #{group2.b}" })
|
51
|
+
|
52
|
+
level2 = Levels::Level.new("two")
|
53
|
+
level2.set_group(:group1, a: 2)
|
54
|
+
level2.set_group(:group2, b: 3)
|
55
|
+
|
56
|
+
levels << level1
|
57
|
+
levels << level2
|
58
|
+
|
59
|
+
# The auditor must be assigned to the configuration, else nested values get
|
60
|
+
# confused since the value is ultimately accessed via the LazyEvaluator's
|
61
|
+
# levels reference. The point is, we're doing something weird here by using
|
62
|
+
# the auditor directly.
|
63
|
+
configuration.instance_variable_set(:@root_observer, root_observer)
|
64
|
+
end
|
65
|
+
|
66
|
+
# A custom event handler to collect the values that are observed.
|
67
|
+
let(:event_handler) { event_handler_class.new }
|
68
|
+
|
69
|
+
# Get the full audit trail for a group.value read.
|
70
|
+
def observe_values(group_key, value_key)
|
71
|
+
group_observer = root_observer.observe_group(event_handler)
|
72
|
+
group_observer.observe_values(levels, group_key, value_key)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "handles simple values" do
|
76
|
+
values = observe_values(:group1, :a)
|
77
|
+
|
78
|
+
event_handler.captured.must_equal a: [1, 2]
|
79
|
+
|
80
|
+
values.final_value.must_equal 2
|
81
|
+
end
|
82
|
+
|
83
|
+
it "handles complex nested values" do
|
84
|
+
values = observe_values(:group2, :x)
|
85
|
+
|
86
|
+
event_handler.captured.must_equal x: [
|
87
|
+
"a = 2, b = 3",
|
88
|
+
[:a, 1],
|
89
|
+
[:a, 2],
|
90
|
+
[:b, 3]
|
91
|
+
]
|
92
|
+
|
93
|
+
values.final_value.must_equal "a = 2, b = 3"
|
94
|
+
end
|
95
|
+
|
96
|
+
it "returns empty values for a non-existent key" do
|
97
|
+
values = observe_values(:no_group, :foo)
|
98
|
+
event_handler.captured.must_equal({})
|
99
|
+
values.must_be :empty?
|
100
|
+
|
101
|
+
values = observe_values(:group1, :no_key)
|
102
|
+
event_handler.captured.must_equal({})
|
103
|
+
values.must_be :empty?
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe "acceptance: the cli event handler" do
|
4
|
+
|
5
|
+
def read_ruby(level_name, ruby_string)
|
6
|
+
level = Levels::Level.new(level_name)
|
7
|
+
input = Levels::Input::Ruby.new(ruby_string)
|
8
|
+
input.read(level)
|
9
|
+
level
|
10
|
+
end
|
11
|
+
|
12
|
+
it "handles lazy evaluation" do
|
13
|
+
|
14
|
+
level1 = read_ruby("One", <<-RUBY)
|
15
|
+
group :names
|
16
|
+
set full_name: -> { [names.first_name, names.last_name].join(" ") }
|
17
|
+
set first_name: "John"
|
18
|
+
set last_name: "Doe"
|
19
|
+
RUBY
|
20
|
+
|
21
|
+
level2 = read_ruby("Two", <<-RUBY)
|
22
|
+
group :names
|
23
|
+
set last_name: "Smith"
|
24
|
+
RUBY
|
25
|
+
|
26
|
+
stream = StringIO.new
|
27
|
+
|
28
|
+
configuration = Levels::Configuration.new([level1, level2])
|
29
|
+
configuration.event_handler = Levels::CliEventHandler.new(stream)
|
30
|
+
|
31
|
+
configuration.names.full_name
|
32
|
+
|
33
|
+
stream.string.must_equal <<-STR
|
34
|
+
> names.full_name
|
35
|
+
> names.first_name
|
36
|
+
+ "John" from One
|
37
|
+
> names.last_name
|
38
|
+
- "Doe" from One
|
39
|
+
+ "Smith" from Two
|
40
|
+
+ "John Smith" from One
|
41
|
+
STR
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe "acceptance: read json" do
|
4
|
+
|
5
|
+
def read_json(level_name, json_string)
|
6
|
+
level = Levels::Level.new(level_name)
|
7
|
+
input = Levels::Input::JSON.new(json_string)
|
8
|
+
input.read(level)
|
9
|
+
level
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:json) {
|
13
|
+
<<-JSON
|
14
|
+
{
|
15
|
+
"types": {
|
16
|
+
"string": "hello",
|
17
|
+
"integer": 123,
|
18
|
+
"float": 1.5,
|
19
|
+
"boolean_true": true,
|
20
|
+
"boolean_false": false,
|
21
|
+
"array_of_string": ["a", "b", "c"],
|
22
|
+
"array_of_integer": [1, 2, 3],
|
23
|
+
"null": null
|
24
|
+
},
|
25
|
+
"group2": {
|
26
|
+
"message": "hello world"
|
27
|
+
}
|
28
|
+
}
|
29
|
+
JSON
|
30
|
+
}
|
31
|
+
|
32
|
+
subject { read_json("test", json) }
|
33
|
+
|
34
|
+
assert_sample_data_set
|
35
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
describe "acceptance: read ruby" do
|
5
|
+
|
6
|
+
def read_ruby(level_name, ruby_string)
|
7
|
+
level = Levels::Level.new(level_name)
|
8
|
+
input = Levels::Input::Ruby.new(ruby_string)
|
9
|
+
input.read(level)
|
10
|
+
level
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "types" do
|
14
|
+
|
15
|
+
let(:ruby) {
|
16
|
+
<<-RUBY
|
17
|
+
group "types"
|
18
|
+
set string: "hello"
|
19
|
+
set integer: 123
|
20
|
+
set float: 1.5
|
21
|
+
set boolean_true: true
|
22
|
+
set boolean_false: false
|
23
|
+
set array_of_string: ["a", "b", "c"]
|
24
|
+
set array_of_integer: [1, 2, 3]
|
25
|
+
set null: nil
|
26
|
+
|
27
|
+
group "group2"
|
28
|
+
set message: "hello world"
|
29
|
+
RUBY
|
30
|
+
}
|
31
|
+
|
32
|
+
subject { read_ruby("test", ruby) }
|
33
|
+
|
34
|
+
assert_sample_data_set
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "computed values" do
|
38
|
+
|
39
|
+
let(:level1_ruby) {
|
40
|
+
<<-RUBY
|
41
|
+
group :names
|
42
|
+
set full_name: -> { [names.first_name, names.last_name].join(" ") }
|
43
|
+
set first_name: "John"
|
44
|
+
set last_name: "Doe"
|
45
|
+
RUBY
|
46
|
+
}
|
47
|
+
|
48
|
+
let(:level2_ruby) {
|
49
|
+
<<-RUBY
|
50
|
+
group "names"
|
51
|
+
set last_name: "Smith"
|
52
|
+
RUBY
|
53
|
+
}
|
54
|
+
|
55
|
+
let(:level1) { read_ruby("ruby level 1", level1_ruby) }
|
56
|
+
let(:level2) { read_ruby("ruby level 2", level2_ruby) }
|
57
|
+
|
58
|
+
subject { Levels::Configuration.new([level1, level2]) }
|
59
|
+
|
60
|
+
it "resolves the computed value" do
|
61
|
+
subject.names.full_name.must_equal "John Smith"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "reading a file" do
|
66
|
+
|
67
|
+
let(:ruby_file) { f("file.rb") }
|
68
|
+
|
69
|
+
def read_ruby_with_file_path_path(path)
|
70
|
+
input = Levels::Input::Ruby.new(<<-RUBY, ruby_file.to_s)
|
71
|
+
group "group1"
|
72
|
+
set message: file("#{path}")
|
73
|
+
set lazy_message: -> { file("#{path}") }
|
74
|
+
RUBY
|
75
|
+
level = Levels::Level.new("test")
|
76
|
+
input.read(level)
|
77
|
+
Levels::Configuration.new([level])
|
78
|
+
end
|
79
|
+
|
80
|
+
it "reads a file in the same directory" do
|
81
|
+
level = read_ruby_with_file_path_path("f1")
|
82
|
+
w("f1", "hello world")
|
83
|
+
level.group1.message.must_equal "hello world"
|
84
|
+
end
|
85
|
+
|
86
|
+
it "reads a file at a relative path" do
|
87
|
+
level = read_ruby_with_file_path_path("d/f1")
|
88
|
+
(ruby_file.dirname + "d").mkdir
|
89
|
+
w("d/f1", "hello world")
|
90
|
+
level.group1.message.must_equal "hello world"
|
91
|
+
end
|
92
|
+
|
93
|
+
it "reads a file at an absolute path" do
|
94
|
+
f = Tempfile.new("f")
|
95
|
+
begin
|
96
|
+
level = read_ruby_with_file_path_path(f.path)
|
97
|
+
f.write "hello world"
|
98
|
+
f.close
|
99
|
+
level.group1.message.must_equal "hello world"
|
100
|
+
ensure
|
101
|
+
f.close!
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
it "fails if the file does not exist" do
|
106
|
+
level = read_ruby_with_file_path_path("f1")
|
107
|
+
level.group1.defined?(:message).must_equal true
|
108
|
+
-> { level.group1.message }.must_raise Levels::Runtime::FileNotFoundError
|
109
|
+
end
|
110
|
+
|
111
|
+
it "lazily reads a file" do
|
112
|
+
level = read_ruby_with_file_path_path("f1")
|
113
|
+
w("f1", "hello world")
|
114
|
+
level.group1.lazy_message.must_equal "hello world"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|