conker 0.11.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.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,32 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ conker (0.11.0)
5
+ activesupport
6
+ addressable
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ activesupport (3.2.9)
12
+ i18n (~> 0.6)
13
+ multi_json (~> 1.0)
14
+ addressable (2.3.2)
15
+ diff-lcs (1.1.3)
16
+ i18n (0.6.1)
17
+ multi_json (1.5.0)
18
+ rspec (2.12.0)
19
+ rspec-core (~> 2.12.0)
20
+ rspec-expectations (~> 2.12.0)
21
+ rspec-mocks (~> 2.12.0)
22
+ rspec-core (2.12.2)
23
+ rspec-expectations (2.12.1)
24
+ diff-lcs (~> 1.1.3)
25
+ rspec-mocks (2.12.0)
26
+
27
+ PLATFORMS
28
+ ruby
29
+
30
+ DEPENDENCIES
31
+ conker!
32
+ rspec
data/LICENSE.MIT ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2010-2013 Sam Stokes, Conrad Irwin, Lee Mallabone,
4
+ Martin Kleppmann
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
data/conker.gemspec ADDED
@@ -0,0 +1,16 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'conker'
3
+ s.authors = ['Sam Stokes', 'Conrad Irwin', 'Lee Mallabone', 'Martin Kleppmann']
4
+ s.email = 'supportive@rapportive.com'
5
+ s.version = '0.11.0'
6
+ s.summary = %q{Conker will conquer your config.}
7
+ s.description = "Configuration library."
8
+ s.homepage = "https://github.com/rapportive/conker"
9
+ s.license = 'MIT'
10
+ s.date = Date.today.to_s
11
+ s.files = `git ls-files`.split("\n")
12
+ s.require_paths = %w(lib)
13
+ s.add_dependency 'activesupport'
14
+ s.add_dependency 'addressable'
15
+ s.add_development_dependency 'rspec'
16
+ end
data/lib/conker.rb ADDED
@@ -0,0 +1,191 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'active_support/core_ext/hash/keys'
3
+ require 'active_support/core_ext/hash/reverse_merge'
4
+ require 'addressable/uri'
5
+
6
+ # Example use:
7
+ # module Conker
8
+ # setup_config!(Rails.env, :A_SECRET => api_credential)
9
+ # end
10
+ module Conker
11
+ ENVIRONMENTS = %w(production development test)
12
+ DUMMY_API_KEY = 'dummy_api_key'.freeze
13
+ DUMMY_CRYPTO_SECRET = 'dummysecretdummysecretdummysecretdummysecretdummysecretdummysecretdummysecre'
14
+
15
+ class Error < StandardError; end
16
+ class MustBeDefined < Error
17
+ def initialize; super('must be defined'); end
18
+ end
19
+ class UnknownType < Error
20
+ def initialize(type); super("unknown type #{type}"); end
21
+ end
22
+ class MissingDefault < Error
23
+ def initialize; super("missing default value"); end
24
+ end
25
+
26
+
27
+ class << self
28
+ # Parse a multi-key hash into globals and raise an informative error message on failure.
29
+ def setup_config!(current_env, hash)
30
+ errors = []
31
+ hash.each do |varname, declaration|
32
+ begin
33
+ Kernel.const_set(varname, declaration.evaluate(current_env, varname.to_s))
34
+ rescue => error
35
+ errors << [varname, error.message]
36
+ end
37
+ end
38
+
39
+ error_message = errors.sort_by {|v, e| v.to_s }.map do |varname, error|
40
+ varname.to_s + ': ' + error
41
+ end.join(", ")
42
+ raise Error, error_message unless errors.empty?
43
+ end
44
+
45
+ # A wrapper around setup_config! that uses ENV["RACK_ENV"] || 'development'
46
+ def setup_rack_environment!(hash)
47
+ ENV["RACK_ENV"] ||= 'development'
48
+
49
+ setup_config!(ENV["RACK_ENV"],
50
+ hash.merge(:RACK_ENV => required_in_production(:development => 'development', :test => 'test')))
51
+ end
52
+
53
+ # Declare an environment variable that is required to be defined in the
54
+ # production environment, and defaults to other values in the test or
55
+ # development environments.
56
+ #
57
+ # You must either specify a :default, or specify defaults for each of
58
+ # :test and :development.
59
+ def required_in_production(declaration_opts={})
60
+ VariableDeclaration.new(declaration_opts.reverse_merge(:required_in => :production))
61
+ end
62
+
63
+ # Declare an environment variable to be used as a credential for accessing
64
+ # an external API (e.g. username, password, API key, access token):
65
+ # shorthand for
66
+ # +required_in_production(:type => :string, :default => 'dummy_api_key')+
67
+ def api_credential(declaration_opts={})
68
+ required_in_production({
69
+ :type => :string,
70
+ :default => DUMMY_API_KEY,
71
+ }.merge(declaration_opts))
72
+ end
73
+
74
+ # Declare an environment variable to be used as a secret key by some
75
+ # encryption algorithm used in our code.
76
+ #
77
+ # To generate a secret suitable for production use, try:
78
+ # openssl rand -hex 256
79
+ # (which will generate 256 bytes = 2048 bits of randomness).
80
+ #
81
+ # The distinction between this and api_credential is mainly for
82
+ # documentation purposes, but they also have different defaults.
83
+ def crypto_secret(declaration_opts={})
84
+ required_in_production({
85
+ :type => :string,
86
+ :default => DUMMY_CRYPTO_SECRET,
87
+ }.merge(declaration_opts))
88
+ end
89
+
90
+ # A redis url is required_in_production with development and test defaulting to localhost.
91
+ def redis_url(opts={})
92
+ required_in_production({
93
+ :development => "redis://localhost/1",
94
+ :test => "redis://localhost/3"
95
+ }.merge(opts))
96
+ end
97
+
98
+ # Declare an environment variable, defaulting to other values if not defined.
99
+ #
100
+ # You must either specify a :default, or specify defaults for each of
101
+ # :production, :test and :development.
102
+ def optional(declaration_opts = {})
103
+ VariableDeclaration.new(declaration_opts)
104
+ end
105
+ end
106
+
107
+
108
+ class VariableDeclaration
109
+ def initialize(declaration_opts)
110
+ declaration_opts.assert_valid_keys :required_in, :type, :default, *ENVIRONMENTS.map(&:to_sym)
111
+ @declaration_opts = declaration_opts.with_indifferent_access
112
+ end
113
+
114
+ def evaluate(current_environment, varname)
115
+ @environment = current_environment
116
+ check_missing_value! varname
117
+ check_missing_default!
118
+ from_config_variable_or_default(varname)
119
+ end
120
+
121
+ private
122
+ def check_missing_value!(varname)
123
+ if required_in_environments.member?(@environment.to_sym) && !ENV[varname]
124
+ raise MustBeDefined
125
+ end
126
+ end
127
+
128
+ def check_missing_default!
129
+ environments_needing_default = ENVIRONMENTS.map(&:to_sym) - required_in_environments
130
+ default_specified = @declaration_opts.key? :default
131
+ all_environments_defaulted = environments_needing_default.all?(&@declaration_opts.method(:key?))
132
+ unless default_specified || all_environments_defaulted
133
+ raise MissingDefault
134
+ end
135
+ end
136
+
137
+ def from_config_variable_or_default(varname)
138
+ if ENV[varname] && @environment != 'test'
139
+ interpret_value(ENV[varname], @declaration_opts[:type])
140
+ else
141
+ default_value
142
+ end
143
+ end
144
+
145
+ def required_in_environments
146
+ Array(@declaration_opts[:required_in]).map(&:to_sym)
147
+ end
148
+
149
+ # Only interpret the default value if it is a string
150
+ # (to avoid coercing nil to '')
151
+ def default_value
152
+ default = @declaration_opts.include?(@environment) ? @declaration_opts[@environment] : @declaration_opts[:default]
153
+ if default.is_a? String
154
+ interpret_value(default, @declaration_opts[:type])
155
+ else
156
+ default
157
+ end
158
+ end
159
+
160
+ def interpret_value(value, type)
161
+ type = type.to_sym if type
162
+ case type
163
+ when :boolean
164
+ value.to_s.downcase == "true" || value.to_i == 1
165
+ # defaults to false if omitted
166
+ when :integer
167
+ Integer(value)
168
+ # defaults to 0 if omitted
169
+ when :float
170
+ value ? Float(value) : 0.0
171
+ # defaults to 0.0 if omitted
172
+ when :url
173
+ raise MustBeDefined if value.nil? # there's nothing sensible to default to
174
+ require 'uri' unless defined? URI
175
+ URI.parse(value.to_s)
176
+ when :addressable
177
+ raise MustBeDefined if value.nil? # there's nothing sensible to default to
178
+ require 'addressable' unless defined? Addressable
179
+ Addressable::URI.parse(value.to_s)
180
+ when :timestamp
181
+ raise MustBeDefined if value.nil? # there's nothing sensible to default to.
182
+ Time.iso8601(value.to_s).utc
183
+ when :string, nil
184
+ value.to_s
185
+ # defaults to '' if omitted
186
+ else
187
+ raise UnknownType, type.to_s
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,235 @@
1
+ require 'conker'
2
+
3
+ describe Conker do
4
+ before :each do
5
+ @env_vars = ENV.keys
6
+ @constants = Kernel.constants
7
+ end
8
+
9
+ after :each do
10
+ # N.B. this doesn't catch if we *changed* any env vars (rather than adding
11
+ # new ones).
12
+ (ENV.keys - @env_vars).each {|k| ENV.delete k }
13
+ # Same caveat doesn't apply here, because Ruby will whinge if we redefine a
14
+ # constant.
15
+ (Kernel.constants - @constants).each do |k|
16
+ Kernel.send :remove_const, k
17
+ end
18
+ end
19
+
20
+
21
+ describe 'basic usage' do
22
+ def setup!(env = :development)
23
+ Conker.module_eval do
24
+ setup_config! env,
25
+ A_SECRET: api_credential(development: nil),
26
+ PORT: required_in_production(type: :integer, default: 42)
27
+ end
28
+ end
29
+
30
+ it 'exposes declared variables as top-level constants' do
31
+ setup!
32
+ ::A_SECRET.should be_nil
33
+ ::PORT.should == 42
34
+ end
35
+
36
+ it 'does not turn random environment variables into constants' do
37
+ ENV['PATH'].should_not be_empty
38
+ setup!
39
+ expect { ::PATH }.to raise_error(NameError, /PATH/)
40
+ end
41
+
42
+ it 'lets environment variables override environmental defaults' do
43
+ ENV['A_SECRET'] = 'beefbeefbeefbeef'
44
+ setup!
45
+ ::A_SECRET.should == 'beef' * 4
46
+ end
47
+
48
+ it 'throws useful errors if required variables are missing' do
49
+ ENV['A_SECRET'].should be_nil
50
+ ENV['PORT'] = '42'
51
+ expect { setup! :production }.to raise_error(/A_SECRET/)
52
+ end
53
+ end
54
+
55
+
56
+ describe 'required variables' do
57
+ def setup!
58
+ env = @env # capture it for block scope
59
+ Conker.module_eval do
60
+ setup_config! env,
61
+ APPNAME: optional(default: 'conker'),
62
+ PORT: required_in_production(type: :integer, default: 3000)
63
+ end
64
+ end
65
+
66
+ describe 'in development' do
67
+ before { @env = :development }
68
+
69
+ it 'allows optional variables to be missing' do
70
+ ENV['APPNAME'].should be_nil
71
+ ENV['PORT'] = '80'
72
+ expect { setup! }.not_to raise_error
73
+ end
74
+
75
+ it 'allows required_in_production variables to be missing' do
76
+ ENV['APPNAME'] = 'widget'
77
+ ENV['PORT'].should be_nil
78
+ expect { setup! }.not_to raise_error
79
+ end
80
+ end
81
+
82
+ describe 'in production' do
83
+ before { @env = :production }
84
+
85
+ it 'allows optional variables to be missing' do
86
+ ENV['APPNAME'].should be_nil
87
+ ENV['PORT'] = '80'
88
+ expect { setup! }.not_to raise_error
89
+ end
90
+
91
+ it 'throws a useful error if required_in_production variables are missing' do
92
+ ENV['APPNAME'] = 'widget'
93
+ ENV['PORT'].should be_nil
94
+ expect { setup! }.to raise_error(/PORT/)
95
+ end
96
+ end
97
+ end
98
+
99
+
100
+ describe 'defaults' do
101
+ def setup!(env = :development)
102
+ Conker.module_eval do
103
+ setup_config! env, NUM_THREADS: optional(type: :integer, test: 1, default: 2)
104
+ end
105
+ end
106
+
107
+ it 'uses the specified value if one is given' do
108
+ ENV['NUM_THREADS'] = '4'
109
+ setup!
110
+ NUM_THREADS.should == 4
111
+ end
112
+
113
+ it 'uses the default value if none is specified' do
114
+ ENV['NUM_THREADS'].should be_nil
115
+ setup!
116
+ NUM_THREADS.should == 2
117
+ end
118
+
119
+ it 'allows overriding defaults for specific environments' do
120
+ ENV['NUM_THREADS'].should be_nil
121
+ setup! :test
122
+ NUM_THREADS.should == 1
123
+ end
124
+ end
125
+
126
+
127
+ describe 'typed variables' do
128
+ describe 'boolean' do
129
+ def setup_sprocket_enabled!(value_string)
130
+ ENV['SPROCKET_ENABLED'] = value_string
131
+ Conker.module_eval do
132
+ setup_config! :development, :SPROCKET_ENABLED => optional(type: :boolean, default: false)
133
+ end
134
+ end
135
+
136
+ it 'parses "true"' do
137
+ setup_sprocket_enabled! 'true'
138
+ SPROCKET_ENABLED.should be_true
139
+ end
140
+
141
+ it 'parses "false"' do
142
+ setup_sprocket_enabled! 'false'
143
+ SPROCKET_ENABLED.should be_false
144
+ end
145
+
146
+ it 'accepts "1" as true' do
147
+ setup_sprocket_enabled! '1'
148
+ SPROCKET_ENABLED.should be_true
149
+ end
150
+
151
+ it 'accepts "0" as false' do
152
+ setup_sprocket_enabled! '0'
153
+ SPROCKET_ENABLED.should be_false
154
+ end
155
+ end
156
+
157
+ describe 'integer' do
158
+ def setup_num_threads!(value_string)
159
+ ENV['NUM_THREADS'] = value_string
160
+ Conker.module_eval do
161
+ setup_config! :development, :NUM_THREADS => optional(type: :integer, default: 2)
162
+ end
163
+ end
164
+
165
+ it 'parses "42"' do
166
+ setup_num_threads! '42'
167
+ NUM_THREADS.should == 42
168
+ end
169
+
170
+ it 'throws an error if the value is not an integer' do
171
+ expect { setup_num_threads! 'one hundred' }.to raise_error(/one hundred/)
172
+ end
173
+ end
174
+
175
+ describe 'float' do
176
+ def setup_log_probability!(value_string)
177
+ ENV['LOG_PROBABILITY'] = value_string
178
+ Conker.module_eval do
179
+ setup_config! :development, :LOG_PROBABILITY => optional(type: :float, default: 1.0)
180
+ end
181
+ end
182
+
183
+ it 'parses "0.5"' do
184
+ setup_log_probability! '0.5'
185
+ LOG_PROBABILITY.should == 0.5
186
+ end
187
+
188
+ it 'throws an error if the value is not a float' do
189
+ expect { setup_log_probability! 'zero' }.to raise_error(/zero/)
190
+ end
191
+ end
192
+
193
+ describe 'url' do
194
+ def setup_api_url!(value_string)
195
+ ENV['API_URL'] = value_string
196
+ Conker.module_eval do
197
+ setup_config! :development, :API_URL => optional(type: :url, default: 'http://example.com/foo')
198
+ end
199
+ end
200
+
201
+ it 'exposes a URI object, not a string' do
202
+ setup_api_url! 'http://localhost:4321/'
203
+ API_URL.host.should == 'localhost'
204
+ end
205
+
206
+ it 'parses the default value too' do
207
+ setup_api_url! nil
208
+ API_URL.host.should == 'example.com'
209
+ end
210
+ end
211
+
212
+ describe 'addressable' do
213
+ def setup_api_url!(value_string)
214
+ ENV['API_URL'] = value_string
215
+ Conker.module_eval do
216
+ setup_config! :development, :API_URL => optional(type: :addressable, default: 'http://example.com/foo')
217
+ end
218
+ end
219
+
220
+ it 'exposes an Addressable::URI object, not a string' do
221
+ setup_api_url! 'http://localhost:4321/'
222
+ API_URL.host.should == 'localhost'
223
+ end
224
+
225
+ it 'parses the default value too' do
226
+ setup_api_url! nil
227
+ API_URL.host.should == 'example.com'
228
+ end
229
+ end
230
+
231
+ describe 'timestamp' do
232
+ xit 'seems to have bit rotted'
233
+ end
234
+ end
235
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: conker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.11.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sam Stokes
9
+ - Conrad Irwin
10
+ - Lee Mallabone
11
+ - Martin Kleppmann
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+ date: 2013-01-09 00:00:00.000000000 Z
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: activesupport
19
+ requirement: !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ! '>='
23
+ - !ruby/object:Gem::Version
24
+ version: '0'
25
+ type: :runtime
26
+ prerelease: false
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: addressable
35
+ requirement: !ruby/object:Gem::Requirement
36
+ none: false
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ type: :runtime
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ - !ruby/object:Gem::Dependency
50
+ name: rspec
51
+ requirement: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ type: :development
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ description: Configuration library.
66
+ email: supportive@rapportive.com
67
+ executables: []
68
+ extensions: []
69
+ extra_rdoc_files: []
70
+ files:
71
+ - Gemfile
72
+ - Gemfile.lock
73
+ - LICENSE.MIT
74
+ - conker.gemspec
75
+ - lib/conker.rb
76
+ - spec/lib/conker_spec.rb
77
+ homepage: https://github.com/rapportive/conker
78
+ licenses:
79
+ - MIT
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project:
98
+ rubygems_version: 1.8.19
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: Conker will conquer your config.
102
+ test_files: []
103
+ has_rdoc: