config_hound 1.2.1 → 1.3.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.
- checksums.yaml +4 -4
- data/README.md +78 -22
- data/lib/config_hound/error.rb +5 -0
- data/lib/config_hound/interpolation.rb +73 -0
- data/lib/config_hound/loader.rb +16 -8
- data/lib/config_hound/resource.rb +2 -1
- data/lib/config_hound/version.rb +1 -1
- data/lib/config_hound.rb +0 -2
- data/spec/config_hound/interpolation_spec.rb +151 -0
- data/spec/features/expansion_spec.rb +19 -0
- data/spec/features/{basics_spec.rb → formats_spec.rb} +1 -1
- data/spec/features/{include_spec.rb → inclusion_spec.rb} +4 -3
- data/spec/features/sources_spec.rb +59 -0
- metadata +15 -9
- data/spec/features/multi_file_spec.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 876182cbec6fa89633e1fd3b2fdbed37b88a714a
|
4
|
+
data.tar.gz: 9c342ccc39c549785c660c4b13b9ab52d17a6024
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c05735b52a238ba1fd28189880ee341b95f61cbebc96ac733e6411531ec197e433ba931acdf660d98958c297db0c3396c3aae4141b5b4388f1ded3f162d290ee
|
7
|
+
data.tar.gz: b9a148f6097f6c42849845a8881367716629d7a97709efab0393ba95c89e01b177abbdcd1654783aeaeabe33c77eed3201c687c9a1d8de42fa23908573eb9b25
|
data/README.md
CHANGED
@@ -1,48 +1,104 @@
|
|
1
1
|
# ConfigHound
|
2
2
|
|
3
|
-
ConfigHound makes it easy to load configuration data
|
4
|
-
spread between multiple files.
|
3
|
+
ConfigHound makes it easy to load configuration data.
|
5
4
|
|
6
5
|
## Usage
|
7
6
|
|
8
|
-
|
9
|
-
config = ConfigHound.load("config.yml")
|
7
|
+
`ConfigHound.load` supports config in JSON, YAML or TOML formats, and returns raw Ruby data.
|
10
8
|
|
11
|
-
|
12
|
-
|
9
|
+
```ruby
|
10
|
+
config = ConfigHound.load("config.yml") # or "config.{json,toml}"
|
11
|
+
```
|
13
12
|
|
14
|
-
|
15
|
-
|
13
|
+
## Remote config
|
14
|
+
|
15
|
+
As well as local files, you can load from any URI supported by `OpenURI`, e.g.
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
# load over HTTP
|
19
|
+
ConfigHound.load("http://config-source/app-config.yml")
|
20
|
+
|
21
|
+
# load from S3
|
22
|
+
require "open-uri-s3"
|
23
|
+
config = ConfigHound.load("s3://config-bucket/app-config.json")
|
24
|
+
```
|
25
|
+
|
26
|
+
## Multiple sources of config
|
27
|
+
|
28
|
+
If you specify a list of config sources, ConfigHound will load them all, and deep-merge the data. Files specified earlier in the list take precedence.
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
ConfigHound.load(["config.yml", "defaults.yml"])
|
32
|
+
```
|
33
|
+
|
34
|
+
You can include raw data (Hashes) in the list, too, which is handy if you have defaults or overrides already in Ruby format.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
overrides = { ... }
|
38
|
+
ConfigHound.load([overrides, "config.yml"])
|
39
|
+
```
|
16
40
|
|
17
41
|
## Inclusion
|
18
42
|
|
19
|
-
|
20
|
-
Just list the
|
43
|
+
You can also "include" other file (or URIs) from _within_ a config file.
|
44
|
+
Just list the paths under the key `_include`.
|
21
45
|
|
22
46
|
For example, in `config.yml`:
|
23
47
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
48
|
+
```yaml
|
49
|
+
pool:
|
50
|
+
size: 10
|
51
|
+
log:
|
52
|
+
file: "app.log"
|
53
|
+
_include:
|
54
|
+
- defaults.yml
|
55
|
+
```
|
30
56
|
|
31
57
|
then in `defaults.yml`
|
32
58
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
59
|
+
```yaml
|
60
|
+
log:
|
61
|
+
level: INFO
|
62
|
+
pool:
|
63
|
+
size: 1
|
64
|
+
```
|
37
65
|
|
38
|
-
Values in the original config file
|
66
|
+
Values in the original config file take precedence over those from included files.
|
39
67
|
Multiple levels of inclusion are possible.
|
40
68
|
|
41
69
|
If the placeholder "`_include`" doesn't suit, you can specify
|
42
70
|
another, e.g.
|
43
71
|
|
44
|
-
|
72
|
+
```ruby
|
73
|
+
config = ConfigHound.load("config.yml", :include_key => "defaults")
|
74
|
+
```
|
75
|
+
|
76
|
+
## Reference expansion
|
77
|
+
|
78
|
+
ConfigHound can expand references of the form `<(X.Y.Z)>` in config values, which can help DRY up configuration, e.g.
|
79
|
+
|
80
|
+
```yaml
|
81
|
+
name: myapp
|
82
|
+
aws:
|
83
|
+
region: us-west-1
|
84
|
+
log:
|
85
|
+
stream: <(name)>-logs-<(aws.region)>
|
86
|
+
```
|
87
|
+
|
88
|
+
Enable reference expansion with the `:expand_refs` option.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
ConfigHound.load(config_files, :expand_refs => true)
|
92
|
+
```
|
93
|
+
|
94
|
+
Reference expansion is performed _after_ all config is loaded and merged, so you can reference config specified in other files.
|
45
95
|
|
46
96
|
## Contributing
|
47
97
|
|
48
98
|
It's on GitHub; you know the drill.
|
99
|
+
|
100
|
+
## See also
|
101
|
+
|
102
|
+
ConfigHound works well with:
|
103
|
+
|
104
|
+
* [ConfigMapper](https://github.com/mdub/config_mapper)
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "config_hound/error"
|
2
|
+
|
3
|
+
module ConfigHound
|
4
|
+
|
5
|
+
# Expand variables
|
6
|
+
#
|
7
|
+
module Interpolation
|
8
|
+
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def expand(input, root = input, seen = Set.new)
|
12
|
+
Context.new(root).expand(input)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Interpolation context
|
16
|
+
#
|
17
|
+
class Context
|
18
|
+
|
19
|
+
def initialize(root, seen = Set.new)
|
20
|
+
@root = root
|
21
|
+
@seen = seen.freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :root
|
25
|
+
attr_reader :seen
|
26
|
+
|
27
|
+
def expand(input)
|
28
|
+
case input
|
29
|
+
when Hash
|
30
|
+
expand_hash(input)
|
31
|
+
when Array
|
32
|
+
input.map { |v| expand(v) }
|
33
|
+
when /\A<\(([\w.]+)\)>\Z/
|
34
|
+
evaluate_expression($1)
|
35
|
+
when /<\([\w.]+\)>/
|
36
|
+
input.gsub(/<\(([\w.]+)\)>/) do
|
37
|
+
evaluate_expression($1)
|
38
|
+
end
|
39
|
+
else
|
40
|
+
input
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def expand_hash(input)
|
47
|
+
input.each_with_object({}) do |(k,v), a|
|
48
|
+
a[k] = expand(v)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def evaluate_expression(expr)
|
53
|
+
if seen.include?(expr)
|
54
|
+
details = seen.map { |e| "<(#{e})>" }.join(", ")
|
55
|
+
raise CircularReferenceError, "circular reference: #{details}"
|
56
|
+
end
|
57
|
+
words = expr.split(".")
|
58
|
+
expansion = root.dig(*words)
|
59
|
+
if expansion.nil?
|
60
|
+
raise ReferenceError, "cannot resolve reference: <(#{expr})>"
|
61
|
+
end
|
62
|
+
subcontext = Context.new(root, seen + [expr])
|
63
|
+
subcontext.expand(expansion)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
ReferenceError = Class.new(ConfigHound::Error)
|
69
|
+
CircularReferenceError = Class.new(ReferenceError)
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
data/lib/config_hound/loader.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "config_hound/deep_merge"
|
2
|
+
require "config_hound/interpolation"
|
2
3
|
require "config_hound/resource"
|
3
4
|
|
4
5
|
module ConfigHound
|
@@ -7,21 +8,28 @@ module ConfigHound
|
|
7
8
|
|
8
9
|
DEFAULT_INCLUDE_KEY = "_include"
|
9
10
|
|
11
|
+
attr_accessor :include_key
|
12
|
+
attr_accessor :expand_refs
|
13
|
+
|
10
14
|
def initialize(options = {})
|
11
|
-
@include_key =
|
15
|
+
@include_key = DEFAULT_INCLUDE_KEY
|
16
|
+
options.each do |k, v|
|
17
|
+
public_send("#{k}=", v)
|
18
|
+
end
|
12
19
|
end
|
13
20
|
|
14
|
-
def load(
|
15
|
-
Array(
|
16
|
-
|
17
|
-
|
21
|
+
def load(sources)
|
22
|
+
raw_hashes = Array(sources).map(&method(:load_source))
|
23
|
+
result = raw_hashes.reverse.reduce({}, &ConfigHound.method(:deep_merge_into))
|
24
|
+
result = Interpolation.expand(result) if expand_refs
|
25
|
+
result
|
18
26
|
end
|
19
27
|
|
20
28
|
private
|
21
29
|
|
22
|
-
|
23
|
-
|
24
|
-
|
30
|
+
def load_source(source)
|
31
|
+
return source if source.is_a?(Hash)
|
32
|
+
resource = Resource[source]
|
25
33
|
raw_data = resource.load
|
26
34
|
includes = Array(raw_data.delete(include_key))
|
27
35
|
included_resources = includes.map do |relative_path|
|
@@ -1,10 +1,11 @@
|
|
1
|
+
require "config_hound/error"
|
1
2
|
require "config_hound/parser"
|
2
3
|
require "open-uri"
|
3
4
|
require "uri"
|
4
5
|
|
5
6
|
module ConfigHound
|
6
7
|
|
7
|
-
LoadError = Class.new(
|
8
|
+
LoadError = Class.new(ConfigHound::Error)
|
8
9
|
|
9
10
|
# Represents a source of configuration data.
|
10
11
|
#
|
data/lib/config_hound/version.rb
CHANGED
@@ -0,0 +1,151 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "config_hound/interpolation"
|
4
|
+
|
5
|
+
describe ConfigHound::Interpolation do
|
6
|
+
|
7
|
+
describe ".expand" do
|
8
|
+
|
9
|
+
let(:output) { described_class.expand(input) }
|
10
|
+
|
11
|
+
context "with a simple config Hash" do
|
12
|
+
|
13
|
+
let(:input) do
|
14
|
+
{
|
15
|
+
"author" => {
|
16
|
+
"name" => "Mike"
|
17
|
+
}
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns the input" do
|
22
|
+
expect(output).to eql(input)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
context "with reference" do
|
28
|
+
|
29
|
+
let(:input) do
|
30
|
+
{
|
31
|
+
"x" => 42,
|
32
|
+
"y" => "<(x)>"
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
it "expands the reference" do
|
37
|
+
expect(output["y"]).to eql(42)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
context "with complex reference" do
|
43
|
+
|
44
|
+
let(:input) do
|
45
|
+
{
|
46
|
+
"vars" => {
|
47
|
+
"author" => {
|
48
|
+
"name" => "Mike"
|
49
|
+
}
|
50
|
+
},
|
51
|
+
"foo" => "<(vars.author.name)>"
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
it "expands the reference" do
|
56
|
+
expect(output["foo"]).to eql("Mike")
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
context "with reference in nested object" do
|
62
|
+
|
63
|
+
let(:input) do
|
64
|
+
{
|
65
|
+
"vars" => {
|
66
|
+
"author" => {
|
67
|
+
"name" => "Mike"
|
68
|
+
}
|
69
|
+
},
|
70
|
+
"foo" => {
|
71
|
+
"bar" => "<(vars.author.name)>"
|
72
|
+
}
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
it "expands the reference" do
|
77
|
+
expect(output["foo"]["bar"]).to eql("Mike")
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
context "with reference embedded in a String" do
|
83
|
+
|
84
|
+
let(:input) do
|
85
|
+
{
|
86
|
+
"size" => 42,
|
87
|
+
"desc" => "Size <(size)>"
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
it "expands the reference" do
|
92
|
+
expect(output["desc"]).to eql("Size 42")
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
context "when multiple references" do
|
98
|
+
|
99
|
+
let(:input) do
|
100
|
+
{
|
101
|
+
"everything" => "<(parts.first)> and <(parts.last)>",
|
102
|
+
"parts" => {
|
103
|
+
"first" => "<(article)> one",
|
104
|
+
"last" => "<(article)> two"
|
105
|
+
},
|
106
|
+
"article" => "Part"
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
it "expands the reference" do
|
111
|
+
expect(output["everything"]).to eql("Part one and Part two")
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
context "with circular reference" do
|
119
|
+
|
120
|
+
let(:input) do
|
121
|
+
{
|
122
|
+
"ping" => "refers to <(pong)>",
|
123
|
+
"pong" => "refers to <(ping)>"
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
it "raises a ReferenceError" do
|
128
|
+
expect {
|
129
|
+
described_class.expand(input)
|
130
|
+
}.to raise_error(ConfigHound::Interpolation::ReferenceError)
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
context "with unresolved reference" do
|
136
|
+
|
137
|
+
let(:input) do
|
138
|
+
{
|
139
|
+
"foo" => "refers to <(bar)>"
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
it "raises a ReferenceError" do
|
144
|
+
expect {
|
145
|
+
described_class.expand(input)
|
146
|
+
}.to raise_error(ConfigHound::Interpolation::ReferenceError)
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "config_hound"
|
4
|
+
|
5
|
+
describe ConfigHound, "expansion" do
|
6
|
+
|
7
|
+
let(:config) { ConfigHound.load("config.yml", :expand_refs => true) }
|
8
|
+
|
9
|
+
given_resource "config.yml", %{
|
10
|
+
var:
|
11
|
+
port: 5678
|
12
|
+
address: host:<(var.port)>
|
13
|
+
}
|
14
|
+
|
15
|
+
it "expands references" do
|
16
|
+
expect(config["address"]).to eq("host:5678")
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "config_hound"
|
4
|
+
|
5
|
+
describe ConfigHound, "sources" do
|
6
|
+
|
7
|
+
let(:defaults) do
|
8
|
+
{
|
9
|
+
"meta" => {
|
10
|
+
"winner" => "defaults"
|
11
|
+
},
|
12
|
+
"data" => {
|
13
|
+
"from-defaults" => "D"
|
14
|
+
}
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
given_resource "fileA.yml", %{
|
19
|
+
meta:
|
20
|
+
winner: fileA
|
21
|
+
data:
|
22
|
+
from-fileA: A
|
23
|
+
}
|
24
|
+
|
25
|
+
given_resource "fileB.yml", %{
|
26
|
+
meta:
|
27
|
+
winner: fileB
|
28
|
+
data:
|
29
|
+
from-fileB: B
|
30
|
+
}
|
31
|
+
|
32
|
+
let(:overrides) do
|
33
|
+
{
|
34
|
+
"meta" => {
|
35
|
+
"winner" => "overrides"
|
36
|
+
},
|
37
|
+
"data" => {
|
38
|
+
"from-overrides" => "O"
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
let(:config) do
|
44
|
+
ConfigHound.load([overrides, "fileA.yml", "fileB.yml", defaults])
|
45
|
+
end
|
46
|
+
|
47
|
+
it "merges file contents with hashes" do
|
48
|
+
data = config.fetch("data")
|
49
|
+
expect(data["from-overrides"]).to eql("O")
|
50
|
+
expect(data["from-fileA"]).to eql("A")
|
51
|
+
expect(data["from-fileB"]).to eql("B")
|
52
|
+
expect(data["from-defaults"]).to eql("D")
|
53
|
+
end
|
54
|
+
|
55
|
+
it "favours the earliest source" do
|
56
|
+
expect(config["meta"]["winner"]).to eq("overrides")
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: config_hound
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-07-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -54,15 +54,19 @@ files:
|
|
54
54
|
- config_hound.gemspec
|
55
55
|
- lib/config_hound.rb
|
56
56
|
- lib/config_hound/deep_merge.rb
|
57
|
+
- lib/config_hound/error.rb
|
58
|
+
- lib/config_hound/interpolation.rb
|
57
59
|
- lib/config_hound/loader.rb
|
58
60
|
- lib/config_hound/parser.rb
|
59
61
|
- lib/config_hound/resource.rb
|
60
62
|
- lib/config_hound/version.rb
|
61
63
|
- spec/config_hound/deep_merge_spec.rb
|
64
|
+
- spec/config_hound/interpolation_spec.rb
|
62
65
|
- spec/config_hound/resource_spec.rb
|
63
|
-
- spec/features/
|
64
|
-
- spec/features/
|
65
|
-
- spec/features/
|
66
|
+
- spec/features/expansion_spec.rb
|
67
|
+
- spec/features/formats_spec.rb
|
68
|
+
- spec/features/inclusion_spec.rb
|
69
|
+
- spec/features/sources_spec.rb
|
66
70
|
- spec/spec_helper.rb
|
67
71
|
homepage: https://github.com/mdub/config_hound
|
68
72
|
licenses:
|
@@ -84,14 +88,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
88
|
version: '0'
|
85
89
|
requirements: []
|
86
90
|
rubyforge_project:
|
87
|
-
rubygems_version: 2.
|
91
|
+
rubygems_version: 2.6.12
|
88
92
|
signing_key:
|
89
93
|
specification_version: 4
|
90
94
|
summary: Sniffs out config, wherever it may be.
|
91
95
|
test_files:
|
92
96
|
- spec/config_hound/deep_merge_spec.rb
|
97
|
+
- spec/config_hound/interpolation_spec.rb
|
93
98
|
- spec/config_hound/resource_spec.rb
|
94
|
-
- spec/features/
|
95
|
-
- spec/features/
|
96
|
-
- spec/features/
|
99
|
+
- spec/features/expansion_spec.rb
|
100
|
+
- spec/features/formats_spec.rb
|
101
|
+
- spec/features/inclusion_spec.rb
|
102
|
+
- spec/features/sources_spec.rb
|
97
103
|
- spec/spec_helper.rb
|
@@ -1,28 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
require "config_hound"
|
4
|
-
|
5
|
-
describe ConfigHound do
|
6
|
-
|
7
|
-
let(:config) { ConfigHound.load(["fileA.yml", "fileB.yml"]) }
|
8
|
-
|
9
|
-
given_resource "fileA.yml", %{
|
10
|
-
source: A
|
11
|
-
fromA: true
|
12
|
-
}
|
13
|
-
|
14
|
-
given_resource "fileB.yml", %{
|
15
|
-
source: B
|
16
|
-
fromB: true
|
17
|
-
}
|
18
|
-
|
19
|
-
it "loads both files" do
|
20
|
-
expect(config).to have_key("fromA")
|
21
|
-
expect(config).to have_key("fromB")
|
22
|
-
end
|
23
|
-
|
24
|
-
it "favours earliest included file" do
|
25
|
-
expect(config["source"]).to eq("A")
|
26
|
-
end
|
27
|
-
|
28
|
-
end
|