unobtainium 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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