chamber 0.0.1 → 0.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ced47025f0acaf5898cd70cb5d99f17f9c72820c
4
- data.tar.gz: d155b92fdd26e2a8328296b677485e0c0d01d77a
3
+ metadata.gz: 06481d8263ba75487270bf6bc366357d8ea45e81
4
+ data.tar.gz: 7095ff800b808dae4ba3470fca3cdcab26072ec1
5
5
  SHA512:
6
- metadata.gz: 950b798ef819faf34cb6319f1f08dd75db210aee8f77de133ed474a178c92c79e954267c2ea96fdb70ee9563863ce76bfec1fff8d9b3de6262f6e2c913f0b866
7
- data.tar.gz: 936ea0a71f1f32b4abc36bc0a709bd3770da3f6fda07b068c5c12be9b1c38decf123b570d0b681f7412a6769f13c94262cfbf11f11cf33cae48864ed69f8c283
6
+ metadata.gz: 5d2971516f0228a5fc17ce2b29375b5fb6fd7475918c9ad6df934727b0e15f7176979e16ce93cb9201cf8b69123485186673d0adcdb000680efb665de5298b0e
7
+ data.tar.gz: 6f38b25d90ade7a0ddc3a07ecb00a069d4f078994f8edab98567434a48767b6459c2079679132df941528252b50208f4b590fdec3d6c3422aa09e0f1a9721f91
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ chamber
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.0.0-p247
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 1.9.3
5
+ - 1.9.2
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Chamber
1
+ # Chamber [![Build Status](https://travis-ci.org/stevenhallen/chamber.png)](https://travis-ci.org/stevenhallen/chamber)
2
2
 
3
3
  Chamber lets you source your Settings from an arbitrary number of YAML files and
4
4
  provides a simple mechanism for overriding settings from the ENV, which is
@@ -20,7 +20,193 @@ Or install it yourself as:
20
20
 
21
21
  ## Usage
22
22
 
