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