cnuconfig 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ +++ 0.0.1 2007-05-16
2
+
3
+ + 1 major enhancement:
4
+ + Initial release
@@ -0,0 +1,19 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/cnuconfig.rb
6
+ lib/cnuhash.rb
7
+ lib/indifferent_access.rb
8
+ lib/cnuconfig/version.rb
9
+ scripts/txt2html
10
+ setup.rb
11
+ test/test_cnuconfig.rb
12
+ test/test_cnuconfig/global.yml
13
+ test/test_cnuconfig/test.yml
14
+ test/test_cnuconfig/test_local.yml
15
+ website/index.html
16
+ website/index.txt
17
+ website/javascripts/rounded_corners_lite.inc.js
18
+ website/stylesheets/screen.css
19
+ website/template.rhtml
@@ -0,0 +1,3 @@
1
+ README for cnuconfig
2
+ =====================
3
+
@@ -0,0 +1,94 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/rdoctask'
8
+ require 'rake/contrib/rubyforgepublisher'
9
+ require 'fileutils'
10
+ require 'hoe'
11
+ include FileUtils
12
+ require File.join(File.dirname(__FILE__), 'lib', 'cnuconfig', 'version')
13
+
14
+ AUTHOR = 'Alex Skryl'
15
+ EMAIL = "askryl@cashnetusa.com"
16
+ DESCRIPTION = "Allows for intelligent overlay YAML configuration files as well as an intuitive mechanism for accessing YAML config data through dottable notation"
17
+ GEM_NAME = 'cnuconfig'
18
+ RUBYFORGE_PROJECT = 'cnuconfig'
19
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
20
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
21
+
22
+ NAME = "cnuconfig"
23
+ REV = nil # UNCOMMENT IF REQUIRED: File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
24
+ VERS = CnuConfig::VERSION::STRING + (REV ? ".#{REV}" : "")
25
+ CLEAN.include ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store']
26
+ RDOC_OPTS = ['--quiet', '--title', 'cnuconfig documentation',
27
+ "--opname", "index.html",
28
+ "--line-numbers",
29
+ "--main", "README",
30
+ "--inline-source"]
31
+
32
+ class Hoe
33
+ def extra_deps
34
+ @extra_deps.reject { |x| Array(x).first == 'hoe' }
35
+ end
36
+ end
37
+
38
+ # Generate all the Rake tasks
39
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
40
+
41
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
42
+ p.author = AUTHOR
43
+ p.description = DESCRIPTION
44
+ p.email = EMAIL
45
+ p.summary = DESCRIPTION
46
+ p.url = HOMEPATH
47
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
48
+ p.test_globs = ["test/**/test_*.rb"]
49
+ p.clean_globs = CLEAN #An array of file patterns to delete on clean.
50
+
51
+ # == Optional
52
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
53
+ #p.extra_deps = [['activesupport']]
54
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
55
+ end
56
+
57
+
58
+ desc 'Generate website files'
59
+ task :website_generate do
60
+ Dir['website/**/*.txt'].each do |txt|
61
+ sh %{ ruby scripts/txt2html #{txt} > #{txt.gsub(/txt$/,'html')} }
62
+ end
63
+ end
64
+
65
+ desc 'Upload website files to rubyforge'
66
+ task :website_upload do
67
+ config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml")))
68
+ host = "#{config["username"]}@rubyforge.org"
69
+ remote_dir = "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}/"
70
+ # remote_dir = "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
71
+ local_dir = 'website'
72
+ sh %{rsync -av #{local_dir}/ #{host}:#{remote_dir}}
73
+ end
74
+
75
+ desc 'Generate and upload website files'
76
+ task :website => [:website_generate, :website_upload]
77
+
78
+ desc 'Release the website and new gem version'
79
+ task :deploy => [:check_version, :website, :release]
80
+
81
+ desc 'Runs tasks website_generate and install_gem as a local deployment of the gem'
82
+ task :local_deploy => [:website_generate, :install_gem]
83
+
84
+ task :check_version do
85
+ unless ENV['VERSION']
86
+ puts 'Must pass a VERSION=x.y.z release version'
87
+ exit
88
+ end
89
+ unless ENV['VERSION'] == VERS
90
+ puts "Please update your version.rb to match the release version, currently #{VERS}"
91
+ exit
92
+ end
93
+ end
94
+
@@ -0,0 +1,498 @@
1
+ require 'socket'
2
+ require 'yaml'
3
+ require 'cnuhash'
4
+ require 'indifferent_access' #from active_support
5
+ #
6
+ #See LICENSE.txt for details
7
+ #
8
+ #=CnuConfig
9
+ #
10
+ # * Provides dottable, hash, array, and argument access to YAML configuration files
11
+ # * Implements multilevel caching to reduce disk accesses
12
+ # * Overlays multiple configuration files in an intelligent manner
13
+ #
14
+ #Config file access example:
15
+ # Given a configuration file named test.yaml and test_local.yaml
16
+ # test.yaml:
17
+ # ...
18
+ # hash_1:
19
+ # foo: "foo"
20
+ # bar: "bar"
21
+ # bok: "bok"
22
+ # ...
23
+ # test_local.yaml:
24
+ # ...
25
+ # hash_1:
26
+ # foo: "foo"
27
+ # bar: "baz"
28
+ # zzz: "zzz"
29
+ # ...
30
+ #
31
+ # irb(main):012:0> CnuConfig.test
32
+ # => {"array_1"=>["a", "b", "c", "d"], "perform_caching"=>true,
33
+ # "default"=>"yo!", "lazy"=>true, "hash_1"=>{"zzz"=>"zzz", "foo"=>"foo",
34
+ # "bok"=>"bok", "bar"=>"baz"}, "secure_login"=>true, "test_mode"=>true}
35
+ #
36
+ # --Notice that the hash produced is the result of merging the above
37
+ # config files in a particular order
38
+ #
39
+ # The overlay order of the config files is defined by SUFFIXES:
40
+ # * nil
41
+ # * _local
42
+ # * _config
43
+ # * _local_config
44
+ # ...
45
+ #
46
+ # ----------------------------------------------------------------------------
47
+ # irb(main):021:0> CnuConfig.test_local
48
+ # => {"hash_1"=>{"zzz"=>"zzz", "foo"=>"foo", "bar"=>"baz"}, "test_mode"=>true}
49
+ #
50
+ class CnuConfig
51
+ # include DogTag # Not needed?
52
+
53
+ unless defined? SUFFIXES
54
+ SUFFIXES = [nil, :local, :config, :local_config]
55
+ if defined? RAILS_ENV
56
+ SUFFIXES << RAILS_ENV
57
+ SUFFIXES << [RAILS_ENV, :local]
58
+ end
59
+ SUFFIXES << Socket.gethostname
60
+ SUFFIXES << [Socket.gethostname, :config_local]
61
+ end
62
+
63
+ @@cache = {}
64
+ @@cache_files = {}
65
+ @@cache_hash = { }
66
+ @@cache_config_files = { } # Keep around incase reload_disabled.
67
+ @@last_auto_check = { }
68
+
69
+ @@reload_disabled = false
70
+ def self._reload_disabled=(x); @@reload_disabled = x.nil? ? false : x; end
71
+
72
+ @@reload_delay = 300
73
+ # The number of seconds between reloading of config files
74
+ # and automatic reload checks.
75
+ def self._reload_delay=(x); @@reload_delay = x || 300; end
76
+
77
+ @@verbose = false
78
+ def self._verbose=(x); @@verbose = x; end
79
+
80
+ @@config_file_loaded = nil
81
+ def self._config_file_loaded=(x); @@config_file_loaded = x; end
82
+ def self._config_file_loaded; @@config_file_loaded; end
83
+
84
+ # Get each config file's yaml hash for the given config name,
85
+ # to be merged later.
86
+ def self.load_config_files(name, force=false)
87
+ name = name.to_s # if name.is_a?(Symbol)
88
+
89
+ # Return last config file hash list loaded,
90
+ # if reload is disabled and files have already been loaded.
91
+ return @@cache_config_files[name] if
92
+ @@reload_disabled &&
93
+ @@cache_config_files[name]
94
+
95
+ now = Time.now
96
+ config_files = self._get_config_files(name)
97
+
98
+ # STDERR.puts "load_config_files(#{name.inspect})"
99
+
100
+ hashes = config_files.collect do |f|
101
+ name, name_x, filename, mtime = *f
102
+
103
+ val, last_mtime, last_loaded = @@cache[filename]
104
+
105
+ # Reload the file if its been more than 5 minutes
106
+ # since last load attempt.
107
+ if val == nil || now - last_loaded > @@reload_delay
108
+ if force || val == nil || mtime != last_mtime
109
+ # mtime is nil if file does not exist.
110
+ if mtime
111
+ File.open( filename ) do | yf |
112
+ STDERR.puts "\nCnuConfig: loading #{filename.inspect}" if @@verbose
113
+ val = YAML::load(yf).freeze
114
+ # STDERR.puts "CnuConfig: loaded #{filename.inspect} => #{val.inspect}"
115
+ (@@config_file_loaded ||= { })[name] = config_files
116
+ end
117
+ end
118
+
119
+ # Save cached config file contents, and mtime.
120
+ @@cache[filename] = [ val, mtime, now ]
121
+
122
+ # Flush merged hash cache.
123
+ @@cache_hash[name] = nil
124
+
125
+ # Config files changed or disappeared.
126
+ @@cache_files[name] = config_files
127
+
128
+ end
129
+ end
130
+
131
+ val
132
+ end
133
+ hashes.compact!
134
+
135
+ # STDERR.puts "load_config_files(#{name.inspect}) => #{hashes.inspect}"
136
+
137
+ # Keep last loaded config files around in case @@reload_dsabled.
138
+ @@cache_config_files[name] = hashes
139
+
140
+ hashes
141
+ end
142
+
143
+
144
+ # Returns a list of all relavant config files as specified
145
+ # by SUFFIXES list.
146
+ # Each element is an Array, containing:
147
+ # [ "the-top-level-config-name",
148
+ # "the-suffixed-config-name",
149
+ # "/the/absolute/path/to/yaml.yml",
150
+ # # The mtime of the yml file or nil,
151
+ # ]
152
+ def self._get_config_files(name)
153
+ # alexg: splatting *suffix allows us to deal with multipart suffixes
154
+ SUFFIXES.map{|suffix| [name, *suffix].compact.join('_')}.collect do |name_x|
155
+ filename = File.expand_path(filename_for_name(name_x))
156
+ [ name,
157
+ name_x,
158
+ filename,
159
+ File.exist?(filename) ? File.stat(filename).mtime : nil,
160
+ ]
161
+ end
162
+ end
163
+
164
+
165
+ def self._config_files(name)
166
+ @@cache_files[name] ||= _get_config_files(name)
167
+ end
168
+
169
+ #Checks whether a config file has been changed and reloads
170
+ #it in case it has
171
+ def self.config_changed?(name)
172
+ # STDERR.puts "config_changed?(#{name.inspect})"
173
+ name = name.to_s # if name.is_a?(Symbol)
174
+ ! (@@cache_files[name] === _get_config_files(name))
175
+ end
176
+
177
+
178
+
179
+ # Get the merged config hash for the named file.
180
+ def self.config_hash(name)
181
+ name = name.to_s # if name.is_a?(Symbol)
182
+ _config_hash(name)
183
+ end
184
+
185
+
186
+ # Returns a cached indifferent access faker hash merged
187
+ # from all config files for a name.
188
+ def self._config_hash(name)
189
+ # STDERR.puts "_config_hash(#{name.inspect})"; result =
190
+ @@cache_hash[name] ||=
191
+ _make_indifferent(
192
+ _merge_hashes(
193
+ load_config_files(name)))
194
+ # STDERR.puts "_config_hash(#{name.inspect}) => #{result.inspect}"; result
195
+ end
196
+
197
+
198
+ # If config files have changed, caches are flushed.
199
+ def self.check_config_changed(name = nil)
200
+ # STDERR.puts "check_config_changed(#{name.inspect})"
201
+ if name == nil
202
+ @@cache_hash.keys.each do | name |
203
+ check_config_changed(name)
204
+ end
205
+ else
206
+ name = name.to_s # if name.is_a?(Symbol)
207
+ # STDERR.puts "CnuConfig: config changed? #{name.inspect} reload_disabled = #{@@reload_disabled}" if @@verbose
208
+ if config_changed?(name) && ! @@reload_disabled
209
+ STDERR.puts "CnuConfig: config changed #{name.inspect}, @@reload_disabled = #{@@reload_disabled}" if @@verbose
210
+ @@cache_hash[name] = nil
211
+ end
212
+ end
213
+ # STDERR.puts "check_config_changed(#{name.inspect}) =>"
214
+ end
215
+
216
+
217
+ # Returns a merge of hashes.
218
+ def self._merge_hashes(hashes)
219
+ hashes.inject({ }) { | n, h | n.weave(h, false) }
220
+ end
221
+
222
+
223
+ # Recursively makes hashes into frozen IndifferentAccess ConfigFakerHash
224
+ # Arrays are also traversed and frozen.
225
+ def self._make_indifferent(x)
226
+ case x
227
+ when Hash
228
+ unless x.frozen?
229
+ x.each_pair do | k, v |
230
+ x[k] = _make_indifferent(v)
231
+ end
232
+ x = HashConfig.new.merge!(x).freeze
233
+ end
234
+ # STDERR.puts "x = #{x.inspect}:#{x.class}"
235
+ when Array
236
+ unless x.frozen?
237
+ x.collect! do | v |
238
+ _make_indifferent(v)
239
+ end
240
+ x.freeze
241
+ end
242
+ end
243
+ x
244
+ end
245
+
246
+ #Calling CnuConfig without an argument gets values from the 'global' configuration file
247
+ #if one exists:
248
+ # irb(main):031:0> CnuConfig[:geocoder_check]
249
+ # => {"url"=>["http://geocoder.us/service/soap", "http://rpc.geocoder.us/Geo/Coder/US"]}
250
+ #
251
+ def self.[](key, file=:global)
252
+ self.get_config_file(file)[key]
253
+ end
254
+
255
+
256
+ def self.with_file(name, *args)
257
+ # STDERR.puts "with_file(#{name.inspect}, #{args.inspect})"; result =
258
+ args.inject(get_config_file(name)) { | v, i |
259
+ # STDERR.puts "v = #{v.inspect}, i = #{i.inspect}"
260
+ case v
261
+ when Hash
262
+ v[i.to_s]
263
+ when Array
264
+ i.is_a?(Integer) ? v[i] : nil
265
+ else
266
+ nil
267
+ end
268
+ }
269
+ # STDERR.puts "with_file(#{name.inspect}, #{args.inspect}) => #{result.inspect}"; result
270
+ end
271
+
272
+ # Get the merged config hash.
273
+ # Will auto check every 5 minutes, for longer running apps.
274
+ def self.get_config_file(name)
275
+ # STDERR.puts "get_config_file(#{name.inspect})"
276
+ name = name.to_s # if name.is_a?(Symbol)
277
+ now = Time.now
278
+ if (! @@last_auto_check[name]) || (now - @@last_auto_check[name]) > @@reload_delay
279
+ @@last_auto_check[name] = now
280
+ check_config_changed(name)
281
+ end
282
+ # result =
283
+ _config_hash(name)
284
+ # STDERR.puts "get_config_file(#{name.inspect}) => #{result.inspect}"; result
285
+ end
286
+
287
+
288
+ def self.with_file_sym(file, *args)
289
+ with_file(file, *args)
290
+ end
291
+
292
+
293
+ def self.global_sym(*args)
294
+ with_file('global', *args)
295
+ end
296
+
297
+
298
+ # Unused?
299
+ def self.can_write_file?(name)
300
+ filename = filename_for_name(name)
301
+ !File.exists?(filename) || File.writable?(filename)
302
+ end
303
+
304
+
305
+ # Unused?
306
+ def self.write_file(name, obj=nil)
307
+ raise exception([:writing_empty_file, name]) unless obj
308
+ File.open(filename_for_name(name), 'w') do |out|
309
+ YAML.dump(obj, out)
310
+ end
311
+ load_config_files(name, true)
312
+ end
313
+
314
+
315
+ # Disables any reloading of config,
316
+ # executes &block,
317
+ # calls check_config_changed,
318
+ # returns result of block
319
+ def self.disable_reload(&block)
320
+ result = nil
321
+ reload_disabled_save = @@reload_disabled
322
+ begin
323
+ @@reload_disabled = true
324
+ result = yield
325
+ ensure
326
+ @@reload_disabled = reload_disabled_save
327
+ check_config_changed unless @@reload_disabled
328
+ end
329
+ result
330
+ end
331
+
332
+
333
+ # Unused?
334
+ def self.rec_merge(hasha, hashb)
335
+ raise(:deprecated)
336
+ hasha = {}.merge(hasha || {})
337
+ hashb = {}.merge(hashb || {})
338
+ return hashb if hasha.empty?
339
+ return hasha if hashb.empty?
340
+ hasha.each_pair{|k,v|
341
+ if v.is_a?(Hash)
342
+ hasha[k]=rec_merge(v,hashb[k])
343
+ else
344
+ hasha[k]=hashb[k] || v
345
+ end
346
+ }
347
+ hasha
348
+ end
349
+
350
+ # Unused?
351
+ def self.recursive_hash_merge(hashes, *path)
352
+ raise(:deprecated)
353
+ hashes = hashes.compact
354
+ case
355
+ when path.empty?
356
+ #this will merge in hashes recursively (its pretty expensive)
357
+ hashes.inject({}){|retval, hash| retval.weave(hash, false) }#rec_merge(retval,hash)}
358
+ when hashes.empty?
359
+ nil
360
+ else
361
+ a = recursive_hash_merge(hashes.last(hashes.size-1), *path)
362
+ b = path.inject(hashes.first) do |cfg, key|
363
+ case cfg
364
+ when Hash then cfg[key.to_s]
365
+ when Array then key.is_a?(Integer) ? cfg[key] : nil
366
+ else nil
367
+ end
368
+ end
369
+ a.is_a?(Hash) && b.is_a?(Hash) ? a.weave(b, false) : (a || b) #rec_merge(a,b) : (a || b)
370
+ end
371
+ end
372
+
373
+
374
+ # Creates a dottable hash for all Hash objects, recursively.
375
+ def self.create_dottable_hash(value)
376
+ _make_indifferent(value)
377
+ end
378
+
379
+ #This allows us to create faker methods out of the structures inside the
380
+ #config files to give us dottable access
381
+ def self.method_missing(method, *args)
382
+ # STDERR.puts "#{self}.method_missing(#{method.inspect}, #{args.inspect})"
383
+ value = with_file(method, *args)
384
+ # STDERR.puts "#{self}.method_missing(#{method.inspect}, #{args.inspect}) => #{value.inspect}"
385
+ value
386
+ end
387
+
388
+ #Forces the reloading of all caches (only for testing)
389
+ def self.reload(force = false)
390
+ if force || ! @@reload_disabled
391
+ return unless ['development', 'integration'].include?(RAILS_ENV)
392
+ @@cache = { }
393
+ @@cache_files = { }
394
+ @@cache_hash = { }
395
+ @@last_auto_check = { }
396
+ end
397
+ nil
398
+ end
399
+
400
+ protected
401
+
402
+ def self.filename_for_name(name, suffix='')
403
+ File.join(CONFIG_ROOT, name.to_s + suffix.to_s + '.yml')
404
+ end
405
+
406
+
407
+ class HashConfig < HashWithIndifferentAccess
408
+ # dotted notation can now be used with arguments (useful for creating mock objects)
409
+ # in the YAML file the method name is a key (just like usual), argument(s)
410
+ # form a nested key, and the value will get returned.
411
+ #
412
+ # For example loading to variable foo a yaml file that looks like:
413
+ # customer:
414
+ # id: 12345678
415
+ # verified:
416
+ # phone: verified
417
+ # address: info_not_available
418
+ # ? [name, employer]
419
+ # : not_verified
420
+ #
421
+ # Allows the following calls:
422
+ # foo.customer.id --> 12345678
423
+ # foo.customer.verified.phone --> verified
424
+ # foo.customer.verified("phone") --> verified
425
+ # foo.customer.verified("name", "employer") --> not_verified
426
+ # foo.customer.verified(:address) --> info_not_available
427
+ #
428
+ # Note that :address is specified as a symbol, where phone is just a string.
429
+ # Depending on what kind of parameter the method being mocked out is going
430
+ # to be called with, define in the YAML file either a string or a symbol.
431
+ # This also works inside the composite array keys.
432
+ def method_missing(method, *args)
433
+ method = method.to_s
434
+ # STDERR.puts "CHF#method_missing(#{method.inspect}, #{args.inspect}) on #{self.inspect}:#{self.class}" if method == 'dup'
435
+ value = self[method]
436
+ case args.size
437
+ when 0:
438
+ ;
439
+ when 1:
440
+ # value = CnuConfig.create_dottable_hash(self[method]).send(args[0])
441
+ value = value.send(args[0])
442
+ else
443
+ value = value[args]
444
+ end
445
+ # value = convert_value(value)
446
+ value
447
+ end
448
+
449
+
450
+ def [](key)
451
+ key = key.to_s if key.kind_of?(Symbol)
452
+ super(key)
453
+ end
454
+
455
+
456
+ # HashWithIndifferentAccess#default is broken!
457
+ define_method(:default_Hash, Hash.instance_method(:default))
458
+
459
+ # Allow hash.default => hash['default']
460
+ # without breaking Hash's usage of default(key)
461
+ @@no_key = [ :no_key ] # magically unique value.
462
+ def default(key = @@no_key)
463
+ key = key.to_s if key.is_a?(Symbol)
464
+ key == @@no_key ? self['default'] : default_Hash(key == @@no_key ? nil : key)
465
+ end
466
+
467
+
468
+ # HashWithIndifferentAccess#update is broken!
469
+ # This took way too long to figure this out:
470
+ #
471
+ # Hash#update returns self,
472
+ # BUT,
473
+ # HashWithIndifferentAccess#update does not!
474
+ #
475
+ # { :a => 1 }.update({ :b => 2, :c => 3 })
476
+ # => { :a => 1, :b => 2, :c => 3 }
477
+ #
478
+ # HashWithIndifferentAccess.new({ :a => 1 }).update({ :b => 2, :c => 3 })
479
+ # => { :b => 2, :c => 3 } # WTF?
480
+ #
481
+ # Subclasses should *never* override methods and break their protocols!!!!
482
+ #
483
+ def update(hash)
484
+ super(hash)
485
+ self
486
+ end
487
+
488
+
489
+ # Override WithIndifferentAccess#convert_value
490
+ # return instances of this class for Hash values.
491
+ def convert_value(value)
492
+ # STDERR.puts "convert_value(#{value.inspect}:#{value.class})"
493
+ value.class == Hash ? self.class.new(value).freeze : value
494
+ end
495
+
496
+ end
497
+
498
+ end