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.
- checksums.yaml +4 -4
- data/Gemfile.lock +5 -1
- data/README.md +3 -2
- data/lib/unobtainium.rb +0 -1
- data/lib/unobtainium/driver.rb +7 -1
- data/lib/unobtainium/runtime.rb +2 -0
- data/lib/unobtainium/support/runner.rb +0 -4
- data/lib/unobtainium/version.rb +1 -1
- data/lib/unobtainium/world.rb +37 -16
- data/media/screenshot.jpg +0 -0
- data/media/video.ogv +0 -0
- data/spec/driver_spec.rb +40 -28
- data/spec/runner_spec.rb +19 -0
- data/spec/spec_helper.rb +1 -0
- data/unobtainium.gemspec +1 -0
- metadata +18 -31
- data/lib/unobtainium/config.rb +0 -304
- data/lib/unobtainium/pathed_hash.rb +0 -199
- data/lib/unobtainium/recursive_merge.rb +0 -55
- data/spec/config_spec.rb +0 -119
- data/spec/data/array.yaml +0 -3
- data/spec/data/arraymerge-local.yaml +0 -2
- data/spec/data/arraymerge.yaml +0 -3
- data/spec/data/empty.yml +0 -1
- data/spec/data/hash.yml +0 -3
- data/spec/data/hashmerge-local.yml +0 -4
- data/spec/data/hashmerge.yml +0 -7
- data/spec/data/mergefail-local.yaml +0 -2
- data/spec/data/mergefail.yaml +0 -5
- data/spec/data/test.json +0 -4
- data/spec/data/world.yml +0 -5
- data/spec/pathed_hash_spec.rb +0 -192
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e3149f2511fbd2ecbbd5af9d19bb097251bdcac9
|
4
|
+
data.tar.gz: 95e5ab479135ada464f851c6ebaaea775033d440
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3a8d32ef7e4162537270294802f1c57ad6a18e736b5c6d0a93dba3e586b8c54d18d184ae611a3892da14cb4c76b28cc4c10ee8d646f3bd4b2b4310000b973bca
|
7
|
+
data.tar.gz: 19c21152cb0c4b0268695cdb577f230da3c42994f48b2b457d29df8a8e5e733c8f5d3b3abb81c7ae9d8e6496694ef7b17b3f5c42e51b0f5d7e653ec539b30f75
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
unobtainium (0.
|
4
|
+
unobtainium (0.6.0)
|
5
|
+
collapsium-config (~> 0.1)
|
5
6
|
sys-proctable (~> 1.0)
|
6
7
|
|
7
8
|
GEM
|
@@ -20,6 +21,9 @@ GEM
|
|
20
21
|
ffi (~> 1.0, >= 1.0.11)
|
21
22
|
codeclimate-test-reporter (0.5.0)
|
22
23
|
simplecov (>= 0.7.1, < 1.0.0)
|
24
|
+
collapsium (0.1.0)
|
25
|
+
collapsium-config (0.1.1)
|
26
|
+
collapsium (~> 0.1)
|
23
27
|
cucumber (2.3.3)
|
24
28
|
builder (>= 2.1.2)
|
25
29
|
cucumber-core (~> 1.4.0)
|
data/README.md
CHANGED
@@ -46,8 +46,9 @@ the `Unobtainium::World` module.
|
|
46
46
|
- The `Driver` class, of course, wraps either of Appium or Selenium drivers:
|
47
47
|
|
48
48
|
```ruby
|
49
|
-
drv = Driver.create(:firefox) # uses Selenium
|
50
|
-
drv = Driver.create(:android) # uses Appium
|
49
|
+
drv = Driver.create(:firefox) # uses Selenium and Firefox
|
50
|
+
drv = Driver.create(:android) # uses Appium (browser or device)
|
51
|
+
drv = Driver.create(:phantomjs) # use Selenium and PhantomJS
|
51
52
|
|
52
53
|
drv.navigate.to "..." # delegates to Selenium or Appium
|
53
54
|
```
|
data/lib/unobtainium.rb
CHANGED
data/lib/unobtainium/driver.rb
CHANGED
@@ -125,7 +125,7 @@ module Unobtainium
|
|
125
125
|
label = label.to_sym
|
126
126
|
|
127
127
|
if not opts.nil?
|
128
|
-
if not
|
128
|
+
if not opts.is_a? Hash
|
129
129
|
raise ArgumentError, "The second argument is expected to be an "\
|
130
130
|
"options Hash!"
|
131
131
|
end
|
@@ -166,11 +166,15 @@ module Unobtainium
|
|
166
166
|
klass = Object.const_get(klassname)
|
167
167
|
Driver.register_implementation(klass, fpath)
|
168
168
|
rescue LoadError => err
|
169
|
+
# :nocov:
|
169
170
|
raise LoadError, "#{err.message}: unknown problem loading driver, "\
|
170
171
|
"aborting!"
|
172
|
+
# :nocov:
|
171
173
|
rescue NameError => err
|
174
|
+
# :nocov:
|
172
175
|
raise LoadError, "#{err.message}: unknown problem loading driver, "\
|
173
176
|
"aborting!"
|
177
|
+
# :nocov:
|
174
178
|
end
|
175
179
|
end
|
176
180
|
end
|
@@ -222,7 +226,9 @@ module Unobtainium
|
|
222
226
|
if not @impl.nil? and @impl.respond_to?(meth)
|
223
227
|
return @impl.send(meth.to_s, *args, &block)
|
224
228
|
end
|
229
|
+
# :nocov:
|
225
230
|
return super
|
231
|
+
# :nocov:
|
226
232
|
end
|
227
233
|
|
228
234
|
private
|
data/lib/unobtainium/runtime.rb
CHANGED
@@ -131,10 +131,6 @@ module Unobtainium
|
|
131
131
|
to_send += children.collect(&:pid)
|
132
132
|
end
|
133
133
|
|
134
|
-
if to_send.empty?
|
135
|
-
raise "This should not happen. I have no pids to send a signal to!"
|
136
|
-
end
|
137
|
-
|
138
134
|
# Alright, send the signal!
|
139
135
|
to_send.each do |pid|
|
140
136
|
# rubocop:disable Lint/HandleExceptions
|
data/lib/unobtainium/version.rb
CHANGED
data/lib/unobtainium/world.rb
CHANGED
@@ -8,8 +8,9 @@
|
|
8
8
|
#
|
9
9
|
require 'unobtainium'
|
10
10
|
|
11
|
+
require 'collapsium-config'
|
12
|
+
|
11
13
|
require 'unobtainium/driver'
|
12
|
-
require 'unobtainium/config'
|
13
14
|
require 'unobtainium/runtime'
|
14
15
|
|
15
16
|
module Unobtainium
|
@@ -17,32 +18,50 @@ module Unobtainium
|
|
17
18
|
# The World module combines other modules, defining simpler entry points
|
18
19
|
# into the gem's functionality.
|
19
20
|
module World
|
21
|
+
|
20
22
|
##
|
21
|
-
# Modules can have class methods, too
|
23
|
+
# Modules can have class methods, too, but it's a little more verbose to
|
24
|
+
# provide them.
|
22
25
|
module ClassMethods
|
23
|
-
# Set the
|
26
|
+
# Set the configuration file
|
24
27
|
def config_file=(name)
|
25
|
-
|
28
|
+
::Collapsium::Config.config_file = name
|
26
29
|
end
|
27
30
|
|
28
31
|
# @return [String] the config file path, defaulting to 'config/config.yml'
|
29
32
|
def config_file
|
30
|
-
return
|
33
|
+
return ::Collapsium::Config.config_file
|
31
34
|
end
|
32
|
-
end # module ClassMethods
|
33
|
-
extend ClassMethods
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
36
|
+
# In order for Unobtainium::World to include Collapsium::Config
|
37
|
+
# functionality, it has to be inherited when the former is
|
38
|
+
# included...
|
39
|
+
def included(klass)
|
40
|
+
set_config_path_default
|
41
|
+
|
42
|
+
klass.class_eval do
|
43
|
+
include ::Collapsium::Config
|
43
44
|
end
|
44
45
|
end
|
45
|
-
|
46
|
+
|
47
|
+
# ... and when it's extended.
|
48
|
+
def extended(world)
|
49
|
+
# :nocov:
|
50
|
+
set_config_path_default
|
51
|
+
|
52
|
+
world.extend(::Collapsium::Config)
|
53
|
+
# :nocov:
|
54
|
+
end
|
55
|
+
|
56
|
+
def set_config_path_default
|
57
|
+
# Override collapsium-config's default config path
|
58
|
+
if ::Collapsium::Config.config_file == \
|
59
|
+
::Collapsium::Config::DEFAULT_CONFIG_PATH
|
60
|
+
::Collapsium::Config.config_file = 'config/config.yml'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end # module ClassMethods
|
64
|
+
extend ClassMethods
|
46
65
|
|
47
66
|
##
|
48
67
|
# (see Driver#create)
|
@@ -89,12 +108,14 @@ module Unobtainium
|
|
89
108
|
# gets created or not.
|
90
109
|
at_end = config.fetch("at_end", "quit")
|
91
110
|
dtor = proc do |the_driver|
|
111
|
+
# :nocov:
|
92
112
|
if the_driver.nil?
|
93
113
|
return
|
94
114
|
end
|
95
115
|
|
96
116
|
meth = at_end.to_sym
|
97
117
|
the_driver.send(meth)
|
118
|
+
# :nocov:
|
98
119
|
end
|
99
120
|
return ::Unobtainium::Runtime.instance.store_with_if(key, dtor) do
|
100
121
|
::Unobtainium::Driver.create(label, options)
|
Binary file
|
data/media/video.ogv
ADDED
Binary file
|
data/spec/driver_spec.rb
CHANGED
@@ -37,17 +37,19 @@ describe ::Unobtainium::Driver do
|
|
37
37
|
::Unobtainium::Driver.register_implementation(MockDriver, "mock_driver.rb")
|
38
38
|
end
|
39
39
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
40
|
+
describe "driver registration" do
|
41
|
+
it "refuses to register a driver with missing methods" do
|
42
|
+
expect do
|
43
|
+
::Unobtainium::Driver.register_implementation(FakeDriver, __FILE__)
|
44
|
+
end.to raise_error(LoadError)
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
47
|
+
it "refuses to register the same driver twice from different locations" do
|
48
|
+
expect do
|
49
|
+
::Unobtainium::Driver.register_implementation(MockDriver, __FILE__ + '1')
|
50
|
+
::Unobtainium::Driver.register_implementation(MockDriver, __FILE__ + '2')
|
51
|
+
end.to raise_error(LoadError)
|
52
|
+
end
|
51
53
|
end
|
52
54
|
|
53
55
|
it "verifies arguments" do
|
@@ -66,29 +68,39 @@ describe ::Unobtainium::Driver do
|
|
66
68
|
end.to raise_error(ArgumentError)
|
67
69
|
end
|
68
70
|
|
69
|
-
|
70
|
-
|
71
|
-
|
71
|
+
describe "driver creation" do
|
72
|
+
it "creates no driver with an unknown label" do
|
73
|
+
expect { ::Unobtainium::Driver.create(:nope) }.to raise_error(LoadError)
|
74
|
+
end
|
72
75
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
76
|
+
it "fails preconditions correctly" do
|
77
|
+
expect do
|
78
|
+
::Unobtainium::Driver.create(:raise_mock)
|
79
|
+
end.to raise_error(RuntimeError)
|
80
|
+
end
|
78
81
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
+
it "creates a driver correctly" do
|
83
|
+
::Unobtainium::Driver.create(:mock)
|
84
|
+
end
|
82
85
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
86
|
+
it "does not create a driver with a nil label" do
|
87
|
+
expect do
|
88
|
+
::Unobtainium::Driver.create(nil)
|
89
|
+
end.to raise_error(ArgumentError)
|
90
|
+
end
|
87
91
|
end
|
88
92
|
|
89
|
-
|
90
|
-
|
91
|
-
|
93
|
+
describe "driver behaviour" do
|
94
|
+
it "delegates to created driver class" do
|
95
|
+
drv = ::Unobtainium::Driver.create(:mock, foo: 42)
|
96
|
+
expect(drv.respond_to?(:passed_options)).to be_truthy
|
97
|
+
_ = drv.passed_options
|
98
|
+
end
|
99
|
+
|
100
|
+
it "passes options through correctly" do
|
101
|
+
drv = ::Unobtainium::Driver.create(:mock, foo: 42)
|
102
|
+
expect(drv.passed_options).to eql foo: 42
|
103
|
+
end
|
92
104
|
end
|
93
105
|
|
94
106
|
describe 'modules' do
|
data/spec/runner_spec.rb
CHANGED
@@ -63,4 +63,23 @@ describe ::Unobtainium::Support::Runner do
|
|
63
63
|
expect { runner.start }.to raise_error(RuntimeError)
|
64
64
|
runner.wait
|
65
65
|
end
|
66
|
+
|
67
|
+
it "kills when destroyed" do
|
68
|
+
runner = ::Unobtainium::Support::Runner.new("foo", %w(sleep 30))
|
69
|
+
runner.start
|
70
|
+
expect(runner.pid).not_to be_nil
|
71
|
+
runner.destroy
|
72
|
+
expect(runner.pid).to be_nil
|
73
|
+
end
|
74
|
+
|
75
|
+
it "cannot be killed twice" do
|
76
|
+
runner = ::Unobtainium::Support::Runner.new("foo", %w(sleep 30))
|
77
|
+
runner.start
|
78
|
+
expect(runner.pid).not_to be_nil
|
79
|
+
runner.kill
|
80
|
+
|
81
|
+
expect do
|
82
|
+
runner.kill
|
83
|
+
end.to raise_error
|
84
|
+
end
|
66
85
|
end
|
data/spec/spec_helper.rb
CHANGED
data/unobtainium.gemspec
CHANGED
@@ -54,6 +54,7 @@ Gem::Specification.new do |spec|
|
|
54
54
|
spec.add_development_dependency "cucumber"
|
55
55
|
|
56
56
|
spec.add_dependency "sys-proctable", "~> 1.0"
|
57
|
+
spec.add_dependency "collapsium-config", "~> 0.1"
|
57
58
|
end
|
58
59
|
# rubocop:enable Style/SpaceAroundOperators
|
59
60
|
# rubocop:enable Style/UnneededPercentQ, Style/ExtraSpacing
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: unobtainium
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jens Finkhaeuser
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-05-
|
11
|
+
date: 2016-05-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -164,6 +164,20 @@ dependencies:
|
|
164
164
|
- - "~>"
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: '1.0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: collapsium-config
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0.1'
|
174
|
+
type: :runtime
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0.1'
|
167
181
|
description: "\n Unobtainium wraps Selenium and Appium in a simple driver abstraction
|
168
182
|
so that\n test code can more easily cover desktop browsers, mobile browsers and
|
169
183
|
mobile\n apps.\n\n Some additional useful functionality for the maintenance
|
@@ -192,35 +206,21 @@ files:
|
|
192
206
|
- features/support/env.rb
|
193
207
|
- features/world.feature
|
194
208
|
- lib/unobtainium.rb
|
195
|
-
- lib/unobtainium/config.rb
|
196
209
|
- lib/unobtainium/driver.rb
|
197
210
|
- lib/unobtainium/drivers/appium.rb
|
198
211
|
- lib/unobtainium/drivers/phantom.rb
|
199
212
|
- lib/unobtainium/drivers/selenium.rb
|
200
|
-
- lib/unobtainium/pathed_hash.rb
|
201
|
-
- lib/unobtainium/recursive_merge.rb
|
202
213
|
- lib/unobtainium/runtime.rb
|
203
214
|
- lib/unobtainium/support/port_scanner.rb
|
204
215
|
- lib/unobtainium/support/runner.rb
|
205
216
|
- lib/unobtainium/support/util.rb
|
206
217
|
- lib/unobtainium/version.rb
|
207
218
|
- lib/unobtainium/world.rb
|
208
|
-
-
|
209
|
-
-
|
210
|
-
- spec/data/arraymerge-local.yaml
|
211
|
-
- spec/data/arraymerge.yaml
|
219
|
+
- media/screenshot.jpg
|
220
|
+
- media/video.ogv
|
212
221
|
- spec/data/driverconfig.yml
|
213
|
-
- spec/data/empty.yml
|
214
|
-
- spec/data/hash.yml
|
215
|
-
- spec/data/hashmerge-local.yml
|
216
|
-
- spec/data/hashmerge.yml
|
217
|
-
- spec/data/mergefail-local.yaml
|
218
|
-
- spec/data/mergefail.yaml
|
219
|
-
- spec/data/test.json
|
220
|
-
- spec/data/world.yml
|
221
222
|
- spec/driver_spec.rb
|
222
223
|
- spec/mock_driver.rb
|
223
|
-
- spec/pathed_hash_spec.rb
|
224
224
|
- spec/port_scanner_spec.rb
|
225
225
|
- spec/runner_spec.rb
|
226
226
|
- spec/runtime_spec.rb
|
@@ -257,22 +257,9 @@ test_files:
|
|
257
257
|
- features/step_definitions/steps.rb
|
258
258
|
- features/support/env.rb
|
259
259
|
- features/world.feature
|
260
|
-
- spec/config_spec.rb
|
261
|
-
- spec/data/array.yaml
|
262
|
-
- spec/data/arraymerge-local.yaml
|
263
|
-
- spec/data/arraymerge.yaml
|
264
260
|
- spec/data/driverconfig.yml
|
265
|
-
- spec/data/empty.yml
|
266
|
-
- spec/data/hash.yml
|
267
|
-
- spec/data/hashmerge-local.yml
|
268
|
-
- spec/data/hashmerge.yml
|
269
|
-
- spec/data/mergefail-local.yaml
|
270
|
-
- spec/data/mergefail.yaml
|
271
|
-
- spec/data/test.json
|
272
|
-
- spec/data/world.yml
|
273
261
|
- spec/driver_spec.rb
|
274
262
|
- spec/mock_driver.rb
|
275
|
-
- spec/pathed_hash_spec.rb
|
276
263
|
- spec/port_scanner_spec.rb
|
277
264
|
- spec/runner_spec.rb
|
278
265
|
- spec/runtime_spec.rb
|
data/lib/unobtainium/config.rb
DELETED
@@ -1,304 +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 'pathname'
|
11
|
-
|
12
|
-
require 'unobtainium/pathed_hash'
|
13
|
-
|
14
|
-
module Unobtainium
|
15
|
-
##
|
16
|
-
# The Config class extends PathedHash by two main pieces of functionality:
|
17
|
-
#
|
18
|
-
# - it loads configuration files and turns them into pathed hashes, and
|
19
|
-
# - it treats environment variables as overriding anything contained in
|
20
|
-
# the configuration file.
|
21
|
-
#
|
22
|
-
# For configuration file loading, a named configuration file will be laoaded
|
23
|
-
# if present. A file with the same name but `-local` appended before the
|
24
|
-
# extension will be loaded as well, overriding any values in the original
|
25
|
-
# configuration file.
|
26
|
-
#
|
27
|
-
# For environment variable support, any environment variable named like a
|
28
|
-
# path into the configuration hash, but with separators transformed to
|
29
|
-
# underscore and all letters capitalized will override values from the
|
30
|
-
# configuration files under that path, i.e. `FOO_BAR` will override `'foo.bar'`.
|
31
|
-
#
|
32
|
-
# Environment variables can contain JSON *only*; if the value can be parsed
|
33
|
-
# as JSON, it becomes a Hash in the configuration tree. If it cannot be parsed
|
34
|
-
# as JSON, it remains a string.
|
35
|
-
#
|
36
|
-
# **Note:** if your configuration file's top-level structure is an array, it
|
37
|
-
# will be returned as a hash with a 'config' key that maps to your file's
|
38
|
-
# contents.
|
39
|
-
# That means that if you are trying to merge a hash with an array config, the
|
40
|
-
# result may be unexpected.
|
41
|
-
class Config < PathedHash
|
42
|
-
# @api private
|
43
|
-
# Very simple YAML parser
|
44
|
-
class YAMLParser
|
45
|
-
require 'yaml'
|
46
|
-
|
47
|
-
# @return parsed string
|
48
|
-
def self.parse(string)
|
49
|
-
YAML.load(string)
|
50
|
-
end
|
51
|
-
end
|
52
|
-
private_constant :YAMLParser
|
53
|
-
|
54
|
-
# @api private
|
55
|
-
# Very simple JSON parser
|
56
|
-
class JSONParser
|
57
|
-
require 'json'
|
58
|
-
|
59
|
-
# @return parsed string
|
60
|
-
def self.parse(string)
|
61
|
-
JSON.parse(string)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
private_constant :JSONParser
|
65
|
-
|
66
|
-
PathedHash::READ_METHODS.each do |method|
|
67
|
-
# Wrap all read functions into something that checks for environment
|
68
|
-
# variables first.
|
69
|
-
define_method(method) do |*args, &block|
|
70
|
-
# If there are no arguments, there's nothing to do with paths. Just
|
71
|
-
# delegate to the hash.
|
72
|
-
if args.empty?
|
73
|
-
return super(*args, &block)
|
74
|
-
end
|
75
|
-
|
76
|
-
# We'll make it rather simple: since the first argument is a key, we
|
77
|
-
# will just transform it to the matching environment variable name,
|
78
|
-
# and see if that environment variable is set.
|
79
|
-
env_name = args[0].to_s.upcase.gsub(split_pattern, '_')
|
80
|
-
contents = nil
|
81
|
-
if env_name != '_'
|
82
|
-
contents = ENV[env_name]
|
83
|
-
end
|
84
|
-
|
85
|
-
# No environment variable set? Fine, just do the usual thing.
|
86
|
-
if contents.nil? or contents.empty?
|
87
|
-
return super(*args, &block)
|
88
|
-
end
|
89
|
-
|
90
|
-
# With an environment variable, we will try to parse it as JSON first.
|
91
|
-
begin
|
92
|
-
return JSONParser.parse(contents)
|
93
|
-
rescue JSON::ParserError
|
94
|
-
return contents
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
class << self
|
100
|
-
# @api private
|
101
|
-
# Mapping of file name extensions to parser types.
|
102
|
-
FILE_TO_PARSER = {
|
103
|
-
'.yml' => YAMLParser,
|
104
|
-
'.yaml' => YAMLParser,
|
105
|
-
'.json' => JSONParser,
|
106
|
-
}.freeze
|
107
|
-
private_constant :FILE_TO_PARSER
|
108
|
-
|
109
|
-
# @api private
|
110
|
-
# If the config file contains an Array, this is what they key of the
|
111
|
-
# returned Hash will be.
|
112
|
-
ARRAY_KEY = 'config'.freeze
|
113
|
-
private_constant :ARRAY_KEY
|
114
|
-
|
115
|
-
##
|
116
|
-
# Loads a configuration file with the given file name. The format is
|
117
|
-
# detected based on one of the extensions in FILE_TO_PARSER.
|
118
|
-
#
|
119
|
-
# @param path [String] the path of the configuration file to load.
|
120
|
-
# @param resolve_extensions [Boolean] flag whether to resolve configuration
|
121
|
-
# hash extensions. (see `#resolve_extensions`)
|
122
|
-
def load_config(path, resolve_extensions = true)
|
123
|
-
# Load base and local configuration files
|
124
|
-
base, config = load_base_config(path)
|
125
|
-
_, local_config = load_local_config(base)
|
126
|
-
|
127
|
-
# Merge local configuration
|
128
|
-
config.recursive_merge!(local_config)
|
129
|
-
|
130
|
-
# Create config from the result
|
131
|
-
cfg = Config.new(config)
|
132
|
-
|
133
|
-
# Now resolve config hashes that extend other hashes.
|
134
|
-
if resolve_extensions
|
135
|
-
cfg.resolve_extensions!
|
136
|
-
end
|
137
|
-
|
138
|
-
return cfg
|
139
|
-
end
|
140
|
-
|
141
|
-
private
|
142
|
-
|
143
|
-
def load_base_config(path)
|
144
|
-
# Make sure the format is recognized early on.
|
145
|
-
base = Pathname.new(path)
|
146
|
-
formats = FILE_TO_PARSER.keys
|
147
|
-
if not formats.include?(base.extname)
|
148
|
-
raise ArgumentError, "Files with extension '#{base.extname}' are not"\
|
149
|
-
" recognized; please use one of #{formats}!"
|
150
|
-
end
|
151
|
-
|
152
|
-
# Don't check the path whether it exists - loading a nonexistent
|
153
|
-
# file will throw a nice error for the user to catch.
|
154
|
-
file = base.open
|
155
|
-
contents = file.read
|
156
|
-
|
157
|
-
# Parse the contents.
|
158
|
-
config = FILE_TO_PARSER[base.extname].parse(contents)
|
159
|
-
|
160
|
-
return base, PathedHash.new(hashify(config))
|
161
|
-
end
|
162
|
-
|
163
|
-
def load_local_config(base)
|
164
|
-
# Now construct a file name for a local override.
|
165
|
-
local = Pathname.new(base.dirname)
|
166
|
-
local = local.join(base.basename(base.extname).to_s + "-local" +
|
167
|
-
base.extname)
|
168
|
-
if not local.exist?
|
169
|
-
return local, nil
|
170
|
-
end
|
171
|
-
|
172
|
-
# We know the local override file exists, but we do want to let any errors
|
173
|
-
# go through that come with reading or parsing it.
|
174
|
-
file = local.open
|
175
|
-
contents = file.read
|
176
|
-
|
177
|
-
local_config = FILE_TO_PARSER[base.extname].parse(contents)
|
178
|
-
|
179
|
-
return local, PathedHash.new(hashify(local_config))
|
180
|
-
end
|
181
|
-
|
182
|
-
def hashify(data)
|
183
|
-
if data.nil?
|
184
|
-
return {}
|
185
|
-
end
|
186
|
-
if data.is_a? Array
|
187
|
-
data = { ARRAY_KEY => data }
|
188
|
-
end
|
189
|
-
return data
|
190
|
-
end
|
191
|
-
end # class << self
|
192
|
-
|
193
|
-
##
|
194
|
-
# Resolve extensions in configuration hashes. If your hash contains e.g.:
|
195
|
-
#
|
196
|
-
# ```yaml
|
197
|
-
# foo:
|
198
|
-
# bar:
|
199
|
-
# some: value
|
200
|
-
# baz:
|
201
|
-
# extends: bar
|
202
|
-
# ```
|
203
|
-
#
|
204
|
-
# Then `'foo.baz.some'` will equal `'value'` after resolving extensions. Note
|
205
|
-
# that `:load_config` calls this function, so normally you don't need to call
|
206
|
-
# it yourself. You can switch this behaviour off in `:load_config`.
|
207
|
-
#
|
208
|
-
# Note that this process has some intended side-effects:
|
209
|
-
#
|
210
|
-
# 1. If a hash can't be extended because the base cannot be found, an error
|
211
|
-
# is raised.
|
212
|
-
# 1. If a hash got successfully extended, the `extends` keyword itself is
|
213
|
-
# removed from the hash.
|
214
|
-
# 1. In a successfully extended hash, an `base` keyword, which contains
|
215
|
-
# the name of the base. In case of multiple recursive extensions, the
|
216
|
-
# final base is stored here.
|
217
|
-
#
|
218
|
-
# Also note that all of this means that :extends and :base are reserved
|
219
|
-
# keywords that cannot be used in configuration files other than for this
|
220
|
-
# purpose!
|
221
|
-
def resolve_extensions!
|
222
|
-
recursive_merge("", "")
|
223
|
-
end
|
224
|
-
|
225
|
-
##
|
226
|
-
# Same as `dup.resolve_extensions!`
|
227
|
-
def resolve_extensions
|
228
|
-
dup.resolve_extensions!
|
229
|
-
end
|
230
|
-
|
231
|
-
private
|
232
|
-
|
233
|
-
def recursive_merge(parent, key)
|
234
|
-
loop do
|
235
|
-
full_key = "#{parent}#{separator}#{key}"
|
236
|
-
|
237
|
-
# Recurse down to the remaining root of the hierarchy
|
238
|
-
base = full_key
|
239
|
-
derived = nil
|
240
|
-
loop do
|
241
|
-
new_base, new_derived = resolve_extension(parent, base)
|
242
|
-
|
243
|
-
if new_derived.nil?
|
244
|
-
break
|
245
|
-
end
|
246
|
-
|
247
|
-
base = new_base
|
248
|
-
derived = new_derived
|
249
|
-
end
|
250
|
-
|
251
|
-
# If recursion found nothing to merge, we're done!
|
252
|
-
if derived.nil?
|
253
|
-
break
|
254
|
-
end
|
255
|
-
|
256
|
-
# Otherwise, merge what needs merging and continue
|
257
|
-
merge_extension(base, derived)
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
|
-
def resolve_extension(grandparent, parent)
|
262
|
-
fetch(parent, {}).each do |key, value|
|
263
|
-
# Recurse into hash values
|
264
|
-
if value.is_a? Hash
|
265
|
-
recursive_merge(parent, key)
|
266
|
-
end
|
267
|
-
|
268
|
-
# No hash, ignore any keys other than the special "extends" key
|
269
|
-
if key != "extends"
|
270
|
-
next
|
271
|
-
end
|
272
|
-
|
273
|
-
# If the key is "extends", return a normalized version of its value.
|
274
|
-
full_value = value.dup
|
275
|
-
if not full_value.start_with?(separator)
|
276
|
-
full_value = "#{grandparent}#{separator}#{value}"
|
277
|
-
end
|
278
|
-
|
279
|
-
if full_value == parent
|
280
|
-
next
|
281
|
-
end
|
282
|
-
return full_value, parent
|
283
|
-
end
|
284
|
-
|
285
|
-
return nil, nil
|
286
|
-
end
|
287
|
-
|
288
|
-
def merge_extension(base, derived)
|
289
|
-
# Remove old 'extends' key, but remember the value
|
290
|
-
extends = self[derived]["extends"]
|
291
|
-
self[derived].delete("extends")
|
292
|
-
|
293
|
-
# Recursively merge base into derived without overwriting
|
294
|
-
self[derived].extend(::Unobtainium::RecursiveMerge)
|
295
|
-
self[derived].recursive_merge!(self[base], false)
|
296
|
-
|
297
|
-
# Then set the "base" keyword, but only if it's not yet set.
|
298
|
-
if not self[derived]["base"].nil?
|
299
|
-
return
|
300
|
-
end
|
301
|
-
self[derived]["base"] = extends
|
302
|
-
end
|
303
|
-
end # class Config
|
304
|
-
end # module Unobtainium
|