constellation 0.0.1

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