rconfig 0.3.2

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/lib/rconfig.rb ADDED
@@ -0,0 +1,39 @@
1
+ #--
2
+ # Copyright (c) 2009 Rahmal Conda <rahmal@gmail.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ $:.unshift File.dirname(__FILE__)
25
+
26
+ require 'rubygems'
27
+ require 'active_support'
28
+ require 'active_support/core_ext'
29
+ require 'active_support/core_ext/hash/conversions'
30
+ require 'active_support/core_ext/hash/indifferent_access'
31
+
32
+ require 'rconfig/core_ext'
33
+ require 'rconfig/config_hash'
34
+ require 'rconfig/properties_file_parser'
35
+ require 'rconfig/rconfig'
36
+
37
+ # Create global reference to RConfig instance
38
+ $config = RConfig.instance
39
+
@@ -0,0 +1,105 @@
1
+ #++
2
+ # Copyright (c) 2009 Rahmal Conda <rahmal@gmail.com>
3
+ #
4
+ # ConfigHash is a special class, derived from HashWithIndifferentAccess.
5
+ # It was specifically created for handling config data or creating mock
6
+ # objects from yaml files. It provides a dotted notation for accessing
7
+ # embedded hash values, similar to the way one might traverse a object tree.
8
+ #--
9
+ class ConfigHash < HashWithIndifferentAccess
10
+
11
+ # HashWithIndifferentAccess#dup always returns HashWithIndifferentAccess!
12
+ def dup
13
+ self.class.new(self)
14
+ end
15
+
16
+ ##
17
+ # Dotted notation can be used with arguments (useful for creating mock objects)
18
+ # in the YAML file the method name is a key, argument(s) form a nested key,
19
+ # so that the correct value is retrieved and returned.
20
+ #
21
+ # For example loading to variable foo a yaml file that looks like:
22
+ # customer:
23
+ # id: 12345678
24
+ # verified:
25
+ # phone: verified
26
+ # :address: info_not_available
27
+ # ? [name, employer]
28
+ # : not_verified
29
+ #
30
+ # Allows the following calls:
31
+ # foo.customer.id => 12345678
32
+ # foo.customer.verified.phone => verified
33
+ # foo.customer.verified("phone") => verified
34
+ # foo.customer.verified(:address) => info_not_available
35
+ # foo.customer.verified("name", "employer") => not_verified
36
+ #
37
+ # Note that :address is specified as a symbol, where phone is just a string.
38
+ # Depending on what kind of parameter the method being mocked out is going
39
+ # to be called with, define in the YAML file either a string or a symbol.
40
+ # This also works inside the composite array keys.
41
+ def method_missing(method, *args)
42
+ method = method.to_s
43
+ # STDERR.puts "CHF#method_missing(#{method.inspect}, #{args.inspect}) on #{self.inspect}:#{self.class}" if method == 'dup'
44
+ value = self[method]
45
+ case args.size
46
+ when 0:
47
+ # e.g.: RConfig.application.method
48
+ ;
49
+ when 1:
50
+ # e.g.: RConfig.application.method(one_arg)
51
+ value = value.send(args[0])
52
+ else
53
+ # e.g.: RConfig.application.method(arg_one, args_two, ...)
54
+ value = value[args]
55
+ end
56
+ # value = convert_value(value)
57
+ value
58
+ end
59
+
60
+ ##
61
+ # Why the &*#^@*^&$ isn't HashWithIndifferentAccess actually doing this?
62
+ def [](key)
63
+ key = key.to_s if key.kind_of?(Symbol)
64
+ super(key)
65
+ end
66
+
67
+ # HashWithIndifferentAccess#default is broken!
68
+ define_method(:default_Hash, Hash.instance_method(:default))
69
+
70
+ ##
71
+ # Allow hash.default => hash['default']
72
+ # without breaking Hash's usage of default(key)
73
+ @@no_key = [ :no_key ] # magically unique value.
74
+ def default(key = @@no_key)
75
+ key = key.to_s if key.is_a?(Symbol)
76
+ key == @@no_key ? self['default'] : default_Hash(key == @@no_key ? nil : key)
77
+ end
78
+
79
+ ##
80
+ # HashWithIndifferentAccess#update is broken!
81
+ # Hash#update returns self,
82
+ # BUT,
83
+ # HashWithIndifferentAccess#update does not!
84
+ #
85
+ # { :a => 1 }.update({ :b => 2, :c => 3 })
86
+ # => { :a => 1, :b => 2, :c => 3 }
87
+ #
88
+ # HashWithIndifferentAccess.new({ :a => 1 }).update({ :b => 2, :c => 3 })
89
+ # => { :b => 2, :c => 3 } # WTF?
90
+ #
91
+ # Subclasses should *never* override methods and break their protocols!!!!
92
+ def update(hash)
93
+ super(hash)
94
+ self
95
+ end
96
+
97
+ ##
98
+ # Override WithIndifferentAccess#convert_value
99
+ # return instances of this class for Hash values.
100
+ def convert_value(value)
101
+ # STDERR.puts "convert_value(#{value.inspect}:#{value.class})"
102
+ value.class == Hash ? self.class.new(value).freeze : value
103
+ end
104
+
105
+ end # class ConfigHash
@@ -0,0 +1,6 @@
1
+ #++
2
+ # Copyright (c) 2009 Rahmal Conda <rahmal@gmail.com>
3
+ #
4
+ #
5
+ #--
6
+ require 'rconfig/core_ext/hash'
@@ -0,0 +1,104 @@
1
+
2
+ #++
3
+ # source: http://rubyforge.org/projects/facets/
4
+ # version: 1.7.46
5
+ # license: Ruby License
6
+ # NOTE: remove this method if the Facets gem is installed.
7
+ # BUG: weave is destructive to values in the source hash that are arrays!
8
+ # (this is acceptable for RConfig's use as the basis for weave!)
9
+ #
10
+ #--
11
+ class Hash
12
+
13
+ ##
14
+ # Weaves the contents of two hashes producing a new hash.
15
+ def weave(other_hash, dont_clobber = true)
16
+ return self unless other_hash
17
+ unless other_hash.kind_of?(Hash)
18
+ raise ArgumentError, "RConfig: (Hash#weave) expected <Hash>, but was <#{other_hash.class}>"
19
+ end
20
+
21
+ self_dup = self.dup # self.clone does not remove freeze!
22
+
23
+ other_hash.each { |key, other_node|
24
+
25
+ self_dup[key] =
26
+
27
+ if self_node = self_dup[key]
28
+
29
+ case self_node
30
+ when Hash
31
+
32
+ # hash1, hash2 => hash3 (recursive +)
33
+ if other_node.is_a?(Hash)
34
+
35
+ self_node.weave(other_node, dont_clobber)
36
+
37
+ # hash, array => error (Can't weave'em, must clobber.)
38
+ elsif other_node.is_a?(Array) && dont_clobber
39
+
40
+ raise(ArgumentError, "RConfig: (Hash#weave) Can't weave Hash and Array")
41
+
42
+ # hash, array => hash[key] = array
43
+ # hash, value => hash[key] = value
44
+ else
45
+ other_node
46
+ end
47
+
48
+ when Array
49
+
50
+ # array, hash => array << hash
51
+ # array1, array2 => array1 + array2
52
+ # array, value => array << value
53
+ if dont_clobber
54
+ case other_node
55
+ when Hash
56
+ self_node << other_node
57
+ when Array
58
+ self_node + other_node
59
+ else
60
+ self_node << other_node
61
+ end
62
+
63
+ # array, hash => hash
64
+ # array1, array2 => array2
65
+ # array, value => value
66
+ else
67
+ other_node
68
+ end
69
+
70
+ else
71
+
72
+ # value, array => array.unshift(value)
73
+ if other_node.is_a?(Array) && dont_clobber
74
+ other_node.unshift(self_node)
75
+
76
+ # value1, value2 => value2
77
+ else
78
+ other_node
79
+ end
80
+
81
+ end # case self_node
82
+
83
+ # Target hash didn't have a node matching the key,
84
+ # so just add it from the source hash.
85
+ # !self_dup.has_key?(key) => self_dup.add(key, other_node)
86
+ else
87
+ other_node
88
+ end
89
+
90
+ } # other_hash.each
91
+
92
+ self_dup # return new weaved hash
93
+ end
94
+
95
+ ##
96
+ # Same as self.weave(other_hash, dont_clobber) except that it weaves other hash
97
+ # to itself, rather than create a new hash.
98
+ def weave!(other_hash, dont_clobber = true)
99
+ weaved_hash = self.weave(other_hash, dont_clobber)
100
+ self.merge!(weaved_hash)
101
+ end
102
+
103
+ end # class Hash
104
+
@@ -0,0 +1,13 @@
1
+
2
+ #--
3
+ # Copyright (c) 2009 Rahmal Conda <rahmal@gmail.com>
4
+ #
5
+ # RConfig Exceptions
6
+ #++
7
+
8
+ # General error in config initialization or operation.
9
+ class ConfigError < StandardError; end
10
+
11
+ # Config path(s) are not set, don't exist, or Invalid in some manner
12
+ class InvalidConfigPathError < ConfigError; end
13
+
@@ -0,0 +1,80 @@
1
+
2
+ #++
3
+ # Copyright (c) 2009 Rahmal Conda <rahmal@gmail.com>
4
+ #
5
+ # This class parses key/value based properties files used for configuration.
6
+ # It is used by rconfig to import configuration files of the aforementioned
7
+ # format. Unlike yaml, and xml files it can only support two levels. First,
8
+ # it can have root level properties.
9
+ #
10
+ # Example:
11
+ # server_url=host.domain.com
12
+ # server_port=8080
13
+ #
14
+ # Secondly, it can have properties grouped into catagories. The group names
15
+ # must be specified within brackets like [ ... ]
16
+ #
17
+ # Example:
18
+ # [server]
19
+ # url=host.domain.com
20
+ # port=8080
21
+ #
22
+ #--
23
+ class PropertiesFileParser
24
+ include Singleton # Don't instantiate this class
25
+
26
+ ##
27
+ # Parse config file and import data into hash to be stored in config.
28
+ #
29
+ def self.parse config_file
30
+ validate(config_file)
31
+
32
+ config = {}
33
+
34
+ # The config is top down.. anything after a [group] gets added as part
35
+ # of that group until a new [group] is found.
36
+ group = nil
37
+ config_file.each do |line| # for each line in the config file
38
+ line.strip!
39
+ unless (/^\#/.match(line)) # skip comments (lines that state with '#')
40
+ if(/\s*=\s*/.match(line)) # if this line is a config property
41
+ key, val = line.split(/\s*=\s*/, 2) # parse key and value from line
42
+ key = key.chomp.strip
43
+ val = val.chomp.strip
44
+ if (val)
45
+ if val =~ /^['"](.*)['"]$/ # if the value is in quotes
46
+ value = $1 # strip out value from quotes
47
+ else
48
+ value = val # otherwise, leave as-is
49
+ end
50
+ else
51
+ value = '' # if value was nil, set it to empty string
52
+ end
53
+
54
+ if group # if this property is part of a group
55
+ config[group][key] = value # then add it to the group
56
+ else
57
+ config[key] = value # otherwise, add it to top-level config
58
+ end
59
+
60
+ # Group lines are parsed into arrays: %w('[', 'group', ']')
61
+ elsif(/^\[(.+)\]$/.match(line).to_a != []) # if this line is a config group
62
+ group = /^\[(.+)\]$/.match(line).to_a[1] # parse the group name from line
63
+ config[group] ||= {} # add group to top-level config
64
+ end
65
+ end
66
+ end
67
+ config # return config hash
68
+ end # def parse
69
+
70
+ ##
71
+ # Validate the config file. Check that the file exists and that it's readable.
72
+ # TODO: Create real error.
73
+ #
74
+ def self.validate config_file
75
+ raise 'Invalid config file name.' unless config_file
76
+ raise Errno::ENOENT, "#{config_file} does not exist" unless File.exist?(config_file.path)
77
+ raise Errno::EACCES, "#{config_file} is not readable" unless File.readable?(config_file.path)
78
+ end
79
+
80
+ end # class PropertiesFileParser
@@ -0,0 +1,871 @@
1
+
2
+ require 'socket'
3
+ require 'yaml'
4
+
5
+ require 'rconfig/properties_file_parser'
6
+ require 'rconfig/config_hash'
7
+ require 'rconfig/core_ext'
8
+ require 'rconfig/exceptions'
9
+
10
+ ##
11
+ #
12
+ # Copyright (c) 2009 Rahmal Conda <rahmal@gmail.com>
13
+ # -------------------------------------------------------------------
14
+ # The complete solution for Ruby Configuration Management. RConfig is a Ruby library that
15
+ # manages configuration within Ruby applications. It bridges the gap between yaml, xml, and
16
+ # key/value based properties files, by providing a centralized solution to handle application
17
+ # configuration from one location. It provides the simplicity of hash-based access, that
18
+ # Rubyists have come to know and love, supporting your configuration style of choice, while
19
+ # providing many new features, and an elegant API.
20
+ #
21
+ # -------------------------------------------------------------------
22
+ # * Simple, easy to install and use.
23
+ # * Supports yaml, xml, and properties files.
24
+ # * Yaml and xml files supprt infinite level of configuration grouping.
25
+ # * Intuitive dot-notation 'key chaining' argument access.
26
+ # * Simple well-known hash/array based argument access.
27
+ # * Implements multilevel caching to reduce disk access.
28
+ # * Short-hand access to 'global' application configuration, and shell environment.
29
+ # * Overlays multiple configuration files to support environment, host, and
30
+ # even locale-specific configuration.
31
+ #
32
+ # -------------------------------------------------------------------
33
+ # The overlay order of the config files is defined by SUFFIXES:
34
+ # * nil
35
+ # * _local
36
+ # * _config
37
+ # * _local_config
38
+ # * _{environment} (.i.e _development)
39
+ # * _{environment}_local (.i.e _development_local)
40
+ # * _{hostname} (.i.e _whiskey)
41
+ # * _{hostname}_config_local (.i.e _whiskey_config_local)
42
+ #
43
+ # -------------------------------------------------------------------
44
+ #
45
+ # Example:
46
+ #
47
+ # shell/console =>
48
+ # export LANG=en
49
+ #
50
+ # demo.yml =>
51
+ # server:
52
+ # address: host.domain.com
53
+ # port: 81
54
+ # ...
55
+ #
56
+ # application.properties =>
57
+ # debug_level = verbose
58
+ # ...
59
+ #
60
+ # demo.rb =>
61
+ # require 'rconfig'
62
+ # RConfig.config_paths = ['$HOME/config', '#{APP_ROOT}/config', '/demo/conf']
63
+ # RConfig.demo[:server][:port] => 81
64
+ # RConfig.demo.server.address => 'host.domain.com'
65
+ #
66
+ # RConfig[:debug_level] => 'verbose'
67
+ # RConfig[:lang] => 'en'
68
+ # ...
69
+ #
70
+ #--
71
+ class RConfig
72
+ include Singleton
73
+
74
+
75
+ # ENV TIER i.e. (development, integration, staging, or production)
76
+ # Defaults to RAILS_ENV if running in Rails, otherwise, it checks
77
+ # if ENV['TIER'] is present. If not, it assumes production.
78
+ unless defined? ENV_TIER
79
+ ENV_TIER = defined?(RAILS_ENV) ? RAILS_ENV : (ENV['TIER'] || 'production')
80
+ end
81
+
82
+ # The type of file used for config. Valid choices
83
+ # include (yml, yaml, xml, conf, config, properties)
84
+ # yml, yaml => yaml files, parsable by YAML library
85
+ # conf, properties => <key=value> based config files
86
+ # xml => self-explanatory
87
+ # Defaults to yml, if not specified.
88
+ YML_FILE_TYPES = [:yml, :yaml] unless defined? YML_FILE_TYPES
89
+ XML_FILE_TYPES = [:xml] unless defined? XML_FILE_TYPES
90
+ CNF_FILE_TYPES = [:cnf, :conf, :config, :properties] unless defined? CNF_FILE_TYPES
91
+ unless defined? CONFIG_FILE_TYPES
92
+ CONFIG_FILE_TYPES = YML_FILE_TYPES + XML_FILE_TYPES + CNF_FILE_TYPES
93
+ end
94
+
95
+ # Use CONFIG_HOSTNAME environment variable to
96
+ # test host-based configurations.
97
+ unless defined? HOSTNAME
98
+ HOSTNAME = ENV['CONFIG_HOSTNAME'] || Socket.gethostname
99
+ end
100
+
101
+ # Short hostname: remove all chars after first ".".
102
+ HOSTNAME_SHORT = HOSTNAME.sub(/\..*$/, '').freeze unless defined? HOSTNAME_SHORT
103
+
104
+ # This is an array of filename suffixes facilitates overriding
105
+ # configuration (i.e. 'services_local', 'services_development').
106
+ # These files get loaded in order of the array the last file
107
+ # loaded gets splatted on top of everything there.
108
+ # Ex. database_whiskey.yml overrides database_integration.yml
109
+ # overrides database.yml
110
+ SUFFIXES = [nil,
111
+ :local,
112
+ :config, :local_config,
113
+ ENV_TIER, [ENV_TIER, :local],
114
+ HOSTNAME_SHORT, [HOSTNAME_SHORT, :config_local],
115
+ HOSTNAME, [HOSTNAME, :config_local]
116
+ ] unless defined? SUFFIXES
117
+
118
+ ### Helper methods for white-box testing and debugging.
119
+
120
+ # Flag indicating whether or not to log errors and
121
+ # errors and application run-time information. It
122
+ # defaults to environment debug level setting, or
123
+ # false if the env variable is not set.
124
+ @@verbose = (ENV['DEBUG_LEVEL'] == 'verbose')
125
+
126
+ # Sets the flag indicating whether or not to log
127
+ # errors and application run-time information.
128
+ def self.verbose=(x)
129
+ @@verbose = x.nil? ? false : x;
130
+ end
131
+
132
+ # A hash of each file that has been loaded.
133
+ @@config_file_loaded = nil
134
+ def self.config_file_loaded=(x)
135
+ @@config_file_loaded = x
136
+ end
137
+ def self.config_file_loaded
138
+ @@config_file_loaded
139
+ end
140
+ ### End Helper methods
141
+
142
+ # The list of directories to search for configuration files.
143
+ @@config_paths = []
144
+
145
+ # Hash of suffixes for a given config name.
146
+ # @@suffixes['name'] vs @@suffix['name_GB']
147
+ @@suffixes = {}
148
+
149
+ # Hash of yaml file names and their respective contents,
150
+ # last modified time, and the last time it was loaded.
151
+ # @@cache[filename] = [yaml_contents, mod_time, time_loaded]
152
+ @@cache = {}
153
+
154
+ # Hash of config file base names and their existing filenames
155
+ # including the suffixes.
156
+ # @@cache_files['ldap'] = ['ldap.yml', 'ldap_local.yml', 'ldap_<hostname>.yml']
157
+ @@cache_files = {}
158
+
159
+ # Hash of config base name and the contents of all its respective
160
+ # files merged into hashes. This hash holds the data that is
161
+ # accessed when RConfig is called. This gets re-created each time
162
+ # the config files are loaded.
163
+ # @@cache_hash['ldap'] = config_hash
164
+ @@cache_hash = {}
165
+
166
+ # The hash holds the same info as @@cache_hash, but it is only
167
+ # loaded once. If reload is disabled, data will from this hash
168
+ # will always be passed back when RConfig is called.
169
+ @@cache_config_files = {} # Keep around incase reload_disabled.
170
+
171
+ # Hash of config base name and the last time it was checked for
172
+ # update.
173
+ # @@last_auto_check['ldap'] = Time.now
174
+ @@last_auto_check = {}
175
+
176
+ # Hash of callbacks Procs for when a particular config file has changed.
177
+ @@on_load = {}
178
+
179
+ # Flag variable indicating whether or not reload should be executed.
180
+ # Defaults to false.
181
+ @@reload_disabled = false
182
+
183
+ # Sets the flag indicating whether or not reload should be executed.
184
+ def self.allow_reload=(reload)
185
+ raise
186
+ @@reload_disabled = (not reload)
187
+ end
188
+
189
+ # Flag indicating whether or not reload should be executed.
190
+ def self.reload?
191
+ !@@reload_disabled
192
+ end
193
+
194
+ # The number of seconds between reloading of config files
195
+ # and automatic reload checks. Defaults to 5 minutes.
196
+ @@reload_interval = 300
197
+
198
+ # Sets the number of seconds between reloading of config files
199
+ # and automatic reload checks. Defaults to 5 minutes.
200
+ def self.reload_interval=(x)
201
+ @@reload_interval = (x || 300)
202
+ end
203
+
204
+ ##
205
+ # Flushes cached config data, so that it can be reloaded from disk.
206
+ # It is recommended that this should be used with caution, and any
207
+ # need to reload in a production setting should minimized or
208
+ # completely avoided if possible.
209
+ def self.reload(force = false)
210
+ if force || reload?
211
+ flush_cache
212
+ end
213
+ nil
214
+ end
215
+
216
+
217
+ ##
218
+ # Convenience method to initialize necessary fields including,
219
+ # config paths, overlay, reload_disabled, and verbose, all at
220
+ # one time.
221
+ def self.initialize(*args)
222
+ case args[0]
223
+ when Hash
224
+ params = args[0].symbolize_keys
225
+ paths = params[:paths]
226
+ ovrlay = params[:overlay]
227
+ reload = params[:reload]
228
+ verbos = params[:verbose]
229
+ else
230
+ paths, ovrlay, reload, verbos = *args
231
+ end
232
+ self.config_paths = paths
233
+ self.overlay = ovrlay
234
+ self.allow_reload = reload
235
+ self.verbose = verbos
236
+ end
237
+
238
+
239
+ ##
240
+ # Sets the list of directories to search for
241
+ # configuration files.
242
+ # The argument must be an array of strings representing
243
+ # the paths to the directories, or a string representing
244
+ # either a single path or a list of paths separated by
245
+ # either a colon (:) or a semi-colon (;).
246
+ # If reload is disabled, it can only be set once.
247
+ def self.config_paths=(paths)
248
+ return if @@reload_disabled && config_paths_set?
249
+ if paths.is_a? String
250
+ path_sep = (paths =~ /;/) ? ';' : ':'
251
+ paths = paths.split(/#{path_sep}+/)
252
+ end
253
+ unless paths.is_a? Array
254
+ raise ArgumentError,
255
+ "Path(s) must be a String or an Array [#{paths.inspect}]"
256
+ end
257
+ if paths.empty?
258
+ raise ArgumentError,
259
+ "Must provide at least one paths: [#{paths.inspect}]"
260
+ end
261
+ paths.all? do |dir|
262
+ dir = CONFIG_ROOT if dir == 'CONFIG_ROOT'
263
+ unless File.directory?(dir)
264
+ raise InvalidConfigPathError,
265
+ "This directory is invalid: [#{dir.inspect}]"
266
+ end
267
+ end
268
+ reload
269
+ @@config_paths = paths
270
+ end
271
+ class << self; alias_method :set_config_paths, :config_paths= end
272
+
273
+ ##
274
+ # Adds the specified path to the list of directories to search for
275
+ # configuration files.
276
+ # It only allows one path to be entered at a time.
277
+ # If reload is disabled, it can onle be set once.
278
+ def self.set_config_path path
279
+ return if @@reload_disabled && config_paths_set?
280
+ return unless path.is_a?(String) # only allow string argument
281
+ path_sep = (path =~ /;/) ? ';' : ':' # if string contains multiple paths
282
+ path = path.split(/#{path_sep}+/)[0] # only accept first one.
283
+
284
+ if @@config_paths.blank?
285
+ set_config_paths(path)
286
+ else
287
+ config_paths << path if File.directory?(path)
288
+ reload
289
+ @@config_paths
290
+ end
291
+ end
292
+ class << self; alias_method :add_config_path, :set_config_path end
293
+
294
+ ##
295
+ # Returns a list of directories to search for
296
+ # configuration files.
297
+ #
298
+ # Can be preset with config_paths=/set_config_path,
299
+ # controlled via ENV['CONFIG_PATH'],
300
+ # or defaulted to CONFIG_ROOT (assumming some sort of
301
+ # application initiation as in RAILS).
302
+ # Defaults to [ CONFIG_ROOT ].
303
+ #
304
+ # Examples:
305
+ # export CONFIG_PATH="$HOME/work/config:CONFIG_ROOT"
306
+ # CONFIG_ROOT = RAILS_ROOT + "/config" unless defined? CONFIG_ROOT
307
+ #
308
+ def self.config_paths
309
+ return @@config_paths unless @@config_paths.blank?
310
+
311
+ begin
312
+ config_paths = ENV['CONFIG_PATH']
313
+ rescue
314
+ verbose_log "Forget something? No config paths! ENV['CONFIG_PATH'] is not set.",
315
+ "Hint: Use config_paths= or set_config_path."
316
+ end
317
+
318
+ begin
319
+ config_paths = [CONFIG_ROOT]
320
+ rescue
321
+ verbose_log "Forget something? No config paths! CONFIG_ROOT is not set.",
322
+ "Hint: Use config_paths= or set_config_path."
323
+ end
324
+
325
+ if @@config_paths.blank?
326
+ raise InvalidConfigPathError,
327
+ "Forget something? No config paths!\n" +
328
+ "Niether ENV['CONFIG_PATH'] or CONFIG_ROOT is set.\n" +
329
+ "Hint: Use config_paths= or set_config_path."
330
+ end
331
+
332
+ @@config_paths
333
+ end
334
+
335
+ ##
336
+ # Indicates whether or not config_paths have been set.
337
+ # Returns true if @@config_paths has at least one directory.
338
+ def self.config_paths_set?
339
+ not @@config_paths.blank?
340
+ end
341
+
342
+
343
+ # Specifies an additional overlay suffix.
344
+ #
345
+ # E.g. 'gb' for UK locale.
346
+ #
347
+ # Defaults from ENV['CONFIG_OVERLAY'].
348
+ def self.overlay
349
+ @@overlay ||= (x = ENV['CONFIG_OVERLAY']) && x.dup.freeze
350
+ end
351
+
352
+ def self.overlay=(x)
353
+ flush_cache if @@overlay != x
354
+ @@overlay = x && x.dup.freeze
355
+ end
356
+
357
+
358
+ # Returns a list of suffixes to try for a given config name.
359
+ #
360
+ # A config name with an explicit overlay (e.g.: 'name_GB')
361
+ # overrides any current _overlay.
362
+ #
363
+ # This allows code to specifically ask for config overlays
364
+ # for a particular locale.
365
+ #
366
+ def self.suffixes(name)
367
+ name = name.to_s
368
+ @@suffixes[name] ||=
369
+ begin
370
+ ol = overlay
371
+ name_x = name.dup
372
+ if name_x.sub!(/_([A-Z]+)$/, '')
373
+ ol = $1
374
+ end
375
+ name_x.freeze
376
+ result = if ol
377
+ ol_ = ol.upcase
378
+ ol = ol.downcase
379
+ x = [ ]
380
+ SUFFIXES.each do | suffix |
381
+ # Standard, no overlay:
382
+ # e.g.: database_<suffix>.yml
383
+ x << suffix
384
+
385
+ # Overlay:
386
+ # e.g.: database_(US|GB)_<suffix>.yml
387
+ x << [ ol_, suffix ]
388
+ end
389
+ [ name_x, x.freeze ]
390
+ else
391
+ [ name.dup.freeze, SUFFIXES.freeze ]
392
+ end
393
+ result.freeze
394
+
395
+ verbose_log "suffixes(#{name}) => #{result.inspect}"
396
+
397
+ result
398
+ end
399
+ end
400
+
401
+
402
+ ##
403
+ # Get each config file's yaml hash for the given config name,
404
+ # to be merged later. Files will only be loaded if they have
405
+ # not been loaded before or the files have changed within the
406
+ # last five minutes, or force is explicitly set to true.
407
+ #
408
+ def self.load_config_files(name, force=false)
409
+ name = name.to_s # if name.is_a?(Symbol)
410
+
411
+ # Return last config file hash list loaded,
412
+ # if reload is disabled and files have already been loaded.
413
+ return @@cache_config_files[name] if
414
+ @@reload_disabled &&
415
+ @@cache_config_files[name]
416
+
417
+ now = Time.now
418
+
419
+ # Get array of all the existing files file the config name.
420
+ config_files = self.get_config_files(name)
421
+
422
+ verbose_log "load_config_files(#{name.inspect})"
423
+
424
+ # Get all the data from all yaml files into as hashes
425
+ hashes = config_files.collect do |f|
426
+ name, name_x, filename, ext, mtime = *f
427
+
428
+ # Get the cached file info the specific file, if
429
+ # it's been loaded before.
430
+ val, last_mtime, last_loaded = @@cache[filename]
431
+
432
+ verbose_log "f = #{f.inspect}",
433
+ "cache #{name_x} filename = #{filename.inspect}",
434
+ "cache #{name_x} val = #{val.inspect}",
435
+ "cache #{name_x} last_mtime = #{last_mtime.inspect}",
436
+ "cache #{name_x} last_loaded = #{last_loaded.inspect}"
437
+
438
+ # Load the file if its never been loaded or its been more than
439
+ # so many minutes since last load attempt. (default: 5 minutes)
440
+ if val.blank? || (now - last_loaded > @@reload_interval)
441
+ if force || val.blank? || mtime != last_mtime
442
+
443
+ verbose_log "mtime #{name.inspect} #{filename.inspect} " +
444
+ "changed #{mtime != last_mtime} : #{mtime.inspect} #{last_mtime.inspect}"
445
+
446
+ # Get contents from config file
447
+ File.open(filename) do |f|
448
+ verbose_log "RConfig: loading #{filename.inspect}"
449
+ val = parse_file(f, ext)
450
+ val = val[name] if ext == :xml # xml document must have root tag matching the file name.
451
+ verbose_log "RConfig: loaded #{filename.inspect} => #{val.inspect}"
452
+ (@@config_file_loaded ||= { })[name] = config_files
453
+ end
454
+
455
+ # Save cached config file contents, and mtime.
456
+ @@cache[filename] = [ val, mtime, now ]
457
+ verbose_log "cache[#{filename.inspect}] = #{@@cache[filename].inspect}"
458
+
459
+ # Flush merged hash cache.
460
+ @@cache_hash[name] = nil
461
+
462
+ # Config files changed or disappeared.
463
+ @@cache_files[name] = config_files
464
+
465
+ end # if val == nil || (now - last_loaded > @@reload_interval)
466
+ end # if force || val == nil || mtime != last_mtime
467
+
468
+ val
469
+ end
470
+ hashes.compact!
471
+
472
+ verbose_log "load_config_files(#{name.inspect}) => #{hashes.inspect}"
473
+
474
+ # Keep last loaded config files around in case @@reload_dsabled.
475
+ @@cache_config_files[name] = hashes #unless hashes.empty?
476
+
477
+ hashes
478
+ end
479
+
480
+ ##
481
+ # Parses file based on file type.
482
+ #
483
+ def self.parse_file(conf_file, ext)
484
+ hash = case ext
485
+ when *YML_FILE_TYPES
486
+ YAML::load(conf_file)
487
+ when *XML_FILE_TYPES
488
+ Hash.from_xml(conf_file)
489
+ when *CNF_FILE_TYPES
490
+ PropertiesFileParser.parse(conf_file)
491
+ else
492
+ #TODO: Raise real error
493
+ raise ConfigError, "Unknown File type:#{ext}"
494
+ end
495
+ hash.freeze
496
+ end
497
+
498
+ ##
499
+ # Returns a list of all relavant config files as specified
500
+ # by _suffixes list.
501
+ # Each element is an Array, containing:
502
+ # [ "the-top-level-config-name",
503
+ # "the-suffixed-config-name",
504
+ # "/the/absolute/path/to/yaml.yml",
505
+ # # The mtime of the yml file or nil, if it doesn't exist.
506
+ # ]
507
+ def self.get_config_files(name)
508
+ files = []
509
+
510
+ config_paths.reverse.each do | dir |
511
+ # splatting *suffix allows us to deal with multipart suffixes
512
+ name_no_overlay, suffixes = suffixes(name)
513
+ suffixes.map { | suffix | [ name_no_overlay, *suffix ].compact.join('_') }.each do | name_x |
514
+ CONFIG_FILE_TYPES.each do |ext|
515
+ filename = filename_for_name(name_x, dir, ext)
516
+ if File.exists?(filename)
517
+ files << [ name, name_x, filename, ext, File.stat(filename).mtime ]
518
+ end
519
+ end
520
+ end
521
+ end
522
+
523
+ verbose_log "get_config_files(#{name}) => #{files.select{|x| x[3]}.inspect}"
524
+
525
+ files
526
+ end
527
+
528
+ ##
529
+ # Return the config file information for the given config name.
530
+ #
531
+ def self.config_files(name)
532
+ @@cache_files[name] ||= get_config_files(name)
533
+ end
534
+
535
+ ##
536
+ # Returns whether or not the config for the given config name has changed
537
+ # since it was last loaded.
538
+ #
539
+ # Returns true if any files for config have changes since
540
+ # last load.
541
+ def self.config_changed?(name)
542
+ verbose_log "config_changed?(#{name.inspect})"
543
+ name = name.to_s # if name.is_a?(Symbol)
544
+ ! (@@cache_files[name] === get_config_files(name))
545
+ end
546
+
547
+ ##
548
+ # Get the merged config hash for the named file.
549
+ # Returns a cached indifferent access faker hash merged
550
+ # from all config files for a name.
551
+ #
552
+ def self.config_hash(name)
553
+ verbose_log "config_hash(#{name.inspect})"
554
+
555
+ name = name.to_s
556
+ unless result = @@cache_hash[name]
557
+ result = @@cache_hash[name] =
558
+ make_indifferent(
559
+ merge_hashes(
560
+ load_config_files(name)
561
+ )
562
+ )
563
+ verbose_log "config_hash(#{name.inspect}): reloaded"
564
+ end
565
+
566
+ result
567
+ end
568
+
569
+
570
+ ##
571
+ # Register a callback when a config has been reloaded. If no config name
572
+ # is specified, the callback will be registered under the name :ANY. The
573
+ # name :ANY will register a callback for any config file change.
574
+ #
575
+ # Example:
576
+ #
577
+ # class MyClass
578
+ # @@my_config = { }
579
+ # RConfig.on_load(:cache) do
580
+ # @@my_config = { }
581
+ # end
582
+ # def my_config
583
+ # @@my_config ||= something_expensive_thing_on_config(RConfig.cache.memory_limit)
584
+ # end
585
+ # end
586
+ #
587
+ def self.on_load(*args, &blk)
588
+ args << :ANY if args.empty?
589
+ proc = blk.to_proc
590
+
591
+ # Call proc on registration.
592
+ proc.call()
593
+
594
+ # Register callback proc.
595
+ args.each do | name |
596
+ name = name.to_s
597
+ (@@on_load[name] ||= [ ]) << proc
598
+ end
599
+ end
600
+
601
+
602
+ EMPTY_ARRAY = [].freeze unless defined? EMPTY_ARRAY
603
+
604
+ # Executes all of the reload callbacks registered to the specified config name,
605
+ # and all of the callbacks registered to run on any config, as specified by the
606
+ # :ANY symbol.
607
+ def self.fire_on_load(name)
608
+ callbacks =
609
+ (@@on_load['ANY'] || EMPTY_ARRAY) +
610
+ (@@on_load[name] || EMPTY_ARRAY)
611
+ callbacks.uniq!
612
+ verbose_log "fire_on_load(#{name.inspect}): callbacks[#{callbacks.inspect}]" unless callbacks.empty?
613
+ callbacks.each{|cb| cb.call()}
614
+ end
615
+
616
+
617
+ # If config files have changed,
618
+ # Caches are flushed, on_load triggers are run.
619
+ def self.check_config_changed(name = nil)
620
+ changed = []
621
+ if name == nil
622
+ @@cache_hash.keys.dup.each do | name |
623
+ if config_has_changed?(name)
624
+ changed << name
625
+ end
626
+ end
627
+ else
628
+ name = name.to_s # if name.is_a?(Symbol)
629
+ if config_has_changed?(name)
630
+ changed << name
631
+ end
632
+ end
633
+
634
+ verbose_log "check_config_changed(#{name.inspect}) => #{changed.inspect}"
635
+
636
+ changed.empty? ? nil : changed
637
+ end
638
+
639
+
640
+ def self.config_has_changed?(name)
641
+ verbose_log "config_has_changed?(#{name.inspect}), reload_disabled=#{@@reload_disabled}"
642
+
643
+ changed = false
644
+
645
+ if config_changed?(name) && reload?
646
+ if @@cache_hash[name]
647
+ @@cache_hash[name] = nil
648
+
649
+ # force on_load triggers.
650
+ fire_on_load(name)
651
+ end
652
+
653
+ changed = true
654
+ end
655
+
656
+ changed
657
+ end
658
+
659
+
660
+ ##
661
+ # Returns a merge of hashes.
662
+ #
663
+ def self.merge_hashes(hashes)
664
+ hashes.inject({ }) { | n, h | n.weave(h, false) }
665
+ end
666
+
667
+
668
+ ##
669
+ # Recursively makes hashes into frozen IndifferentAccess ConfigFakerHash
670
+ # Arrays are also traversed and frozen.
671
+ #
672
+ def self.make_indifferent(x)
673
+ case x
674
+ when Hash
675
+ unless x.frozen?
676
+ x.each_pair do | k, v |
677
+ x[k] = make_indifferent(v)
678
+ end
679
+ x = ConfigHash.new.merge!(x).freeze
680
+ end
681
+ verbose_log "make_indefferent: x = #{x.inspect}:#{x.class}"
682
+ when Array
683
+ unless x.frozen?
684
+ x.collect! do | v |
685
+ make_indifferent(v)
686
+ end
687
+ x.freeze
688
+ end
689
+ # Freeze Strings.
690
+ when String
691
+ x.freeze
692
+ end
693
+
694
+ x
695
+ end
696
+
697
+
698
+ ##
699
+ # This method provides shorthand to retrieve confiuration data that
700
+ # is global in scope, and used on an application or environment-wide
701
+ # level. The default location that it checks is the application file.
702
+ # The application config file is a special config file that should be
703
+ # used for config data that is broad in scope and used throughout the
704
+ # application. Since RConfig gives special regard to the application
705
+ # config file, thought should be given to whatever config information
706
+ # is placed there.
707
+ #
708
+ # Most config data will be specific to particular part of the
709
+ # application (i.e. database, web service), and should therefore
710
+ # be placed in its own specific config file, such as database.yml,
711
+ # or services.xml
712
+ #
713
+ # This method also acts as a wrapper for ENV. If no value is
714
+ # returned from the application config, it will also check
715
+ # ENV for a value matching the specified key.
716
+ #
717
+ # Ex.1 RConfig[:test_mode] =>
718
+ # RConfig.application[:test_mode] ||
719
+ # RConfig.application.test_mode
720
+ #
721
+ # Ex.2 RConfig[:web_app_root] => ENV['WEB_APP_ROOT']
722
+ #
723
+ # NOTE: The application config file can be in any of
724
+ # the supported formats (yml, xml, conf, etc.)
725
+ #
726
+ def self.[](key, file=:application)
727
+ self.get_config_file(file)[key] || ENV[key.to_s.upcase]
728
+ end
729
+
730
+ ##
731
+ # Get the value specified by the args, in the file specified by th name
732
+ #
733
+ def self.with_file(name, *args)
734
+ # verbose_log "with_file(#{name.inspect}, #{args.inspect})"; result =
735
+ args.inject(get_config_file(name)) { | v, i |
736
+ # verbose_log "v = #{v.inspect}, i = #{i.inspect}"
737
+ case v
738
+ when Hash
739
+ v[i.to_s]
740
+ when Array
741
+ i.is_a?(Integer) ? v[i] : nil
742
+ else
743
+ nil
744
+ end
745
+ }
746
+ # verbose_log "with_file(#{name.inspect}, #{args.inspect}) => #{result.inspect}"; result
747
+ end
748
+
749
+ ##
750
+ # Get the merged config hash.
751
+ # Will auto check every 5 minutes, for longer running apps.
752
+ #
753
+ def self.get_config_file(name)
754
+ name = name.to_s
755
+ now = Time.now
756
+
757
+ if (! @@last_auto_check[name]) || (now - @@last_auto_check[name]) > @@reload_interval
758
+ @@last_auto_check[name] = now
759
+ check_config_changed(name)
760
+ end
761
+
762
+ result = config_hash(name)
763
+
764
+ verbose_log "get_config_file(#{name.inspect}) => #{result.inspect}"
765
+
766
+ result
767
+ end
768
+
769
+
770
+ ##
771
+ # Disables any reloading of config,
772
+ # executes &block,
773
+ # calls check_config_changed,
774
+ # returns result of block
775
+ #
776
+ def self.disable_reload(&block)
777
+ # This should increment @@reload_disabled on entry, decrement on exit.
778
+ result = nil
779
+ reload_disabled_save = @@reload_disabled
780
+ begin
781
+ @@reload_disabled = true
782
+ result = yield
783
+ ensure
784
+ @@reload_disabled = reload_disabled_save
785
+ check_config_changed unless @@reload_disabled
786
+ end
787
+ result
788
+ end
789
+
790
+ ##
791
+ # Creates a dottable hash for all Hash objects, recursively.
792
+ #
793
+ def self.create_dottable_hash(value)
794
+ make_indifferent(value)
795
+ end
796
+
797
+ ##
798
+ # Short-hand access to config file by its name.
799
+ #
800
+ # Example:
801
+ #
802
+ # RConfig.provider(:foo) => RConfig.with_file(:provider).foo
803
+ # RConfig.provider.foo => RConfig.with_file(:provider).foo
804
+ #
805
+ def self.method_missing(method, *args)
806
+ value = with_file(method, *args)
807
+ verbose_log "#{self}.method_missing(#{method.inspect}, #{args.inspect}) => #{value.inspect}"
808
+ value
809
+ end
810
+
811
+ ##
812
+ # Creating an instance isn't required. But if you just have to a reference to RConfig
813
+ # you can get it using RConfig.instance. It's a singleton, so there's never more than
814
+ # one. The instance has no state, and no methods of it's own accept what it inherits
815
+ # from object. But this method delegates back to the class, so configuration data is
816
+ # still accessible.
817
+
818
+ # Example:
819
+ #
820
+ # config = RConfig.instance
821
+ # config.provider(:foo) => RConfig.provider(:foo)
822
+ # config.provider.foo => RConfig.provider.foo
823
+ #
824
+ def method_missing(method, *args)
825
+ self.class.method_missing(method, *args)
826
+ end
827
+
828
+ ##
829
+ # Creating an instance isn't required. But if you just have to a reference to RConfig
830
+ # you can get it using RConfig.instance. It's a singleton, so there's never more than
831
+ # one. The instance has no state, and no methods of it's own accept what it inherits
832
+ # from object. But this method delegates back to the class, so configuration data is
833
+ # still accessible.
834
+ #
835
+ # Example:
836
+ #
837
+ # config = RConfig.instance
838
+ # config[:foo] => RConfig[:foo]
839
+ #
840
+ def [](key)
841
+ self.class[key]
842
+ end
843
+
844
+ protected
845
+
846
+ ##
847
+ # Flushes cached config data. This should avoided in production
848
+ # environments, if possible.
849
+ def self.flush_cache
850
+ @@suffixes = { }
851
+ @@cache = { }
852
+ @@cache_files = { }
853
+ @@cache_hash = { }
854
+ @@last_auto_check = { }
855
+ self
856
+ end
857
+
858
+ ##
859
+ # Get complete file name, including file path for the given config name
860
+ # and directory.
861
+ def self.filename_for_name(name, dir = config_paths[0], ext = :yml)
862
+ File.join(dir, "#{name}.#{ext}")
863
+ end
864
+
865
+ ##
866
+ # Helper method for logging verbose messages.
867
+ def self.verbose_log *args
868
+ $stderr.puts(args.join("\n")) if @@verbose
869
+ end
870
+
871
+ end # class RConfig