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.
- data/Rakefile +54 -0
- data/VERSION.yml +4 -0
- data/bin/active_config +46 -0
- data/lib/active_config/hash_config.rb +110 -0
- data/lib/active_config/hash_weave.rb +72 -0
- data/lib/active_config/suffixes.rb +85 -0
- data/lib/active_config.rb +389 -0
- data/lib/cnu_config.rb +55 -0
- data/test/active_config_test/global.yml +1 -0
- data/test/active_config_test/test.yml +14 -0
- data/test/active_config_test/test_GB.yml +13 -0
- data/test/active_config_test/test_US.yml +15 -0
- data/test/active_config_test/test_config.yml +0 -0
- data/test/active_config_test/test_local.yml +9 -0
- data/test/active_config_test/test_production.yml +1 -0
- data/test/active_config_test.rb +426 -0
- data/test/active_config_test_multi/patha/test.yml +14 -0
- data/test/active_config_test_multi/pathb/test_local.yml +2 -0
- data/test/active_config_test_multi.rb +56 -0
- data/test/cnu_config_test/global.yml +1 -0
- data/test/cnu_config_test/test.yml +14 -0
- data/test/cnu_config_test/test_GB.yml +13 -0
- data/test/cnu_config_test/test_US.yml +15 -0
- data/test/cnu_config_test/test_local.yml +9 -0
- data/test/cnu_config_test.rb +405 -0
- data/test/env_test.rb +76 -0
- metadata +40 -32
@@ -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
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
secure_login: false
|