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 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