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.
Files changed (90) hide show
  1. data/.gitignore +17 -0
  2. data/.rbenv-version +1 -0
  3. data/CHANGELOG.md +4 -0
  4. data/Gemfile +12 -0
  5. data/Guardfile +14 -0
  6. data/LICENSE +22 -0
  7. data/README.md +315 -0
  8. data/Rakefile +28 -0
  9. data/bin/levels +130 -0
  10. data/examples/01_base.rb +6 -0
  11. data/examples/01_merge_to_json.sh +27 -0
  12. data/examples/01_prod.json +8 -0
  13. data/examples/02_base.rb +4 -0
  14. data/examples/02_merge_with_file.sh +20 -0
  15. data/examples/02_value +1 -0
  16. data/levels.gemspec +20 -0
  17. data/lib/levels.rb +77 -0
  18. data/lib/levels/audit.rb +24 -0
  19. data/lib/levels/audit/group_observer.rb +26 -0
  20. data/lib/levels/audit/nested_group_observer.rb +37 -0
  21. data/lib/levels/audit/root_observer.rb +63 -0
  22. data/lib/levels/audit/value.rb +64 -0
  23. data/lib/levels/audit/value_observer.rb +46 -0
  24. data/lib/levels/audit/values.rb +66 -0
  25. data/lib/levels/configuration.rb +98 -0
  26. data/lib/levels/configured_group.rb +62 -0
  27. data/lib/levels/event_handler.rb +127 -0
  28. data/lib/levels/group.rb +61 -0
  29. data/lib/levels/input/json.rb +17 -0
  30. data/lib/levels/input/ruby.rb +120 -0
  31. data/lib/levels/input/system.rb +63 -0
  32. data/lib/levels/input/yaml.rb +17 -0
  33. data/lib/levels/key.rb +28 -0
  34. data/lib/levels/key_values.rb +54 -0
  35. data/lib/levels/lazy_evaluator.rb +54 -0
  36. data/lib/levels/level.rb +80 -0
  37. data/lib/levels/method_missing.rb +14 -0
  38. data/lib/levels/output/json.rb +33 -0
  39. data/lib/levels/output/system.rb +29 -0
  40. data/lib/levels/output/yaml.rb +19 -0
  41. data/lib/levels/runtime.rb +30 -0
  42. data/lib/levels/setup.rb +132 -0
  43. data/lib/levels/system/constants.rb +8 -0
  44. data/lib/levels/system/key_formatter.rb +15 -0
  45. data/lib/levels/system/key_generator.rb +50 -0
  46. data/lib/levels/system/key_parser.rb +67 -0
  47. data/lib/levels/version.rb +3 -0
  48. data/test/acceptance/audit_test.rb +105 -0
  49. data/test/acceptance/event_handler_test.rb +43 -0
  50. data/test/acceptance/read_json_test.rb +35 -0
  51. data/test/acceptance/read_ruby_test.rb +117 -0
  52. data/test/acceptance/read_system_test.rb +121 -0
  53. data/test/acceptance/read_yaml_test.rb +38 -0
  54. data/test/acceptance/setup_test.rb +115 -0
  55. data/test/acceptance/write_json_test.rb +39 -0
  56. data/test/acceptance/write_system_test.rb +68 -0
  57. data/test/acceptance/write_yaml_test.rb +33 -0
  58. data/test/bin/merge_test.rb +194 -0
  59. data/test/bin/options_test.rb +41 -0
  60. data/test/helper.rb +12 -0
  61. data/test/support/acceptance_spec.rb +58 -0
  62. data/test/support/base_spec.rb +14 -0
  63. data/test/support/bin_spec.rb +65 -0
  64. data/test/support/tempfile_helper.rb +35 -0
  65. data/test/unit/audit/group_observer_test.rb +24 -0
  66. data/test/unit/audit/nested_group_observer_test.rb +28 -0
  67. data/test/unit/audit/root_observer_test.rb +54 -0
  68. data/test/unit/audit/value_observer_test.rb +63 -0
  69. data/test/unit/audit/value_test.rb +41 -0
  70. data/test/unit/audit/values_test.rb +86 -0
  71. data/test/unit/configuration_test.rb +72 -0
  72. data/test/unit/configured_group_test.rb +75 -0
  73. data/test/unit/group_test.rb +105 -0
  74. data/test/unit/input/json_test.rb +32 -0
  75. data/test/unit/input/ruby_test.rb +140 -0
  76. data/test/unit/input/system_test.rb +59 -0
  77. data/test/unit/input/yaml_test.rb +33 -0
  78. data/test/unit/key_test.rb +45 -0
  79. data/test/unit/key_values_test.rb +106 -0
  80. data/test/unit/lazy_evaluator_test.rb +38 -0
  81. data/test/unit/level_test.rb +89 -0
  82. data/test/unit/levels_test.rb +23 -0
  83. data/test/unit/output/json_test.rb +55 -0
  84. data/test/unit/output/system_test.rb +32 -0
  85. data/test/unit/output/yaml_test.rb +38 -0
  86. data/test/unit/runtime_test.rb +40 -0
  87. data/test/unit/system/key_formatter_test.rb +43 -0
  88. data/test/unit/system/key_generator_test.rb +21 -0
  89. data/test/unit/system/key_parser_test.rb +207 -0
  90. metadata +215 -0
@@ -0,0 +1,8 @@
1
+ module Levels
2
+ module System
3
+
4
+ TYPE_SUFFIX = "TYPE"
5
+ ARRAY_DELIMITER_SUFFIX = "DELIMITER"
6
+
7
+ end
8
+ end
@@ -0,0 +1,15 @@
1
+ module Levels
2
+ module System
3
+ class KeyFormatter
4
+
5
+ def initialize(prefix = nil, joiner = nil)
6
+ @prefix = prefix || ""
7
+ @joiner = joiner || "_"
8
+ end
9
+
10
+ def create(*parts)
11
+ (@prefix + parts.join(@joiner)).upcase
12
+ end
13
+ end
14
+ end
15
+ end
@@ -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,3 @@
1
+ module Levels
2
+ VERSION = "0.1.0"
3
+ 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