ravioli 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +465 -0
- data/Rakefile +24 -0
- data/lib/ravioli.rb +35 -0
- data/lib/ravioli/builder.rb +218 -0
- data/lib/ravioli/configuration.rb +101 -0
- data/lib/ravioli/engine.rb +20 -0
- data/lib/ravioli/version.rb +5 -0
- metadata +259 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 10956034d9851d2c30cc00a73bbb95ece033cb8ca1d6bc66dee4ad106335b430
|
4
|
+
data.tar.gz: 340b538b33f19016fbc494633ef809a79b1df518016af43de1c84f5428ce42fd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e639d69ce9ccd5e373805b8e06edc8d2e5a468af695ce40dc6630cafbfe2d8ae403fe63cda38ba61b6d6b8becdbdca846566d6f5aea374a4ae9905ae66527d74
|
7
|
+
data.tar.gz: f71dd8b25999a8972fc3ac9173c488ce337233f5f2496dcdb084c59067ea48056026ac65d9f1a8f176aac52c5eca9b5c71b6202998ebc40761bb6ff0064c491e
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2020 Flip Sasser
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,465 @@
|
|
1
|
+
# Ravioli.rb 🍝
|
2
|
+
|
3
|
+
|
4
|
+
**Grab a fork and twist your configuration spaghetti in a single, delicious dumpling!**
|
5
|
+
|
6
|
+
Ravioli combines all of your app's runtime configuration into a unified, simple interface. **It combines YAML or JSON configuration files, encrypted Rails credentials, and ENV vars into one easy-to-consume interface** so you can focus on writing code and not on where configuration comes from.
|
7
|
+
|
8
|
+
**Ravioli turns this...**
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
key = ENV.fetch("THING_API_KEY") { Rails.credentials.thing&["api_key"] || raise("I need an API key for thing to work") }
|
12
|
+
```
|
13
|
+
|
14
|
+
**...into this:**
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
key = Rails.config.dig!(:thing, :api_key)
|
18
|
+
```
|
19
|
+
|
20
|
+
**🚨 FYI:** Ravioli is two libraries: a Ruby gem (this doc), and a [JavaScript NPM package](src/README.md). The NPM docs contain specifics about how to [use Ravioli in the Rails asset pipeline](src/README.md#using-in-the-rails-asset-pipeline), in [a Node web server](src/README.md#using-in-a-server), or [bundled into a client using Webpack](src/README.md#using-with-webpack), [Rollup](src/README.md#using-with-rollup), or [whatever else](src/README.md#using-with-another-bundler).
|
21
|
+
|
22
|
+
## Table of Contents
|
23
|
+
|
24
|
+
1. [Installation](#installation)
|
25
|
+
2. [Usage](#usage)
|
26
|
+
3. [Automatic Configuration](#automatic-configuration)
|
27
|
+
4. [Manual Configuration](#manual-configuration)
|
28
|
+
5. [Deploying](#deploying)
|
29
|
+
6. [License](#license)
|
30
|
+
<!-- 5. [JavaScript library](#javascript-library) -->
|
31
|
+
|
32
|
+
## Installation
|
33
|
+
|
34
|
+
<!--Ravioli comes as a Ruby gem or an NPM package; they work marginally differently. Let's focus on Ruby/Rails for now.
|
35
|
+
-->
|
36
|
+
1. Add `gem "ravioli"` to your `Gemfile`
|
37
|
+
2. Run `bundle install`
|
38
|
+
3. Add an initializer (totally optional): `rails generate ravioli:install` - Ravioli will do **everything** automatically for you if you skip this step, because I aim to *please*
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
Ravioli turns your app's configuration environment into a [PORO](http://blog.jayfields.com/2007/10/ruby-poro.html) with direct accessors and a few special methods. By *default*, it adds the method `Rails.config` that returns a Ravioli instance. You can access all of your app's configuration from there. _This is totally optional_ and you can also do everything manually, but for the sake of these initial examples, we'll use the `Rails.config` setup.
|
43
|
+
|
44
|
+
Either way, for the following examples, imagine we had the following configuration structure:*
|
45
|
+
|
46
|
+
```yaml
|
47
|
+
host: "example.com"
|
48
|
+
url: "https://www.example.com"
|
49
|
+
sender: "reply-welcome@example.com"
|
50
|
+
|
51
|
+
database:
|
52
|
+
host: "localhost"
|
53
|
+
port: "5432"
|
54
|
+
|
55
|
+
sendgrid:
|
56
|
+
api_key: "12345"
|
57
|
+
|
58
|
+
sentry:
|
59
|
+
api_key: "12345"
|
60
|
+
environment: <%= Rails.env %>
|
61
|
+
dsn: "https://sentry.io/whatever?api_key=12345"
|
62
|
+
```
|
63
|
+
|
64
|
+
<small>*this structure is the end result of Ravioli's loading process; it has nothing to do with filesystem organization or config file layout. We'll talk about that in a bit, so just slow your roll about loading up config files until then.</small>
|
65
|
+
|
66
|
+
**Got it? Good.** Let's access some configuration,
|
67
|
+
|
68
|
+
### Accessing values directly
|
69
|
+
|
70
|
+
Ravioli objects support direct accessors:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
Rails.config.host #=> "example.com"
|
74
|
+
Rails.config.database.port #=> "5432"
|
75
|
+
Rails.config.not.here #=> NoMethodError (undefined method `here' for nil:NilClass)
|
76
|
+
```
|
77
|
+
|
78
|
+
### Accessing configuration values safely by key path
|
79
|
+
|
80
|
+
#### Traversing the keypath with `dig`
|
81
|
+
|
82
|
+
You can traverse deeply nested config values safely with `dig`:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
Rails.config.dig(:database, :port) #=> "5432"
|
86
|
+
Rails.config.dig(:not, :here) #=> nil
|
87
|
+
```
|
88
|
+
|
89
|
+
This works the same in principle as the [`dig`](https://ruby-doc.org/core-2.7.2/Hash.html#method-i-dig) method on `Hash` objects, with the added benefit of not caring about key type (both symbols and strings are accepted).
|
90
|
+
|
91
|
+
#### Providing fallback values with `fetch`
|
92
|
+
|
93
|
+
You can provide a sane fallback value using `fetch`, which works like `dig` but accepts a block:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
Rails.config.fetch(:database, :port) { "5678" } #=> "5432" is returned from the config
|
97
|
+
Rails.config.fetch(:not, :here) { "PRESENT!" } #=> "PRESENT!" is returned from the block
|
98
|
+
```
|
99
|
+
|
100
|
+
**Note that `fetch` differs from the [`fetch`](https://ruby-doc.org/core-2.7.2/Hash.html#method-i-fetch) method on `Hash` objects.** Ravioli's `fetch` accepts keys as arguments, and does not accept a `default` argument - instead, the default _must_ appear inside of a block.
|
101
|
+
|
102
|
+
#### Requiring configuration values with `dig!`
|
103
|
+
|
104
|
+
If a part of your app cannot operate without a configuration value, e.g. an API key is required to make an API call, you can use `dig!`, which behaves identically to `dig` except it will raise a `KeyMissingError` if no value is specified:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
uri = URI("https://api.example.com/things/1")
|
108
|
+
request = Net::HTTP::Get.new(uri)
|
109
|
+
request["X-Example-API-Key"] = Rails.config.dig!(:example, :api_key) #=> Ravioli::KeyMissingError (could not find configuration value at key path [:example, :api_key])
|
110
|
+
```
|
111
|
+
|
112
|
+
#### Allowing for blank values with `safe` (or `dig(*keys, safe: true)`)
|
113
|
+
|
114
|
+
As a convenience for avoiding the billion dollar mistake, you can use `safe` to ensure you're operating on a configuration object, even if it has not been set for your environment:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
Rails.config.dig(:google) #=> nil
|
118
|
+
Rails.config.safe(:google) #=> #<Ravioli::Configuration {}>
|
119
|
+
Rails.config.dig(:google, safe: true) #=> #<Ravioli::Configuration {}>
|
120
|
+
```
|
121
|
+
|
122
|
+
Use `safe` when, for example, you don't want your code to explode because a root config key is not set. Here's an example:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
class GoogleMapsClient
|
126
|
+
include HTTParty
|
127
|
+
|
128
|
+
config = Rails.config.safe(:google)
|
129
|
+
headers "Auth-Token" => config.token, "Other-Header" => config.other_thing
|
130
|
+
base_uri config.fetch(:base_uri) { "https://api.google.com/maps-do-stuff-cool-right" }
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
### Querying for presence
|
135
|
+
|
136
|
+
In addition to direct accessors, you can append a `?` to a method to see if a value exists. For example:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
Rails.config.database.host? #=> true
|
140
|
+
Rails.config.database.password? #=> false
|
141
|
+
```
|
142
|
+
|
143
|
+
### `ENV` variables take precedence over loaded configuration
|
144
|
+
|
145
|
+
I guess the headline is the thing: `ENV` variables take precedence over loaded configuration files. When loading or querying your configuration, Ravioli checks for a capitalized `ENV` variable corresponding to the keypath you're searching.
|
146
|
+
|
147
|
+
For example:
|
148
|
+
|
149
|
+
```env
|
150
|
+
Rails.config.dig(:database, :url)
|
151
|
+
|
152
|
+
# ...is equivalent to...
|
153
|
+
|
154
|
+
ENV.fetch("DATABASE_URL") { Rails.config.database&.url }
|
155
|
+
```
|
156
|
+
|
157
|
+
This means that you can use Ravioli instead of querying `ENV` for its keys, and it'll get you the right value every time.
|
158
|
+
|
159
|
+
## Automatic Configuration
|
160
|
+
|
161
|
+
**The fastest way to use Ravioli is via automatic configuration,** bootstrapping it into the `Rails.config` method. This is the default experience when you `require "ravioli"`, either explicitly through an initializer or implicitly through `gem "ravioli"` in your Gemfile.
|
162
|
+
|
163
|
+
**Automatic configuration takes the following steps for you:**
|
164
|
+
|
165
|
+
### 1. Adds a `staging` flag
|
166
|
+
|
167
|
+
First, Ravioli adds a `staging` flag to `Rails.config`. It defaults to `true` if:
|
168
|
+
|
169
|
+
1. `ENV["RAILS_ENV"]` is set to "production"
|
170
|
+
2. `ENV["STAGING"]` is not blank
|
171
|
+
|
172
|
+
Using [query accessors](#querying-for-presence), you can access this value as `Rails.config.staging?`.
|
173
|
+
|
174
|
+
**BUT, as I am a generous and loving man,** Ravioli will also ensure `Rails.env.staging?` returns `true` if 1 and 2 are true above:
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
ENV["RAILS_ENV"] = "production"
|
178
|
+
Rails.env.staging? #=> false
|
179
|
+
Rails.env.production? #=> true
|
180
|
+
|
181
|
+
ENV["STAGING"] = "totes"
|
182
|
+
Rails.env.staging? #=> true
|
183
|
+
Rails.env.production? #=> true
|
184
|
+
```
|
185
|
+
|
186
|
+
### 2. Loads every plaintext configuration file it can find
|
187
|
+
|
188
|
+
Ravioli will traverse your `config/` directory looking for every YAML or JSON file it can find. It loads them in arbitrary order, and keys them by name. For example, with the following directory layout:
|
189
|
+
|
190
|
+
```
|
191
|
+
config/
|
192
|
+
app.yml
|
193
|
+
cable.yml
|
194
|
+
database.yml
|
195
|
+
mailjet.json
|
196
|
+
```
|
197
|
+
|
198
|
+
...the automatically loaded configuration will look like
|
199
|
+
|
200
|
+
```
|
201
|
+
# ...the contents of app.yml
|
202
|
+
cable:
|
203
|
+
# ...the contents of cable.yml
|
204
|
+
database:
|
205
|
+
# ...the contents of database.yml
|
206
|
+
mailjet:
|
207
|
+
# ...the contents of mailjet.json
|
208
|
+
```
|
209
|
+
|
210
|
+
**NOTE THAT APP.YML GOT LOADED INTO THE ROOT OF THE CONFIGURATION!** This is because the automatic loading system assumes you want some configuration values that aren't nested. It effectively calls [`load_configuration_file(filename, key: File.basename(filename) != "app")`](#load_configuration_file), which ensures that, for example, the values in `config/mailjet.json` get loaded under `Rails.config.mailjet` while the valuaes in `config/app.yml` get loaded directly into `Rails.config`.
|
211
|
+
|
212
|
+
### 3. Loads and combines encrypted credentials
|
213
|
+
|
214
|
+
Ravioli will then check for [encrypted credentials](https://guides.rubyonrails.org/security.html#custom-credentials). It loads credentials in the following order:
|
215
|
+
|
216
|
+
1. First, it loads `config/credentials.yml.enc`
|
217
|
+
2. Then, it loads and applies `config/credentials/RAILS_ENV.yml.enc` over top of what it has already loaded
|
218
|
+
3. Finally, IF `Rails.config.staging?` IS TRUE, it loads and applies `config/credentials/staging.yml.enc`
|
219
|
+
|
220
|
+
This allows you to use your secure credentials stores without duplicating information; you can simply layer environment-specific values over top of
|
221
|
+
|
222
|
+
### All put together, it does this:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
def Rails.config
|
226
|
+
@config ||= Ravioli.build(strict: Rails.env.production?) do |config|
|
227
|
+
config.add_staging_flag!
|
228
|
+
config.auto_load_config_files!
|
229
|
+
config.auto_load_credentials!
|
230
|
+
end
|
231
|
+
end
|
232
|
+
```
|
233
|
+
|
234
|
+
I documented that because, you know, you can do parts of that yourself when we get into the weeds with.........
|
235
|
+
|
236
|
+
## Manual configuration
|
237
|
+
|
238
|
+
If any of the above doesn't suit you, by all means, Ravioli is flexible enough for you to build your own instance. There are a number of things you can change, so read through to see what you can do by going your own way.
|
239
|
+
|
240
|
+
### Using `Ravioli.build`
|
241
|
+
|
242
|
+
The best way to build your own configuration is by calling `Ravioli.build`. It will yield an instance of a `Ravioli::Builder`, which has lots of convenient methods for loading configuration files, credentials, and the like. It works like so:
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
configuration = Ravioli.build do |config|
|
246
|
+
config.load_configuration_file("whatever.yml")
|
247
|
+
config.whatever = {things: true}
|
248
|
+
end
|
249
|
+
```
|
250
|
+
|
251
|
+
This will yield a configured instance of `Ravioli::Configuration` with structure
|
252
|
+
|
253
|
+
```yaml
|
254
|
+
rubocop:
|
255
|
+
# ...the contents of whatever.yml
|
256
|
+
whatever:
|
257
|
+
things: true
|
258
|
+
```
|
259
|
+
|
260
|
+
`Ravioli.build` also does a few handy things:
|
261
|
+
|
262
|
+
- It freezes the configuration object so it is immutable,
|
263
|
+
- It caches the final configuration in `Ravioli.configurations`, and
|
264
|
+
- It sets `Ravioli.default` to the most-recently built configuration
|
265
|
+
|
266
|
+
### Direct construction with `Ravioli::Configuration.new`
|
267
|
+
|
268
|
+
You can also directly construct a configuration object by passing a hash to `Ravioli::Configuration.new`. This is basically the same thing as an `OpenStruct` with the added [helper methods of a Ravioli object](#usage):
|
269
|
+
|
270
|
+
```ruby
|
271
|
+
config = Ravioli::Configuration.new(whatever: true, test: {things: "stuff"})
|
272
|
+
config.dig(:test, :things) #=> "stuff
|
273
|
+
```
|
274
|
+
|
275
|
+
### Alternatives to using `Rails.config`
|
276
|
+
|
277
|
+
By default, Ravioli loads a default configuration in `Rails.config`. If you are already using `Rails.config` for something else, or you just hate the idea of all those letters, you can do it however else makes sense to you: in a constant (e.g. `Config` or `App`), or somewhere else entirely (you could, for example, define a `Config` module, mix it in to your classes where it's needed, and access it via a `config` instance method).
|
278
|
+
|
279
|
+
Here's an example using an `App` constant:
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
# config/initializers/_config.rb
|
283
|
+
App = Raviloli.build { |config| ... }
|
284
|
+
```
|
285
|
+
|
286
|
+
You can also point it to `Rails.config` if you'd like to access configuration somewhere other than `Rails.config`, but you want to enjoy the benefits of [automatic configuration](#automatic-configuration):
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
# config/initializers/_config.rb
|
290
|
+
App = Rails.config
|
291
|
+
```
|
292
|
+
|
293
|
+
You could also opt-in to configuration access with a module:
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
module Config
|
297
|
+
def config
|
298
|
+
Ravioli.default || Ravioli.build {|config| ... }
|
299
|
+
end
|
300
|
+
end
|
301
|
+
```
|
302
|
+
|
303
|
+
### `add_staging_flag!`
|
304
|
+
|
305
|
+
|
306
|
+
### `load_config_file`
|
307
|
+
|
308
|
+
Let's imagine we have this config file:
|
309
|
+
|
310
|
+
`config/mailjet.yml`
|
311
|
+
|
312
|
+
```yaml
|
313
|
+
development:
|
314
|
+
api_key: "NOT_USED"
|
315
|
+
|
316
|
+
test:
|
317
|
+
api_key: "VCR"
|
318
|
+
|
319
|
+
staging:
|
320
|
+
api_key: "12345678"
|
321
|
+
|
322
|
+
production:
|
323
|
+
api_key: "98765432"
|
324
|
+
```
|
325
|
+
|
326
|
+
In an initializer, generate your Ravioli instance and load it up:
|
327
|
+
|
328
|
+
|
329
|
+
```ruby
|
330
|
+
# config/initializers/_ravioli.rb`
|
331
|
+
Config = Ravioli.build do
|
332
|
+
load_config_file(:mailjet) # given a symbol, it automatically assumes you meant `config/mailjet.yml`
|
333
|
+
load_config_file("config/mailjet") # same as above
|
334
|
+
load_config_file("lib/mailjet/config") # looks for `Rails.root.join("lib", "mailjet", "config.yml")
|
335
|
+
end
|
336
|
+
```
|
337
|
+
|
338
|
+
`config/initializers/_ravioli.rb`
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
Config = Ravioli.build do
|
342
|
+
%i[new_relic sentry google].each do |service|
|
343
|
+
load_config_file(service)
|
344
|
+
end
|
345
|
+
|
346
|
+
load_credentials # just load the base credentials file
|
347
|
+
load_credentials("credentials/production") if Rails.env.production? # add production overrides when appropriate
|
348
|
+
|
349
|
+
self.staging = File.exists?("./staging.txt") # technically you could do this ... I don't know why you would, but technically you could
|
350
|
+
end
|
351
|
+
```
|
352
|
+
|
353
|
+
Configuration values take precedence in the order they are applied. For example, if you load two config files defining `host`, the latest one will overwrite the earlier one's value.
|
354
|
+
|
355
|
+
|
356
|
+
### `load_credentials`
|
357
|
+
|
358
|
+
Imagine the following encrypted YAML files:
|
359
|
+
|
360
|
+
#### `config/credentials.yml.enc`
|
361
|
+
|
362
|
+
Accessing the credentials with `rails credentials:edit`, let's say you have the following encrypted file:
|
363
|
+
|
364
|
+
```yaml
|
365
|
+
mailet:
|
366
|
+
api_key: "12345"
|
367
|
+
```
|
368
|
+
|
369
|
+
#### `config/credentials/production.yml.enc`
|
370
|
+
|
371
|
+
Edit with `rails credentials:edit --environment production`
|
372
|
+
|
373
|
+
```yaml
|
374
|
+
mailet:
|
375
|
+
api_key: "67891"
|
376
|
+
```
|
377
|
+
|
378
|
+
You can then load credentials like so:
|
379
|
+
|
380
|
+
``config/initializers/_ravioli.rb`
|
381
|
+
|
382
|
+
```ruby
|
383
|
+
Config = Ravioli.build do
|
384
|
+
# Load the base credentials
|
385
|
+
load_credentials
|
386
|
+
|
387
|
+
# Load the env-specific credentials file. It will look for `config/credentials/#{Rails.env}.key`
|
388
|
+
# just like Rails does. But in this case, it falls back on e.g. `ENV["PRODUCTION_KEY"]` if that
|
389
|
+
# file is missing (as it should be when deployed to a remote server)
|
390
|
+
load_credentials("credentials/#{Rails.env}", env_key: "#{Rails.env}_KEY")
|
391
|
+
|
392
|
+
# Load the staging credentials. Because we did not provide an `env_key` argument, this will
|
393
|
+
# default to looking for `ENV["RAILS_STAGING_KEY"]` or `ENV["RAILS_MASTER_KEY"]`.
|
394
|
+
load_credentials("credentials/staging") if Rails.env.production? && srand.zero?
|
395
|
+
end
|
396
|
+
```
|
397
|
+
|
398
|
+
|
399
|
+
|
400
|
+
You can manually define your configuration in an initializer if you don't want the automatic configuration assumptions to step on any toes.
|
401
|
+
|
402
|
+
For the following examples, imagine a file in `config/sentry.yml`:
|
403
|
+
|
404
|
+
```yaml
|
405
|
+
development:
|
406
|
+
dsn: "https://dev_user:pass@sentry.io/dsn/12345"
|
407
|
+
environment: "development"
|
408
|
+
|
409
|
+
production:
|
410
|
+
dsn: "https://prod_user:pass@sentry.io/dsn/12345"
|
411
|
+
environment: "production"
|
412
|
+
|
413
|
+
staging:
|
414
|
+
environment: "staging"
|
415
|
+
```
|
416
|
+
|
417
|
+
## Deploying
|
418
|
+
|
419
|
+
### Encryption keys in ENV
|
420
|
+
|
421
|
+
Because Ravioli merges environment-specific credentials over top of the root credentials file, you'll need to provide encryption keys for two (or, if you have a staging setup, three) different files in ENV vars. As such, Ravioli looks for decryption keys in a fallback-specific way. Here's where it looks for each file:
|
422
|
+
|
423
|
+
<table><thead><tr><th>File</th><th>First it tries...</th><th>Then it tries...</th></tr></thead><tbody><tr><td>
|
424
|
+
|
425
|
+
`config/credentials.yml.enc`
|
426
|
+
|
427
|
+
</td><td>
|
428
|
+
|
429
|
+
`ENV["RAILS_BASE_KEY"]`
|
430
|
+
|
431
|
+
</td><td>
|
432
|
+
|
433
|
+
`ENV["RAILS_MASTER_KEY"]`
|
434
|
+
|
435
|
+
</td></tr><tr><td>
|
436
|
+
|
437
|
+
`config/credentials/production.yml.enc`
|
438
|
+
|
439
|
+
</td><td>
|
440
|
+
|
441
|
+
`ENV["RAILS_PRODUCTION_KEY"]`
|
442
|
+
|
443
|
+
</td><td>
|
444
|
+
|
445
|
+
`ENV["RAILS_MASTER_KEY"]`
|
446
|
+
|
447
|
+
</td></tr><tr><td>
|
448
|
+
|
449
|
+
`config/credentials/staging.yml.enc` (only if running on staging)
|
450
|
+
|
451
|
+
</td><td>
|
452
|
+
|
453
|
+
`ENV["RAILS_STAGING_KEY"]`
|
454
|
+
|
455
|
+
</td><td>
|
456
|
+
|
457
|
+
`ENV["RAILS_MASTER_KEY"]`
|
458
|
+
|
459
|
+
</td></tr></tbody></table>
|
460
|
+
|
461
|
+
Credentials are loaded in that order, too, so that you can have a base setup on `config/credentials.yml.enc`, overlay that with production-specific stuff from `config/credentials/production.yml.enc`, and then short-circuit or redirect some stuff in `config/credentials/staging.yml.enc` for staging environments.
|
462
|
+
|
463
|
+
## License
|
464
|
+
|
465
|
+
Ravioli is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "bundler/setup"
|
5
|
+
rescue LoadError
|
6
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
7
|
+
end
|
8
|
+
|
9
|
+
require "rdoc/task"
|
10
|
+
|
11
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
12
|
+
rdoc.rdoc_dir = "rdoc"
|
13
|
+
rdoc.title = "Ravioli"
|
14
|
+
rdoc.options << "--line-numbers"
|
15
|
+
rdoc.rdoc_files.include("README.md")
|
16
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
17
|
+
end
|
18
|
+
|
19
|
+
APP_RAKEFILE = File.expand_path("spec/fixtures/dummy/Rakefile", __dir__)
|
20
|
+
load "rails/tasks/engine.rake"
|
21
|
+
|
22
|
+
load "rails/tasks/statistics.rake"
|
23
|
+
|
24
|
+
require "bundler/gem_tasks"
|
data/lib/ravioli.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/all"
|
4
|
+
|
5
|
+
# These are the basic building blocks of Ravioli
|
6
|
+
require_relative "ravioli/builder"
|
7
|
+
require_relative "ravioli/configuration"
|
8
|
+
require_relative "ravioli/version"
|
9
|
+
|
10
|
+
##
|
11
|
+
# Ravioli contains helper methods for building Configuration instances and accessing them, as well
|
12
|
+
# as the Builder class for help loading configuration files and encrypted credentials
|
13
|
+
module Ravioli
|
14
|
+
NAME = "Ravioli"
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def build(class_name: "Configuration", namespace: nil, strict: false, &block)
|
18
|
+
builder = Builder.new(class_name: class_name, namespace: namespace, strict: strict)
|
19
|
+
yield builder if block_given?
|
20
|
+
builder.build!.tap do |configuration|
|
21
|
+
configurations.push(configuration)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def default
|
26
|
+
configurations.last
|
27
|
+
end
|
28
|
+
|
29
|
+
def configurations
|
30
|
+
@configurations ||= []
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
require_relative "ravioli/engine" if defined?(Rails)
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/all"
|
4
|
+
require_relative "configuration"
|
5
|
+
|
6
|
+
module Ravioli
|
7
|
+
# The Builder clas provides a simple interface for building a Ravioli configuration. It has
|
8
|
+
# methods for loading configuration files and encrypted credentials, and forwards direct
|
9
|
+
# configuration on to the configuration instance. This allows us to keep a clean separation of
|
10
|
+
# concerns (builder: loads configuration details; configuration: provides access to information
|
11
|
+
# in memory).
|
12
|
+
class Builder
|
13
|
+
def initialize(class_name: "Configuration", namespace: nil, strict: false)
|
14
|
+
configuration_class = if namespace.present?
|
15
|
+
namespace.class_eval <<-EOC, __FILE__, __LINE__ + 1
|
16
|
+
class #{class_name.to_s.classify} < Ravioli::Configuration; end
|
17
|
+
EOC
|
18
|
+
namespace.const_get(class_name)
|
19
|
+
else
|
20
|
+
Ravioli::Configuration
|
21
|
+
end
|
22
|
+
@strict = !!strict
|
23
|
+
@configuration = configuration_class.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# Automatically infer a `staging?` status
|
27
|
+
def add_staging_flag!(is_staging = Rails.env.production? && ENV["STAGING"].present?)
|
28
|
+
configuration.staging = is_staging
|
29
|
+
Rails.env.class_eval <<-EOC, __FILE__, __LINE__ + 1
|
30
|
+
def staging?
|
31
|
+
config = Rails.try(:config)
|
32
|
+
return false unless config&.is_a?(Ravioli::Configuration)
|
33
|
+
|
34
|
+
config.staging?
|
35
|
+
end
|
36
|
+
EOC
|
37
|
+
is_staging
|
38
|
+
end
|
39
|
+
|
40
|
+
# Load YAML or JSON files in config/**/* (except for locales)
|
41
|
+
def auto_load_config_files!
|
42
|
+
config_dir = Rails.root.join("config")
|
43
|
+
Dir[config_dir.join("{[!locales/]**/*,*}.{json,yaml,yml}")].each do |config_file|
|
44
|
+
load_config_file(config_file, key: !File.basename(config_file, File.extname(config_file)).casecmp("app").zero?)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Load config/credentials**/*.yml.enc files (assuming we can find a key)
|
49
|
+
def auto_load_credentials!
|
50
|
+
# Load the base config
|
51
|
+
load_credentials(key_path: "config/master.key", env_name: "base")
|
52
|
+
|
53
|
+
# Load any environment-specific configuration on top of it
|
54
|
+
load_credentials("config/credentials/#{Rails.env}", key_path: "config/credentials/#{Rails.env}.key", env_name: "master")
|
55
|
+
|
56
|
+
# Apply staging configuration on top of THAT, if need be
|
57
|
+
load_credentials("config/credentials/staging", key_path: "config/credentials/staging.key") if configuration.staging?
|
58
|
+
end
|
59
|
+
|
60
|
+
# When the builder is done working, lock the configuration and return it
|
61
|
+
def build!
|
62
|
+
configuration.freeze
|
63
|
+
end
|
64
|
+
|
65
|
+
# Load a config file either with a given path or by name (e.g. `config/whatever.yml` or `:whatever`)
|
66
|
+
def load_config_file(path, options = {})
|
67
|
+
config = parse_config_file(path, options)
|
68
|
+
configuration.append(config) if config.present?
|
69
|
+
rescue => error
|
70
|
+
warn "Could not load config file #{path}", error
|
71
|
+
end
|
72
|
+
|
73
|
+
# Load secure credentials using a key either from a file or the ENV
|
74
|
+
def load_credentials(path = "credentials", key_path: path, env_name: path.split("/").last)
|
75
|
+
credentials = parse_credentials(path, env_name: env_name, key_path: key_path)
|
76
|
+
configuration.append(credentials) if credentials.present?
|
77
|
+
rescue => error
|
78
|
+
warn "Could not decrypt `#{path}.yml.enc' with key file `#{key_path}' or `ENV[\"#{env_name}\"]'", error
|
79
|
+
{}
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
ENV_KEYS = %w[default development production shared staging test].freeze
|
85
|
+
EXTNAMES = %w[yml yaml json].freeze
|
86
|
+
|
87
|
+
attr_reader :configuration
|
88
|
+
|
89
|
+
def extract_environmental_config(config)
|
90
|
+
# Check if the config hash is keyed by environment - if not, just return it as-is. It's
|
91
|
+
# considered "keyed by environment" if it contains ONLY env-specific keys.
|
92
|
+
return config unless (config.keys & ENV_KEYS).any? && (config.keys - ENV_KEYS).empty?
|
93
|
+
|
94
|
+
# Combine environmental config in the following order:
|
95
|
+
# 1. Shared config
|
96
|
+
# 2. Environment-specific
|
97
|
+
# 3. Staging-specific (if we're in a staging environment)
|
98
|
+
environments = ["shared", Rails.env.to_s]
|
99
|
+
environments.push("staging") if configuration.staging?
|
100
|
+
config.values_at(*environments).inject({}) { |final_config, environment_config|
|
101
|
+
final_config.deep_merge((environment_config || {}))
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
# rubocop:disable Style/MethodMissingSuper
|
106
|
+
# rubocop:disable Style/MissingRespondToMissing
|
107
|
+
def method_missing(*args, &block)
|
108
|
+
configuration.send(*args, &block)
|
109
|
+
end
|
110
|
+
# rubocop:enable Style/MissingRespondToMissing
|
111
|
+
# rubocop:enable Style/MethodMissingSuper
|
112
|
+
|
113
|
+
def parse_config_file(path, options = {})
|
114
|
+
path = path_to_config_file_path(path)
|
115
|
+
|
116
|
+
config = case path.extname.downcase
|
117
|
+
when ".json"
|
118
|
+
parse_json_config_file(path)
|
119
|
+
when ".yml", ".yaml"
|
120
|
+
parse_yaml_config_file(path)
|
121
|
+
else
|
122
|
+
raise ParseError.new("#{Ravioli::NAME} doesn't know how to parse #{path}")
|
123
|
+
end
|
124
|
+
|
125
|
+
# At least expect a hash to be returned from the loaded config file
|
126
|
+
return {} unless config.is_a?(Hash)
|
127
|
+
|
128
|
+
# Extract a merged config based on the Rails.env (if the file is keyed that way)
|
129
|
+
config = extract_environmental_config(config)
|
130
|
+
|
131
|
+
# Key the configuration according the passed-in options
|
132
|
+
key = options.delete(:key) { true }
|
133
|
+
return config if key == false # `key: false` means don't key the configuration at all
|
134
|
+
|
135
|
+
if key == true
|
136
|
+
# `key: true` means key it automatically based on the filename
|
137
|
+
name = File.basename(path, File.extname(path))
|
138
|
+
name = File.dirname(path).split(Pathname::SEPARATOR_PAT).last if name.casecmp("config").zero?
|
139
|
+
else
|
140
|
+
# `key: :anything_else` means use `:anything_else` as the key
|
141
|
+
name = key.to_s
|
142
|
+
end
|
143
|
+
|
144
|
+
{name => config}
|
145
|
+
end
|
146
|
+
|
147
|
+
def parse_credentials(path, key_path: path, env_name: path.split("/").last)
|
148
|
+
env_name = env_name.to_s
|
149
|
+
env_name = "RAILS_#{env_name.upcase}_KEY" unless env_name.upcase == env_name
|
150
|
+
key_path = path_to_config_file_path(key_path, extnames: "key", quiet: true)
|
151
|
+
options = {key_path: key_path}
|
152
|
+
options[:env_key] = ENV[env_name].present? ? env_name : SecureRandom.hex(6)
|
153
|
+
|
154
|
+
path = path_to_config_file_path(path, extnames: "yml.enc")
|
155
|
+
credentials = Rails.application.encrypted(path, options)&.config || {}
|
156
|
+
credentials
|
157
|
+
end
|
158
|
+
|
159
|
+
def parse_json_config_file(path)
|
160
|
+
contents = File.read(path)
|
161
|
+
JSON.parse(contents).deep_transform_keys { |key| key.to_s.underscore }
|
162
|
+
end
|
163
|
+
|
164
|
+
def parse_yaml_config_file(path)
|
165
|
+
require "erb"
|
166
|
+
contents = File.read(path)
|
167
|
+
erb = ERB.new(contents).tap { |renderer| renderer.filename = path.to_s }
|
168
|
+
YAML.safe_load(erb.result, aliases: true)
|
169
|
+
end
|
170
|
+
|
171
|
+
def path_to_config_file_path(path, extnames: EXTNAMES, quiet: false)
|
172
|
+
original_path = path.dup
|
173
|
+
unless path.is_a?(Pathname)
|
174
|
+
path = path.to_s
|
175
|
+
path = path.match?(Pathname::SEPARATOR_PAT) ? Pathname.new(path) : Pathname.new("config").join(path)
|
176
|
+
end
|
177
|
+
path = Rails.root.join(path) unless path.absolute?
|
178
|
+
|
179
|
+
# Try to guess an extname, if we weren't given one
|
180
|
+
if path.extname.blank?
|
181
|
+
Array(extnames).each do |extname|
|
182
|
+
other_path = path.sub_ext(".#{extname}")
|
183
|
+
if other_path.exist?
|
184
|
+
path = other_path
|
185
|
+
break
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
warn "Could not resolve a configuration file at #{original_path.inspect}" unless quiet || path.exist?
|
191
|
+
|
192
|
+
path
|
193
|
+
end
|
194
|
+
|
195
|
+
def warn(message, error = $!)
|
196
|
+
message = "[#{Ravioli::NAME}] #{message}"
|
197
|
+
message = "#{message}:\n\n#{error.cause.inspect}" if error&.cause.present?
|
198
|
+
if @strict
|
199
|
+
raise BuildError.new(message, error)
|
200
|
+
else
|
201
|
+
Rails.logger.warn(message) if defined? Rails
|
202
|
+
$stderr.write message # rubocop:disable Rails/Output
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
class BuildError < StandardError
|
208
|
+
def initialize(message, cause = nil)
|
209
|
+
super message
|
210
|
+
@cause = cause
|
211
|
+
end
|
212
|
+
|
213
|
+
def cause
|
214
|
+
@cause || super
|
215
|
+
end
|
216
|
+
end
|
217
|
+
class ParseError < StandardError; end
|
218
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/all"
|
4
|
+
require "ostruct"
|
5
|
+
|
6
|
+
module Ravioli
|
7
|
+
class Configuration < OpenStruct
|
8
|
+
attr_reader :key_path
|
9
|
+
|
10
|
+
def initialize(attributes = {})
|
11
|
+
super({})
|
12
|
+
@key_path = attributes.delete(:key_path)
|
13
|
+
append(attributes)
|
14
|
+
end
|
15
|
+
|
16
|
+
# def ==(other)
|
17
|
+
# other = other.table if other.respond_to?(:table)
|
18
|
+
# other == table
|
19
|
+
# end
|
20
|
+
|
21
|
+
def append(attributes = {})
|
22
|
+
attributes.each do |key, value|
|
23
|
+
self[key.to_sym] = cast(key.to_sym, value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def dig(*keys, safe: false)
|
28
|
+
return safe(*keys) if safe
|
29
|
+
|
30
|
+
fetch_env_key_for(keys) do
|
31
|
+
keys.inject(self) do |value, key|
|
32
|
+
value = value.try(:[], key)
|
33
|
+
break if value.blank?
|
34
|
+
value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def dig!(*keys)
|
40
|
+
fetch(*keys) { raise KeyMissingError.new("Could not find value at key path #{keys.inspect}") }
|
41
|
+
end
|
42
|
+
|
43
|
+
def fetch(*keys)
|
44
|
+
dig(*keys) || yield
|
45
|
+
end
|
46
|
+
|
47
|
+
def pretty_print(printer = nil)
|
48
|
+
table.pretty_print(printer)
|
49
|
+
end
|
50
|
+
|
51
|
+
def safe(*keys)
|
52
|
+
fetch(*keys) { build(keys) }
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def build(keys, attributes = {})
|
58
|
+
attributes[:key_path] = key_path_for(keys)
|
59
|
+
child = self.class.new(attributes)
|
60
|
+
child.freeze if frozen?
|
61
|
+
child
|
62
|
+
end
|
63
|
+
|
64
|
+
def cast(key, value)
|
65
|
+
if value.is_a?(Hash)
|
66
|
+
original_value = dig(*Array(key))
|
67
|
+
value = original_value.table.deep_merge(value.deep_symbolize_keys) if original_value.is_a?(self.class)
|
68
|
+
build(key, value)
|
69
|
+
else
|
70
|
+
fetch_env_key_for(key) {
|
71
|
+
if value.is_a?(Array)
|
72
|
+
value.each_with_index.map { |subvalue, index| cast(Array(key) + [index], subvalue) }
|
73
|
+
else
|
74
|
+
value
|
75
|
+
end
|
76
|
+
}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def fetch_env_key_for(keys, &block)
|
81
|
+
env_key = key_path_for(keys).join("_").upcase
|
82
|
+
ENV.fetch(env_key, &block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def key_path_for(keys)
|
86
|
+
Array(key_path) + Array(keys)
|
87
|
+
end
|
88
|
+
|
89
|
+
# rubocop:disable Style/MethodMissingSuper
|
90
|
+
# rubocop:disable Style/MissingRespondToMissing
|
91
|
+
def method_missing(method, *args, &block)
|
92
|
+
# Return proper booleans from query methods
|
93
|
+
return send(method.to_s.chomp("?")).present? if args.empty? && method.to_s.ends_with?("?")
|
94
|
+
super
|
95
|
+
end
|
96
|
+
# rubocop:enable Style/MissingRespondToMissing
|
97
|
+
# rubocop:enable Style/MethodMissingSuper
|
98
|
+
end
|
99
|
+
|
100
|
+
class KeyMissingError < StandardError; end
|
101
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ravioli
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
# Bootstrap Ravioli onto the Rails app
|
6
|
+
initializer "ravioli", before: "load_environment_config" do |app|
|
7
|
+
Rails.extend Ravioli::Config unless Rails.respond_to?(:config)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Config
|
12
|
+
def config
|
13
|
+
Ravioli.default || Ravioli.build(namespace: Rails.application&.class&.module_parent, strict: Rails.env.production?) do |config|
|
14
|
+
config.add_staging_flag!
|
15
|
+
config.auto_load_config_files!
|
16
|
+
config.auto_load_credentials!
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ravioli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Flip Sasser
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 6.0.3.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '6.0'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 6.0.3.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: pry
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rails
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '6'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '6'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rspec
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '3.9'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3.9'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rspec-rails
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rubocop
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0.8'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0.8'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: rubocop-ordered_methods
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.6'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0.6'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: rubocop-performance
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: 1.5.2
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 1.5.2
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: rubocop-rails
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: 2.5.2
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: 2.5.2
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
name: rubocop-rspec
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - "~>"
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '1.39'
|
152
|
+
type: :development
|
153
|
+
prerelease: false
|
154
|
+
version_requirements: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - "~>"
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '1.39'
|
159
|
+
- !ruby/object:Gem::Dependency
|
160
|
+
name: simplecov
|
161
|
+
requirement: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
- !ruby/object:Gem::Dependency
|
174
|
+
name: sqlite3
|
175
|
+
requirement: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
type: :development
|
181
|
+
prerelease: false
|
182
|
+
version_requirements: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0'
|
187
|
+
- !ruby/object:Gem::Dependency
|
188
|
+
name: standard
|
189
|
+
requirement: !ruby/object:Gem::Requirement
|
190
|
+
requirements:
|
191
|
+
- - "~>"
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '0.4'
|
194
|
+
type: :development
|
195
|
+
prerelease: false
|
196
|
+
version_requirements: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - "~>"
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0.4'
|
201
|
+
- !ruby/object:Gem::Dependency
|
202
|
+
name: yard
|
203
|
+
requirement: !ruby/object:Gem::Requirement
|
204
|
+
requirements:
|
205
|
+
- - "~>"
|
206
|
+
- !ruby/object:Gem::Version
|
207
|
+
version: '0.9'
|
208
|
+
type: :development
|
209
|
+
prerelease: false
|
210
|
+
version_requirements: !ruby/object:Gem::Requirement
|
211
|
+
requirements:
|
212
|
+
- - "~>"
|
213
|
+
- !ruby/object:Gem::Version
|
214
|
+
version: '0.9'
|
215
|
+
description: Ravioli combines all of your app's runtime configuration into a unified,
|
216
|
+
simple interface. It automatically loads and combines YAML config files, encrypted
|
217
|
+
Rails credentials, and ENV vars so you can focus on writing code and not on where
|
218
|
+
configuration comes from
|
219
|
+
email:
|
220
|
+
- hello@flipsasser.com
|
221
|
+
executables: []
|
222
|
+
extensions: []
|
223
|
+
extra_rdoc_files: []
|
224
|
+
files:
|
225
|
+
- MIT-LICENSE
|
226
|
+
- README.md
|
227
|
+
- Rakefile
|
228
|
+
- lib/ravioli.rb
|
229
|
+
- lib/ravioli/builder.rb
|
230
|
+
- lib/ravioli/configuration.rb
|
231
|
+
- lib/ravioli/engine.rb
|
232
|
+
- lib/ravioli/version.rb
|
233
|
+
homepage: https://github.com/flipsasser/ravioli
|
234
|
+
licenses:
|
235
|
+
- MIT
|
236
|
+
metadata:
|
237
|
+
homepage_uri: https://github.com/flipsasser/ravioli
|
238
|
+
source_code_uri: https://github.com/flipsasser/ravioli
|
239
|
+
post_install_message:
|
240
|
+
rdoc_options: []
|
241
|
+
require_paths:
|
242
|
+
- lib
|
243
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
244
|
+
requirements:
|
245
|
+
- - ">="
|
246
|
+
- !ruby/object:Gem::Version
|
247
|
+
version: 2.3.0
|
248
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
249
|
+
requirements:
|
250
|
+
- - ">="
|
251
|
+
- !ruby/object:Gem::Version
|
252
|
+
version: '0'
|
253
|
+
requirements: []
|
254
|
+
rubygems_version: 3.0.3
|
255
|
+
signing_key:
|
256
|
+
specification_version: 4
|
257
|
+
summary: Grab a fork and twist all your configuration spaghetti into a single, delicious
|
258
|
+
bundle
|
259
|
+
test_files: []
|