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.
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: []