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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 62dde544dbbf73a7c92b1c34bade67318bfa8852
4
- data.tar.gz: 2ffcd21462aef14d31031e0d26fcb562f61690af
3
+ metadata.gz: e3149f2511fbd2ecbbd5af9d19bb097251bdcac9
4
+ data.tar.gz: 95e5ab479135ada464f851c6ebaaea775033d440
5
5
  SHA512:
6
- metadata.gz: 14d68f7c5d72f352338c5fd914585b9bba184bd8a38ae76e4fdc0e87a9d03e91e15a2e1706ae8fdfd3b9eba70ebb9510c9b52c475788142f63dfc2e43d82b88f
7
- data.tar.gz: eeec64dacd68be00b4a402e705aff209af4313d0ecac8a7824b4ceee479e1bd5edf7368c7c4ff4fa2b863d5a31a0ae1293d83e5916c60b64470222ef89327921
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.5.1)
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
@@ -10,6 +10,5 @@
10
10
  require 'unobtainium/version'
11
11
 
12
12
  require 'unobtainium/driver'
13
- require 'unobtainium/config'
14
13
  require 'unobtainium/runtime'
15
14
  require 'unobtainium/world'
@@ -125,7 +125,7 @@ module Unobtainium
125
125
  label = label.to_sym
126
126
 
127
127
  if not opts.nil?
128
- if not (opts.is_a? Hash or opts.is_a? ::Unobtainium::PathedHash)
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
@@ -27,9 +27,11 @@ module Unobtainium
27
27
 
28
28
  # Create our own finalizer
29
29
  ObjectSpace.define_finalizer(self) do
30
+ # :nocov:
30
31
  @objects.keys.each do |key|
31
32
  delete(key)
32
33
  end
34
+ # :nocov:
33
35
  end
34
36
  end
35
37
 
@@ -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
@@ -8,5 +8,5 @@
8
8
  #
9
9
  module Unobtainium
10
10
  # The current release version
11
- VERSION = "0.5.1".freeze
11
+ VERSION = "0.6.0".freeze
12
12
  end
@@ -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 config file path.
26
+ # Set the configuration file
24
27
  def config_file=(name)
25
- @config_file = name
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 @config_file || "config/config.yml"
33
+ return ::Collapsium::Config.config_file
31
34
  end
32
- end # module ClassMethods
33
- extend ClassMethods
34
35
 
35
- ##
36
- # Return the global configuration, loaded from `World#config_file`
37
- def config
38
- return ::Unobtainium::Runtime.instance.store_with_if(:config) do
39
- begin
40
- ::Unobtainium::Config.load_config(::Unobtainium::World.config_file)
41
- rescue Errno::ENOENT
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
- end
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
- it "refuses to register a driver with missing methods" do
41
- expect do
42
- ::Unobtainium::Driver.register_implementation(FakeDriver, __FILE__)
43
- end.to raise_error(LoadError)
44
- end
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
- it "refuses to register the same driver twice from different locations" do
47
- expect do
48
- ::Unobtainium::Driver.register_implementation(MockDriver, __FILE__ + '1')
49
- ::Unobtainium::Driver.register_implementation(MockDriver, __FILE__ + '2')
50
- end.to raise_error(LoadError)
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
- it "creates no driver with an unknown label" do
70
- expect { ::Unobtainium::Driver.create(:nope) }.to raise_error(LoadError)
71
- end
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
- it "fails preconditions correctly" do
74
- expect do
75
- ::Unobtainium::Driver.create(:raise_mock)
76
- end.to raise_error(RuntimeError)
77
- end
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
- it "creates a driver correctly" do
80
- ::Unobtainium::Driver.create(:mock)
81
- end
82
+ it "creates a driver correctly" do
83
+ ::Unobtainium::Driver.create(:mock)
84
+ end
82
85
 
83
- it "delegates to created driver class" do
84
- drv = ::Unobtainium::Driver.create(:mock, foo: 42)
85
- expect(drv.respond_to?(:passed_options)).to be_truthy
86
- _ = drv.passed_options
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
- it "passes options through correctly" do
90
- drv = ::Unobtainium::Driver.create(:mock, foo: 42)
91
- expect(drv.passed_options).to eql foo: 42
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
@@ -8,4 +8,5 @@ end
8
8
  require 'simplecov'
9
9
  SimpleCov.start do
10
10
  add_filter 'unobtainium/drivers'
11
+ add_filter 'spec'
11
12
  end
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.5.1
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 00:00:00.000000000 Z
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
- - spec/config_spec.rb
209
- - spec/data/array.yaml
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
@@ -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