conker 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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: