config_hound 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bf68a0009e160c2de3a2f7a14e189f526327a275
4
- data.tar.gz: e6845c1983d410b7b89653b0b1a9e00e76d83102
3
+ metadata.gz: 876182cbec6fa89633e1fd3b2fdbed37b88a714a
4
+ data.tar.gz: 9c342ccc39c549785c660c4b13b9ab52d17a6024
5
5
  SHA512:
6
- metadata.gz: 23399f2c12c0360b4f972e5bec88b8839ebdafcfc2f00f39047a0a5d3b90609e4a6a373d9541300755024b1b715686cbe5644943bcb62a15fc38336fbf63dd1c
7
- data.tar.gz: 0569f52b04d4e1304e7b52d0fa95f98efb3e902ac85014faf48e050fca5224aa713da6c3a463c06ad80048af5a88719bee19d5ae768143444d11798b383a6482
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 that is
4
- spread between multiple files.
3
+ ConfigHound makes it easy to load configuration data.
5
4
 
6
5
  ## Usage
7
6
 
8
- # load YAML
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
- # load JSON
12
- config = ConfigHound.load("config.json")
9
+ ```ruby
10
+ config = ConfigHound.load("config.yml") # or "config.{json,toml}"
11
+ ```
13
12
 
14
- # load TOML
15
- config = ConfigHound.load("config.toml")
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
- ConfigHound let's you include defaults from other files.
20
- Just list the file paths (or URLs) under the key "_include",
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
- _include:
25
- - defaults.yml
26
- pool:
27
- size: 10
28
- log:
29
- file: "app.log"
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
- log:
34
- level: INFO
35
- pool:
36
- size: 1
59
+ ```yaml
60
+ log:
61
+ level: INFO
62
+ pool:
63
+ size: 1
64
+ ```
37
65
 
38
- Values in the original config file override those from included files.
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
- config = ConfigHound.load("config.yml", :include_key => "defaults")
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,5 @@
1
+ module ConfigHound
2
+
3
+ Error = Class.new(StandardError)
4
+
5
+ end
@@ -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
@@ -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 = options.fetch(:include_key, DEFAULT_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(paths)
15
- Array(paths).reverse.map do |path|
16
- load_resource(Resource[path])
17
- end.reduce({}, &ConfigHound.method(:deep_merge_into))
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
- attr_reader :include_key
23
-
24
- def load_resource(resource)
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(StandardError)
8
+ LoadError = Class.new(ConfigHound::Error)
8
9
 
9
10
  # Represents a source of configuration data.
10
11
  #
@@ -1,3 +1,3 @@
1
1
  module ConfigHound
2
- VERSION = "1.2.1"
2
+ VERSION = "1.3.0"
3
3
  end
data/lib/config_hound.rb CHANGED
@@ -8,5 +8,3 @@ module ConfigHound
8
8
  end
9
9
 
10
10
  end
11
-
12
-
@@ -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
@@ -2,7 +2,7 @@ require "spec_helper"
2
2
 
3
3
  require "config_hound"
4
4
 
5
- describe ConfigHound do
5
+ describe ConfigHound, "formats" do
6
6
 
7
7
  def load(path)
8
8
  ConfigHound.load(path)
@@ -1,5 +1,8 @@
1
+ require "spec_helper"
1
2
 
2
- describe ConfigHound do
3
+ require "config_hound"
4
+
5
+ describe ConfigHound, "inclusion" do
3
6
 
4
7
  let(:options) { {} }
5
8
 
@@ -193,5 +196,3 @@ describe ConfigHound do
193
196
 
194
197
  end
195
198
  end
196
-
197
-
@@ -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.2.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: 2015-09-04 00:00:00.000000000 Z
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/basics_spec.rb
64
- - spec/features/include_spec.rb
65
- - spec/features/multi_file_spec.rb
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.4.8
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/basics_spec.rb
95
- - spec/features/include_spec.rb
96
- - spec/features/multi_file_spec.rb
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