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