23
- TODO: Write usage instructions here
23
+ The following instructions are for a Rails app hosted on heroku with both
24
+ staging and production heroku environments.
25
+
26
+ Create a Settings class that extends Chamber in `app/models/settings.rb`:
27
+
28
+ ```ruby
29
+ class Settings
30
+ extend Chamber
31
+ end
32
+ ```
33
+
34
+ Create a `config/settings.yml` that has this structure:
35
+
36
+ ```yml
37
+ development:
38
+ some_setting: value for dev
39
+ some_password:
40
+ environment: ENV_VAR_NAME
41
+ test:
42
+ some_setting: value for test
43
+ some_password:
44
+ environment: ENV_VAR_NAME
45
+ staging:
46
+ some_setting: value for staging
47
+ some_password:
48
+ environment: ENV_VAR_NAME
49
+ production:
50
+ some_setting: value for production
51
+ some_password:
52
+ environment: ENV_VAR_NAME
53
+ ```
54
+
55
+ Call `source` in your Settings class:
56
+
57
+ ```ruby
58
+ class Settings
59
+ extend Chamber
60
+
61
+ source Rails.root.join('config', 'settings.yml'), namespace: Rails.env, override_from_environment: true
62
+ end
63
+ ```
64
+
65
+ Add environment-specific files for development and test to supply the values for
66
+ those environments. Make sure to add these to .gitignore.
67
+
68
+ Add another call to `source` for these files:
69
+
70
+ ```ruby
71
+ class Settings
72
+ extend Chamber
73
+
74
+ source Rails.root.join('config', 'settings.yml'), namespace: Rails.env, override_from_environment: true
75
+ source Rails.root.join('config', "credentials-#{Rails.env}.yml")
76
+ end
77
+ ```
78
+
79
+ Use `heroku config` to set the `ENV_VAR_NAME` value for the staging and
80
+ production remotes.
81
+
82
+ Now you can access your settings in your code from `Settings.instance` (assuming
83
+ you extended Chamber in a class named `Settings`).
84
+
85
+ In other words, given a configuration file like this:
86
+
87
+ ```yml
88
+ s3:
89
+ access_key_id: value
90
+ secret_access_key: value
91
+ bucket: value
92
+ ```
93
+
94
+ the corresponding Paperclip configuration would look like this:
95
+
96
+ ```ruby
97
+ Paperclip::Attachment.default_options.merge!(
98
+ storage: 's3',
99
+ s3_credentials: {
100
+ access_key_id: Settings.instance.s3.access_key_id,
101
+ secret_access_key: Settings.instance.s3.secret_access_key
102
+ },
103
+ bucket: Settings.instance.s3.bucket,
104
+ ...
105
+ ```
106
+
107
+ ## General Principles
108
+
109
+ ### Support best practices with sensitive information
110
+
111
+ Generally this is expressed in this overly simplified form: "Don't store
112
+ sensitive information in git." A better way to say it is that you should store
113
+ sensitive information separate from non-sensitive information. There's nothing
114
+ inherently wrong with storing sensitive information in git. You just wouldn't
115
+ want to store it in a public repository.
116
+
117
+ If it weren't for this concern, managing settings would be trivial, easily
118
+ solved use any number of approaches (e.g., [like using YAML and ERB in an
119
+ initializer](http://urgetopunt.com/rails/2009/09/12/yaml-config-with-erb.html).
120
+
121
+ I recommend adding a pattern like this to `.gitignore`:
122
+
123
+ ```
124
+ # Ignore the environment-specific files that contain the real credentials:
125
+ /config/credentials-*.yml
126
+
127
+ # But don't ignore the example file that shows the structure:
128
+ !/config/credentials-example.yml
129
+ ```
130
+
131
+ You would then use Chamber like this:
132
+
133
+ ```ruby
134
+ class Settings
135
+ extend Chamber
136
+ source Rails.root.join('config', "credentials-#{Rails.env}.yml")
137
+ end
138
+ ```
139
+
140
+ ### Support arbitrary organization
141
+
142
+ You should be able to organize your settings files however you like. You want
143
+ one big jumbo settings.yml? You can do that with Chamber. You want a distinct
144
+ settings file for each specific concern? You can do that too.
145
+
146
+ Chamber supports this by allowing:
147
+
148
+ * Arbitrary number of files:
149
+
150
+ ```ruby
151
+ class Settings
152
+ extend Chamber
153
+
154
+ source Rails.root.join('config', 'settings.yml')
155
+ source Rails.root.join('config', 'facebook.yml')
156
+ source Rails.root.join('config', 'twitter.yml')
157
+ source Rails.root.join('config', 'google-plus.yml')
158
+ end
159
+ ```
160
+
161
+ * Environment-specific filenames (e.g., `settings-#{Rails.env}.yml`)
162
+
163
+ * Namespaces:
164
+
165
+ ```ruby
166
+ class Settings
167
+ extend Chamber
168
+
169
+ source Rails.root.join('config', 'settings.yml'), namespace: Rails.env
170
+ end
171
+ ```
172
+
173
+ ### Support overriding setting values at runtime from ENV
174
+
175
+ [heroku](http://heroku.com) addons are configured from ENV. To support this,
176
+ Chamber's `source` method provides an `override_from_environment` option; e.g.,
177
+
178
+ ```ruby
179
+ class Settings
180
+ extend Chamber
181
+
182
+ source Rails.root.join('config', 'settings.yml'), override_from_environment: true
183
+ end
184
+ ```
185
+
186
+ ## Ideas
187
+
188
+ * Add a rake task for validating environments (do all environments have the same
189
+ settings?)
190
+
191
+ * Add a rake task for setting Heroku environment variables.
192
+
193
+ ## Alternatives
194
+
195
+ ### figaro
196
+
197
+ [figaro](https://github.com/laserlemon/figaro)
198
+
199
+ ### idkfa
200
+
201
+ [idkfa](https://github.com/bendyworks/idkfa)
202
+
203
+ ### settingslogic
204
+
205
+ [settingslogic](https://github.com/binarylogic/settingslogic)
206
+
207
+ ### Others?
208
+
209
+ I'd love to hear of other gems and/or approaches to settings!
24
210
 
25
211
  ## Contributing
26
212
 
data/Rakefile CHANGED
@@ -1 +1,6 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/chamber.gemspec CHANGED
@@ -24,6 +24,8 @@ CHAMBER
24
24
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
25
25
  spec.require_paths = ["lib"]
26
26
 
27
+ spec.add_runtime_dependency "hashie", "~> 2.0"
28
+
27
29
  spec.add_development_dependency "bundler", "~> 1.3"
28
30
  spec.add_development_dependency "rake"
29
31
  spec.add_development_dependency "rspec", "~> 2.14"
data/lib/chamber.rb CHANGED
@@ -1,5 +1,84 @@
1
- require "chamber/version"
1
+ require 'erb'
2
+ require 'hashie'
3
+ require 'yaml'
4
+
5
+ require 'chamber/version'
2
6
 
3
7
  module Chamber
4
- # Your code goes here...
8
+ class ChamberInvalidOptionError < ArgumentError; end
9
+
10
+ def source(filename, options={})
11
+ assert_valid_keys(options)
12
+
13
+ add_source(filename, options)
14
+ end
15
+
16
+ def load!
17
+ sources.each do |source|
18
+ filename, options = source
19
+
20
+ load_source!(filename, options)
21
+ end
22
+ end
23
+
24
+ def clear!
25
+ @chamber_instance = nil
26
+ @chamber_sources = nil
27
+ end
28
+
29
+ def reload!
30
+ @chamber_instance = nil
31
+ load!
32
+ end
33
+
34
+ def instance
35
+ @chamber_instance ||= Hashie::Mash.new
36
+ end
37
+
38
+ private
39
+
40
+ def assert_valid_keys(options)
41
+ unknown_keys = options.keys - [:namespace, :override_from_environment]
42
+
43
+ raise(ChamberInvalidOptionError, options) unless unknown_keys.empty?
44
+ end
45
+
46
+ def sources
47
+ @chamber_sources ||= []
48
+ end
49
+
50
+ def add_source(filename, options)
51
+ sources << [filename, options]
52
+ end
53
+
54
+ def load_source!(filename, options)
55
+ return unless File.exists?(filename)
56
+
57
+ hash = hash_from_source(filename, options[:namespace])
58
+ if options[:override_from_environment]
59
+ override_from_environment!(hash)
60
+ end
61
+
62
+ instance.deep_merge!(hash)
63
+ end
64
+
65
+ def hash_from_source(filename, namespace)
66
+ contents = open(filename).read
67
+ hash = YAML.load(ERB.new(contents).result).to_hash || {}
68
+ hash = Hashie::Mash.new(hash)
69
+
70
+ namespace ? hash.fetch(namespace) : hash
71
+ end
72
+
73
+ def override_from_environment!(hash)
74
+ hash.each_pair do |key, value|
75
+ next unless value.is_a?(Hash)
76
+
77
+ if value.environment
78
+ hash[key] = ENV[value.environment]
79
+ else
80
+ override_from_environment!(value)
81
+ end
82
+ end
83
+ end
5
84
  end
@@ -1,3 +1,3 @@
1
1
  module Chamber
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -0,0 +1,224 @@
1
+ require 'spec_helper'
2
+
3
+ require 'tempfile'
4
+
5
+ class Settings
6
+ extend Chamber
7
+ end
8
+
9
+ describe Chamber do
10
+ before do
11
+ Settings.clear!
12
+ end
13
+
14
+ describe '.source' do
15
+ context 'when an invalid option is specified' do
16
+ let(:options) do
17
+ { foo: 'bar' }
18
+ end
19
+
20
+ it 'raises ChamberInvalidOptionError' do
21
+ expect { Settings.source('filename', options) }.to raise_error(Chamber::ChamberInvalidOptionError)
22
+ end
23
+ end
24
+
25
+ context 'when no options are specified' do
26
+ it 'does not raise an error' do
27
+ expect { Settings.source('filename') }.not_to raise_error
28
+ end
29
+ end
30
+
31
+ context 'when valid options are specified' do
32
+ context 'and options only contains :namespace' do
33
+ let(:options) do
34
+ { namespace: 'bar' }
35
+ end
36
+
37
+ it 'does not raise an error' do
38
+ expect { Settings.source('filename', options) }.not_to raise_error
39
+ end
40
+ end
41
+
42
+ context 'and options only contains :override_from_environment' do
43
+ let(:options) do
44
+ { override_from_environment: 'bar' }
45
+ end
46
+
47
+ it 'does not raise an error' do
48
+ expect { Settings.source('filename', options) }.not_to raise_error
49
+ end
50
+ end
51
+
52
+ context 'and options contains both :namespace and :override_from_environment' do
53
+ let(:options) do
54
+ {
55
+ namespace: 'bar',
56
+ override_from_environment: 'bar'
57
+ }
58
+ end
59
+
60
+ it 'does not raise an error' do
61
+ expect { Settings.source('filename', options) }.not_to raise_error
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ describe '.load!' do
68
+ context 'when a non-existent file is specified' do
69
+ let(:file) { Tempfile.new('test') }
70
+ let!(:filename) { file.path }
71
+
72
+ before do
73
+ file.close
74
+ file.unlink
75
+ expect(File.exists?(filename)).to be_false
76
+ Settings.source filename
77
+ end
78
+
79
+ it 'does not raise an error' do
80
+ expect { Settings.load! }.not_to raise_error
81
+ end
82
+
83
+ it 'leaves the instance empty' do
84
+ Settings.load!
85
+ expect(Settings.instance).to be_empty
86
+ end
87
+ end
88
+
89
+ context 'when an existing file is specified' do
90
+ let(:file) { Tempfile.new('test') }
91
+ let(:filename) { file.path }
92
+ let(:content) do
93
+ <<-CONTENT
94
+ secret:
95
+ environment: CHAMBER_TEST
96
+ development:
97
+ foo: bar dev
98
+ test:
99
+ foo: bar test
100
+ CONTENT
101
+ end
102
+
103
+ before do
104
+ file.write(content)
105
+ file.close
106
+ end
107
+
108
+ after do
109
+ file.unlink
110
+ end
111
+
112
+ context 'and no options are specified' do
113
+ before { Settings.source(filename) }
114
+
115
+ let(:expected) do
116
+ {
117
+ 'secret' => {
118
+ 'environment' => 'CHAMBER_TEST'
119
+ },
120
+ 'development' => {
121
+ 'foo' => 'bar dev'
122
+ },
123
+ 'test' => {
124
+ 'foo' => 'bar test'
125
+ }
126
+ }
127
+ end
128
+
129
+ it 'loads all settings' do
130
+ Settings.load!
131
+ expect(Settings.instance.to_hash).to eq expected
132
+ end
133
+ end
134
+
135
+ context 'and the :namespace option is specified' do
136
+ before { Settings.source(filename, namespace: namespace) }
137
+
138
+ context 'and it is valid' do
139
+ let(:namespace) { 'development' }
140
+ let(:expected) do
141
+ {
142
+ 'foo' => 'bar dev'
143
+ }
144
+ end
145
+
146
+ it 'loads settings for the specified namespace' do
147
+ Settings.load!
148
+ expect(Settings.instance.to_hash).to eq expected
149
+ end
150
+ end
151
+
152
+ context 'and it is not valid' do
153
+ let(:namespace) { 'staging' }
154
+
155
+ it 'raises a KeyError' do
156
+ expect { Settings.load! }.to raise_error(KeyError)
157
+ end
158
+ end
159
+ end
160
+
161
+ context 'and the :override_from_environment option is specified' do
162
+ before { Settings.source(filename, override_from_environment: true) }
163
+
164
+ context 'and the environment variable is present' do
165
+ before { ENV['CHAMBER_TEST'] = 'value' }
166
+
167
+ it 'overrides the settings from the environment' do
168
+ Settings.load!
169
+ expect(Settings.instance.secret).to eq 'value'
170
+ end
171
+ end
172
+
173
+ context 'and the environment variable is not present' do
174
+ before { ENV.delete('CHAMBER_TEST') }
175
+
176
+ it 'sets the value to nil' do
177
+ Settings.load!
178
+ expect(Settings.instance.secret).to be_nil
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ describe '.reload!' do
186
+ context 'when a filename is changed after it is sourced and loaded' do
187
+ let(:file) { Tempfile.new('test') }
188
+ let!(:filename) { file.path }
189
+ let(:content) do
190
+ <<-CONTENT
191
+ initial: value
192
+ CONTENT
193
+ end
194
+ let(:modified) do
195
+ <<-MODIFIED
196
+ modified: changed
197
+ MODIFIED
198
+ end
199
+
200
+ before do
201
+ file.write(content)
202
+ file.close
203
+ Settings.source(filename)
204
+ Settings.load!
205
+ end
206
+
207
+ after do
208
+ file.unlink
209
+ end
210
+
211
+ it 'reloads the settings' do
212
+ File.open(filename, 'w') { |writer| writer.write(modified) }
213
+
214
+ expect { Settings.reload! }.to change { Settings.instance.to_hash }.from({ 'initial' => 'value' }).to({ 'modified' => 'changed' })
215
+ end
216
+ end
217
+ end
218
+
219
+ describe '.instance' do
220
+ it 'is a Hashie::Mash' do
221
+ expect(Settings.instance).to be_a(Hashie::Mash)
222
+ end
223
+ end
224
+ end
data/spec/spec_helper.rb CHANGED
@@ -2,3 +2,11 @@ require 'simplecov'
2
2
  SimpleCov.start
3
3
 
4
4
  require 'rspec'
5
+ require 'chamber'
6
+
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.filter_run focused: true
10
+ config.alias_example_to :fit, focused: true
11
+ config.run_all_when_everything_filtered = true
12
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chamber
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - stevenhallen
@@ -9,8 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-10-29 00:00:00.000000000 Z
12
+ date: 2013-11-15 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: hashie
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ~>
19
+ - !ruby/object:Gem::Version
20
+ version: '2.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ version: '2.0'
14
28
  - !ruby/object:Gem::Dependency
15
29
  name: bundler
16
30
  requirement: !ruby/object:Gem::Requirement
@@ -79,6 +93,9 @@ extensions: []
79
93
  extra_rdoc_files: []
80
94
  files:
81
95
  - .gitignore
96
+ - .ruby-gemset
97
+ - .ruby-version
98
+ - .travis.yml
82
99
  - Gemfile
83
100
  - LICENSE.txt
84
101
  - README.md
@@ -86,6 +103,7 @@ files:
86
103
  - chamber.gemspec
87
104
  - lib/chamber.rb
88
105
  - lib/chamber/version.rb
106
+ - spec/lib/chamber_spec.rb
89
107
  - spec/spec_helper.rb
90
108
  homepage: http://github.com/stevenhallen/chamber
91
109
  licenses:
@@ -107,9 +125,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
125
  version: '0'
108
126
  requirements: []
109
127
  rubyforge_project:
110
- rubygems_version: 2.1.10
128
+ rubygems_version: 2.0.3
111
129
  signing_key:
112
130
  specification_version: 4
113
131
  summary: Heroku-friendly Settings
114
132
  test_files:
133
+ - spec/lib/chamber_spec.rb
115
134
  - spec/spec_helper.rb
135
+ has_rdoc: