levels 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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