ravioli 0.1.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/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: []
|