invar 0.4.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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +26 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +324 -0
- data/RELEASE_NOTES.md +75 -0
- data/Rakefile +15 -0
- data/invar.gemspec +41 -0
- data/lib/invar/file_locator.rb +86 -0
- data/lib/invar/rake.rb +47 -0
- data/lib/invar/rake_tasks.rb +178 -0
- data/lib/invar/scope.rb +58 -0
- data/lib/invar/test.rb +16 -0
- data/lib/invar/version.rb +6 -0
- data/lib/invar.rb +273 -0
- metadata +176 -0
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
data/.rspec
ADDED
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
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
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
|