jlawler-activeconfig 0.1.4 → 0.2.0

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.
@@ -0,0 +1,389 @@
1
+ require 'socket'
2
+ require 'yaml'
3
+ require 'active_config/hash_weave' # Hash#weave
4
+ # REMOVE DEPENDENCY ON active_support.
5
+ require 'rubygems'
6
+ require 'active_support'
7
+ require 'active_support/core_ext'
8
+ require 'active_support/core_ext/hash/indifferent_access'
9
+ require 'active_config/hash_config'
10
+ require 'active_config/suffixes'
11
+ require 'erb'
12
+
13
+
14
+ ##
15
+ # See LICENSE.txt for details
16
+ #
17
+ #=ActiveConfig
18
+ #
19
+ # * Provides dottable, hash, array, and argument access to YAML
20
+ # configuration files
21
+ # * Implements multilevel caching to reduce disk accesses
22
+ # * Overlays multiple configuration files in an intelligent manner
23
+ #
24
+ # Config file access example:
25
+ # Given a configuration file named test.yaml and test_local.yaml
26
+ # test.yaml:
27
+ # ...
28
+ # hash_1:
29
+ # foo: "foo"
30
+ # bar: "bar"
31
+ # bok: "bok"
32
+ # ...
33
+ # test_local.yaml:
34
+ # ...
35
+ # hash_1:
36
+ # foo: "foo"
37
+ # bar: "baz"
38
+ # zzz: "zzz"
39
+ # ...
40
+ #
41
+ # irb> ActiveConfig.test
42
+ # => {"array_1"=>["a", "b", "c", "d"], "perform_caching"=>true,
43
+ # "default"=>"yo!", "lazy"=>true, "hash_1"=>{"zzz"=>"zzz", "foo"=>"foo",
44
+ # "bok"=>"bok", "bar"=>"baz"}, "secure_login"=>true, "test_mode"=>true}
45
+ #
46
+ # --Notice that the hash produced is the result of merging the above
47
+ # config files in a particular order
48
+ #
49
+ # The overlay order of the config files is defined by ActiveConfig._get_file_suffixes:
50
+ # * nil
51
+ # * _local
52
+ # * _config
53
+ # * _local_config
54
+ # * _{environment} (.i.e _development)
55
+ # * _{environment}_local (.i.e _development_local)
56
+ # * _{hostname} (.i.e _whiskey)
57
+ # * _{hostname}_config_local (.i.e _whiskey_config_local)
58
+ #
59
+ # ------------------------------------------------------------------
60
+ # irb> ActiveConfig.test_local
61
+ # => {"hash_1"=>{"zzz"=>"zzz", "foo"=>"foo", "bar"=>"baz"}, "test_mode"=>true}
62
+ #
63
+
64
+ class ActiveConfig
65
+ EMPTY_ARRAY = [ ].freeze unless defined? EMPTY_ARRAY
66
+ def _suffixes
67
+ @suffixes_obj
68
+ end
69
+ # ActiveConfig.new take options from a hash (or hash like) object.
70
+ # Valid keys are:
71
+ # :path : Where it can find the config files, defaults to ENV['ACTIVE_CONFIG_PATH'], or RAILS_ROOT/etc
72
+ # :root_file : Defines the file that holds "top level" configs. (ie active_config.key). Defaults to "global"
73
+ # :suffixes : Either a suffixes object, or an array of suffixes symbols with their priority. See the ActiveConfig::Suffixes object
74
+ # :config_refresh : How often we should check for update config files
75
+ #
76
+ #
77
+ #FIXME TODO
78
+ def initialize opts={}
79
+ @config_path=opts[:path] || ENV['ACTIVE_CONFIG_PATH'] || (defined?(RAILS_ROOT) ? File.join(RAILS_ROOT,'etc') : nil)
80
+ @root_file=opts[:root_file] || 'global'
81
+ if ActiveConfig::Suffixes===opts[:suffixes]
82
+ @suffixes_obj = opts[:suffixes]
83
+ end
84
+ @suffixes_obj ||= Suffixes.new self, opts[:suffixes]
85
+ @suffixes_obj.ac_instance=self
86
+ @config_refresh =
87
+ (opts.has_key?(:config_refresh) ? opts[:config_refresh].to_i : 300)
88
+ @on_load = { }
89
+ self._flush_cache
90
+ end
91
+ def _config_path
92
+ @config_path_ary ||=
93
+ begin
94
+ path_sep = (@config_path =~ /;/) ? /;/ : /:/ # Make Wesha happy
95
+ path = @config_path.split(path_sep).reject{ | x | x.empty? }
96
+ path.map!{|x| x.freeze }.freeze
97
+ end
98
+ end
99
+
100
+ # DON'T CALL THIS IN production.
101
+ def _flush_cache *types
102
+ if types.size == 0 or types.include? :hash
103
+ @cache_hash = { }
104
+ @hash_times = Hash.new(0)
105
+ end
106
+ if types.size == 0 or types.include? :file
107
+ @file_times = Hash.new(0)
108
+ @file_cache = { }
109
+ end
110
+ self
111
+ end
112
+
113
+ def _reload_disabled=(x)
114
+ @reload_disabled = x.nil? ? false : x
115
+ end
116
+
117
+ def _reload_delay=(x)
118
+ @config_refresh = x || 300
119
+ end
120
+
121
+ def _verbose=(x)
122
+ @verbose = x.nil? ? false : x;
123
+ end
124
+
125
+ ##
126
+ # Get each config file's yaml hash for the given config name,
127
+ # to be merged later. Files will only be loaded if they have
128
+ # not been loaded before or the files have changed within the
129
+ # last five minutes, or force is explicitly set to true.
130
+ #
131
+ # If file contains the comment:
132
+ #
133
+ # # ACTIVE_CONFIG:ERB
134
+ #
135
+ # It will be run through ERb before YAML parsing
136
+ # with the following object bound:
137
+ #
138
+ # active_config.config_file => <<the name of the config.yml file>>
139
+ # active_config.config_directory => <<the directory of the config.yml>>
140
+ # active_config.config_name => <<the config name>>
141
+ # active_config.config_files => <<Array of config files to be parsed>>
142
+ #
143
+ def _load_config_files(name, force=false)
144
+ name = name.to_s
145
+ now = Time.now
146
+
147
+ # Get array of all the existing files file the config name.
148
+ config_files = _config_files(name)
149
+
150
+ #$stderr.puts config_files.inspect
151
+ # Get all the data from all yaml files into as hashes
152
+ _fire_on_load(name)
153
+ hashes = config_files.collect do |f|
154
+ filename=f
155
+ val=nil
156
+ mod_time=nil
157
+ next unless File.exists?(filename)
158
+ next(@file_cache[filename]) unless (mod_time=File.stat(filename).mtime) != @file_times[filename]
159
+ begin
160
+ File.open( filename ) { | yf |
161
+ val = yf.read
162
+ }
163
+ # If file has a # ACTIVE_CONFIG:ERB comment,
164
+ # Process it as an ERb first.
165
+ if /^\s*#\s*ACTIVE_CONFIG\s*:\s*ERB/i.match(val)
166
+ # Prepare a object visible from ERb to
167
+ # allow basic substitutions into YAMLs.
168
+ active_config = HashWithIndifferentAccess.new({
169
+ :config_file => filename,
170
+ :config_directory => File.dirname(filename),
171
+ :config_name => name,
172
+ :config_files => config_files,
173
+ })
174
+ val = ERB.new(val).result(binding)
175
+ end
176
+ # Read file data as YAML.
177
+ val = YAML::load(val)
178
+ # STDERR.puts "ActiveConfig: loaded #{filename.inspect} => #{val.inspect}"
179
+ (@config_file_loaded ||= { })[name] = config_files
180
+ rescue Exception => e
181
+ end
182
+ @file_cache[filename]=val
183
+ @file_times[filename]=mod_time
184
+ @file_cache[filename]
185
+ end
186
+ hashes.compact
187
+ end
188
+
189
+
190
+ def get_config_file(name)
191
+ # STDERR.puts "get_config_file(#{name.inspect})"
192
+ name = name.to_s # if name.is_a?(Symbol)
193
+ now = Time.now
194
+ # $stderr.puts (!!@reload_disabled).inspect
195
+ # $stderr.puts (@hash_times[name.to_sym]).inspect
196
+ #$stderr.puts ( now.to_i - @hash_times[name.to_sym] > @config_refresh).inspect
197
+ #return cached if cached is still good
198
+ return @cache_hash[name.to_sym] if
199
+ (now.to_i - @hash_times[name.to_sym] < @config_refresh)
200
+ #return cached if we have something cached and no reload_disabled flag
201
+ return @cache_hash[name.to_sym] if @cache_hash[name.to_sym] and @reload_disabled
202
+ # $stderr.puts "NOT USING CACHED AND RELOAD DISABLED" if @reload_disabled
203
+ @cache_hash[name.to_sym]=begin
204
+ x = _config_hash(name)
205
+ @hash_times[name.to_sym]=now.to_i
206
+ x
207
+ end
208
+ end
209
+
210
+ ##
211
+ # Returns a list of all relavant config files as specified
212
+ # by the suffixes object.
213
+ def _config_files(name)
214
+ _suffixes.for(name).inject([]) do | files,name_x |
215
+ _config_path.reverse.inject(files) do |files, dir |
216
+ files << File.join(dir, name_x.to_s + '.yml')
217
+ end
218
+ end
219
+ end
220
+
221
+ def _config_hash(name)
222
+ unless result = @cache_hash[name]
223
+ result = @cache_hash[name] =
224
+ _make_indifferent_and_freeze(
225
+ _load_config_files(name).inject({ }) { | n, h | n.weave(h, false) })
226
+ end
227
+ #$stderr.puts result.inspect
228
+ result
229
+ end
230
+
231
+
232
+ ##
233
+ # Register a callback when a config has been reloaded.
234
+ #
235
+ # The config :ANY will register a callback for any config file change.
236
+ #
237
+ # Example:
238
+ #
239
+ # class MyClass
240
+ # @my_config = { }
241
+ # ActiveConfig.on_load(:global) do
242
+ # @my_config = { }
243
+ # end
244
+ # def my_config
245
+ # @my_config ||= something_expensive_thing_on_config(ACTIVEConfig.global.foobar)
246
+ # end
247
+ # end
248
+ #
249
+ def on_load(*args, &blk)
250
+ args << :ANY if args.empty?
251
+ proc = blk.to_proc
252
+
253
+ # Call proc on registration.
254
+ proc.call()
255
+
256
+ # Register callback proc.
257
+ args.each do | name |
258
+ name = name.to_s
259
+ (@on_load[name] ||= [ ]) << proc
260
+ end
261
+ end
262
+
263
+ # Do reload callbacks.
264
+ def _fire_on_load(name)
265
+ callbacks =
266
+ (@on_load['ANY'] || EMPTY_ARRAY) +
267
+ (@on_load[name] || EMPTY_ARRAY)
268
+ callbacks.uniq!
269
+ STDERR.puts "_fire_on_load(#{name.inspect}): callbacks = #{callbacks.inspect}" if @verbose && ! callbacks.empty?
270
+ callbacks.each do | cb |
271
+ cb.call()
272
+ end
273
+ end
274
+
275
+ def _check_config_changed(iname=nil)
276
+ iname=iname.nil? ? @cache_hash.keys.dup : [*iname]
277
+ ret=iname.map{ | name |
278
+ # STDERR.puts "ActiveConfig: config changed? #{name.inspect} reload_disabled = #{@reload_disabled}" if @verbose
279
+ if config_changed?(name) && ! @reload_disabled
280
+ STDERR.puts "ActiveConfig: config changed #{name.inspect}" if @verbose
281
+ if @cache_hash[name]
282
+ @cache_hash[name] = nil
283
+
284
+ # force on_load triggers.
285
+ name
286
+ end
287
+ end
288
+ }.compact
289
+ return nil if ret.empty?
290
+ ret
291
+ end
292
+
293
+ ##
294
+ # Recursively makes hashes into frozen IndifferentAccess ConfigFakerHash
295
+ # Arrays are also traversed and frozen.
296
+ #
297
+ def _make_indifferent_and_freeze(x)
298
+ return x if x.frozen?
299
+ case x
300
+ when HashConfig
301
+ x.each_pair do | k, v |
302
+ x[k] = _make_indifferent_and_freeze(v)
303
+ end
304
+ when Hash
305
+ x = HashConfig.new.merge!(x)
306
+ x.each_pair do | k, v |
307
+ x[k] = _make_indifferent_and_freeze(v)
308
+ end
309
+ # STDERR.puts "x = #{x.inspect}:#{x.class}"
310
+ when Array
311
+ x.collect! do | v |
312
+ _make_indifferent_and_freeze(v)
313
+ end
314
+ end
315
+ x.freeze
316
+ end
317
+
318
+
319
+ def with_file(name, *args)
320
+ # STDERR.puts "with_file(#{name.inspect}, #{args.inspect})"; result =
321
+ args.inject(get_config_file(name)) { | v, i |
322
+ # STDERR.puts "v = #{v.inspect}, i = #{i.inspect}"
323
+ case v
324
+ when Hash
325
+ v[i.to_s]
326
+ when Array
327
+ i.is_a?(Integer) ? v[i] : nil
328
+ else
329
+ nil
330
+ end
331
+ }
332
+ # STDERR.puts "with_file(#{name.inspect}, #{args.inspect}) => #{result.inspect}"; result
333
+ end
334
+
335
+ #If you are using this in production code, you fail.
336
+ def reload(force = false)
337
+ if force || ! @reload_disabled
338
+ _flush_cache
339
+ end
340
+ nil
341
+ end
342
+
343
+ ##
344
+ # Disables any reloading of config,
345
+ # executes &block,
346
+ # calls check_config_changed,
347
+ # returns result of block
348
+ #
349
+ def disable_reload(&block)
350
+ # This should increment @reload_disabled on entry, decrement on exit.
351
+ # -- kurt 2007/06/12
352
+ result = nil
353
+ reload_disabled_save = @reload_disabled
354
+ begin
355
+ @reload_disabled = true
356
+ result = yield
357
+ ensure
358
+ @reload_disabled = reload_disabled_save
359
+ _check_config_changed unless @reload_disabled
360
+ end
361
+ result
362
+ end
363
+
364
+ ##
365
+ # Gets a value from the global config file
366
+ #
367
+ def [](key, file=_root_file)
368
+ get_config_file(file)[key]
369
+ end
370
+
371
+ ##
372
+ # Short-hand access to config file by its name.
373
+ #
374
+ # Example:
375
+ #
376
+ # ActiveConfig.global(:foo) => ActiveConfig.with_file(:global).foo
377
+ # ActiveConfig.global.foo => ActiveConfig.with_file(:global).foo
378
+ #
379
+ def method_missing(method, *args)
380
+ if method.to_s=~/^_(.*)/
381
+ _flush_cache
382
+ return @suffixes.send($1, *args)
383
+ else
384
+ value = with_file(method, *args)
385
+ value
386
+ end
387
+ end
388
+ end
389
+
data/lib/cnu_config.rb ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'active_config'
3
+
4
+ class ActiveConfig::SuffixesWithOverlay < ActiveConfig::Suffixes
5
+ def overlay= new_overlay
6
+ symbols[:overlay]=(new_overlay.respond_to?(:upcase) ? new_overlay.upcase : new_overlay)
7
+ end
8
+ def initialize(*args)
9
+ super
10
+ @symbols[:overlay]=proc { |sym_table| ENV['ACTIVE_CONFIG_OVERLAY']}
11
+ @priority=[
12
+ nil,
13
+ [:overlay, nil],
14
+ [:local],
15
+ [:overlay, [:local]],
16
+ :config,
17
+ [:overlay, :config],
18
+ :local_config,
19
+ [:overlay, :local_config],
20
+ :hostname,
21
+ [:overlay, :hostname],
22
+ [:hostname, :local_config],
23
+ [:overlay, [:hostname, :local_config]]
24
+ ]
25
+ end
26
+ end
27
+ class CnuConfigClass < ActiveConfig
28
+ attr :_overlay
29
+ def _suffixes
30
+ @_suffixes_obj||=Suffixes.new
31
+ end
32
+ def _overlay= x
33
+ _suffixes.priority=[
34
+ nil,
35
+ [:overlay, nil],
36
+ [:local],
37
+ [:overlay, [:local]],
38
+ :config,
39
+ [:overlay, :config],
40
+ :local_config,
41
+ [:overlay, :local_config],
42
+ :hostname,
43
+ [:overlay, :hostname],
44
+ [:hostname, :config_local],
45
+ [:overlay, [:hostname, :config_local]]
46
+ ]
47
+ super
48
+ end
49
+ def _config_path
50
+ @config_path||= ENV['CNU_CONFIG_PATH']
51
+ end
52
+ end
53
+ CnuConfig=CnuConfigClass.new
54
+
55
+
@@ -0,0 +1 @@
1
+ using_array_index: 101
@@ -0,0 +1,14 @@
1
+ #test_mode: false
2
+ secure_login: true
3
+ perform_caching: true
4
+ lazy: true
5
+
6
+ default: "yo!" # Test for #default override for .default notation
7
+
8
+ hash_1:
9
+ foo: "foo"
10
+ bar: "bar"
11
+ bok: "bok"
12
+
13
+
14
+ array_1: [ a, b, c, d ]
@@ -0,0 +1,13 @@
1
+ #test_mode: false
2
+ secure_login: true
3
+ perform_caching: true
4
+ lazy: true
5
+
6
+ default: "yo!" # Test for #default override for .default notation
7
+
8
+ hash_1:
9
+ foo: "foo"
10
+ bar: "bar"
11
+ bok: "GB"
12
+ gb: "GB"
13
+
@@ -0,0 +1,15 @@
1
+ #test_mode: false
2
+ secure_login: true
3
+ perform_caching: true
4
+ lazy: true
5
+
6
+ default: "yo!" # Test for #default override for .default notation
7
+
8
+ hash_1:
9
+ foo: "foo"
10
+ bar: "bar"
11
+ bok: "US"
12
+ gb: ~
13
+
14
+
15
+
File without changes
@@ -0,0 +1,9 @@
1
+ test_mode: true
2
+
3
+
4
+ hash_1:
5
+ foo: "foo"
6
+ bar: "baz"
7
+ zzz: "zzz"
8
+
9
+
@@ -0,0 +1 @@
1
+ secure_login: false