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,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