hierarchical_config 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README.markdown +105 -0
  2. data/lib/hierarchical_config.rb +110 -0
  3. 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: []