config_logic 0.1.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/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
|
+
|