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,6 @@
1
+ group :webserver
2
+ set hostname: "localhost"
3
+
4
+ group :task_queue
5
+ set workers: 1
6
+ set queues: -> { ["high", "low", webserver.hostname] }
@@ -0,0 +1,27 @@
1
+ #!/bin/bash
2
+ #
3
+ # This example sets up three levels:
4
+ #
5
+ # * "Base" from examples/01_base.rb
6
+ # * "Prod" from examples/01_prod.json
7
+ # * "System Environment" with no prefix.
8
+ #
9
+ # Base defines the possible keys with default vaules, Prod changes a few of
10
+ # those values, and System changes one more.
11
+ #
12
+ # A log of the levels used, and where each value came from is written to
13
+ # STDERR. The merged output as JSON is written to STDOUT.
14
+
15
+ examples="$(cd "$(dirname "$BASH_SOURCE")" && pwd)"
16
+
17
+ # Alter a value via the system environment.
18
+ export TASK_QUEUE_WORKERS='10'
19
+
20
+ bundle exec levels \
21
+ --output json \
22
+ --level "Base" \
23
+ --level "Prod" \
24
+ --system \
25
+ $examples/01_base.rb \
26
+ $examples/01_prod.json
27
+
@@ -0,0 +1,8 @@
1
+ {
2
+ "webserver": {
3
+ "hostname": "example.com"
4
+ },
5
+ "task_queue": {
6
+ "workers": 5
7
+ }
8
+ }
@@ -0,0 +1,4 @@
1
+ group :examples
2
+ set file_name: nil
3
+ set file_value1: file("02_value")
4
+ set file_value2: -> { file(examples.file_name) }
@@ -0,0 +1,20 @@
1
+ #!/bin/bash
2
+ #
3
+ # This example shows how to store and read a value from a file.
4
+ #
5
+ # * examples/02_base.rb reads a file twice. Once where the file name
6
+ # is known, and once where it is unknown until set via an
7
+ # environment variable.
8
+
9
+ examples="$(cd "$(dirname "$BASH_SOURCE")" && pwd)"
10
+
11
+ # Alter a value via the system environment.
12
+ export EXAMPLES_FILE_NAME='02_value'
13
+
14
+ bundle exec levels \
15
+ --output json \
16
+ --level "Base" \
17
+ --system \
18
+ $examples/02_base.rb
19
+
20
+
@@ -0,0 +1 @@
1
+ Hello from the file!
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/levels/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Ryan Carver"]
6
+ gem.email = ["ryan@ryancarver.com"]
7
+ gem.description = %q{A tool for reading and writing configuration data.}
8
+ gem.summary = %q{A tool for reading and writing configuration data.}
9
+ gem.homepage = "https://github.com/rcarver/levels"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "levels"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Levels::VERSION
17
+
18
+ gem.add_development_dependency "rake"
19
+ gem.add_development_dependency "minitest", "~>4.0"
20
+ end
@@ -0,0 +1,77 @@
1
+ require "json"
2
+ require "yaml"
3
+ require "open3"
4
+
5
+ require "levels/version"
6
+
7
+ require "levels/method_missing"
8
+ require "levels/runtime"
9
+
10
+ require "levels/audit"
11
+ require "levels/configuration"
12
+ require "levels/configured_group"
13
+ require "levels/event_handler"
14
+ require "levels/group"
15
+ require "levels/key"
16
+ require "levels/key_values"
17
+ require "levels/lazy_evaluator"
18
+ require "levels/level"
19
+ require "levels/setup"
20
+
21
+ require "levels/input/json"
22
+ require "levels/input/ruby"
23
+ require "levels/input/system"
24
+ require "levels/input/yaml"
25
+
26
+ require "levels/output/json"
27
+ require "levels/output/system"
28
+ require "levels/output/yaml"
29
+
30
+ require "levels/system/constants"
31
+ require "levels/system/key_formatter"
32
+ require "levels/system/key_generator"
33
+ require "levels/system/key_parser"
34
+
35
+ module Levels
36
+
37
+ # Error thrown if a group is defined more than once.
38
+ DuplicateGroup = Class.new(StandardError)
39
+
40
+ # Error thrown when attempting to access a group that has not been defined.
41
+ UnknownGroup = Class.new(StandardError)
42
+
43
+ # Error thrown when attempting to read a key that has not been defined.
44
+ UnknownKey = Class.new(StandardError)
45
+
46
+ # Public: Begin a new setup. The setup is used to add one or more
47
+ # levels, then merged into a configuration.
48
+ #
49
+ # Examples
50
+ #
51
+ # setup = Levels.setup
52
+ # setup.add "Base", "file.rb"
53
+ # setup.add_system
54
+ # my_config = setup.merge
55
+ #
56
+ # Returns a Levels::Setup.
57
+ def self.setup
58
+ Levels::Setup.new
59
+ end
60
+
61
+ # Public: Get a merged configuration by using the setup.
62
+ #
63
+ # Examples
64
+ #
65
+ # my_config = Levels.merge do |setup|
66
+ # setup.add "Base", "file.rb"
67
+ # setup.add_system
68
+ # end
69
+ #
70
+ # Returns a Levels::Configuration.
71
+ def self.merge
72
+ setup = self.setup
73
+ yield setup if block_given?
74
+ setup.merge
75
+ end
76
+ end
77
+
@@ -0,0 +1,24 @@
1
+ module Levels
2
+ # In order to understand which of many possible values is actually used at
3
+ # runtime, Levels provides an audit trail for each value that's accessed.
4
+ # The audit trail is reported via the Levels::EventHandler interface.
5
+ module Audit
6
+
7
+ # Internal: Begin an audit.
8
+ #
9
+ # evaluator - Ducktype #call used to interpret raw values.
10
+ #
11
+ # Returns a Levels::Audit::RootObserver.
12
+ def self.start(evaluator)
13
+ Levels::Audit::RootObserver.new(evaluator)
14
+ end
15
+
16
+ autoload :GroupObserver, 'levels/audit/group_observer'
17
+ autoload :NestedGroupObserver, 'levels/audit/nested_group_observer'
18
+ autoload :RootObserver, 'levels/audit/root_observer'
19
+ autoload :Value, 'levels/audit/value'
20
+ autoload :ValueObserver, 'levels/audit/value_observer'
21
+ autoload :Values, 'levels/audit/values'
22
+
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ module Levels
2
+ module Audit
3
+ # The GroupObserver notifies when a value is accessed.
4
+ class GroupObserver
5
+
6
+ # Initialize a new GroupObserver.
7
+ #
8
+ # value_observer - Levels::Audit::ValueObserver.
9
+ # event_handler - Levels::EventHandler.
10
+ #
11
+ def initialize(value_observer, event_handler)
12
+ @value_observer = value_observer
13
+ @event_handler = event_handler
14
+ end
15
+
16
+ # Retrieve the value at a group+value key and notify that it was read.
17
+ #
18
+ # Returns a Levels::Audit::Values.
19
+ def observe_values(levels, group_key, value_key)
20
+ values = @value_observer.observe_values(levels, group_key, value_key)
21
+ @event_handler.on_values(values)
22
+ values
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,37 @@
1
+ module Levels
2
+ module Audit
3
+ # The NestedGroupObserver is like a GroupObserver, but used to
4
+ # observe what happens during recursive value evaluation.
5
+ class NestedGroupObserver
6
+
7
+ # Initialize a new NestedGroupObserver.
8
+ #
9
+ # value_observer - Levels::Audit::ValueObserver.
10
+ #
11
+ def initialize(value_observer)
12
+ @value_observer = value_observer
13
+ @values = []
14
+ end
15
+
16
+ # Retrieve the value at a group+value key.
17
+ #
18
+ # Returns a Levels::Audit::Values.
19
+ def observe_values(levels, group_key, value_key)
20
+ values = @value_observer.observe_values(levels, group_key, value_key)
21
+ @values << values
22
+ values
23
+ end
24
+
25
+ # Private: Notify that the observed values were seen. After notifying of
26
+ # the observed values, the set of observed values is reset.
27
+ #
28
+ # event_handler - Levels::EventHandler.
29
+ #
30
+ # Returns nothing.
31
+ def notify_nested(event_handler)
32
+ @values.each { |v| event_handler.on_nested_values(v) }
33
+ @values.clear
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,63 @@
1
+ module Levels
2
+ module Audit
3
+ # The RootObserver observes all accesses to group and value data.
4
+ class RootObserver
5
+
6
+ # Initialize a new RootObserver
7
+ #
8
+ # evaluator - Ducktype #call used to interpret raw values.
9
+ #
10
+ def initialize(evaluator)
11
+ @evaluator = evaluator
12
+ @current_value_stack = []
13
+ end
14
+
15
+ # Get an observer to watch when values are accessed from a group.
16
+ #
17
+ # event_handler - Levels::EventHandler to receive observations.
18
+ #
19
+ # Returns a Levels::Audit::GroupObserver.
20
+ def observe_group(user_observer)
21
+ if current_value
22
+ observer = NestedGroupObserver.new(value_observer)
23
+ current_value.add_nested_group_observer(observer)
24
+ observer
25
+ else
26
+ GroupObserver.new(value_observer, user_observer)
27
+ end
28
+ end
29
+
30
+ # Private: Set the current value context. This is used to capture
31
+ # recursive values.
32
+ #
33
+ # value - Levels::Audit::Value.
34
+ #
35
+ # Yields.
36
+ #
37
+ # Returns nothing.
38
+ def with_current_value(value)
39
+ begin
40
+ @current_value_stack << value
41
+ yield
42
+ ensure
43
+ @current_value_stack.pop
44
+ end
45
+ end
46
+
47
+ # Private: Get the current value.
48
+ #
49
+ # Returns a Levels::Audit::Value or nil.
50
+ def current_value
51
+ @current_value_stack.last
52
+ end
53
+
54
+ protected
55
+
56
+ def value_observer
57
+ ValueObserver.new(self, @evaluator)
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+
@@ -0,0 +1,64 @@
1
+ module Levels
2
+ module Audit
3
+ # The Value represents one piece of configuration data that was originally
4
+ # stored in a Level.
5
+ class Value
6
+
7
+ # Initialize a new Value.
8
+ #
9
+ # level_name - String the name of the level this value was in.
10
+ # final - Boolean true if this is the "final" value for a set of
11
+ # levels.
12
+ #
13
+ def initialize(level_name, final, value = :__no_value__)
14
+ @level_name = level_name
15
+ @final = final
16
+ @nested_group_observers = []
17
+ if value == :__no_value__
18
+ @value = yield self if block_given?
19
+ else
20
+ @value = value
21
+ end
22
+ end
23
+
24
+ # Returns a String the name of the level.
25
+ attr_reader :level_name
26
+
27
+ # Returns the actual value.
28
+ attr_reader :value
29
+
30
+ # Returns the actual value.
31
+ alias raw value
32
+
33
+ # Returns a Boolean true if this is the final value.
34
+ def final?
35
+ !!@final
36
+ end
37
+
38
+ # Returns a Boolean true if this value is recursive.
39
+ def recursive?
40
+ !@nested_group_observers.empty?
41
+ end
42
+
43
+ # Public: Trigger a notification of the nested values. For any nested
44
+ # values, the event handler will receive the `#on_nested_values` message.
45
+ #
46
+ # event_handler - Levels::EventHandler.
47
+ def notify(event_handler)
48
+ @nested_group_observers.each do |ngo|
49
+ ngo.notify_nested(event_handler)
50
+ end
51
+ end
52
+
53
+ # Private: This is used to accumulate recursive group access.
54
+ def add_nested_group_observer(nested_group_observer)
55
+ @nested_group_observers << nested_group_observer
56
+ end
57
+
58
+ def inspect
59
+ value.inspect
60
+ end
61
+ end
62
+ end
63
+ end
64
+
@@ -0,0 +1,46 @@
1
+ module Levels
2
+ module Audit
3
+ # The ValueObserver iterates over levels to find the appriate values. It
4
+ # accumulates all possible values and indicates which is the "final" value.
5
+ class ValueObserver
6
+
7
+ # Initialize a new ValueObserver.
8
+ #
9
+ # root_observer - Levels::Audit::RootObserver.
10
+ # evaluator - Ducktype #call used to interpret raw values.
11
+ #
12
+ def initialize(root_observer, evaluator)
13
+ @root_observer = root_observer
14
+ @evaluator = evaluator
15
+ end
16
+
17
+ # Get the possible values for a group+value key. The values are retrieved
18
+ # and evaluated in context of the RootObserver so that recursive values
19
+ # are tracked.
20
+ #
21
+ # levels - Array of Levels::Level.
22
+ # group_key - Levels::Key.
23
+ # value_key - Levels::Key.
24
+ #
25
+ # Returns a Levels::Audit::Values.
26
+ def observe_values(levels, group_key, value_key)
27
+ valid_levels = levels.find_all do |level|
28
+ level.defined?(group_key) &&
29
+ level[group_key].defined?(value_key)
30
+ end
31
+
32
+ values = valid_levels.map.with_index do |level, index|
33
+ group = level[group_key]
34
+
35
+ Value.new(level._level_name, index == valid_levels.size - 1) do |value|
36
+ @root_observer.with_current_value(value) do
37
+ @evaluator.call(group[value_key])
38
+ end
39
+ end
40
+ end
41
+
42
+ Values.new(group_key, value_key, values)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,66 @@
1
+ module Levels
2
+ module Audit
3
+ # The Values is a set of possible values for a group+value key.
4
+ class Values
5
+
6
+ # Initialize a new Values.
7
+ #
8
+ # group_key - Levels::Key.
9
+ # value_key - Levels::Key.
10
+ # values - Array of Levels::Audit::Value.
11
+ #
12
+ def initialize(group_key, value_key, values)
13
+ @group_key = group_key
14
+ @value_key = value_key
15
+ @values = values
16
+ end
17
+
18
+ # Public: Returns a Levels::Key.
19
+ attr_reader :group_key
20
+
21
+ # Public: Returns a Levels::Key.
22
+ attr_reader :value_key
23
+
24
+ # Public: Returns the Levels::Audit::Value marked final.
25
+ def final
26
+ @values.find { |v| v.final? }
27
+ end
28
+
29
+ # Public: Returns the actual user-defined final value.
30
+ def final_value
31
+ @values.find { |v| v.final? }.value
32
+ end
33
+
34
+ include Enumerable
35
+
36
+ # Public: Iterate over all potential values.
37
+ def each(&block)
38
+ @values.each(&block)
39
+ end
40
+
41
+ # Public: Returns true if there are no potential values.
42
+ def empty?
43
+ @values.empty?
44
+ end
45
+
46
+ # Public: Returns true if there is only a final value.
47
+ def only_final?
48
+ size == 1 && final
49
+ end
50
+
51
+ # Public: Returns true if any of the values are recursive.
52
+ def recursive?
53
+ @values.any? { |v| v.recursive? }
54
+ end
55
+
56
+ # Public: Returns the number of potential values.
57
+ def size
58
+ @values.size
59
+ end
60
+
61
+ def inspect
62
+ "<Values #{group_key.inspect} #{value_key.inspect} #{@values.inspect}>"
63
+ end
64
+ end
65
+ end
66
+ end