constellation 0.0.1

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/README.md ADDED
@@ -0,0 +1,113 @@
1
+ ## Constellation
2
+
3
+ Constellation is a powerful configuration system. It's great for
4
+ API client libraries and applications and anywhere else you need to
5
+ let your users set some configuration parameters.
6
+
7
+ ## Usage
8
+
9
+ ### Ruby Parameters
10
+
11
+ Start by creating a class and calling `acts_as_constellation`
12
+
13
+ ```ruby
14
+ class MyConfiguration
15
+ acts_as_constellation
16
+ end
17
+ ```
18
+
19
+ With just this, you have a basic Hash configuration. The only way to set
20
+ properties is to pass them in Ruby:
21
+
22
+ ```ruby
23
+ config = MyConfiguration.new(:foo => 'bar')
24
+ ...
25
+ config.foo # => "bar"
26
+ ```
27
+
28
+ ### `ENV`
29
+
30
+ To add support for `ENV` hash configuration, set `env_params`:
31
+
32
+ ```ruby
33
+ class MyConfiguration
34
+ self.env_params = { :foo => 'MY_FOO' }
35
+ end
36
+
37
+ ...
38
+ ENV['MY_FOO'] = 'bar'
39
+ ...
40
+ config = MyConfiguration.new
41
+ config.foo # => "bar"
42
+
43
+ ### Configuration Files
44
+
45
+ To add support for config files, set `config_file` to a path. The Constellation
46
+ will look up a config file in that location relative to two places ("base paths"):
47
+
48
+ * the current working directory (`Dir.pwd`)
49
+ * the user's home directory (`ENV['HOME']`)
50
+
51
+ ```ruby
52
+ class MyConfiguration
53
+ self.config_file = 'my/config.yml'
54
+ end
55
+ ```
56
+
57
+ If `./my/config.yml` contains the following
58
+
59
+ ```yml
60
+ ---
61
+ foo: bar
62
+ ```
63
+
64
+ then `MyConfiguration.new.foo` will return `"bar"`.
65
+
66
+ ### From Gems
67
+
68
+ If you set `config_file` to a path *and* set `load_from_gems` to `true`, then
69
+ Constellation will add all of the loaded gem directories to the list of base paths.
70
+
71
+ ```ruby
72
+ class MyConfiguration
73
+ self.config_file = 'my/config.yml'
74
+ self.load_from_gems = true
75
+ end
76
+ ```
77
+
78
+ ## Order of Precedence
79
+
80
+ Constellation will load parameters in the order listed above. Given
81
+
82
+ ```ruby
83
+ class MyConfiguration
84
+ self.env_params = { :foo => 'MY_FOO' }
85
+ self.config_file = 'my/config.yml'
86
+ self.load_from_gems = true
87
+ end
88
+ ```
89
+
90
+ Constellation will first look in a Hash passed in, then in `ENV`, then in
91
+ `./my/config.yml`, then in `~/my/config.yml`, then in `GEM_PATH/my/config.yml` for
92
+ each loaded gem.
93
+
94
+ ## File Parsers
95
+
96
+ Constellation will do the right thing if `config_file` ends with `.yml`, `.yaml`, or
97
+ `.json`. If it's a different format, you'll have to tell Constellation how to parse it
98
+ by redefining `parse_config_file`:
99
+
100
+ ```ruby
101
+ class MyConfiguration
102
+ self.config_file = '.myrc'
103
+
104
+ def parse_config_file(contents)
105
+ result = {}
106
+ contents.split("\n").each do |line|
107
+ k, v = line.split(/:\s*/)
108
+ result[k] = v
109
+ end
110
+ result
111
+ end
112
+ end
113
+ ```
@@ -0,0 +1,110 @@
1
+ module Constellation
2
+
3
+ class ParseError < StandardError
4
+ def initialize(file)
5
+ super("Could not parse #{file}. Try overriding #parse_config_file")
6
+ end
7
+ end
8
+
9
+ module ActsAs
10
+ def acts_as_constellation
11
+ extend Constellation::ClassMethods
12
+ self.env_params = {}
13
+ include Constellation::InstanceMethods
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ attr_accessor :env_params, :config_file, :load_from_gems
19
+ end
20
+
21
+ module InstanceMethods
22
+
23
+ def initialize(data = nil)
24
+ @data = {}
25
+ reverse_merge(data || {})
26
+ fall_back_on_env
27
+ fall_back_on_file(Dir.pwd)
28
+ fall_back_on_file(ENV['HOME'])
29
+ fall_back_on_gems
30
+ end
31
+
32
+ def method_missing(name, *arguments, &block)
33
+ if data.has_key?(name.to_s)
34
+ data[name.to_s]
35
+ else
36
+ super
37
+ end
38
+ end
39
+
40
+ def respond_to?(name)
41
+ data.has_key?(name.to_s) || super
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :data
47
+
48
+ def fall_back_on_env
49
+ env_values = self.class.env_params.inject({}) do |sum, (prop, env_prop)|
50
+ sum[prop] = ENV[env_prop] if ENV.has_key?(env_prop)
51
+ sum
52
+ end
53
+ reverse_merge(env_values)
54
+ end
55
+
56
+ def fall_back_on_file(dir)
57
+ return if relative_config_file.nil?
58
+ f = File.expand_path(relative_config_file, dir)
59
+ cfg = load_config_file(f)
60
+ return unless cfg.respond_to?(:each) && cfg.respond_to?(:[])
61
+ reverse_merge(cfg)
62
+ end
63
+
64
+ def fall_back_on_gems
65
+ return unless self.class.load_from_gems
66
+ gem_paths.each { |p| fall_back_on_file(p) }
67
+ end
68
+
69
+ def reverse_merge(hash)
70
+ hash.each do |prop, value|
71
+ data[prop.to_s] ||= value
72
+ end
73
+ end
74
+
75
+ def relative_config_file
76
+ self.class.config_file
77
+ end
78
+
79
+ def load_config_file(full_path)
80
+ return unless File.exists?(full_path)
81
+
82
+ contents = File.read(full_path)
83
+
84
+ parsed = parse_config_file(contents)
85
+ return parsed if parsed
86
+
87
+ case full_path
88
+ when /\.ya?ml$/
89
+ require 'yaml'
90
+ YAML.load(contents)
91
+ when /\.json$/
92
+ require 'multi_json'
93
+ MultiJson.decode(File.read(full_path))
94
+ else
95
+ raise Constellation::ParseError.new(full_path)
96
+ end
97
+ end
98
+
99
+ def parse_config_file(contents)
100
+ end
101
+
102
+ def gem_paths
103
+ Gem.loaded_specs.values.map(&:gem_dir)
104
+ end
105
+
106
+ end
107
+
108
+ end
109
+
110
+ Class.send :include, Constellation::ActsAs
@@ -0,0 +1,115 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ describe Constellation do
6
+
7
+ let(:load_from_gems) { false }
8
+ let(:path) { 'config/chomper.yml' }
9
+ let(:home) { ENV['HOME'] }
10
+
11
+ let(:config_class) do
12
+ Class.new.tap do |c|
13
+ c.acts_as_constellation
14
+ c.env_params = { foo: 'MY_FOO', bar: 'MY_BAR' }
15
+ c.config_file = path
16
+ c.load_from_gems = load_from_gems
17
+ end
18
+ end
19
+
20
+ def write(base_dir, content)
21
+ full_path = File.join(base_dir, path)
22
+ FileUtils.mkdir_p File.dirname(full_path)
23
+ File.open(full_path, 'w') { |f| f << content }
24
+ end
25
+
26
+ describe 'configuration sources' do
27
+ subject { config_class.new(foo: 'paramfoo') }
28
+
29
+ let(:cwd) { '/somewhere' }
30
+
31
+ before do
32
+ ENV['MY_FOO'] = 'envfoo'
33
+ ENV['MY_BAR'] = 'envbar'
34
+
35
+ write(cwd, YAML.dump({ 'foo' => 'dotfoo', 'bar' => 'dotbar', 'baz' => 'dotbaz' }))
36
+ write(home, YAML.dump({ 'foo' => 'homefoo', 'bar' => 'homebar', 'baz' => 'homebaz', 'qux' => 'homequx' }))
37
+ end
38
+
39
+ after { ENV.delete('MY_FOO'); ENV.delete('MY_BAR') }
40
+
41
+ it('prefers passed parameters') { Dir.chdir(cwd) { subject.foo.should == 'paramfoo' } }
42
+ it('falls back on ENV') { Dir.chdir(cwd) { subject.bar.should == 'envbar' } }
43
+ it('falls back on CWD/path') { Dir.chdir(cwd) { subject.baz.should == 'dotbaz' } }
44
+ it('falls back on ~/path') { Dir.chdir(cwd) { subject.qux.should == 'homequx' } }
45
+ end
46
+
47
+ describe 'load_from_gems' do
48
+ subject { config_class.new }
49
+
50
+ let(:gem_dir) { '/gems/some_gem' }
51
+ let(:gems) {
52
+ { 'some_gem' => stub('Configuration', gem_dir: gem_dir) }
53
+ }
54
+
55
+ before do
56
+ Gem.stubs(:loaded_specs).returns(gems)
57
+ write(home, YAML.dump({ 'foo' => 'homefoo' }))
58
+ write(gem_dir, YAML.dump({ 'foo' => 'gemfoo', 'bar' => 'gembar' }))
59
+ end
60
+
61
+ context('with load_from_gems off') do
62
+ it("doesn't load from gems") { subject.should_not respond_to(:bar) }
63
+ end
64
+
65
+ context('with load_from_gems on') do
66
+ let(:load_from_gems) { true }
67
+ it('prefers ~/path') { subject.foo.should == 'homefoo' }
68
+ it("falls back on [gems]/path") { subject.bar.should == 'gembar' }
69
+ end
70
+ end
71
+
72
+ describe 'file parsing' do
73
+ subject { config_class.new }
74
+
75
+ context 'with a .yml file' do
76
+ let(:path) { 'config.yml' }
77
+ before { write(home, YAML.dump({ 'foo' => 'yamlfoo' })) }
78
+ it('parses as YAML') { subject.foo.should == 'yamlfoo' }
79
+ end
80
+
81
+ context 'with a .json file' do
82
+ let(:path) { 'config.json' }
83
+ before { write(home, MultiJson.encode({ 'foo' => 'jsonfoo' })) }
84
+ it('parses as JSON') { subject.foo.should == 'jsonfoo' }
85
+ end
86
+
87
+ context 'with an unknown extension' do
88
+ let(:path) { 'config.xqx' }
89
+ before { write(home, "foo: xqxfoo") }
90
+ it('throws an exception') do
91
+ expect { subject }.to raise_error(Constellation::ParseError)
92
+ end
93
+ end
94
+
95
+ context 'with a custom parser' do
96
+ let(:path) { 'config.col' }
97
+ let(:subject) do
98
+ config_class.class_eval do
99
+ define_method :parse_config_file do |contents|
100
+ contents.split("\n").inject({}) do |sum, line|
101
+ k, v = line.split(':')
102
+ sum[k] = v
103
+ sum
104
+ end
105
+ end
106
+ end
107
+ config_class.new
108
+ end
109
+
110
+ before { write(home, "foo:colonfoo") }
111
+
112
+ it('parses with the given parse method') { subject.foo.should == 'colonfoo' }
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+
3
+ require "constellation"
4
+ require 'mocha'
5
+ require 'multi_json'
6
+ Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each{ |f| require f }
@@ -0,0 +1,6 @@
1
+ require 'fakefs/safe'
2
+
3
+ RSpec.configure do |c|
4
+ c.before(:each) { FakeFS.activate! }
5
+ c.after(:each) { FakeFS.deactivate! }
6
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: constellation
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - James A. Rosen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-08 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: multi_json
16
+ requirement: &70328840574360 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70328840574360
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70328840573860 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70328840573860
36
+ - !ruby/object:Gem::Dependency
37
+ name: fakefs
38
+ requirement: &70328840573400 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70328840573400
47
+ - !ruby/object:Gem::Dependency
48
+ name: mocha
49
+ requirement: &70328840572920 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70328840572920
58
+ description: Load configuration settings from ENV, dotfiles, and gems
59
+ email:
60
+ - james.a.rosen@gmail.com
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - lib/constellation.rb
66
+ - README.md
67
+ - spec/constellation_spec.rb
68
+ - spec/spec_helper.rb
69
+ - spec/support/fakefs.rb
70
+ homepage: ''
71
+ licenses: []
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project:
90
+ rubygems_version: 1.8.10
91
+ signing_key:
92
+ specification_version: 3
93
+ summary: Load configuration settings
94
+ test_files:
95
+ - spec/constellation_spec.rb
96
+ - spec/spec_helper.rb
97
+ - spec/support/fakefs.rb