constellation 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +113 -0
- data/lib/constellation.rb +110 -0
- data/spec/constellation_spec.rb +115 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/fakefs.rb +6 -0
- metadata +97 -0
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
|
data/spec/spec_helper.rb
ADDED
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
|