rconfig 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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