unobtainium 0.5.1 → 0.6.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.
@@ -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