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,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
@@ -0,0 +1 @@
1
+ 1.9.3-p194
@@ -0,0 +1,4 @@
1
+ 0.0.1 / YYYY-MM-DD
2
+ ==================
3
+
4
+ * Initial version
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in config-env.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem "guard"
8
+ gem "guard-minitest"
9
+ gem "rb-fsevent"
10
+ gem "terminal-notifier-guard"
11
+ gem 'fivemat'
12
+ end
@@ -0,0 +1,14 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'minitest' do
5
+ watch(%r|^test/(.*)\/?(.*)_test\.rb|)
6
+ watch(%r|^lib/(.*)([^/]+)\.rb|) { |m|
7
+ [
8
+ "test/#{m[1]}#{m[2]}_test.rb",
9
+ "test/#{m[1].sub(/^levels/, 'unit')}#{m[2]}_test.rb"
10
+ ]
11
+ }
12
+ watch(%r|^test/helper\.rb|) { "test" }
13
+ watch(%r|^bin/|) { "test/bin" }
14
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Ryan Carver
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,315 @@
1
+ # Levels
2
+
3
+ [![Build Status](https://secure.travis-ci.org/rcarver/levels.png)](http://travis-ci.org/rcarver/levels)
4
+
5
+ Levels is a tool for merging configuration data. A level is a set of
6
+ key/value pairs that represent your data. Multiple levels, written in a
7
+ variety of formats can be merged in a predictable, useful way to form
8
+ a final configuration.
9
+
10
+ > **KRAMER:** *I'm completely changing the configuration of the apartment. You're not gonna believe it when you see it. A whole new lifestyle.*
11
+
12
+ > **JERRY:** *What are you doing?*
13
+
14
+ > **KRAMER:** *Levels.*
15
+
16
+ ## Creating a level
17
+
18
+ A level is made up of one or more groups. A group is a set of key/value
19
+ pairs. To describe a very simple web application made up of a server and
20
+ a task queue, you could write this (in JSON).
21
+
22
+ ```json
23
+ {
24
+ "server": {
25
+ "hostname": "example.com"
26
+ },
27
+ "task_queue": {
28
+ "workers": 5,
29
+ "queues": ["high", "low"]
30
+ }
31
+ }
32
+ ```
33
+
34
+ Now consider having a common "base" configuration, with slight
35
+ differences in development and production. Our base configuration
36
+ defines the possible keys, with default values.
37
+
38
+ A "production" level can override the relevant values like this.
39
+
40
+ ```json
41
+ {
42
+ "server": {
43
+ "hostname": "example.com"
44
+ },
45
+ "task_queue": {
46
+ "workers": 5
47
+ }
48
+ }
49
+ ```
50
+
51
+ The system's environment may be used as a level. To alter any value at
52
+ runtime, follow a convention to set the appropriate environment
53
+ variable.
54
+
55
+ ```bash
56
+ TASK_QUEUE_WORKERS="10"
57
+ ```
58
+
59
+ ### Writing a level
60
+
61
+ A level may be written in one of many formats.
62
+
63
+ * **RUBY** is the most common and powerful for hand written configs.
64
+ * **JSON** is convenient for machine generated configs.
65
+ * **YAML** is good for both hand written and machine generated configs.
66
+ * **Environment Variables** are useful for local or runtime
67
+ configuration. This syntax may not be used for the "base" level.
68
+
69
+ #### Data Types
70
+
71
+ Levels has a limited understanding of data types by design. The guiding
72
+ principles are:
73
+
74
+ * It must be possible to represent any value in an environment
75
+ variable.
76
+ * Use only types that are native in JSON.
77
+
78
+ Therefore, Levels only supports the following types:
79
+
80
+ * **string** (Ruby `String`)
81
+ * **integer** (Ruby `Fixnum`)
82
+ * **float** (Ruby `Float`)
83
+ * **boolean** (Ruby `TrueClass` or `FalseClass`)
84
+ * **array** (Ruby `Array`) of values, which are also typed.
85
+ * **null** (Ruby `NilClass`)
86
+
87
+ Notice that JSON's Object is not supported. This is because groups are
88
+ objects, so key/values pairs are already available. It's difficult to
89
+ represent key/value pairs in an environment variable, so it fails that
90
+ test as well.
91
+
92
+ Fortunately, these simple types are perfectly adequate for the purposes
93
+ of system configuration.
94
+
95
+ #### Ruby Syntax
96
+
97
+ The Ruby DSL is a clean, simple format. It aims to be readable, writable and
98
+ editable. It looks like this:
99
+
100
+ ```ruby
101
+ group :server
102
+ set hostname: "example.com"
103
+
104
+ group :task_queue
105
+ set workers: 5
106
+ set queues: ["high", "low"]
107
+ ```
108
+
109
+ The Ruby syntax supports **computed values**.
110
+
111
+ ```ruby
112
+ group :task_queue
113
+ set queues: -> { [server.hostname, "high", "low"] }
114
+ ```
115
+
116
+ ##### Extending the Ruby Runtime
117
+
118
+ To extend the runtime environment, add methods to `Levels::Runtime`.
119
+ Those methods can return a value directly, or return a Proc for
120
+ lazy evaluation.
121
+
122
+ ```ruby
123
+ module Levels::Runtime
124
+ # This helper decrypts a value using the merged value of
125
+ # `secret_keys.sha_key`.
126
+ def encrypted(encrypted_value)
127
+ -> { SHA.decrypt(encrypted_value, secret_keys.sha_key) }
128
+ end
129
+ end
130
+ ```
131
+
132
+ With this runtime helper, you can now write:
133
+
134
+ ```ruby
135
+ group :aws
136
+ set secret_key: encrypted("your aws secret key")
137
+ ```
138
+
139
+ ##### Builtin runtime extensions
140
+
141
+ These functions are provided by the default Levels Runtime.
142
+
143
+ * `file(path)` reads the value from a file. The file path is
144
+ interpreted as relative to the Ruby file unless it begins with '/'.
145
+ File storage can be useful when configuring large strings such as
146
+ SSL keys.
147
+
148
+ #### JSON Syntax
149
+
150
+ JSON syntax is straightforward. Because the datatypes supported by
151
+ Levels are the same as supported by JSON, there's nothing else you need
152
+ to know.
153
+
154
+ ```json
155
+ {
156
+ "server": {
157
+ "hostname": "example.com"
158
+ },
159
+ "task_queue": {
160
+ "workers": 5,
161
+ "queues": ["high", "low"]
162
+ }
163
+ }
164
+ ```
165
+
166
+ #### YAML Syntax
167
+
168
+ YAML syntax is also exactly as you would expect.
169
+
170
+ ```yaml
171
+ ---
172
+ server:
173
+ hostname: example.com
174
+ task_queue:
175
+ workers: 5
176
+ queues:
177
+ - high
178
+ - low
179
+ ```
180
+
181
+ #### Environment Variables syntax
182
+
183
+ The environment variables syntax has rules for defining keys and values.
184
+
185
+ The format of each key is `[PREFIX]<GROUP>_<KEY>`.
186
+
187
+ * `PREFIX` is an optional prefix for all keys.
188
+ * `GROUP` is the name of the group in all caps.
189
+ * `KEY` is the name of the key in all caps.
190
+ * `GROUP` and `KEY` are separated by an underscore (`_`).
191
+
192
+ The example looks like this (without a prefix).
193
+
194
+ ```sh
195
+ SERVER_HOSTNAME="example.com"
196
+ TASK_QUEUE_WORKERS="5"
197
+ TASK_QUEUE_QUEUES="high:low"
198
+ ```
199
+
200
+ ##### Typecasting
201
+
202
+ You'll notice that `TASK_QUEUE_WORKERS` should be an integer, and
203
+ `TASK_QUEUE_QUEUES` should be an array. Levels will typecast each value
204
+ based on the key's type in the "base" level. Or, you may define each
205
+ value's type explicitly.
206
+
207
+ To set the type of a value, set `<GROUP>_<KEY>_TYPE` to one of the
208
+ following:
209
+
210
+ * `string` - The value is taken as is.
211
+ * `integer` - The value is converted to an integer via Ruby's `to_i`.
212
+ * `float` - The value is converted to a float via Ruby's `to_f`.
213
+ * `boolean` - The value is `true` if it's "true" or "1", else `false`.
214
+ * `array` - The value is split using colon (`:`) or
215
+ `<GROUP>_<KEY>_DELIMITER`. The values of the resulting array may be
216
+ typecast using `<GROUP>_<KEY>_TYPE_TYPE`.
217
+
218
+ Any value may be set to Ruby's `nil` (`NULL`) by setting it to an empty
219
+ string.
220
+
221
+ Some examples:
222
+
223
+ ```sh
224
+ SAMPLE_MY_NULL=""
225
+
226
+ SAMPLE_MY_INT="123"
227
+ SAMPLE_MY_INT_TYPE="integer"
228
+
229
+ SAMPLE_MY_BOOL="true"
230
+ SAMPLE_MY_BOOL_TYPE="boolean"
231
+
232
+ SAMPLE_MY_STRING_ARRAY="a:b:c"
233
+ SAMPLE_MY_STRING_ARRAY_TYPE="array"
234
+
235
+ SAMPLE_MY_INT_ARRAY="1:2:3"
236
+ SAMPLE_MY_INT_ARRAY_TYPE="array"
237
+ SAMPLE_MY_INT_ARRAY_TYPE_TYPE="integer"
238
+
239
+ SAMPLE_MY_CSV_ARRAY="one,two,three"
240
+ SAMPLE_MY_CSV_ARRAY_TYPE="array"
241
+ SAMPLE_MY_CSV_ARRAY_DELIMITER=","
242
+ ```
243
+
244
+ ## Using a Configuration
245
+
246
+ Once a level has been written, you can read and merge it. Once merged
247
+ into a Configuration, you can use it at runtime in a Ruby process, or
248
+ output it as JSON, YAML or environment variables.
249
+
250
+ Any number of levels, including the system environment, may be merged.
251
+ The system environment is typically merged last, but it's not required.
252
+
253
+ **From the command line**, Levels can generate JSON, YAML or environment
254
+ variables. The generated configuration is written to STDOUT. Both JSON
255
+ and Environment Variables look exactly like the input formats above.
256
+
257
+ ```sh
258
+ levels \
259
+ --output json \
260
+ --level "Base" \
261
+ --level "Prod" \
262
+ --system \
263
+ base.rb \
264
+ prod.json
265
+ ```
266
+
267
+ **Within a Ruby program**, a `Levels::Configuration` is an object. You
268
+ can build one with `Levels.merge`.
269
+
270
+ ```ruby
271
+ # Merge multiple input levels from various sources - file, API and
272
+ # environment variables.
273
+ config = Levels.merge do |levels|
274
+ levels.add "Base", HTTP.get("https://server/config.json")
275
+ levels.add "Prod", "prod.json"
276
+ levels.add_system
277
+ end
278
+ ```
279
+
280
+ The resulting `config` object works like this.
281
+
282
+ ```ruby
283
+ # Dot syntax.
284
+ config.server.hostname # => "example.com"
285
+ config.task_queue.workers # => 5
286
+ config.task_queue.queues # => ["high", "low"]
287
+
288
+ # Hash syntax.
289
+ config[:server][:hostname] # => "example.com"
290
+ config[:task_queue][:workers] # => 5
291
+ config[:task_queue][:queues] # => ["high", "low"]
292
+ ```
293
+
294
+ An attempt to read an unknown group or key will throw an exception.
295
+
296
+ ```ruby
297
+ config.some_group # raises Levels::UnknownGroup
298
+ config.server.some_value # raises Levels::UnknownKey
299
+ ```
300
+
301
+ You can find out if a group or key exists.
302
+
303
+ ```ruby
304
+ config.defined?(:other) # => false
305
+ config.defined?(:server) # => true
306
+ config.server.defined?(:other) # => false
307
+ config.server.defined?(:hostname) # => true
308
+ ```
309
+
310
+ ## Author
311
+
312
+ Ryan Carver / @rcarver
313
+
314
+ Copyright (c) Ryan Carver 2012. Made available under the MIT license.
315
+
@@ -0,0 +1,28 @@
1
+ require 'rake/testtask'
2
+
3
+ desc "Run all tests"
4
+ task :default => :testall
5
+
6
+ desc "Run all of the tests"
7
+ task :testall => [:test, :examples]
8
+
9
+ desc "Run the unit tests"
10
+ Rake::TestTask.new do |t|
11
+ t.libs.push "lib", "test"
12
+ t.test_files = FileList['test/**/*_test.rb']
13
+ t.verbose = true
14
+ end
15
+
16
+ desc "Run the examples"
17
+ task :examples do
18
+ Dir["examples/*.sh"].each do |script|
19
+ sh script
20
+ end
21
+ end
22
+
23
+ begin
24
+ require 'bundler'
25
+ Bundler::GemHelper.install_tasks
26
+ rescue
27
+ STDERR.puts "bundler is not available"
28
+ end
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'levels'
4
+ require 'optparse'
5
+ require 'pathname'
6
+
7
+ # If "--system" is the last flag, and no PREFIX is set, then
8
+ # the first file argument will be interpreted as the PREFIX.
9
+ #
10
+ # Examples
11
+ #
12
+ # # This would be interpreted as "one.rb is the system prefix".
13
+ # --level One --system one.rb
14
+ # # What we want is
15
+ # --level One --system '' one.rb
16
+ #
17
+ def fix_system_is_last_arg(argv)
18
+ index = ARGV.index("--system") or return
19
+ value = argv[index + 1] or return
20
+
21
+ unless value =~ /^-/ || value =~ /^[A-Z_]/
22
+ ARGV.insert(index + 1, "")
23
+ end
24
+ end
25
+
26
+ fix_system_is_last_arg(ARGV)
27
+
28
+ # By default we'll output JSON
29
+ @output = true
30
+ @output_format = "json"
31
+
32
+ # Colorize by default.
33
+ @colorize = true
34
+
35
+ # If output is "system", change the prefix.
36
+ @system_output_prefix = nil
37
+
38
+ # Read input from the system.
39
+ @system = false
40
+ @system_input_prefix = nil
41
+
42
+ # Accumulate the names of levels for file arguments.
43
+ @level_names = []
44
+
45
+ OptionParser.new do |opts|
46
+
47
+ opts.banner = "Usage: levels [options] [files]"
48
+
49
+ opts.on("-l", "--level [NAME]", "Name the level from a file.") do |n|
50
+ @level_names << n
51
+ end
52
+
53
+ opts.on("-s", "--system [PREFIX]", "Read the system as a level.") do |p|
54
+ @system = true
55
+ @system_input_prefix = p if p && !p.empty?
56
+ end
57
+
58
+ opts.on("-o", "--output FORMAT", "The format to output. (json, system)") do |o|
59
+ @output_format = o
60
+ end
61
+
62
+ opts.on("-p", "--prefix PREFIX", "Prefix for system output.") do |p|
63
+ @system_output_prefix = p
64
+ end
65
+
66
+ opts.on("-n", "--no-output", "Don't output the result.") do
67
+ @output = false
68
+ end
69
+
70
+ opts.on("--[no-]color", "Colorize the output.") do |bool|
71
+ @colorize = bool
72
+ end
73
+
74
+ opts.on("-v", "--version", "Show the levels version.") do
75
+ STDOUT.puts "Levels #{Levels::VERSION}"
76
+ end
77
+
78
+ opts.on("-h", "--help", "Show this help.") do
79
+ STDOUT.puts opts.to_s
80
+ end
81
+
82
+ end.parse!
83
+
84
+ # Files are any remaining arguments after parsing.
85
+ @files = ARGV.dup
86
+
87
+ @setup = Levels.setup
88
+
89
+ # Read each file into a level.
90
+ @files.each.with_index do |file, index|
91
+ pn = Pathname.new(file)
92
+ level_name = @level_names[index] || pn.basename.to_s
93
+ STDERR.puts "Add level #{level_name.inspect} from #{pn.basename}"
94
+
95
+ @setup.add level_name, file
96
+ end
97
+
98
+ # Read the system using the existing levels as a base, then add it
99
+ # as another level.
100
+ if @system
101
+ level_name = "System Environment"
102
+ if @system_input_prefix
103
+ STDERR.puts "Add level #{level_name.inspect} with prefix #{@system_input_prefix}"
104
+ else
105
+ STDERR.puts "Add level #{level_name.inspect}"
106
+ end
107
+ @setup.add_system @system_input_prefix, level_name, ENV
108
+ end
109
+
110
+ # Write the configuration to stdout.
111
+ if @output && (@files.any? || @system)
112
+ configuration = @setup.merge
113
+ configuration.event_handler = Levels::CliEventHandler.new(STDERR, @colorize)
114
+
115
+ output = nil
116
+
117
+ case @output_format
118
+ when "json"
119
+ output = Levels::Output::JSON.new
120
+ when "yaml"
121
+ output = Levels::Output::YAML.new
122
+ when "system"
123
+ key_formatter = Levels::System::KeyFormatter.new(@system_output_prefix)
124
+ output = Levels::Output::System.new(key_formatter)
125
+ end
126
+
127
+ if output
128
+ STDOUT.puts output.generate(configuration.to_enum)
129
+ end
130
+ end