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 +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
|