invar 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 58a64d36feebf42e3d2e44e3cee20197683055dbb276f38701344db45a050e27
4
+ data.tar.gz: f39265a13a4ae8a49cb2b7944599c75bab6723422d3b92f5838b777d7fa8dfab
5
+ SHA512:
6
+ metadata.gz: c8d134ac530b0e9a2046922cc1055621d4120e92840eedee5fd5d527b1cd5a6880577a78cc4cbfa065dc751d48d9883a0edd36e661dcd83151b6967f1501cf6d
7
+ data.tar.gz: e27ee5476e9efcf5871f78e50dd428398306a2dc48092b56c55a04be3e98c30fd2fad2d899ed7d35fff70ef297d2ede51941dd22197eb56222155f8b5d93be48
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,26 @@
1
+ inherit_from: ../.rubocop.yml
2
+
3
+ AllCops:
4
+ Exclude:
5
+ - 'bin/*'
6
+
7
+ TargetRubyVersion: 2.7
8
+
9
+ Layout/LineLength:
10
+ Exclude:
11
+ - 'spec/**/*.rb'
12
+
13
+ # setting to 6 to match RubyMine autoformat
14
+ Layout/FirstArrayElementIndentation:
15
+ IndentationWidth: 6
16
+
17
+
18
+ # rspec blocks are huge by design
19
+ Metrics/BlockLength:
20
+ Exclude:
21
+ - 'spec/**/*.rb'
22
+
23
+ Metrics/ModuleLength:
24
+ Exclude:
25
+ - 'spec/**/*.rb'
26
+
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.7.1
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at robin@tenjin.ca. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in invar.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Robin Miller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,324 @@
1
+ # Invar
2
+
3
+ Single source of immutable truth for managing application configs, secrets, and environment variable data.
4
+
5
+ ## Big Picture
6
+
7
+ Invar's main purpose is to enhance and simplify how applications know about their system environment and config.
8
+
9
+ Using it in code looks like this:
10
+
11
+ ```ruby
12
+ invar = Invar.new(namespace: 'my-app')
13
+
14
+ db_host = invar / :config / :database / :host
15
+ ```
16
+
17
+ ### Background
18
+
19
+ Managing application config in a [12 Factor](http://12factor.net/config) style is a good idea, but simple environment
20
+ variables have some downsides:
21
+
22
+ * They are not groupable / nestable
23
+ * Secrets might be leaked to untrustable subprocesses or 3rd party logging services (eg. an error dump including the
24
+ whole ENV)
25
+ * Secrets might be stored in plaintext (eg. in cron or scripts). It's better to store secrets as encrypted.
26
+ * Cannot be easily checked against a schema for early error detection
27
+ * Ruby's core ENV does not accept symbols as keys (a minor nuisance, but it counts)
28
+
29
+ > **Fun Fact:** Invar is named for an [alloy used in clockmaking](https://en.wikipedia.org/wiki/Invar) - it's short for "**invar**iable".
30
+
31
+ ### Features
32
+
33
+ Here's what this Gem provides:
34
+
35
+ * File location defaults from
36
+ the [XDG Base Directory](https://en.wikipedia.org/wiki/Freedesktop.org#Base_Directory_Specification)
37
+ file location standard
38
+ * File schema using [dry-schema](https://dry-rb.org/gems/dry-schema/main/)
39
+ * Distinction between configs and secrets
40
+ * Secrets encrypted using [Lockbox](https://github.com/ankane/lockbox)
41
+ * Access configs and ENV variables using symbols or case-insensitive strings.
42
+ * Enforced key uniqueness
43
+ * Helpful Rake tasks
44
+ * Meaningful error messages with suggestions
45
+ * Immutable
46
+
47
+ ### Anti-Features
48
+
49
+ Things that Invar intentionally does **not** support:
50
+
51
+ * Multiple config sources
52
+ * No subtle overrides
53
+ * No frustration about file edits not working... because you're editing the wrong file
54
+ * No remembering finicky precedence order
55
+ * Modes
56
+ * No forgetting to set the mode before running rake, etc
57
+ * No proliferation of files irrelevant to the current situation
58
+ * Config file code interpretation (eg. ERB in YAML)
59
+ * Reduced security hazard
60
+ * No value ambiguity
61
+
62
+ ### But That's Bonkers
63
+
64
+ It might be! This is a bit of an experiment. Some things may appear undesirable at first glance, but it's usually for a
65
+ reason.
66
+
67
+ Some situations might legitimately need a more complex configuration setup. But perhaps reflect on whether it's a code
68
+ smell nudging you to:
69
+
70
+ * Reduce your application into smaller parts (eg. microservices etc)
71
+ * Reduce the number of service providers
72
+ * Improve your collaboration or deployment procedures and automation
73
+
74
+ You know your situation better than this README can.
75
+
76
+ ## Concepts and Jargon
77
+
78
+ ### Configurations
79
+
80
+ Configurations are values that depend on the *local system environment*. They do not generally change depending on what
81
+ you're *doing*. Examples include:
82
+
83
+ * Database name
84
+ * API options
85
+ * Gem or library configurations
86
+
87
+ ### Secrets
88
+
89
+ This is stuff you don't want to be read by anyone. Rails calls this concept "credentials." Examples include:
90
+
91
+ * Usernames and passwords
92
+ * API keys
93
+
94
+ **This is not a replacement for a password-manager**. Use a
95
+ proper [password sharing tool](https://en.wikipedia.org/wiki/List_of_password_managers) as the primary method for
96
+ sharing passwords within your team. This is especially true for the master encryption key used to secure the secrets
97
+ file.
98
+
99
+ Similarly, you should use a unique encryption key for each environment (eg. your development laptop vs a server).
100
+
101
+ ### XDG Base Directory
102
+
103
+ This is a standard that defines where applications should store their files (config, data, etc). The relevant part is
104
+ that it looks in a couple of places for config files, declared in a pair of environment variables:
105
+
106
+ 1. `XDG_CONFIG_HOME`
107
+ - Note: Ignored when `$HOME` directory is undefined. Often the case for system daemons.
108
+ - Default: `~/.config/`
109
+ 2. `XDG_CONFIG_DIRS`
110
+ - Fallback locations, declared as a colon-separated list in priority order.
111
+ - Default: `/etc/xdg/`
112
+
113
+ You can check your current values by running this in a terminal:
114
+
115
+ env | grep XDG
116
+
117
+ ### Namespace
118
+
119
+ The name of the app's subdirectory under the relevant XDG Base Directory.
120
+
121
+ eg:
122
+
123
+ `~/.config/name-of-app`
124
+
125
+ or
126
+
127
+ `/etc/xdg/name-of-app`
128
+
129
+ ## Installation
130
+
131
+ Add this line to your application's Gemfile:
132
+
133
+ ```ruby
134
+ gem 'invar'
135
+ ```
136
+
137
+ And then run in a terminal:
138
+
139
+ bundle install
140
+
141
+ ## Usage
142
+
143
+ ### Testing
144
+
145
+ Invar automatically loads the normal runtime configuration from the config files created by the Rake tasks (details in
146
+ next section), but tests may need to override some of those values.
147
+
148
+ Call `#pretend` on the relevant selector:
149
+
150
+ ```ruby
151
+ # Your application require Invar as normal:
152
+ require 'invar'
153
+
154
+ invar = Invar.new(namespace: 'my-app')
155
+
156
+ # ... then, in your test suite:
157
+ require 'invar/test'
158
+
159
+ # Usually this would be in a test suite hook,
160
+ # like Cucumber's `BeforeAll` or RSpec's `before(:all)`
161
+ invar[:config][:theme].pretend dark_mode: true
162
+ ```
163
+
164
+ Calling `#pretend` without requiring `invar/test` will raise an `ImmutableRealityError`.
165
+
166
+ To override values immediately after the config files are read, use an `Invar.after_load` block:
167
+
168
+ ```ruby
169
+ Invar.after_load do |invar|
170
+ invar[:config][:database].pretend name: 'my_app_test'
171
+ end
172
+
173
+ # This Invar will return database name 'my_app_test'
174
+ invar = Invar.new(namespace: 'my-app')
175
+
176
+ puts invar / :config / :database
177
+ ```
178
+
179
+ ### Rake Tasks
180
+
181
+ In your `Rakefile`, add:
182
+
183
+ ```ruby
184
+ require 'invar/rake'
185
+ ```
186
+
187
+ Then you can use the rake tasks as reported by `rake -T`
188
+
189
+ #### Show Search Paths
190
+
191
+ This will show you the XDG search locations.
192
+
193
+ bundle exec rake invar:paths
194
+
195
+ #### Create Config File
196
+
197
+ bundle exec rake invar:create:configs
198
+
199
+ If a config file already exists in any of the search path locations, it will yell at you.
200
+
201
+ #### Edit Config File
202
+
203
+ bundle exec rake invar:edit:configs
204
+
205
+ #### Create Secrets File
206
+
207
+ To create the config file, run this in a terminal:
208
+
209
+ bundle exec rake invar:create:secrets
210
+
211
+ It will print out the generated master key. Save it to a password manager.
212
+
213
+ If you do not want it to be displayed (eg. you're in public), you can pipe it to a file:
214
+
215
+ bundle exec rake invar:create:secrets > master_key
216
+
217
+ Then handle the `master_key` file as needed.
218
+
219
+ #### Edit Secrets File
220
+
221
+ To edit the secrets file, run this and provide the file's encryption key:
222
+
223
+ bundle exec rake invar:edit:secrets
224
+
225
+ The file will be decrypted and opened in your default editor (eg. nano). Once you have exited the editor, it will be
226
+ re-encrypted (remember to save, too!).
227
+
228
+ ### Code
229
+
230
+ Assuming file `~/.config/my-app/config.yml` with:
231
+
232
+ ```yml
233
+ ---
234
+ database:
235
+ name: 'my_app_development'
236
+ host: 'localhost'
237
+ ```
238
+
239
+ And an encrypted file `~/.config/my-app/secrets.yml` with:
240
+
241
+ ```yml
242
+ ---
243
+ database:
244
+ user: 'my_app'
245
+ pass: 'sekret'
246
+ ```
247
+
248
+ Then in `my-app.rb`, you can fetch those values:
249
+
250
+ ```ruby
251
+ require 'invar'
252
+
253
+ invar = Invar.new 'my-app'
254
+
255
+ # Use the slash operator to fetch values (sort of like Pathname)
256
+ puts invar / :config / :database / :host
257
+
258
+ # String keys are okay, too. Also it's case-insensitive
259
+ puts invar / 'config' / 'database' / 'host'
260
+
261
+ # Secrets are kept in a separate tree
262
+ puts invar / :secret / :database / :username
263
+
264
+ # And you can get ENV variables. This should print your HOME directory.
265
+ puts invar / :config / :home
266
+
267
+ # You can also use [] notation, which may be nicer in some situations (like #pretend)
268
+ puts invar[:config][:database][:host]
269
+ ```
270
+
271
+ > **FAQ**: Why not support a dot syntax like `invar.config.database.host`?
272
+ >
273
+ > **A**: Because key names could collide with method names, like `inspect`, `dup`, or `tap`.
274
+
275
+ ### Custom Locations
276
+
277
+ You can customize the search paths by setting the environment variables `XDG_CONFIG_HOME` and/or `XDG_CONFIG_DIRS` any
278
+ time you run a Rake task or your application.
279
+
280
+ # Looks in /tmp instead of ~/.config/
281
+ XDG_CONFIG_HOME=/tmp bundle exec rake invar:paths
282
+
283
+ ## Alternatives
284
+
285
+ Some other gems with different approaches:
286
+
287
+ - [RubyConfig](https://github.com/rubyconfig/config)
288
+ - [Anyway Config](https://github.com/palkan/anyway_config)
289
+ - [AppConfig](https://github.com/Oshuma/app_config)
290
+ - [Fiagro](https://github.com/laserlemon/figaro)
291
+
292
+ ## Contributing
293
+
294
+ Bug reports and pull requests are welcome on GitHub at https://github.com/TenjinInc/invar.
295
+
296
+ Valued topics:
297
+
298
+ * Error messages (clarity, hinting)
299
+ * Documentation
300
+ * API
301
+ * Security correctness
302
+
303
+ This project is intended to be a friendly space for collaboration, and contributors are expected to adhere to the
304
+ [Contributor Covenant](http://contributor-covenant.org) code of conduct. Play nice.
305
+
306
+ ### Core Developers
307
+
308
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests. You
309
+ can also run `bin/console` for an interactive prompt that will allow you to experiment.
310
+
311
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
312
+ version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
313
+ push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
314
+
315
+ Documentation is produced by Yard. Run `bundle exec rake yard`. The goal is to have 100% documentation coverage and 100%
316
+ test coverage.
317
+
318
+ Release notes are provided in `RELEASE_NOTES.md`, and should vaguely
319
+ follow [Keep A Changelog](https://keepachangelog.com/en/1.0.0/) recommendations.
320
+
321
+ ## License
322
+
323
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
324
+
data/RELEASE_NOTES.md ADDED
@@ -0,0 +1,75 @@
1
+ # Release Notes
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Major Changes
6
+
7
+ * none
8
+
9
+ ### Minor Changes
10
+
11
+ * none
12
+
13
+ ### Bugfixes
14
+
15
+ * none
16
+
17
+ ## [0.4.0] - 2022-12-08
18
+
19
+ ### Major Changes
20
+
21
+ * Renamed project to Invar
22
+
23
+ ### Minor Changes
24
+
25
+ * none
26
+
27
+ ### Bugfixes
28
+
29
+ * `#pretend` symbolizes provided key(s)
30
+ * `Scope#to_h` recursively calls `.to_h` on subscopes
31
+
32
+ ## [0.3.0] - Unreleased beta
33
+
34
+ ### Major Changes
35
+
36
+ * none
37
+
38
+ ### Minor Changes
39
+
40
+ * Added support for validation using dry-schema
41
+ * Added #key? to Scope
42
+
43
+ ### Bugfixes
44
+
45
+ * none
46
+
47
+ ## [0.2.0] - Unreleased beta
48
+
49
+ ### Major Changes
50
+
51
+ * Overrides are now done with #pretend
52
+
53
+ ### Minor Changes
54
+
55
+ * Master key is stripped of whitespace when read from file
56
+ * Added known keys to KeyError message
57
+ * Docs improvements
58
+
59
+ ### Bugfixes
60
+
61
+ * none
62
+
63
+ ## [0.1.0] - Unreleased beta
64
+
65
+ ### Major Changes
66
+
67
+ * Initial prototype
68
+
69
+ ### Minor Changes
70
+
71
+ * none
72
+
73
+ ### Bugfixes
74
+
75
+ * none
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'yard'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task default: :spec
10
+
11
+ YARD::Rake::YardocTask.new do |t|
12
+ t.files = %w[lib/**/*.rb]
13
+ # t.options = %w[--some-option]
14
+ t.stats_options = ['--list-undoc']
15
+ end
data/invar.gemspec ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'invar/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'invar'
10
+ spec.version = Invar::VERSION
11
+ spec.authors = ['Robin Miller']
12
+ spec.email = ['robin@tenjin.ca']
13
+
14
+ spec.summary = 'Single source of truth for environmental configuration.'
15
+ spec.description = <<~DESC
16
+ Locates and loads config YAML files based on XDG standard with the encrypted secrets file kept separately.
17
+ Includes useful rake tasks to make management easier. No code execution in config. Rails-independent. Gluten free.
18
+ DESC
19
+ spec.homepage = 'https://github.com/TenjinInc/invar'
20
+ spec.license = 'MIT'
21
+ spec.metadata = {
22
+ 'rubygems_mfa_required' => 'true'
23
+ }
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+
30
+ spec.required_ruby_version = '>= 2.7'
31
+
32
+ spec.add_dependency 'dry-schema', '>= 1.0'
33
+ spec.add_dependency 'lockbox', '>= 1.0'
34
+
35
+ spec.add_development_dependency 'bundler', '~> 2.3'
36
+ spec.add_development_dependency 'fakefs', '~> 1.9'
37
+ spec.add_development_dependency 'rake', '~> 13.0'
38
+ spec.add_development_dependency 'rspec', '~> 3.12'
39
+ spec.add_development_dependency 'simplecov', '~> 0.21'
40
+ spec.add_development_dependency 'yard', '~> 0.9'
41
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'invar/version'
4
+ require 'invar/scope'
5
+
6
+ require 'yaml'
7
+ require 'lockbox'
8
+ require 'pathname'
9
+
10
+ module Invar
11
+ # XDG values based on https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
12
+ module XDG
13
+ # Default values for various XDG variables
14
+ module Defaults
15
+ # Default XDG config directory within the user's $HOME.
16
+ CONFIG_HOME = '~/.config'
17
+
18
+ # Default XDG config direction within the broader system paths
19
+ CONFIG_DIRS = '/etc/xdg'
20
+ end
21
+ end
22
+
23
+ # Common file extension constants, excluding the dot.
24
+ module EXT
25
+ # File extension for YAML files
26
+ YAML = 'yml'
27
+ end
28
+
29
+ # Locates a config file within XDG standard location(s) and namespace subdirectory.
30
+ class FileLocator
31
+ attr_reader :search_paths
32
+
33
+ # Builds a new instance that will search in the namespace.
34
+ #
35
+ # @param [String] namespace Name of the subdirectory within the XDG standard location(s)
36
+ # @raise [InvalidNamespaceError] if the namespace is nil or empty string
37
+ def initialize(namespace)
38
+ raise InvalidNamespaceError, 'namespace cannot be nil' if namespace.nil?
39
+ raise InvalidNamespaceError, 'namespace cannot be an empty string' if namespace.empty?
40
+
41
+ @namespace = namespace
42
+
43
+ home_config_dir = ENV.fetch('XDG_CONFIG_HOME', XDG::Defaults::CONFIG_HOME)
44
+ alt_config_dirs = ENV.fetch('XDG_CONFIG_DIRS', XDG::Defaults::CONFIG_DIRS).split(':')
45
+
46
+ source_dirs = alt_config_dirs
47
+ source_dirs.unshift(home_config_dir) if ENV.key? 'HOME'
48
+ @search_paths = source_dirs.collect { |path| Pathname.new(path) / @namespace }.collect(&:expand_path)
49
+
50
+ freeze
51
+ end
52
+
53
+ # Locates the file with the given same. You may optionally provide an extension as a second argument.
54
+ #
55
+ # These are equivalent:
56
+ # find('config.yml')
57
+ # find('config', 'yml')
58
+ #
59
+ # @param [String] basename The file's basename
60
+ # @param [String] ext the file extension, excluding the dot.
61
+ # @return [Pathname] the path of the located file
62
+ # @raise [AmbiguousSourceError] if the file is found in multiple locations
63
+ # @raise [FileNotFoundError] if the file cannot be found
64
+ def find(basename, ext = nil)
65
+ basename = [basename, ext].join('.') if ext
66
+
67
+ full_paths = search_paths.collect { |dir| dir / basename }
68
+ files = full_paths.select(&:exist?)
69
+
70
+ if files.size > 1
71
+ msg = "Found more than 1 #{ basename } file: #{ files.join(', ') }."
72
+ raise AmbiguousSourceError, "#{ msg } #{ AmbiguousSourceError::HINT }"
73
+ end
74
+
75
+ files.first || raise(FileNotFoundError, "Could not find #{ basename }")
76
+ end
77
+
78
+ # Raised when the file cannot be found in any of the XDG search locations.
79
+ class FileNotFoundError < RuntimeError
80
+ end
81
+
82
+ # Raised when the provided namespace is invalid.
83
+ class InvalidNamespaceError < ArgumentError
84
+ end
85
+ end
86
+ end