hierarchical_config 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.markdown +105 -0
- data/lib/hierarchical_config.rb +110 -0
- metadata +49 -0
data/README.markdown
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
## What is it
|
2
|
+
|
3
|
+
HierarchicalConfig is a library that implments a strategy for configuring an application in a static, declarative, robust, and intuitive way
|
4
|
+
|
5
|
+
## Principles
|
6
|
+
|
7
|
+
1. You should not be able to change your config once it's loaded. That's
|
8
|
+
not configuration, that's globals.
|
9
|
+
2. You should be able to check in to source control a config file that
|
10
|
+
holds defaults and defines requirements.
|
11
|
+
3. You should be able to define defaults accross multiple environments
|
12
|
+
without repeating yourself.
|
13
|
+
4. You should be able to change configuration per box based on deploy
|
14
|
+
that does not affect the defaults or requirements that are checked in
|
15
|
+
to source control.
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
1. require 'hierarchical_config'
|
20
|
+
2. MY_APP_CONFIG = HierarchicalConfig.load_config( 'config_name', 'config_directory', 'environment_name' )
|
21
|
+
|
22
|
+
## How does it work
|
23
|
+
|
24
|
+
HierarchicalConfig loads a yaml file from the config directory. Each top
|
25
|
+
level name in the yaml file is either a default stanza (for one or more
|
26
|
+
environments,) or a specific environment.
|
27
|
+
|
28
|
+
It applies least specific rules first and proceeds to most specific. It
|
29
|
+
then reads an optional overrides file and does the same. If any REQUIRED
|
30
|
+
values have not been overriden by actual values, it raises an exception.
|
31
|
+
|
32
|
+
The object that it returns is a deeply nested tree of configuration that
|
33
|
+
can't be modified and raises exceptions if you ask for values that it
|
34
|
+
doesn't know about.
|
35
|
+
|
36
|
+
* No more having environments load without the mail server address
|
37
|
+
configured.
|
38
|
+
* No more silent failures and returning nil for something you thought
|
39
|
+
was configured.
|
40
|
+
* No more copy and pasting configuration accross environments.
|
41
|
+
* No more stupid YAML tricks to dry up your configuration.
|
42
|
+
* No more tricky config that changes based on runtime side effects.
|
43
|
+
* No more config that is hidden from developers and not in source
|
44
|
+
control (unless you specifically need to.)
|
45
|
+
|
46
|
+
## Example
|
47
|
+
|
48
|
+
### config/app.yml
|
49
|
+
|
50
|
+
defaults:
|
51
|
+
root:
|
52
|
+
child_a: 1
|
53
|
+
child_b: 2
|
54
|
+
child_c:
|
55
|
+
grandchild_a: 3
|
56
|
+
grandchild_b: 4
|
57
|
+
super_secret_password: !REQUIRED
|
58
|
+
|
59
|
+
defaults[development,test]:
|
60
|
+
super_secret_password: not_that_secret
|
61
|
+
|
62
|
+
development:
|
63
|
+
root:
|
64
|
+
child_b: 8
|
65
|
+
|
66
|
+
### config/app-overrides.yml
|
67
|
+
|
68
|
+
production:
|
69
|
+
super_secret_password: cant_trust_dev_with_this_we_symlink_this_file
|
70
|
+
|
71
|
+
## Results
|
72
|
+
|
73
|
+
### development
|
74
|
+
|
75
|
+
:root:
|
76
|
+
:child_a: 1
|
77
|
+
:child_b: 8
|
78
|
+
:child_c:
|
79
|
+
:grandchild_a: 3
|
80
|
+
:grandchild_b: 4
|
81
|
+
:super_secret_password: not_that_secret
|
82
|
+
|
83
|
+
### test
|
84
|
+
|
85
|
+
:root:
|
86
|
+
:child_a: 1
|
87
|
+
:child_b: 2
|
88
|
+
:child_c:
|
89
|
+
:grandchild_a: 3
|
90
|
+
:grandchild_b: 4
|
91
|
+
:super_secret_password: not_that_secret
|
92
|
+
|
93
|
+
### production
|
94
|
+
|
95
|
+
:root:
|
96
|
+
:child_a: 1
|
97
|
+
:child_b: 2
|
98
|
+
:child_c:
|
99
|
+
:grandchild_a: 3
|
100
|
+
:grandchild_b: 4
|
101
|
+
:super_secret_password: cant_trust_dev_with_this_we_symlink_this_file
|
102
|
+
|
103
|
+
### staging
|
104
|
+
|
105
|
+
RuntimeError: ["app.super_secret_password is REQUIRED for staging"]
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'yaml'
|
3
|
+
require 'erb'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
module HierarchicalConfig
|
7
|
+
REQUIRED = :REQUIRED
|
8
|
+
YAML.add_builtin_type( 'REQUIRED' ){ REQUIRED }
|
9
|
+
|
10
|
+
class OpenStruct < ::OpenStruct
|
11
|
+
def method_missing( mid, *args ) # :nodoc:
|
12
|
+
mname = mid.id2name
|
13
|
+
len = args.length
|
14
|
+
if mname.chomp!('=')
|
15
|
+
if len != 1
|
16
|
+
raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1)
|
17
|
+
end
|
18
|
+
modifiable[new_ostruct_member(mname)] = args[0]
|
19
|
+
elsif len == 0 && @table.key?( mid )
|
20
|
+
@table[mid]
|
21
|
+
else
|
22
|
+
raise NoMethodError, "undefined method `#{mname}' for #{self}", caller(1)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_hash
|
27
|
+
@table.inject({}) do |hash, key_value|
|
28
|
+
key, value = *key_value
|
29
|
+
hash[key] = value.respond_to?( :to_hash ) ? value.to_hash : value
|
30
|
+
hash
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def load_config( name, dir, environment )
|
37
|
+
primary_config_file = "#{dir}/#{name}.yml"
|
38
|
+
overrides_config_file = "#{dir}/#{name}-overrides.yml"
|
39
|
+
|
40
|
+
config_hash = load_hash_for_env( primary_config_file, environment )
|
41
|
+
|
42
|
+
if File.exists?( overrides_config_file )
|
43
|
+
overrides_config_hash = load_hash_for_env( overrides_config_file, environment )
|
44
|
+
config_hash = deep_merge( config_hash, overrides_config_hash )
|
45
|
+
end
|
46
|
+
|
47
|
+
config_hash, errors = lock_down_and_ostructify!( config_hash, name, environment )
|
48
|
+
|
49
|
+
raise errors.inspect unless errors.empty?
|
50
|
+
|
51
|
+
OpenStruct.new(config_hash).freeze
|
52
|
+
end
|
53
|
+
|
54
|
+
def load_hash_for_env( file, environment )
|
55
|
+
yaml_config = YAML::load(ERB.new(IO.read(file)).result)
|
56
|
+
|
57
|
+
ordered_stanza_labels = []
|
58
|
+
ordered_stanza_labels << 'defaults' if yaml_config.key? 'defaults'
|
59
|
+
ordered_stanza_labels += yaml_config.keys.grep(/^defaults\[.*#{environment}/).sort_by{ |a| a.count(',') }
|
60
|
+
ordered_stanza_labels << environment if yaml_config.key? environment
|
61
|
+
|
62
|
+
config = ordered_stanza_labels.inject({}) do |acc, label|
|
63
|
+
deep_merge( acc, yaml_config[label] )
|
64
|
+
end
|
65
|
+
|
66
|
+
rescue StandardError => e
|
67
|
+
raise <<-ERROR
|
68
|
+
Error loading config from file #{file}.
|
69
|
+
#{$!.inspect}
|
70
|
+
#{$@}
|
71
|
+
ERROR
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# merges two hashes with nested hashes if present
|
77
|
+
def deep_merge( hash1, hash2 )
|
78
|
+
hash1 = hash1.dup
|
79
|
+
( hash1.keys + hash2.keys ).each do | key |
|
80
|
+
if hash1.key?( key ) && hash2.key?( key ) &&
|
81
|
+
hash1[key].is_a?( Hash ) && hash2[key].is_a?( Hash )
|
82
|
+
hash1[key] = deep_merge( hash1[key], hash2[key] )
|
83
|
+
elsif hash2.key?( key )
|
84
|
+
hash1[key] = hash2[key]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
hash1
|
88
|
+
end
|
89
|
+
|
90
|
+
# Mutator method that does three things:
|
91
|
+
# * checks if any of the keys were required and not set. Upon finding
|
92
|
+
# it adds key to the error set
|
93
|
+
# * recursively sets open structs for deep hashes
|
94
|
+
# * recursively freezes config objects
|
95
|
+
def lock_down_and_ostructify!( hash, path, environment)
|
96
|
+
errors = []
|
97
|
+
hash.each do | key, value |
|
98
|
+
case
|
99
|
+
when value.respond_to?( :keys ) && value.respond_to?( :values )
|
100
|
+
child_hash, child_errors = lock_down_and_ostructify!( value, path + '.' + key, environment )
|
101
|
+
errors += child_errors
|
102
|
+
hash[key] = OpenStruct.new(child_hash).freeze
|
103
|
+
when value == REQUIRED
|
104
|
+
errors << "#{path}.#{key} is REQUIRED for #{environment}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
return hash, errors
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hierarchical_config
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- timgaleckas
|
9
|
+
- tjbladez
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2011-11-23 00:00:00.000000000Z
|
14
|
+
dependencies: []
|
15
|
+
description: Robust strategy for defining the configuration accross environements,
|
16
|
+
machines, clusters
|
17
|
+
email: tim@galeckas.com, nick@tjbladez.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- lib/hierarchical_config.rb
|
23
|
+
- README.markdown
|
24
|
+
homepage: http://github.com/timgaleckas/hierarchical_config
|
25
|
+
licenses: []
|
26
|
+
post_install_message:
|
27
|
+
rdoc_options: []
|
28
|
+
require_paths:
|
29
|
+
- lib
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ! '>='
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubyforge_project:
|
44
|
+
rubygems_version: 1.8.11
|
45
|
+
signing_key:
|
46
|
+
specification_version: 3
|
47
|
+
summary: Robust strategy for defining the configuration accross environements, machines,
|
48
|
+
clusters
|
49
|
+
test_files: []
|