unobtainium 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,199 +0,0 @@
1
- # coding: utf-8
2
- #
3
- # unobtainium
4
- # https://github.com/jfinkhaeuser/unobtainium
5
- #
6
- # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium contributors.
7
- # All rights reserved.
8
- #
9
-
10
- require 'unobtainium/recursive_merge'
11
-
12
- module Unobtainium
13
-
14
- ##
15
- # The PathedHash class wraps Hash by offering pathed access on top of
16
- # regular access, i.e. instead of `h["first"]["second"]` you can write
17
- # `h["first.second"]`.
18
- #
19
- # The main benefit is much simpler code for accessing nested structured.
20
- # For any given path, PathedHash will return nil from `[]` if *any* of
21
- # the path components do not exist.
22
- #
23
- # Similarly, intermediate nodes will be created when you write a value
24
- # for a path.
25
- #
26
- # PathedHash also includes RecursiveMerge.
27
- class PathedHash
28
- include RecursiveMerge
29
-
30
- DEFAULT_PROC = proc do |hash, key|
31
- case key
32
- when String
33
- sym = key.to_sym
34
- hash[sym] if hash.key?(sym)
35
- when Symbol
36
- str = key.to_s
37
- hash[str] if hash.key?(str)
38
- end
39
- end.freeze
40
-
41
- ##
42
- # Initializer. Accepts `nil`, hashes or pathed hashes.
43
- #
44
- # @param init [NilClass, Hash] initial values.
45
- def initialize(init = nil)
46
- if init.nil?
47
- @data = {}
48
- else
49
- @data = init.dup
50
- end
51
- @separator = '.'
52
-
53
- @data.default_proc = DEFAULT_PROC
54
- end
55
-
56
- # @return [String] the separator is the character or pattern splitting paths.
57
- attr_accessor :separator
58
-
59
- # @api private
60
- # Methods redefined to support pathed read access.
61
- READ_METHODS = [
62
- :[], :default, :delete, :fetch, :has_key?, :include?, :key?, :member?,
63
- ].freeze
64
-
65
- # @api private
66
- # Methods redefined to support pathed write access.
67
- WRITE_METHODS = [
68
- :[]=, :store,
69
- ].freeze
70
-
71
- ##
72
- # @return [RegExp] the pattern to split paths at; based on `separator`
73
- def split_pattern
74
- /(?<!\\)#{Regexp.escape(@separator)}/
75
- end
76
-
77
- (READ_METHODS + WRITE_METHODS).each do |method|
78
- # Wrap all accessor functions to deal with paths
79
- define_method(method) do |*args, &block|
80
- # If there are no arguments, there's nothing to do with paths. Just
81
- # delegate to the hash.
82
- if args.empty?
83
- return @data.send(method, *args, &block)
84
- end
85
-
86
- # With any of the dispatch methods, we know that the first argument has
87
- # to be a key. We'll try to split it by the path separator.
88
- components = args[0].to_s.split(split_pattern)
89
- loop do
90
- if components.empty? or not components[0].empty?
91
- break
92
- end
93
- components.shift
94
- end
95
-
96
- # If there are no components, return self/the root
97
- if components.empty?
98
- return self
99
- end
100
-
101
- # This PathedHash is already the leaf-most Hash
102
- if components.length == 1
103
- # Weird edge case: if we didn't have to shift anything, then it's
104
- # possible we inadvertently changed a symbol key into a string key,
105
- # which could mean looking fails.
106
- # We can detect that by comparing copy[0] to a symbolized version of
107
- # components[0].
108
- copy = args.dup
109
- if copy[0] != components[0].to_sym
110
- copy[0] = components[0]
111
- end
112
- return @data.send(method, *copy, &block)
113
- end
114
-
115
- # Deal with other paths. The frustrating part here is that for nested
116
- # hashes, only this outermost one is guaranteed to know anything about
117
- # path splitting, so we'll have to recurse down to the leaf here.
118
- #
119
- # For write methods, we need to create intermediary hashes.
120
- leaf = recursive_fetch(components, @data,
121
- create: WRITE_METHODS.include?(method))
122
- if leaf.is_a? Hash
123
- leaf.default_proc = DEFAULT_PROC
124
- end
125
- if leaf.nil?
126
- leaf = @data
127
- end
128
-
129
- # If we have a leaf, we want to send the requested method to that
130
- # leaf.
131
- copy = args.dup
132
- copy[0] = components.last
133
- return leaf.send(method, *copy, &block)
134
- end
135
- end
136
-
137
- # @return [String] string representation
138
- def to_s
139
- @data.to_s
140
- end
141
-
142
- # @return [PathedHash] duplicate, as `.dup` usually works
143
- def dup
144
- PathedHash.new(@data.dup)
145
- end
146
-
147
- # In place merge, as it usually works for hashes.
148
- # @return [PathedHash] self
149
- def merge!(*args, &block)
150
- # FIXME: we may need other methods like this. This is used by
151
- # RecursiveMerge, so we know it's required.
152
- PathedHash.new(super)
153
- end
154
-
155
- ##
156
- # Map any missing method to the Hash implementation
157
- def respond_to_missing?(meth, include_private = false)
158
- if not @data.nil? and @data.respond_to?(meth, include_private)
159
- return true
160
- end
161
- return super
162
- end
163
-
164
- ##
165
- # Map any missing method to the Hash implementation
166
- def method_missing(meth, *args, &block)
167
- if not @data.nil? and @data.respond_to?(meth)
168
- return @data.send(meth.to_s, *args, &block)
169
- end
170
- return super
171
- end
172
-
173
- private
174
-
175
- ##
176
- # Given the path components, recursively fetch any but the last key.
177
- def recursive_fetch(path, data, options = {})
178
- # For the leaf element, we do nothing because that's where we want to
179
- # dispatch to.
180
- if path.length == 1
181
- return data
182
- end
183
-
184
- # Split path into head and tail; for the next iteration, we'll look use only
185
- # head, and pass tail on recursively.
186
- head = path[0]
187
- tail = path.slice(1, path.length)
188
-
189
- # If we're a write function, then we need to create intermediary objects,
190
- # i.e. what's at head if nothing is there.
191
- if options[:create] and data[head].nil?
192
- data[head] = {}
193
- end
194
-
195
- # Ok, recurse.
196
- return recursive_fetch(tail, data[head], options)
197
- end
198
- end # class PathedHash
199
- end # module Unobtainium
@@ -1,55 +0,0 @@
1
- # coding: utf-8
2
- #
3
- # unobtainium
4
- # https://github.com/jfinkhaeuser/unobtainium
5
- #
6
- # Copyright (c) 2016 Jens Finkhaeuser and other unobtainium contributors.
7
- # All rights reserved.
8
- #
9
-
10
- module Unobtainium
11
- ##
12
- # Provides recursive merge functions for hashes. Used in PathedHash.
13
- module RecursiveMerge
14
- ##
15
- # Recursively merge `:other` into this Hash.
16
- #
17
- # This starts by merging the leaf-most Hash entries. Arrays are merged
18
- # by addition.
19
- #
20
- # For everything that's neither Hash or Array, if the `:overwrite`
21
- # parameter is true, the entry from `:other` is used. Otherwise the entry
22
- # from `:self` is used.
23
- #
24
- # @param other [Hash] the hash to merge into `:self`
25
- # @param overwrite [Boolean] see method description.
26
- def recursive_merge!(other, overwrite = true)
27
- if other.nil?
28
- return self
29
- end
30
-
31
- merger = proc do |_, v1, v2|
32
- # rubocop:disable Style/GuardClause
33
- if v1.is_a? Hash and v2.is_a? Hash
34
- next v1.merge(v2, &merger)
35
- elsif v1.is_a? Array and v2.is_a? Array
36
- next v1 + v2
37
- end
38
- if overwrite
39
- next v2
40
- else
41
- next v1
42
- end
43
- # rubocop:enable Style/GuardClause
44
- end
45
- merge!(other, &merger)
46
- end
47
-
48
- ##
49
- # Same as `dup.recursive_merge!`
50
- # @param (see #recursive_merge!)
51
- def recursive_merge(other, overwrite = true)
52
- dup.recursive_merge!(other, overwrite)
53
- end
54
- end # module RecursiveMerge
55
- end # module Unobtainium
data/spec/config_spec.rb DELETED
@@ -1,119 +0,0 @@
1
- require 'spec_helper'
2
- require_relative '../lib/unobtainium/config'
3
-
4
- describe ::Unobtainium::Config do
5
- before :each do
6
- @data_path = File.join(File.dirname(__FILE__), 'data')
7
- end
8
-
9
- it "fails to load a nonexistent file" do
10
- expect { ::Unobtainium::Config.load_config("_nope_.yaml") }.to \
11
- raise_error Errno::ENOENT
12
- end
13
-
14
- it "is asked to load an unrecognized extension" do
15
- expect { ::Unobtainium::Config.load_config("_nope_.cfg") }.to \
16
- raise_error ArgumentError
17
- end
18
-
19
- it "loads a yaml config with a top-level hash correctly" do
20
- config = File.join(@data_path, 'hash.yml')
21
- cfg = ::Unobtainium::Config.load_config(config)
22
-
23
- expect(cfg["foo"]).to eql "bar"
24
- expect(cfg["baz"]).to eql "quux"
25
- end
26
-
27
- it "loads a yaml config with a top-level array correctly" do
28
- config = File.join(@data_path, 'array.yaml')
29
- cfg = ::Unobtainium::Config.load_config(config)
30
-
31
- expect(cfg["config"]).to eql %w(foo bar)
32
- end
33
-
34
- it "loads a JSON config correctly" do
35
- config = File.join(@data_path, 'test.json')
36
- cfg = ::Unobtainium::Config.load_config(config)
37
-
38
- expect(cfg["foo"]).to eql "bar"
39
- expect(cfg["baz"]).to eql 42
40
- end
41
-
42
- it "merges a hashed config correctly" do
43
- config = File.join(@data_path, 'hashmerge.yml')
44
- cfg = ::Unobtainium::Config.load_config(config)
45
-
46
- expect(cfg["asdf"]).to eql 1
47
- expect(cfg["foo.bar"]).to eql "baz"
48
- expect(cfg["foo.quux"]).to eql [1, 42]
49
- expect(cfg["foo.baz"]).to eql 3.14
50
- expect(cfg["blargh"]).to eql false
51
- end
52
-
53
- it "merges an array config correctly" do
54
- config = File.join(@data_path, 'arraymerge.yaml')
55
- cfg = ::Unobtainium::Config.load_config(config)
56
-
57
- expect(cfg["config"]).to eql %w(foo bar baz)
58
- end
59
-
60
- it "merges an array and hash config" do
61
- config = File.join(@data_path, 'mergefail.yaml')
62
- cfg = ::Unobtainium::Config.load_config(config)
63
-
64
- expect(cfg["config"]).to eql %w(array in main config)
65
- expect(cfg["local"]).to eql "override is a hash"
66
- end
67
-
68
- it "overrides configuration variables from the environment" do
69
- config = File.join(@data_path, 'hash.yml')
70
- cfg = ::Unobtainium::Config.load_config(config)
71
-
72
- ENV["BAZ"] = "override"
73
- expect(cfg["foo"]).to eql "bar"
74
- expect(cfg["baz"]).to eql "override"
75
- end
76
-
77
- it "treats an empty YAML file as an empty hash" do
78
- config = File.join(@data_path, 'empty.yml')
79
- cfg = ::Unobtainium::Config.load_config(config)
80
- expect(cfg).to be_empty
81
- end
82
-
83
- it "extends configuration hashes" do
84
- config = File.join(@data_path, 'driverconfig.yml')
85
- cfg = ::Unobtainium::Config.load_config(config)
86
-
87
- # First, test for non-extended values
88
- expect(cfg["drivers.mock.mockoption"]).to eql 42
89
- expect(cfg["drivers.branch1.branch1option"]).to eql "foo"
90
- expect(cfg["drivers.branch2.branch2option"]).to eql "bar"
91
- expect(cfg["drivers.leaf.leafoption"]).to eql "baz"
92
-
93
- # Now test extended values
94
- expect(cfg["drivers.branch1.mockoption"]).to eql 42
95
- expect(cfg["drivers.branch2.mockoption"]).to eql 42
96
- expect(cfg["drivers.leaf.mockoption"]).to eql 42
97
-
98
- expect(cfg["drivers.branch2.branch1option"]).to eql "foo"
99
- expect(cfg["drivers.leaf.branch1option"]).to eql "override" # not "foo" !
100
-
101
- expect(cfg["drivers.leaf.branch2option"]).to eql "bar"
102
-
103
- # Also test that all levels go back to base == mock
104
- expect(cfg["drivers.branch1.base"]).to eql 'mock'
105
- expect(cfg["drivers.branch2.base"]).to eql 'mock'
106
- expect(cfg["drivers.leaf.base"]).to eql 'mock'
107
- end
108
-
109
- it "extends configuration hashes when the base does not exist" do
110
- config = File.join(@data_path, 'driverconfig.yml')
111
- cfg = ::Unobtainium::Config.load_config(config)
112
-
113
- # Ensure the hash contains its own value
114
- expect(cfg["drivers.base_does_not_exist.some"]).to eql "value"
115
-
116
- # Also ensure the "base" is set properly
117
- expect(cfg["drivers.base_does_not_exist.base"]).to eql "nonexistent_base"
118
- end
119
- end
data/spec/data/array.yaml DELETED
@@ -1,3 +0,0 @@
1
- ---
2
- - foo
3
- - bar
@@ -1,2 +0,0 @@
1
- ---
2
- - baz
@@ -1,3 +0,0 @@
1
- ---
2
- - foo
3
- - bar
data/spec/data/empty.yml DELETED
@@ -1 +0,0 @@
1
- ---
data/spec/data/hash.yml DELETED
@@ -1,3 +0,0 @@
1
- ---
2
- foo: bar
3
- baz: quux
@@ -1,4 +0,0 @@
1
- ---
2
- blargh: false
3
- foo:
4
- baz: 3.14
@@ -1,7 +0,0 @@
1
- ---
2
- asdf: 1
3
- foo:
4
- bar: baz
5
- quux:
6
- - 1
7
- - 42
@@ -1,2 +0,0 @@
1
- ---
2
- local: override is a hash
@@ -1,5 +0,0 @@
1
- ---
2
- - array
3
- - in
4
- - main
5
- - config
data/spec/data/test.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "foo": "bar",
3
- "baz": 42
4
- }
data/spec/data/world.yml DELETED
@@ -1,5 +0,0 @@
1
- ---
2
- drivers:
3
- mock:
4
- option: value
5
- driver: mock
@@ -1,192 +0,0 @@
1
- require 'spec_helper'
2
- require_relative '../lib/unobtainium/pathed_hash'
3
-
4
- describe ::Unobtainium::PathedHash do
5
- describe "#initialize" do
6
- it "can be constructed without values" do
7
- ph = ::Unobtainium::PathedHash.new
8
- expect(ph.empty?).to eql true
9
- end
10
-
11
- it "can be constructed with values" do
12
- ph = ::Unobtainium::PathedHash.new(foo: 42)
13
- expect(ph.empty?).to eql false
14
- expect(ph[:foo]).to eql 42
15
- end
16
-
17
- it "can be constructed with a nil value" do
18
- ph = ::Unobtainium::PathedHash.new(nil)
19
- expect(ph.empty?).to eql true
20
- end
21
- end
22
-
23
- describe "Hash-like" do
24
- it "responds to Hash functions" do
25
- ph = ::Unobtainium::PathedHash.new
26
- [:invert, :delete, :fetch].each do |meth|
27
- expect(ph.respond_to?(meth)).to eql true
28
- end
29
- end
30
-
31
- it "can be used like a hash" do
32
- ph = ::Unobtainium::PathedHash.new(foo: 42)
33
- inverted = ph.invert
34
- expect(inverted.empty?).to eql false
35
- expect(inverted[42]).to eql :foo
36
- end
37
-
38
- it "delegates to Hash if it's nothing to do with paths" do
39
- ph = ::Unobtainium::PathedHash.new(foo: 42)
40
- expect(ph.default).to be_nil
41
- end
42
- end
43
-
44
- it "can recursively read entries via a path" do
45
- sample = {
46
- "foo" => 42,
47
- "bar" => {
48
- "baz" => "quux",
49
- "blah" => [1, 2],
50
- }
51
- }
52
- ph = ::Unobtainium::PathedHash.new(sample)
53
-
54
- expect(ph["foo"]).to eql 42
55
- expect(ph["bar.baz"]).to eql "quux"
56
- expect(ph["bar.blah"]).to eql [1, 2]
57
-
58
- expect(ph["nope"]).to eql nil
59
- expect(ph["bar.nope"]).to eql nil
60
- end
61
-
62
- it "behaves consistently if in a path the first node cannot be found" do
63
- sample = {
64
- "foo" => 42,
65
- }
66
- ph = ::Unobtainium::PathedHash.new(sample)
67
-
68
- expect(ph["nope.bar"]).to eql nil
69
- end
70
-
71
- it "can be used with indifferent access from string key" do
72
- sample = {
73
- "foo" => 42,
74
- }
75
- ph = ::Unobtainium::PathedHash.new(sample)
76
-
77
- expect(ph["foo"]).to eql 42
78
- expect(ph[:foo]).to eql 42
79
- end
80
-
81
- it "can be used with indifferent access from symbol key" do
82
- sample = {
83
- foo: 42,
84
- bar: {
85
- baz: 'quux',
86
- }
87
- }
88
- ph = ::Unobtainium::PathedHash.new(sample)
89
-
90
- expect(ph["foo"]).to eql 42
91
- expect(ph[:foo]).to eql 42
92
-
93
- expect(ph['bar.baz']).to eql 'quux'
94
- end
95
-
96
- it "treats a single separator as the root" do
97
- sample = { "foo" => 42 }
98
- ph = ::Unobtainium::PathedHash.new(sample)
99
-
100
- expect(ph[ph.separator]["foo"]).to eql 42
101
- end
102
-
103
- it "treats an empty path as the root" do
104
- sample = { "foo" => 42 }
105
- ph = ::Unobtainium::PathedHash.new(sample)
106
-
107
- expect(ph[""]["foo"]).to eql 42
108
- end
109
-
110
- it "can recursively write entries via a path" do
111
- ph = ::Unobtainium::PathedHash.new
112
- ph["foo.bar"] = 42
113
- expect(ph["foo.bar"]).to eql 42
114
- end
115
-
116
- it "has the same string representation as the hash it's initialized from" do
117
- h = { foo: 42 }
118
- ph = ::Unobtainium::PathedHash.new(h)
119
- expect(ph.to_s).to eql h.to_s
120
- end
121
-
122
- it "understands absolute paths (starting with separator)" do
123
- sample = {
124
- "foo" => 42,
125
- "bar" => {
126
- "baz" => "quux",
127
- "blah" => [1, 2],
128
- }
129
- }
130
- ph = ::Unobtainium::PathedHash.new(sample)
131
-
132
- expect(ph["bar.baz"]).to eql "quux"
133
- expect(ph[".bar.baz"]).to eql "quux"
134
- end
135
-
136
- it "recursively merges with overwriting" do
137
- sample1 = {
138
- "foo" => {
139
- "bar" => 42,
140
- "baz" => "quux",
141
- }
142
- }
143
- sample2 = {
144
- "foo" => {
145
- "baz" => "override"
146
- }
147
- }
148
-
149
- ph1 = ::Unobtainium::PathedHash.new(sample1)
150
- ph2 = ph1.recursive_merge(sample2)
151
-
152
- expect(ph2["foo.bar"]).to eql 42
153
- expect(ph2["foo.baz"]).to eql "override"
154
- end
155
-
156
- it "recursively merges without overwriting" do
157
- sample1 = {
158
- "foo" => {
159
- "bar" => 42,
160
- "baz" => "quux",
161
- }
162
- }
163
- sample2 = {
164
- "foo" => {
165
- "baz" => "override"
166
- }
167
- }
168
-
169
- ph1 = ::Unobtainium::PathedHash.new(sample1)
170
- ph2 = ph1.recursive_merge(sample2, false)
171
-
172
- expect(ph2["foo.bar"]).to eql 42
173
- expect(ph2["foo.baz"]).to eql "quux"
174
- end
175
-
176
- it "can write with indifferent access without overwriting" do
177
- sample = {
178
- foo: {
179
- bar: 42,
180
- baz: 'quux',
181
- }
182
- }
183
- ph = ::Unobtainium::PathedHash.new(sample)
184
-
185
- expect(ph['foo.bar']).to eql 42
186
- expect(ph['foo.baz']).to eql 'quux'
187
-
188
- ph['foo.bar'] = 123
189
- expect(ph['foo.bar']).to eql 123
190
- expect(ph['foo.baz']).to eql 'quux'
191
- end
192
- end