config_logic 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +159 -0
- data/TODO +0 -0
- data/lib/config_logic/cache.rb +22 -0
- data/lib/config_logic/config_logic.rb +54 -0
- data/lib/config_logic/core_ext/array.rb +15 -0
- data/lib/config_logic/core_ext/class.rb +7 -0
- data/lib/config_logic/file_cache.rb +78 -0
- data/lib/config_logic/logger.rb +40 -0
- data/lib/config_logic/logic_element.rb +62 -0
- data/lib/config_logic/multiplexer.rb +22 -0
- data/lib/config_logic/overlay.rb +24 -0
- data/lib/config_logic/tree_cache.rb +132 -0
- data/lib/config_logic.rb +27 -0
- data/spec/cache_spec.rb +24 -0
- data/spec/config_logic_spec.rb +59 -0
- data/spec/file_cache_spec.rb +40 -0
- data/spec/logic_element_spec.rb +43 -0
- data/spec/multiplexer_spec.rb +30 -0
- data/spec/overlay_spec.rb +35 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/tree_cache_spec.rb +56 -0
- metadata +97 -0
data/README.rdoc
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
= Config Logic
|
2
|
+
|
3
|
+
Config Logic is a configuration management tool for Ruby/Rails applications. It
|
4
|
+
wraps any set of config files in a managed access layer which supports
|
5
|
+
|
6
|
+
* caching
|
7
|
+
* multi argument, hash style, or dot style access
|
8
|
+
* ordered overlays
|
9
|
+
* dynamic or static multiplexing
|
10
|
+
|
11
|
+
|
12
|
+
== Requirements
|
13
|
+
|
14
|
+
The following gems will be installed along with config_logic
|
15
|
+
|
16
|
+
* activesupport (2.2.2+)
|
17
|
+
* buffered_logger (0.1.2+)
|
18
|
+
|
19
|
+
|
20
|
+
== Installation
|
21
|
+
|
22
|
+
gem install config_logic
|
23
|
+
|
24
|
+
|
25
|
+
== Usage
|
26
|
+
|
27
|
+
Imagine that your application has a config directory with the following layout,
|
28
|
+
|
29
|
+
config/config_prod.yml -->
|
30
|
+
|
31
|
+
:key1: 1
|
32
|
+
:key2: 2
|
33
|
+
:key3: 3
|
34
|
+
|
35
|
+
config/config_dev.yml -->
|
36
|
+
|
37
|
+
:key1: 11
|
38
|
+
:key2: 12
|
39
|
+
:key3: 13
|
40
|
+
|
41
|
+
config/dir1/config_prod.yml -->
|
42
|
+
|
43
|
+
:key1: 11
|
44
|
+
:key2: 12
|
45
|
+
:key3: 13
|
46
|
+
|
47
|
+
config/dir2/config_dev.yml -->
|
48
|
+
|
49
|
+
:key4: 1
|
50
|
+
:key5: 2
|
51
|
+
:key6: 3
|
52
|
+
|
53
|
+
|
54
|
+
=== Initialization
|
55
|
+
|
56
|
+
c = ConfigLogic.new('path/to/config/dir')
|
57
|
+
c = ConfigLogic.new(['path1', 'path2'])
|
58
|
+
|
59
|
+
|
60
|
+
=== Data Access
|
61
|
+
|
62
|
+
c.config.key1 => 1
|
63
|
+
c[:config][:key1] => 1
|
64
|
+
c['config']['key1'] => 1
|
65
|
+
c(:config, :key1) => 1
|
66
|
+
c('config', 'key1') => 1
|
67
|
+
c.unknown => nil
|
68
|
+
c.config.unknown => nil
|
69
|
+
|
70
|
+
|
71
|
+
=== Rebuilding the Config Cache
|
72
|
+
|
73
|
+
c.reload!
|
74
|
+
|
75
|
+
|
76
|
+
=== Applying Overlays
|
77
|
+
|
78
|
+
An Overlay allows multiple config values to be merged in a specified order. The
|
79
|
+
merge order is determined by the order of the key names in the :inputs parameter.
|
80
|
+
|
81
|
+
==== Global
|
82
|
+
|
83
|
+
c = ConfigLogic.new('path/to/config/dir', :overlays => [ { :name => 'config',
|
84
|
+
:inputs => [:config_prod, :config_dev] } ] )
|
85
|
+
c.config.key1 => 11
|
86
|
+
c.config.key2 => 12
|
87
|
+
c.config.key3 => 13
|
88
|
+
c.config.dir1.key1 => 11
|
89
|
+
c.config.dir1.key2 => 12
|
90
|
+
c.config.dir1.key3 => 13
|
91
|
+
c.config.dir1.key4 => 1
|
92
|
+
c.config.dir1.key5 => 2
|
93
|
+
c.config.dir1.key6 => 3
|
94
|
+
|
95
|
+
==== Local
|
96
|
+
|
97
|
+
c.config.insert_overlay( { :name => 'config',
|
98
|
+
:inputs => [:config_prod, :config_dev] } )
|
99
|
+
c.config.key1 => 11
|
100
|
+
c.config.key2 => 12
|
101
|
+
c.config.key3 => 13
|
102
|
+
c.config.dir1.key1 => 1
|
103
|
+
c.config.dir1.key2 => 2
|
104
|
+
c.config.dir1.key3 => 3
|
105
|
+
c.config.dir1.key4 => nil
|
106
|
+
|
107
|
+
|
108
|
+
=== Applying Multiplexers
|
109
|
+
|
110
|
+
A multiplexer groups multiple config values and uses the result of a supplied code
|
111
|
+
block to choose the appropriate one. Multiplexers can be static or dynamic. A
|
112
|
+
static multiplexer is evaluated when it is created and its result replaces the
|
113
|
+
key group specified. A dynamic multiplexer is evaluated every time it's called.
|
114
|
+
Multiplexers are static by default.
|
115
|
+
|
116
|
+
|
117
|
+
==== Global
|
118
|
+
|
119
|
+
c = ConfigLogic.new('path/to/config/dir', :multiplexers => [ { :name => 'key',
|
120
|
+
:selector => Proc.new {1 + 1 },
|
121
|
+
:inputs => {1 => :key1, 2 => :key2, 3 => :key3} } ] )
|
122
|
+
c.config.key => 2
|
123
|
+
c.config.key1 => nil
|
124
|
+
c.config.key2 => nil
|
125
|
+
c.config.key3 => nil
|
126
|
+
c.config.dir1.key => 2
|
127
|
+
c.config.dir1.key1 => nil
|
128
|
+
c.config.dir1.key2 => nil
|
129
|
+
c.config.dir1.key3 => nil
|
130
|
+
|
131
|
+
==== Local
|
132
|
+
|
133
|
+
c.config.insert_multiplexer({ :name => 'key',
|
134
|
+
:selector => Proc.new {1 + 1 },
|
135
|
+
:inputs => {1 => :key1, 2 => :key2, 3 => :key3} })
|
136
|
+
c.config.key => 2
|
137
|
+
c.config.key1 => nil
|
138
|
+
c.config.key2 => nil
|
139
|
+
c.config.key3 => nil
|
140
|
+
c.config.dir1.key => nil
|
141
|
+
c.config.dir1.key1 => 1
|
142
|
+
c.config.dir1.key2 => 2
|
143
|
+
c.config.dir1.key3 => 3
|
144
|
+
|
145
|
+
==== Static vs Dynamic
|
146
|
+
|
147
|
+
A dynamic multiplexer will re-evaluate the selecor proc every time it's called
|
148
|
+
|
149
|
+
c.config.insert_multiplexer({ :name => 'key',
|
150
|
+
:static => false,
|
151
|
+
:selector => Proc.new { rand(3) + 1 },
|
152
|
+
:inputs => {1 => :key1, 2 => :key2, 3 => :key3} })
|
153
|
+
c.config.key => 2 # time = 0
|
154
|
+
c.config.key => 1 # time = 0 + n
|
155
|
+
|
156
|
+
|
157
|
+
== Credits
|
158
|
+
|
159
|
+
Inspired by RConfig (Rahmal Conda) / activeconfig (Enova Financial)
|
data/TODO
ADDED
File without changes
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class ConfigLogic::Cache < SimpleDelegator
|
2
|
+
include ConfigLogic::Logger
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize(load_paths, params = {})
|
6
|
+
@load_paths = [load_paths].flatten
|
7
|
+
reload!(params)
|
8
|
+
super(@cache)
|
9
|
+
end
|
10
|
+
|
11
|
+
def reload!(params = {})
|
12
|
+
__setobj__(@cache)
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def each
|
17
|
+
@cache.each do |node|
|
18
|
+
yield node
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class ConfigLogic < SimpleDelegator
|
2
|
+
extend Forwardable
|
3
|
+
|
4
|
+
def_delegators :@cache, :reload!
|
5
|
+
def_delegators :@data, :inspect
|
6
|
+
|
7
|
+
def initialize(load_paths, params = {})
|
8
|
+
@cache = TreeCache.new(load_paths, params)
|
9
|
+
@path = params[:path] || []
|
10
|
+
@data = @cache[*@path]
|
11
|
+
super(@data)
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](*keys)
|
15
|
+
new_path = @path + keys
|
16
|
+
val = @cache[*new_path]
|
17
|
+
|
18
|
+
case val
|
19
|
+
when Hash
|
20
|
+
self.clone_with_new_path(new_path)
|
21
|
+
when ConfigLogic::LogicElement
|
22
|
+
val.output
|
23
|
+
else val
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def clone_with_new_path(path)
|
30
|
+
clone = self.dup
|
31
|
+
clone.instance_variable_set('@path', path)
|
32
|
+
new_delegate = \
|
33
|
+
clone.instance_variable_set('@data', @cache[*path])
|
34
|
+
clone.__setobj__(new_delegate)
|
35
|
+
clone
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def method_missing(method_symbol, *args)
|
41
|
+
method_name = method_symbol.to_s
|
42
|
+
element_matcher = ConfigLogic::LogicElement.available_elements.join('|')
|
43
|
+
|
44
|
+
if (val = self[method_symbol])
|
45
|
+
val
|
46
|
+
elsif /^insert_(#{element_matcher})$/ === method_name
|
47
|
+
type = ConfigLogic::LogicElement.name_to_type($1)
|
48
|
+
@cache.insert_logic_element(type.new(args.first), @data) if type
|
49
|
+
else
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Array
|
2
|
+
|
3
|
+
def stringify_symbols
|
4
|
+
self.map { |val| val.is_a?(Symbol) ? val.to_s : val }
|
5
|
+
end
|
6
|
+
|
7
|
+
def symbolize_strings
|
8
|
+
self.map { |val| val.is_a?(Symbol) ? val.to_s : val }
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_hash
|
12
|
+
self.inject({}) { |h, (key, val)| h[key] = val; h }
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'find'
|
3
|
+
|
4
|
+
class ConfigLogic::FileCache < ConfigLogic::Cache
|
5
|
+
|
6
|
+
PARSER_MAP = {
|
7
|
+
['.yaml', '.yml'] => lambda do |path| YAML::load_file(path) end,
|
8
|
+
['.txt'] => lambda do |path| File.read(path) end,
|
9
|
+
['.json'] => lambda do |path| end }.freeze
|
10
|
+
|
11
|
+
VALID_EXTENSIONS = PARSER_MAP.map { |(extensions, parser_proc)| extensions }.flatten!.freeze
|
12
|
+
|
13
|
+
def reload!(params = {})
|
14
|
+
@cache = rebuild_primary_cache(params)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def rebuild_primary_cache(params)
|
21
|
+
file_cache = []
|
22
|
+
load_file = Proc.new do |path|
|
23
|
+
end
|
24
|
+
|
25
|
+
valid_files = @load_paths.inject([]) do |valid, path|
|
26
|
+
valid << find_valid_files(path)
|
27
|
+
end.flatten
|
28
|
+
|
29
|
+
valid_files.each do |path|
|
30
|
+
content = parse(path)
|
31
|
+
file_cache << [path, content] if content
|
32
|
+
log.debug "found file #{BLUE} #{path}"
|
33
|
+
end
|
34
|
+
|
35
|
+
file_cache.inject({}) do
|
36
|
+
|h, (path, content)| h[path] = content; h
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_valid_files(path)
|
41
|
+
valid_files = []
|
42
|
+
Find.find(path) do |file|
|
43
|
+
valid_files << file if valid_file_ext?(file)
|
44
|
+
end
|
45
|
+
valid_files
|
46
|
+
end
|
47
|
+
|
48
|
+
# parsing helpers
|
49
|
+
|
50
|
+
def parse(path)
|
51
|
+
parser = find_parser(path)
|
52
|
+
return unless parser
|
53
|
+
|
54
|
+
begin
|
55
|
+
parser.call(path)
|
56
|
+
rescue => e
|
57
|
+
log.warn "#{RED} #{path} #{RESET} is not a valid config file"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def find_parser(path)
|
62
|
+
parser = PARSER_MAP.detect { |(extensions, parse_proc)| extensions.include?(File.extname(path)) }
|
63
|
+
parser ? parser.last : nil
|
64
|
+
end
|
65
|
+
|
66
|
+
# path helpers
|
67
|
+
|
68
|
+
def load_path_exists?(path)
|
69
|
+
unless File.exists?(path)
|
70
|
+
log.warn "#{RED} #{path} #{RESET} does not exist"; false
|
71
|
+
else true end
|
72
|
+
end
|
73
|
+
|
74
|
+
def valid_file_ext?(path)
|
75
|
+
VALID_EXTENSIONS.include?(File.extname(path))
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'buffered_logger'
|
2
|
+
|
3
|
+
class ConfigLogic::Log
|
4
|
+
private_class_method :new
|
5
|
+
LOG_LEVEL = 6
|
6
|
+
|
7
|
+
def self.init_log(log_level, color)
|
8
|
+
@log = BufferedLogger.new(STDOUT, log_level || LOG_LEVEL, default_format)
|
9
|
+
log.disable_color unless color
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.log
|
13
|
+
@log ||= BufferedLogger.new(STDOUT, LOG_LEVEL, default_format)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def self.default_format
|
19
|
+
{ :debug => "$negative DEBUG: $reset %s",
|
20
|
+
:warn => "$yellow WARNING: $reset %s",
|
21
|
+
:error => "$red ERROR: $reset %s" }
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
module ConfigLogic::Logger
|
28
|
+
module Colors
|
29
|
+
RED = '$red'
|
30
|
+
BLUE = '$blue'
|
31
|
+
GREEN = '$green'
|
32
|
+
YELLOW = '$yellow'
|
33
|
+
RESET = '$reset'
|
34
|
+
end
|
35
|
+
include Colors
|
36
|
+
|
37
|
+
def log
|
38
|
+
ConfigLogic::Log.log
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
class ConfigLogic::LogicElement
|
4
|
+
include ConfigLogic::Logger
|
5
|
+
|
6
|
+
MIN_INPUTS = 2
|
7
|
+
NAME = 'logic_element'
|
8
|
+
STATIC = true
|
9
|
+
|
10
|
+
attr_reader :name, :inputs
|
11
|
+
cattr_reader :min_inputs, :types
|
12
|
+
@@types = Set.new
|
13
|
+
@@min_inputs = MIN_INPUTS
|
14
|
+
|
15
|
+
def initialize(params)
|
16
|
+
@name = params[:name] || NAME
|
17
|
+
@static = params[:static].nil? ? STATIC : params[:static]
|
18
|
+
@inputs = HashWithIndifferentAccess.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def set_input(input_name, value)
|
22
|
+
@inputs[input_name] = value
|
23
|
+
end
|
24
|
+
|
25
|
+
def input_names
|
26
|
+
@inputs.keys
|
27
|
+
end
|
28
|
+
|
29
|
+
def static?
|
30
|
+
@static
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
|
35
|
+
def inherited(child)
|
36
|
+
@@types << child
|
37
|
+
end
|
38
|
+
|
39
|
+
def available_elements
|
40
|
+
@@types.map { |t| t.simple_name.downcase }
|
41
|
+
end
|
42
|
+
|
43
|
+
def name_to_type(name)
|
44
|
+
if available_elements.include?(name)
|
45
|
+
('ConfigLogic::' + name.classify).constantize
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def inputs_valid?
|
54
|
+
input_count = @inputs.values.compact.size
|
55
|
+
if input_count < min_inputs
|
56
|
+
log.error("number of inputs (#{input_count}) is less than minimum (#{@@min_inputs})")
|
57
|
+
false
|
58
|
+
else true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class ConfigLogic::Multiplexer < ConfigLogic::LogicElement
|
2
|
+
|
3
|
+
attr_reader :selector
|
4
|
+
|
5
|
+
def initialize(params)
|
6
|
+
super
|
7
|
+
@multiplexer = params[:inputs]
|
8
|
+
@inputs = HashWithIndifferentAccess.new(@multiplexer.values.to_hash)
|
9
|
+
@selector = params[:selector]
|
10
|
+
end
|
11
|
+
|
12
|
+
def output
|
13
|
+
return unless inputs_valid?
|
14
|
+
case @selector
|
15
|
+
when Proc
|
16
|
+
@inputs[@multiplexer[@selector.call]]
|
17
|
+
else
|
18
|
+
@inputs[@multiplexer[@selector]]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class ConfigLogic::Overlay < ConfigLogic::LogicElement
|
2
|
+
|
3
|
+
attr_reader :order
|
4
|
+
|
5
|
+
def initialize(params)
|
6
|
+
super
|
7
|
+
@order = params[:inputs]
|
8
|
+
@inputs = HashWithIndifferentAccess.new(@order.to_hash)
|
9
|
+
end
|
10
|
+
|
11
|
+
# merge if possible, return highest priority data if not
|
12
|
+
#
|
13
|
+
def output
|
14
|
+
return unless inputs_valid?
|
15
|
+
if @inputs.values.all? { |d| d.is_a? Hash }
|
16
|
+
@order.inject({}) do |output, input_name|
|
17
|
+
output.deep_merge!(@inputs[input_name])
|
18
|
+
end
|
19
|
+
else
|
20
|
+
@inputs[@order.last]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
class ConfigLogic::TreeCache < ConfigLogic::Cache
|
2
|
+
|
3
|
+
def initialize(load_paths, params = {})
|
4
|
+
@global_logic = params.inject([]) do |a, (type_name, elements)|
|
5
|
+
type_name = type_name.to_s.singularize
|
6
|
+
type = ConfigLogic::LogicElement.name_to_type(type_name)
|
7
|
+
type ? a << elements.map { |settings| type.new(settings) } : a
|
8
|
+
end.flatten
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](*key_path)
|
13
|
+
return @cache if key_path.blank?
|
14
|
+
key_path.inject(@cache) do |val, key|
|
15
|
+
val[key] if val
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def reload!(params = {})
|
20
|
+
@cache = rebuild_primary_cache(params)
|
21
|
+
unless @cache.empty?
|
22
|
+
trim_cache! unless params[:no_trim]
|
23
|
+
@flat_cache = flatten_tree_cache
|
24
|
+
apply_global_logic
|
25
|
+
end
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def insert_logic_element(element, node)
|
30
|
+
input_names = element.input_names
|
31
|
+
input_names.each do |input_name|
|
32
|
+
element.set_input(input_name, node[input_name])
|
33
|
+
end
|
34
|
+
log.debug "inserted logic element:\n#{element.pretty_inspect}"
|
35
|
+
|
36
|
+
node[element.name] = element.static? ? element.output : element
|
37
|
+
prune!(node, input_names)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def rebuild_primary_cache(params)
|
43
|
+
file_cache = ConfigLogic::FileCache.new(@load_paths)
|
44
|
+
return {} unless valid_file_cache?(file_cache)
|
45
|
+
|
46
|
+
tree_cache = HashWithIndifferentAccess.new
|
47
|
+
file_cache.each do |path, content|
|
48
|
+
tree_cache = tree_cache.deep_merge(hashify_path(path, content))
|
49
|
+
end
|
50
|
+
tree_cache
|
51
|
+
end
|
52
|
+
|
53
|
+
def hashify_path(path, content)
|
54
|
+
path_keys = split_file_path(path)
|
55
|
+
content = HashWithIndifferentAccess.new({ path_keys.pop => content })
|
56
|
+
path_keys.reverse.inject(content) do |content, key|
|
57
|
+
HashWithIndifferentAccess.new({ key => content })
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def trim_cache!
|
62
|
+
return if @cache.size > 1
|
63
|
+
|
64
|
+
until @cache.size > 1 do
|
65
|
+
@cache = @cache[@cache.keys.first]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# node cache helpers
|
70
|
+
|
71
|
+
def flatten_tree_cache
|
72
|
+
flatten = Proc.new do |tree_cache|
|
73
|
+
tree_cache.inject([]) do |hashes, (k,v)|
|
74
|
+
if v.is_a? Hash
|
75
|
+
hashes << v << flatten.call(v)
|
76
|
+
else hashes end
|
77
|
+
end.flatten
|
78
|
+
end
|
79
|
+
flatten.call(@cache) << @cache
|
80
|
+
end
|
81
|
+
|
82
|
+
def nodes_with_keys(keys, min_matches = 1)
|
83
|
+
self.select do |node|
|
84
|
+
node_keys = node.keys
|
85
|
+
node_keys.size - (node_keys - keys).size >= min_matches
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def prune!(node, keys)
|
90
|
+
node.reject! { |k, v| keys.include?(k) }
|
91
|
+
end
|
92
|
+
|
93
|
+
# logic helpers
|
94
|
+
|
95
|
+
def apply_global_logic
|
96
|
+
@global_logic.each do |element|
|
97
|
+
log.debug "applying #{element.class}: #{[element.name, element.input_names].inspect}"
|
98
|
+
insert_global_logic_element(element)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def insert_global_logic_element(element)
|
103
|
+
matching_nodes = nodes_with_keys(element.input_names, element.min_inputs)
|
104
|
+
log.debug "matching nodes:\n#{matching_nodes.pretty_inspect}"
|
105
|
+
matching_nodes.each do |node|
|
106
|
+
insert_logic_element(element, node)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# path helpers
|
111
|
+
|
112
|
+
def split_file_path(path)
|
113
|
+
basename = File.basename(path, '.*')
|
114
|
+
dirname = File.dirname(path)
|
115
|
+
dirname.split('/')[1..-1] << basename
|
116
|
+
end
|
117
|
+
|
118
|
+
def valid_file_cache?(file_cache)
|
119
|
+
if file_cache.empty?
|
120
|
+
log.error "file cache is empty"; false
|
121
|
+
else true end
|
122
|
+
end
|
123
|
+
|
124
|
+
# enumerable
|
125
|
+
|
126
|
+
def each
|
127
|
+
@flat_cache.each do |node|
|
128
|
+
yield node
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
data/lib/config_logic.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
$: << File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
5
|
+
require 'active_support/inflector'
|
6
|
+
require 'active_support/core_ext/string/inflections'
|
7
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
8
|
+
require 'active_support/core_ext/hash/deep_merge'
|
9
|
+
require 'buffered_logger'
|
10
|
+
require 'forwardable'
|
11
|
+
require 'delegate'
|
12
|
+
|
13
|
+
class ConfigLogic < SimpleDelegator
|
14
|
+
module Logger; end
|
15
|
+
include ConfigLogic::Logger
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'config_logic/core_ext/class'
|
19
|
+
require 'config_logic/core_ext/array'
|
20
|
+
require 'config_logic/logger'
|
21
|
+
require 'config_logic/cache'
|
22
|
+
require 'config_logic/file_cache'
|
23
|
+
require 'config_logic/logic_element'
|
24
|
+
require 'config_logic/multiplexer'
|
25
|
+
require 'config_logic/overlay'
|
26
|
+
require 'config_logic/tree_cache'
|
27
|
+
require 'config_logic/config_logic'
|
data/spec/cache_spec.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
2
|
+
|
3
|
+
describe ConfigLogic::Cache do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
@c = ConfigLogic::Cache.new(CONFIG_DIR)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should initialize' do
|
10
|
+
@c.should be_an_instance_of(ConfigLogic::Cache)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should delegate unknown calls to the cache container' do
|
14
|
+
cache = @c.instance_variable_get('@cache')
|
15
|
+
cache.public_methods(false).each do |m|
|
16
|
+
@c.should respond_to(m)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should be enumerable' do
|
21
|
+
@c.should respond_to(:each)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
2
|
+
|
3
|
+
describe ConfigLogic do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
@c = ConfigLogic.new(CONFIG_DIR)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should initialize' do
|
10
|
+
@c.should be_an_instance_of(ConfigLogic)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should delegate unknown calls to cache node pointed to by current path' do
|
14
|
+
node = @c.instance_variable_get('@data')
|
15
|
+
node.public_methods(false).each do |m|
|
16
|
+
@c.should respond_to(m)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should delegate :reload! and :inspect calls to cache' do
|
21
|
+
@c.should respond_to(:reload!)
|
22
|
+
@c.should respond_to(:inspect)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should be accessible via different access methods' do
|
26
|
+
@c[:dir1, :config1, :key1, :nestedkey1].should == 1
|
27
|
+
@c[:dir1][:config1][:key1][:nestedkey1].should == 1
|
28
|
+
@c['dir1', 'config1', 'key1', 'nestedkey1'].should == 1
|
29
|
+
@c['dir1']['config1']['key1']['nestedkey1'].should == 1
|
30
|
+
@c.dir1.config1.key1.nestedkey1.should == 1
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should return ConfigLogic hashes for any path that doesnt point to a simple value' do
|
34
|
+
@c.dir1.should be_a_kind_of(ConfigLogic)
|
35
|
+
@c.dir1.config1.should be_a_kind_of(ConfigLogic)
|
36
|
+
@c.dir1.config1.key1.should be_a_kind_of(ConfigLogic)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should apply local overlay elements' do
|
40
|
+
@c.dir1.config1.insert_overlay({:name => :over, :inputs => [:key1, :key2, :key3]})
|
41
|
+
overlayed_result = {"nestedkey1" => 1, "nestedkey2" => 2, "nestedkey3" => 3,
|
42
|
+
"nestedkey4" => 4, "nestedkey5" => 5, "nestedkey6" => 6,
|
43
|
+
"nestedkey7" => 7, "nestedkey8" => 8, "nestedkey9" => 9}
|
44
|
+
@c[:dir1][:config1][:over].to_hash.should == overlayed_result
|
45
|
+
@c[:dir1][:config1].keys.size.should == 1
|
46
|
+
@c[:dir1][:config2].keys.size.should == 3
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should apply local multiplexer elements' do
|
50
|
+
@c.dir1.insert_multiplexer({:name => :multi, :selector => 1, :inputs => {1 => :config1, 2 => :config2, 3 => :config3}})
|
51
|
+
multiplexed_result = {"key1"=>{"nestedkey1"=>1, "nestedkey2"=>2, "nestedkey3"=>3},
|
52
|
+
"key2"=>{"nestedkey4"=>4, "nestedkey5"=>5, "nestedkey6"=>6},
|
53
|
+
"key3"=>{"nestedkey7"=>7, "nestedkey8"=>8, "nestedkey9"=>9}}
|
54
|
+
@c[:dir1][:multi].to_hash.should == multiplexed_result
|
55
|
+
@c[:dir1].keys.size.should == 1
|
56
|
+
@c.keys.size.should == 5
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
2
|
+
|
3
|
+
describe ConfigLogic::FileCache do
|
4
|
+
|
5
|
+
def find_valid_files(path)
|
6
|
+
valid_files = ConfigLogic::FileCache::VALID_EXTENSIONS.inject([]) do |valid, ext|
|
7
|
+
valid << Dir["#{path}/**/*#{ext}"]
|
8
|
+
end.flatten
|
9
|
+
valid_files.reject { |f| f =~ /syntax_error/ }
|
10
|
+
end
|
11
|
+
|
12
|
+
def cache_valid?(path, cache)
|
13
|
+
cache = cache.keys
|
14
|
+
valid_files = find_valid_files(path)
|
15
|
+
cache.empty?.should be_false
|
16
|
+
cache.each do |cached_file|
|
17
|
+
valid_files.include?(cached_file).should be_true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
before :each do
|
22
|
+
@c = ConfigLogic::FileCache.new(CONFIG_DIR)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should initialize' do
|
26
|
+
@c.should be_an_instance_of(ConfigLogic::FileCache)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should build a valid file cache during initialization' do
|
30
|
+
cache_valid?(CONFIG_DIR, @c.instance_variable_get('@cache'))
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should rebuild cache during a reload!' do
|
34
|
+
config_dir = "#{CONFIG_DIR}/dir1"
|
35
|
+
@c.instance_variable_set('@load_paths', [config_dir])
|
36
|
+
@c.reload!
|
37
|
+
cache_valid?(config_dir, @c.instance_variable_get('@cache'))
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
2
|
+
|
3
|
+
describe ConfigLogic::LogicElement do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
settings = {:name => :a_logic_element}
|
7
|
+
@e = ConfigLogic::LogicElement.new(settings)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should initialize' do
|
11
|
+
@e.should be_an_instance_of(ConfigLogic::LogicElement)
|
12
|
+
@e.name.should == :a_logic_element
|
13
|
+
@e.static?.should == true
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should specify the minimum number of inputs' do
|
17
|
+
@e.class.min_inputs.should == 2
|
18
|
+
@e.min_inputs.should == 2
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should respond with all registered component types' do
|
22
|
+
(@e.class.available_elements - ['multiplexer', 'overlay']).should == []
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should set input values' do
|
26
|
+
@e.set_input(:a, 1)
|
27
|
+
@e.set_input(:b, 2)
|
28
|
+
@e.set_input(:c, 3)
|
29
|
+
@e.inputs.should == {'a' => 1, 'b' => 2, 'c' => 3}
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should determine if the input state is valid' do
|
33
|
+
@e.send(:inputs_valid?).should == false
|
34
|
+
@e.instance_variable_set('@inputs', {'a' => 1, 'b' => 2})
|
35
|
+
@e.send(:inputs_valid?).should == true
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should convert element name to type' do
|
39
|
+
@e.class.name_to_type('multiplexer').should == ConfigLogic::Multiplexer
|
40
|
+
@e.class.name_to_type('overlay').should == ConfigLogic::Overlay
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
2
|
+
|
3
|
+
describe ConfigLogic::Multiplexer do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
settings = { :name => :a_multiplexer,
|
7
|
+
:selector => Proc.new { 1 + 1 },
|
8
|
+
:inputs => {1 => :a, 2 => :b, 3 => :c} }
|
9
|
+
@m = ConfigLogic::Multiplexer.new(settings)
|
10
|
+
@m.set_input(:a, 10)
|
11
|
+
@m.set_input(:b, 11)
|
12
|
+
@m.set_input(:c, 12)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should initialize' do
|
16
|
+
@m.should be_an_instance_of(ConfigLogic::Multiplexer)
|
17
|
+
@m.should be_a_kind_of(ConfigLogic::LogicElement)
|
18
|
+
@m.name.should == :a_multiplexer
|
19
|
+
@m.inputs.should == {'a' => 10, 'b' => 11, 'c' => 12}
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should return selector proc' do
|
23
|
+
@m.should respond_to(:selector)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should correctly multiplex outputs based on selector' do
|
27
|
+
@m.output.should == 11
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
2
|
+
|
3
|
+
describe ConfigLogic::Overlay do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
settings = { :name => :an_overlay, :inputs => [:input1, :input2, :input3] }
|
7
|
+
@e = ConfigLogic::Overlay.new(settings)
|
8
|
+
@e.set_input(:input1, {:a => 1})
|
9
|
+
@e.set_input(:input2, {:b => 2})
|
10
|
+
@e.set_input(:input3, {:a => 2, :c => 3})
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should initialize' do
|
14
|
+
@e.should be_an_instance_of(ConfigLogic::Overlay)
|
15
|
+
@e.should be_a_kind_of(ConfigLogic::LogicElement)
|
16
|
+
@e.name.should == :an_overlay
|
17
|
+
@e.inputs.should == {'input1' => {'a' => 1},
|
18
|
+
'input2' => {'b' => 2},
|
19
|
+
'input3' => {'a' => 2, 'c' => 3}}
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should output a propper overlay when all inputs are hashes' do
|
23
|
+
@e.output.should == {'a' => 2, 'b' => 2, 'c' => 3}
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should output a propper overlay when not all inputs are hashes' do
|
27
|
+
settings = { :name => :an_overlay, :inputs => [:input1, :input2, :input3] }
|
28
|
+
e = ConfigLogic::Overlay.new(settings)
|
29
|
+
e.set_input(:input1, 1)
|
30
|
+
e.set_input(:input2, {:a => 1})
|
31
|
+
e.set_input(:input3, 3)
|
32
|
+
e.output.should == 3
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "spec_helper.rb")
|
2
|
+
|
3
|
+
describe ConfigLogic::TreeCache do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
@c = ConfigLogic::TreeCache.new(CONFIG_DIR)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should initialize' do
|
10
|
+
@c.should be_an_instance_of(ConfigLogic::TreeCache)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should build a valid tree cache' do
|
14
|
+
@c.to_hash.keys.sort.should == ['config1', 'config2', 'config3', 'dir1', 'dir4']
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should rebuild cache during a reload!' do
|
18
|
+
config_dir = "#{CONFIG_DIR}/dir1"
|
19
|
+
@c.instance_variable_set('@load_paths', [config_dir])
|
20
|
+
@c.reload!
|
21
|
+
@c.to_hash.keys.sort.should == ['config1', 'config2', 'config3']
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should apply global overlay elements' do
|
25
|
+
@c = ConfigLogic::TreeCache.new(CONFIG_DIR, :overlays => [{:name => :over, :inputs => [:key1, :key2, :key3]}] )
|
26
|
+
overlayed_result = {"nestedkey1" => 1, "nestedkey2" => 2, "nestedkey3" => 3,
|
27
|
+
"nestedkey4" => 4, "nestedkey5" => 5, "nestedkey6" => 6,
|
28
|
+
"nestedkey7" => 7, "nestedkey8" => 8, "nestedkey9" => 9}
|
29
|
+
@c[:dir1][:config1][:over].to_hash.should == overlayed_result
|
30
|
+
@c[:dir1][:config2][:over].to_hash.should == overlayed_result
|
31
|
+
@c[:dir1][:config3][:over].to_hash.should == overlayed_result
|
32
|
+
@c[:dir1][:config3].keys.size.should == 1
|
33
|
+
@c[:config1][:over].to_hash.should == overlayed_result
|
34
|
+
@c[:config2][:over].to_hash.should == overlayed_result
|
35
|
+
@c[:config3][:over].to_hash.should == overlayed_result
|
36
|
+
@c[:config3].keys.size.should == 1
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should apply global multiplexer elements' do
|
40
|
+
@c = ConfigLogic::TreeCache.new(CONFIG_DIR, :multiplexers => [{:name => :multi, :selector => 1, :inputs => {1 => :config1, 2 => :config2, 3 => :config3}}])
|
41
|
+
multiplexed_result = {"key1"=>{"nestedkey1"=>1, "nestedkey2"=>2, "nestedkey3"=>3},
|
42
|
+
"key2"=>{"nestedkey4"=>4, "nestedkey5"=>5, "nestedkey6"=>6},
|
43
|
+
"key3"=>{"nestedkey7"=>7, "nestedkey8"=>8, "nestedkey9"=>9}}
|
44
|
+
@c[:dir1][:multi].to_hash.should == multiplexed_result
|
45
|
+
@c[:dir1].keys.size.should == 1
|
46
|
+
@c[:multi].to_hash.should == multiplexed_result
|
47
|
+
@c.keys.size.should == 3
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should return node at any path' do
|
51
|
+
@c[:dir1, :config1, :key1, :nestedkey1].should == 1
|
52
|
+
@c[:dir1].should be_a_kind_of(Hash)
|
53
|
+
@c[:dir1, :config1, :key1].should be_a_kind_of(Hash)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: config_logic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.1.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alex Skryl
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-08-17 00:00:00 -05:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: activesupport
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 2.2.2
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: buffered_logger
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 0.1.2
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
description: An intuitive configuration access layer
|
39
|
+
email: rut216@gmail.com
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files: []
|
45
|
+
|
46
|
+
files:
|
47
|
+
- lib/config_logic.rb
|
48
|
+
- lib/config_logic/logger.rb
|
49
|
+
- lib/config_logic/config_logic.rb
|
50
|
+
- lib/config_logic/logic_element.rb
|
51
|
+
- lib/config_logic/cache.rb
|
52
|
+
- lib/config_logic/tree_cache.rb
|
53
|
+
- lib/config_logic/file_cache.rb
|
54
|
+
- lib/config_logic/multiplexer.rb
|
55
|
+
- lib/config_logic/core_ext/array.rb
|
56
|
+
- lib/config_logic/core_ext/class.rb
|
57
|
+
- lib/config_logic/overlay.rb
|
58
|
+
- spec/cache_spec.rb
|
59
|
+
- spec/file_cache_spec.rb
|
60
|
+
- spec/logic_element_spec.rb
|
61
|
+
- spec/multiplexer_spec.rb
|
62
|
+
- spec/overlay_spec.rb
|
63
|
+
- spec/config_logic_spec.rb
|
64
|
+
- spec/spec_helper.rb
|
65
|
+
- spec/tree_cache_spec.rb
|
66
|
+
- README.rdoc
|
67
|
+
- TODO
|
68
|
+
has_rdoc: true
|
69
|
+
homepage: http://github.com/skryl
|
70
|
+
licenses: []
|
71
|
+
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: "0"
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: "0"
|
89
|
+
requirements: []
|
90
|
+
|
91
|
+
rubyforge_project:
|
92
|
+
rubygems_version: 1.6.2
|
93
|
+
signing_key:
|
94
|
+
specification_version: 3
|
95
|
+
summary: A configuration access layer for Ruby/Rails applications
|
96
|
+
test_files: []
|
97
|
+
|