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,63 @@
1
+ module Levels
2
+ module Input
3
+ # This input creates an env level from the system environment
4
+ # (ENV in ruby). It does so by using an existing level as a
5
+ # template for the group names and values. For each value, it
6
+ # attempts to typecast a String value into the same type found in
7
+ # the template.
8
+ #
9
+ # Examples
10
+ #
11
+ # # Given a template level with a single group:
12
+ # settings: {
13
+ # hostname: "example.com",
14
+ # workers: 5,
15
+ # queues: ["low", "high", "other"]
16
+ # }
17
+ #
18
+ # # These environment variables will set new values.
19
+ # SETTINGS_HOSTNAME=foo.com
20
+ # SETTINGS_WORKERS=1
21
+ # SETTINGS_QUEUES=high:low:other
22
+ #
23
+ class System
24
+
25
+ # Initialize a new system reader.
26
+ #
27
+ # template - Enumerator that defines the possible keys.
28
+ # prefix - String prefix for the keys (default: no
29
+ # prefix).
30
+ #
31
+ def initialize(template, key_formatter = nil, env_hash = ENV)
32
+ @template = template
33
+ @env_hash = env_hash
34
+ @key_formatter = key_formatter || Levels::System::KeyFormatter.new
35
+ @key_parser = Levels::System::KeyParser.new(@key_formatter)
36
+ end
37
+
38
+ def read(level)
39
+ @template.each do |group_name, group|
40
+ env_data = {}
41
+ group_data = {}
42
+ group.each do |key, value|
43
+ group_data[key.to_sym] = value
44
+ end
45
+ @env_hash.each do |key, value|
46
+ match_key = @key_formatter.create(group_name, "(.+)")
47
+ matcher = Regexp.new("^#{match_key}$")
48
+ if key =~ matcher
49
+ attr_name = $1.downcase.to_sym
50
+ if group_data.key?(attr_name)
51
+ cast_value = @key_parser.parse(@env_hash, key, group_data[attr_name])
52
+ env_data[attr_name] = cast_value
53
+ end
54
+ end
55
+ end
56
+ if env_data.any?
57
+ level.set_group(group_name, env_data)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,17 @@
1
+ module Levels
2
+ module Input
3
+ class YAML
4
+
5
+ def initialize(yaml_string)
6
+ @yaml = ::YAML.load(yaml_string)
7
+ end
8
+
9
+ def read(level)
10
+ @yaml.each do |group_name, group|
11
+ level.set_group(group_name, group)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,28 @@
1
+ module Levels
2
+ class Key
3
+
4
+ def initialize(key)
5
+ @key = key
6
+ end
7
+
8
+ def to_sym
9
+ @key.to_sym
10
+ end
11
+
12
+ def eql?(other)
13
+ other.class == self.class && to_sym == other.to_sym
14
+ end
15
+
16
+ alias == eql?
17
+
18
+ def hash
19
+ self.class.hash ^ to_sym.hash
20
+ end
21
+
22
+ def inspect
23
+ "<Levels::Key #{to_sym.inspect}>"
24
+ end
25
+
26
+ alias to_s inspect
27
+ end
28
+ end
@@ -0,0 +1,54 @@
1
+ module Levels
2
+ class KeyValues
3
+
4
+ def Key(key)
5
+ case key
6
+ when Levels::Key then key
7
+ else Levels::Key.new(key)
8
+ end
9
+ end
10
+
11
+ include Enumerable
12
+
13
+ def initialize(data = nil)
14
+ @hash = {}
15
+ data.each { |k, v| self[k] = v } if data
16
+ end
17
+
18
+ def [](key)
19
+ @hash[Key(key)]
20
+ end
21
+
22
+ def pair(key)
23
+ return Key(key), self[key]
24
+ end
25
+
26
+ def []=(key, value)
27
+ @hash[Key(key)] = value
28
+ end
29
+
30
+ def key?(key)
31
+ @hash.key?(Key(key))
32
+ end
33
+
34
+ def each(&block)
35
+ @hash.each(&block)
36
+ end
37
+
38
+ def eql?(other)
39
+ self.class == other.class && @hash.eql?(other.instance_variable_get(:@hash))
40
+ end
41
+
42
+ alias == eql?
43
+
44
+ def hash
45
+ self.class.hash ^ @hash.hash
46
+ end
47
+
48
+ def inspect
49
+ "<Levels::KeyValues>"
50
+ end
51
+
52
+ alias to_s inspect
53
+ end
54
+ end
@@ -0,0 +1,54 @@
1
+ module Levels
2
+ # Whenever a value is read, it's interpreted by the LazyEvaluator. This class
3
+ # implements all of the interpolation rules.
4
+ class LazyEvaluator
5
+
6
+ # Internal: Initialize a new LazyEvaluator.
7
+ #
8
+ # level - Levels::Level or equivalent, used to find referenced
9
+ # groups and keys.
10
+ # key_formatter - Levels::System::KeyFormatter.
11
+ #
12
+ def initialize(level, key_formatter = nil)
13
+ @level = level
14
+ @key_formatter = key_formatter || Levels::System::KeyFormatter.new
15
+ end
16
+
17
+ # Internal: Interpret the value.
18
+ def call(value)
19
+ loop do
20
+ case value
21
+ #when /\$\{[A-Z_]+\}/
22
+ when Proc
23
+ dsl = DSL.new(@level)
24
+ value = dsl.instance_exec(&value)
25
+ when Array
26
+ return value.map { |v| call(v) }
27
+ else
28
+ return value
29
+ end
30
+ end
31
+ end
32
+
33
+ # This is the class that evaluations Proc values. When you define a value
34
+ # as a Proc, it's evaluation in the context of an instance of this class.
35
+ class DSL
36
+ include Levels::MethodMissing
37
+ include Levels::Runtime
38
+
39
+ def initialize(level)
40
+ @level = level
41
+ end
42
+
43
+ # Public: Determine if a group exists.
44
+ def defined?(group_key)
45
+ @level.defined?(group_key)
46
+ end
47
+
48
+ # Public: Retrieve a group.
49
+ def [](group_key)
50
+ @level[group_key]
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,80 @@
1
+ module Levels
2
+ # A Level is a named set of groups. A Configuration is made up of multiple
3
+ # levels with clear semantics on how those levels are merged. You generally won't
4
+ # instantiate a Level directly, but instead load one from an external source.
5
+ #
6
+ # Examples
7
+ #
8
+ # level = Levels::Level.new("My Level")
9
+ #
10
+ # level.set_group(:group1, a: 1, b: 2)
11
+ # level.set_group(:group2, c: 3, d: 4)
12
+ #
13
+ # level.group1 # => { a: 1, b: 2 }
14
+ # level.group2 # => { c: 3, d: 4 }
15
+ #
16
+ class Level
17
+ include Levels::MethodMissing
18
+
19
+ # Internal: Initialize a new level.
20
+ #
21
+ # name - String name of the level.
22
+ #
23
+ def initialize(name)
24
+ @name = name
25
+ @groups = Levels::KeyValues.new
26
+ end
27
+
28
+ # Public: Get a group by name.
29
+ #
30
+ # group_name - Symbol name of the group.
31
+ #
32
+ # Returns a Levels::Group.
33
+ # Raises Levels::UnknownGroup if the group is not defined.
34
+ def [](group_name)
35
+ @groups[group_name] or raise UnknownGroup, "#{group_name.inspect} group is not defined"
36
+ end
37
+
38
+ # Public: Determine if a group has been defined.
39
+ #
40
+ # Returns a Boolean.
41
+ def defined?(group_name)
42
+ @groups.key?(group_name)
43
+ end
44
+
45
+ # Internal: Define a group.
46
+ #
47
+ # group_name - Symbol name of the group.
48
+ # hash - Hash of values.
49
+ #
50
+ # Returns nothing.
51
+ def set_group(group_name, hash)
52
+ if @groups.key?(group_name)
53
+ raise DuplicateGroup, "#{group_name} has already been defined"
54
+ end
55
+ @groups[group_name] = Group.new(hash)
56
+ end
57
+
58
+ def to_s
59
+ "<Levels::Level #{@name.inspect}>"
60
+ end
61
+
62
+ # Returns an Enumerator which yields [group_name, Group#to_enum].
63
+ def to_enum
64
+ Enumerator.new do |y|
65
+ @groups.each do |name, group|
66
+ y << [name.to_sym, group.to_enum]
67
+ end
68
+ end
69
+ end
70
+
71
+ def _level_name
72
+ @name
73
+ end
74
+
75
+ def eql_hash?(hash)
76
+ key_values = Levels::KeyValues.new(hash)
77
+ @groups.all? { |name, group| group.eql_hash?(key_values[name]) }
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,14 @@
1
+ module Levels
2
+ # Enables dot syntax for levels and groups.
3
+ module MethodMissing
4
+
5
+ def method_missing(message, *args, &block)
6
+ raise ArgumentError, "arguments are not allowed: #{message}(#{args.inspect})" if args.any?
7
+ if message =~ /^(.*)\?$/
8
+ self.defined?($1.to_sym)
9
+ else
10
+ self[message]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,33 @@
1
+ module Levels
2
+ module Output
3
+ class JSON
4
+
5
+ SPACE = " ".freeze
6
+ JSON_OPTS = {
7
+ indent: SPACE * 2,
8
+ space: SPACE * 1,
9
+ space_before: SPACE * 0,
10
+ object_nl: "\n",
11
+ array_nl: "\n",
12
+ allow_nan: false,
13
+ max_nesting: 10
14
+ }
15
+
16
+ def initialize(json_opts = nil)
17
+ @json_opts = json_opts || JSON_OPTS
18
+ end
19
+
20
+ def generate(enumerator)
21
+ hash = {}
22
+ enumerator.each do |group_name, group|
23
+ hash[group_name] = {}
24
+ group.each do |key, value|
25
+ hash[group_name][key] = value
26
+ end
27
+ end
28
+ ::JSON.generate(hash, @json_opts)
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ module Levels
2
+ module Output
3
+ class System
4
+
5
+ def initialize(key_formatter = nil)
6
+ @key_formatter = key_formatter || Levels::System::KeyFormatter.new
7
+ @key_generator = Levels::System::KeyGenerator.new(key_formatter)
8
+ end
9
+
10
+ def generate(enumerator)
11
+ flat_enum = Enumerator.new do |y|
12
+ enumerator.each do |group_name, group|
13
+ group.each do |key, value|
14
+ y << [group_name, key, value]
15
+ end
16
+ end
17
+ end
18
+ vars = @key_generator.generate(flat_enum)
19
+ vars.map { |k, v| "export #{k}=#{quote v}" }.join("\n")
20
+ end
21
+
22
+ protected
23
+
24
+ def quote(value)
25
+ %("#{value}")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ module Levels
2
+ module Output
3
+ class YAML
4
+
5
+ def generate(enumerator)
6
+ hash = {}
7
+ enumerator.each do |group_name, group|
8
+ hash[group_name.to_s] = {}
9
+ group.each do |key, value|
10
+ hash[group_name.to_s][key.to_s] = value
11
+ end
12
+ end
13
+ ::YAML.dump(hash)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+
@@ -0,0 +1,30 @@
1
+ module Levels
2
+ # Public: Methods in this module are available within any Ruby input.
3
+ # You may extend it with any additonal methods you require.
4
+ module Runtime
5
+
6
+ FileNotFoundError = Class.new(RuntimeError)
7
+
8
+ # Public: Read the value from a file on disk. The file
9
+ # will not be read until the key is accessed.
10
+ #
11
+ # file_path - String path to the file. The path may be absolute,
12
+ # or relative to the Ruby file calling this function.
13
+ #
14
+ # Returns a Proc that reads the file when called.
15
+ # That proc raises Levels::Ruby::FileNotFoundError if the file does
16
+ # not exist.
17
+ def file(file_path)
18
+ return nil if file_path.nil?
19
+ caller_path = Pathname.new(caller[0]).dirname
20
+ -> do
21
+ path = caller_path + file_path
22
+ if path.exist?
23
+ path.read
24
+ else
25
+ raise FileNotFoundError, path.to_s
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,132 @@
1
+ module Levels
2
+ class Setup
3
+
4
+ # Internal: Initialize a new Levels::Setup.
5
+ def initialize
6
+ @inputs = []
7
+ end
8
+
9
+ # Public: Add a level of configuration.
10
+ #
11
+ # name - String name of the level.
12
+ # source - Anything that can be identified as an input source. File
13
+ # path, code or any object that responds to #read is a valid
14
+ # source.
15
+ #
16
+ # Returns nothing.
17
+ def add(name, source)
18
+ level = -> { Levels::Level.new(name) }
19
+ input = Input.new(source)
20
+ @inputs << [level, input]
21
+ end
22
+
23
+ # Public: Add the system environment as a level of configuration.
24
+ #
25
+ # prefix - String the prefix for environment variables (default
26
+ # none).
27
+ # name - String the name of the level (default: "System
28
+ # Environment").
29
+ # env_hash - Hash of environment variables (default: ENV).
30
+ #
31
+ # Returns nothing.
32
+ def add_system(prefix = nil, name = nil, env_hash = ENV)
33
+ key_formatter = Levels::System::KeyFormatter.new(prefix)
34
+ level = -> { Levels::Level.new(name || "System Environment") }
35
+ input = -> template { Levels::Input::System.new(template, key_formatter, env_hash) }
36
+ @inputs << [level, input]
37
+ end
38
+
39
+ # Public: Add an already initialized Level.
40
+ #
41
+ # level - Levels::Level.
42
+ #
43
+ # Returns nothing.
44
+ def add_level(level)
45
+ @inputs << [-> { level }, NullInput.new]
46
+ end
47
+
48
+ # Public: Parse all inputs sources and get a Configuration.
49
+ #
50
+ # Returns a Levels::Configuration.
51
+ def merge
52
+ levels = []
53
+ @inputs.each do |level_proc, input|
54
+ level = level_proc.call
55
+ case input
56
+ when Proc
57
+ template = Levels::Configuration.new(levels)
58
+ input = input.call(template.to_enum)
59
+ input.read(level)
60
+ else
61
+ input.read(level)
62
+ end
63
+ levels << level
64
+ end
65
+ Levels::Configuration.new(levels)
66
+ end
67
+
68
+ class NullInput
69
+
70
+ def read(level)
71
+ # noop
72
+ end
73
+ end
74
+
75
+ # This class transforms any supported object into Level data. The object
76
+ # can be any of:
77
+ #
78
+ # * A file path to Ruby, JSON or YAML
79
+ # * Ruby, JSON or YAML code
80
+ # * An object that responds to `#read(level)`.
81
+ #
82
+ class Input
83
+
84
+ def initialize(source)
85
+ @source = source
86
+ end
87
+
88
+ # Read the input into a Level.
89
+ # Raises an ArgumentError if the source cannot be used as an input.
90
+ def read(level)
91
+ input.read(level)
92
+ end
93
+
94
+ # Returns a Levels::Input.
95
+ # Raises an ArgumentError if the format couldn't be determined.
96
+ def input
97
+ format, source, *args = identify
98
+ case format
99
+ when :custom then source
100
+ when :ruby then Levels::Input::Ruby.new(source, *args)
101
+ when :json then Levels::Input::JSON.new(source)
102
+ when :yaml then Levels::Input::YAML.new(source)
103
+ else raise ArgumentError, "Could not identify the format: #{format.inspect}"
104
+ end
105
+ end
106
+
107
+ # Determine the format of the source and read it from disk if
108
+ # it's a file.
109
+ def identify
110
+ if @source.respond_to?(:read)
111
+ return :custom, @source
112
+ end
113
+ pn = Pathname.new(@source)
114
+ if pn.exist?
115
+ case pn.extname
116
+ when ".rb" then [:ruby, pn.read, pn.to_s, 1]
117
+ when ".json" then [:json, pn.read]
118
+ when ".yaml", ".yml" then [:yaml, pn.read]
119
+ else raise ArgumentError, "Could not identify the file type: #{pn.extname}"
120
+ end
121
+ else
122
+ case @source
123
+ when /\A\w*{/ then [:json, @source]
124
+ when /\A---$/ then [:yaml, @source]
125
+ when /\A\w*group/ then [:ruby, @source, "Code from String", 1]
126
+ else raise ArgumentError, "Could not identify the source: #{@source.inspect}"
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end