ravioli 0.1.0 → 0.1.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10956034d9851d2c30cc00a73bbb95ece033cb8ca1d6bc66dee4ad106335b430
4
- data.tar.gz: 340b538b33f19016fbc494633ef809a79b1df518016af43de1c84f5428ce42fd
3
+ metadata.gz: 7531708baaf2764477fc99c00343d6ccb10873695ccd6d20c56cb38a21f6520d
4
+ data.tar.gz: 13b46317a8d6b9fa5f167cab39c3cef998889951fb99b44d0177612798c087e8
5
5
  SHA512:
6
- metadata.gz: e639d69ce9ccd5e373805b8e06edc8d2e5a468af695ce40dc6630cafbfe2d8ae403fe63cda38ba61b6d6b8becdbdca846566d6f5aea374a4ae9905ae66527d74
7
- data.tar.gz: f71dd8b25999a8972fc3ac9173c488ce337233f5f2496dcdb084c59067ea48056026ac65d9f1a8f176aac52c5eca9b5c71b6202998ebc40761bb6ff0064c491e
6
+ metadata.gz: 79ba9026307074c13efb196b3fd3213ac76668def0064bb4c13a1abf0244c74be128bd259ec472aa09ef36b0bd26f4f5097f097206e42def11b59e04e9f01d25
7
+ data.tar.gz: ea102e1c1ad70d0fe6b6cfa9460804cf726a8bde10c404dc7eb03e598f6d6e03f7c9d4aca848c5f1f8b9642f906155af7feed4cde336d0328b4f137d1839bc4e
data/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  # Ravioli.rb 🍝
2
2
 
3
-
4
3
  **Grab a fork and twist your configuration spaghetti in a single, delicious dumpling!**
5
4
 
6
5
  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.
@@ -35,7 +34,7 @@ key = Rails.config.dig!(:thing, :api_key)
35
34
  -->
36
35
  1. Add `gem "ravioli"` to your `Gemfile`
37
36
  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*
37
+ 3. Add an initializer (totally optional): `rails generate ravioli:install` - Ravioli will do **everything** automatically for you if you skip this step, because I'm here to put a little meat on your bones.
39
38
 
40
39
  ## Usage
41
40
 
@@ -207,7 +206,7 @@ mailjet:
207
206
  # ...the contents of mailjet.json
208
207
  ```
209
208
 
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`.
209
+ **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_file(filename, key: File.basename(filename) != "app")`](#load_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
210
 
212
211
  ### 3. Loads and combines encrypted credentials
213
212
 
@@ -217,7 +216,7 @@ Ravioli will then check for [encrypted credentials](https://guides.rubyonrails.o
217
216
  2. Then, it loads and applies `config/credentials/RAILS_ENV.yml.enc` over top of what it has already loaded
218
217
  3. Finally, IF `Rails.config.staging?` IS TRUE, it loads and applies `config/credentials/staging.yml.enc`
219
218
 
220
- This allows you to use your secure credentials stores without duplicating information; you can simply layer environment-specific values over top of
219
+ This allows you to use your secure credentials stores without duplicating information; you can simply layer environment-specific values over top of a "root" `config/credentials.yml.enc` file.
221
220
 
222
221
  ### All put together, it does this:
223
222
 
@@ -225,7 +224,7 @@ This allows you to use your secure credentials stores without duplicating inform
225
224
  def Rails.config
226
225
  @config ||= Ravioli.build(strict: Rails.env.production?) do |config|
227
226
  config.add_staging_flag!
228
- config.auto_load_config_files!
227
+ config.auto_load_files!
229
228
  config.auto_load_credentials!
230
229
  end
231
230
  end
@@ -243,16 +242,16 @@ The best way to build your own configuration is by calling `Ravioli.build`. It w
243
242
 
244
243
  ```ruby
245
244
  configuration = Ravioli.build do |config|
246
- config.load_configuration_file("whatever.yml")
245
+ config.load_file("things.yml")
247
246
  config.whatever = {things: true}
248
247
  end
249
248
  ```
250
249
 
251
- This will yield a configured instance of `Ravioli::Configuration` with structure
250
+ This will return a configured instance of `Ravioli::Configuration` with structure
252
251
 
253
252
  ```yaml
254
- rubocop:
255
- # ...the contents of whatever.yml
253
+ things:
254
+ # ...the contents of things.yml
256
255
  whatever:
257
256
  things: true
258
257
  ```
@@ -303,7 +302,7 @@ end
303
302
  ### `add_staging_flag!`
304
303
 
305
304
 
306
- ### `load_config_file`
305
+ ### `load_file`
307
306
 
308
307
  Let's imagine we have this config file:
309
308
 
@@ -329,24 +328,24 @@ In an initializer, generate your Ravioli instance and load it up:
329
328
  ```ruby
330
329
  # config/initializers/_ravioli.rb`
331
330
  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")
331
+ load_file(:mailjet) # given a symbol, it automatically assumes you meant `config/mailjet.yml`
332
+ load_file("config/mailjet") # same as above
333
+ load_file("lib/mailjet/config") # looks for `Rails.root.join("lib", "mailjet", "config.yml")
335
334
  end
336
335
  ```
337
336
 
338
337
  `config/initializers/_ravioli.rb`
339
338
 
340
339
  ```ruby
341
- Config = Ravioli.build do
340
+ Config = Ravioli.build do |config|
342
341
  %i[new_relic sentry google].each do |service|
343
- load_config_file(service)
342
+ config.load_file(service)
344
343
  end
345
344
 
346
- load_credentials # just load the base credentials file
347
- load_credentials("credentials/production") if Rails.env.production? # add production overrides when appropriate
345
+ config.load_credentials # just load the base credentials file
346
+ config.load_credentials("credentials/production") if Rails.env.production? # add production overrides when appropriate
348
347
 
349
- self.staging = File.exists?("./staging.txt") # technically you could do this ... I don't know why you would, but technically you could
348
+ config.staging = File.exists?("./staging.txt") # technically you could do this ... I don't know why you would, but technically you could
350
349
  end
351
350
  ```
352
351
 
@@ -418,47 +417,47 @@ staging:
418
417
 
419
418
  ### Encryption keys in ENV
420
419
 
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"]`
420
+ Here are a few facts about credentials in Rails and how they're deployed:
430
421
 
431
- </td><td>
422
+ 1. Rails assumes you want to use the file that matches your environment, if it exists (e.g. `RAILS_ENV=production` will look for `config/credentials/production.yml.enc`)
423
+ 2. Rails does _not_ support environment-specfic keys, but it _does_ now aggressively loads credentials at boot time.
432
424
 
433
- `ENV["RAILS_MASTER_KEY"]`
425
+ **This means `RAILS_MASTER_KEY` MUST be the decryption key for your environment-specific credential file, if one exists.**
434
426
 
435
- </td></tr><tr><td>
427
+ But, 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 way that mirrors Rails' assumptions, but allows progressive layering of credentials.
436
428
 
437
- `config/credentials/production.yml.enc`
429
+ Here are a few examples
438
430
 
439
- </td><td>
431
+ <table><thead><tr><th>File</th><th>First it tries...</th><th>Then it tries...</th></tr></thead><tbody>
440
432
 
441
- `ENV["RAILS_PRODUCTION_KEY"]`
433
+ <tr>
434
+ <td><code>config/credentials.yml.enc</code></td>
435
+ <td><code>ENV["RAILS_MASTER_KEY"]</code></td>
436
+ <td><code>ENV["RAILS_ROOT_KEY"]</code></td>
437
+ </tr>
442
438
 
443
- </td><td>
439
+ <tr>
440
+ <td><code>config/credentials/#{RAILS_ENV}.yml.enc</code></td>
441
+ <td><code>ENV["RAILS_MASTER_KEY"]</code></td>
442
+ <td><code>ENV["RAILS_#{RAILS_ENV}_KEY"]</code></td>
443
+ </tr>
444
444
 
445
- `ENV["RAILS_MASTER_KEY"]`
446
445
 
447
- </td></tr><tr><td>
446
+ <tr>
447
+ <td><code>config/credentials/staging.yml.enc</code></td>
448
+ <td><code>ENV["RAILS_MASTER_KEY"]</code></td>
449
+ <td><code>ENV["RAILS_STAGING_KEY"]</code></td>
450
+ </tr>
448
451
 
449
- `config/credentials/staging.yml.enc` (only if running on staging)
452
+ </tbody></table>
450
453
 
451
- </td><td>
452
-
453
- `ENV["RAILS_STAGING_KEY"]`
454
-
455
- </td><td>
456
-
457
- `ENV["RAILS_MASTER_KEY"]`
454
+ 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.
458
455
 
459
- </td></tr></tbody></table>
456
+ #### TLDR:
460
457
 
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.
458
+ 1. Set `RAILS_MASTER_KEY` to the key for your specific environment
459
+ 2. Set `RAILS_STAGING_KEY` to the key for your staging credentials (if deploying to staging AND you have staging-specific credentials)
460
+ 3. Set `RAILS_ROOT_KEY` to the key for your root credentials (if you have anything in `config/credentials.yml.enc`)
462
461
 
463
462
  ## License
464
463
 
data/lib/ravioli.rb CHANGED
@@ -7,28 +7,36 @@ require_relative "ravioli/builder"
7
7
  require_relative "ravioli/configuration"
8
8
  require_relative "ravioli/version"
9
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
10
+ # The root namespace for all of Ravioli, and owner of two handly
11
+ # configuration-related class methods
13
12
  module Ravioli
14
- NAME = "Ravioli"
15
-
16
13
  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
14
+ # Forwards arguments to a {Ravioli::Builder}. See
15
+ # {Ravioli::Builder#new} for complete documentation.
16
+ #
17
+ # @param namespace [String, Module, Class] the name of, or a direct reference to, the module or class your Configuration class should namespace itself within
18
+ # @param class_name [String] the name of the namespace's Configuration class
19
+ # @param strict [boolean] whether or not the Builder instance should throw errors when there are errors loading configuration files or encrypted credentials
20
+ def build(namespace: nil, class_name: "Configuration", strict: false, &block)
21
+ builder = Builder.new(
22
+ class_name: class_name,
23
+ hijack: true,
24
+ namespace: namespace,
25
+ strict: strict,
26
+ )
27
+ yield builder if block
28
+ builder.build!
27
29
  end
28
30
 
31
+ # Returns a list of all of the configuration instances
29
32
  def configurations
30
33
  @configurations ||= []
31
34
  end
35
+
36
+ # Returns the most-recently configured Ravioli instance that has been built with {Ravioli::build}.
37
+ def default
38
+ configurations.last
39
+ end
32
40
  end
33
41
  end
34
42
 
@@ -1,18 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/all"
4
+ require "erb"
4
5
  require_relative "configuration"
5
6
 
6
7
  module Ravioli
7
- # The Builder clas provides a simple interface for building a Ravioli configuration. It has
8
+ # The Builder class provides a simple interface for building a Ravioli configuration. It has
8
9
  # methods for loading configuration files and encrypted credentials, and forwards direct
9
10
  # configuration on to the configuration instance. This allows us to keep a clean separation of
10
11
  # concerns (builder: loads configuration details; configuration: provides access to information
11
12
  # in memory).
13
+ #
14
+ # == ENV variables and encrypted credentials keys
15
+ #
16
+ # <table><thead><tr><th>File</th><th>First it tries...</th><th>Then it tries...</th></tr></thead><tbody><tr><td>
17
+ #
18
+ # `config/credentials.yml.enc`
19
+ #
20
+ # </td><td>
21
+ #
22
+ # `ENV["RAILS_BASE_KEY"]`
23
+ #
24
+ # </td><td>
25
+ #
26
+ # `ENV["RAILS_MASTER_KEY"]`
27
+ #
28
+ # </td></tr><tr><td>
29
+ #
30
+ # `config/credentials/production.yml.enc`
31
+
32
+ # </td><td>
33
+ #
34
+ # `ENV["RAILS_PRODUCTION_KEY"]`
35
+ #
36
+ # </td><td>
37
+ #
38
+ # `ENV["RAILS_MASTER_KEY"]`
39
+ #
40
+ # </td></tr><tr><td>
41
+ #
42
+ # `config/credentials/staging.yml.enc` (only if running on staging)
43
+ #
44
+ # </td><td>
45
+ #
46
+ # `ENV["RAILS_STAGING_KEY"]`
47
+ #
48
+ # </td><td>
49
+ #
50
+ # `ENV["RAILS_MASTER_KEY"]`
51
+ #
52
+ # </td></tr></tbody></table>
12
53
  class Builder
13
- def initialize(class_name: "Configuration", namespace: nil, strict: false)
54
+ def initialize(class_name: "Configuration", hijack: false, namespace: nil, strict: false)
14
55
  configuration_class = if namespace.present?
15
56
  namespace.class_eval <<-EOC, __FILE__, __LINE__ + 1
57
+ # class Configuration < Ravioli::Configuration; end
16
58
  class #{class_name.to_s.classify} < Ravioli::Configuration; end
17
59
  EOC
18
60
  namespace.const_get(class_name)
@@ -21,49 +63,81 @@ module Ravioli
21
63
  end
22
64
  @strict = !!strict
23
65
  @configuration = configuration_class.new
66
+ @reload_credentials = Set.new
67
+ @reload_paths = Set.new
68
+ @hijack = !!hijack
69
+
70
+ if @hijack
71
+ # Put this builder on the configurations stack - it will intercept setters on the underyling
72
+ # configuration object as it loads files, and mark those files as needing a reload once
73
+ # loading is complete
74
+ Ravioli.configurations.push(self)
75
+ end
24
76
  end
25
77
 
26
- # Automatically infer a `staging?` status
78
+ # Automatically infer a `staging` status from the current environment
79
+ #
80
+ # @param is_staging [boolean, #present?] whether or not the current environment is considered a staging environment
27
81
  def add_staging_flag!(is_staging = Rails.env.production? && ENV["STAGING"].present?)
82
+ is_staging = is_staging.present?
28
83
  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
84
  end
39
85
 
40
- # Load YAML or JSON files in config/**/* (except for locales)
41
- def auto_load_config_files!
86
+ # Iterates through the config directory (including nested folders) and
87
+ # calls {Ravioli::Builder::load_file} on each JSON or YAML file it
88
+ # finds. Ignores `config/locales`.
89
+ def auto_load_files!
42
90
  config_dir = Rails.root.join("config")
43
91
  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?)
92
+ auto_load_file(config_file)
45
93
  end
46
94
  end
47
95
 
48
- # Load config/credentials**/*.yml.enc files (assuming we can find a key)
96
+ # Loads Rails encrypted credentials that it can. Checks for corresponding private key files, or ENV vars based on the {Ravioli::Builder credentials preadmlogic}
49
97
  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")
98
+ # Load the root config (supports using the master key or `RAILS_ROOT_KEY`)
99
+ load_credentials(
100
+ key_path: "config/master.key",
101
+ env_names: %w[master root],
102
+ )
103
+
104
+ # Load any environment-specific configuration on top of it. Since Rails will try
105
+ # `RAILS_MASTER_KEY` from the environment, we assume the same
106
+ load_credentials(
107
+ "config/credentials/#{Rails.env}",
108
+ key_path: "config/credentials/#{Rails.env}.key",
109
+ env_names: ["master"],
110
+ )
55
111
 
56
112
  # 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?
113
+ if configuration.staging?
114
+ load_credentials(
115
+ "config/credentials/staging",
116
+ env_names: %w[staging master],
117
+ key_path: "config/credentials/staging.key",
118
+ )
119
+ end
58
120
  end
59
121
 
60
122
  # When the builder is done working, lock the configuration and return it
61
123
  def build!
124
+ if @hijack
125
+ # Replace this builder with the underlying configuration on the configurations stack...
126
+ Ravioli.configurations.delete(self)
127
+ Ravioli.configurations.push(configuration)
128
+
129
+ # ...and then reload any config file that referenced the configuration the first time it was
130
+ # loaded!
131
+ @reload_paths.each do |path|
132
+ auto_load_file(path)
133
+ end
134
+ end
135
+
62
136
  configuration.freeze
63
137
  end
64
138
 
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 = {})
139
+ # Load a file either with a given path or by name (e.g. `config/whatever.yml` or `:whatever`)
140
+ def load_file(path, options = {})
67
141
  config = parse_config_file(path, options)
68
142
  configuration.append(config) if config.present?
69
143
  rescue => error
@@ -71,11 +145,30 @@ module Ravioli
71
145
  end
72
146
 
73
147
  # 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
148
+ def load_credentials(path = "credentials", key_path: path, env_names: path.split("/").last)
149
+ error = nil
150
+ env_names = Array(env_names).map { |env_name| parse_env_name(env_name) }
151
+ env_names.each do |env_name|
152
+ credentials = parse_credentials(path, env_name: env_name, key_path: key_path)
153
+ if credentials.present?
154
+ configuration.append(credentials)
155
+ return credentials
156
+ end
157
+ rescue => e
158
+ error = e
159
+ end
160
+
161
+ if error
162
+ attempted_names = ["key file `#{key_path}'"]
163
+ attempted_names.push(*env_names.map { |env_name| "`ENV[\"#{env_name}\"]'" })
164
+ attempted_names = attempted_names.to_sentence(two_words_connector: " or ", last_word_connector: ", or ")
165
+ warn(
166
+ "Could not decrypt `#{path}.yml.enc' with #{attempted_names}",
167
+ error,
168
+ critical: false,
169
+ )
170
+ end
171
+
79
172
  {}
80
173
  end
81
174
 
@@ -86,6 +179,13 @@ module Ravioli
86
179
 
87
180
  attr_reader :configuration
88
181
 
182
+ def auto_load_file(config_file)
183
+ basename = File.basename(config_file, File.extname(config_file))
184
+ dirname = File.dirname(config_file)
185
+ key = %w[app application].exclude?(basename) && dirname != config_dir
186
+ load_file(config_file, key: key)
187
+ end
188
+
89
189
  def extract_environmental_config(config)
90
190
  # Check if the config hash is keyed by environment - if not, just return it as-is. It's
91
191
  # considered "keyed by environment" if it contains ONLY env-specific keys.
@@ -105,12 +205,23 @@ module Ravioli
105
205
  # rubocop:disable Style/MethodMissingSuper
106
206
  # rubocop:disable Style/MissingRespondToMissing
107
207
  def method_missing(*args, &block)
208
+ if @current_path
209
+ @reload_paths.add(@current_path)
210
+ end
211
+
212
+ if @current_credentials
213
+ @reload_credentials.add(@current_credentials)
214
+ end
215
+
108
216
  configuration.send(*args, &block)
109
217
  end
110
218
  # rubocop:enable Style/MissingRespondToMissing
111
219
  # rubocop:enable Style/MethodMissingSuper
112
220
 
113
221
  def parse_config_file(path, options = {})
222
+ # Stash a reference to the file we're parsing, so we can reload it later if it tries to use
223
+ # the configuration object
224
+ @current_path = path
114
225
  path = path_to_config_file_path(path)
115
226
 
116
227
  config = case path.extname.downcase
@@ -119,9 +230,11 @@ module Ravioli
119
230
  when ".yml", ".yaml"
120
231
  parse_yaml_config_file(path)
121
232
  else
122
- raise ParseError.new("#{Ravioli::NAME} doesn't know how to parse #{path}")
233
+ raise ParseError.new("Ravioli doesn't know how to parse #{path}")
123
234
  end
124
235
 
236
+ # We are no longer loading anything
237
+ @current_path = nil
125
238
  # At least expect a hash to be returned from the loaded config file
126
239
  return {} unless config.is_a?(Hash)
127
240
 
@@ -144,16 +257,22 @@ module Ravioli
144
257
  {name => config}
145
258
  end
146
259
 
147
- def parse_credentials(path, key_path: path, env_name: path.split("/").last)
260
+ def parse_env_name(env_name)
148
261
  env_name = env_name.to_s
149
- env_name = "RAILS_#{env_name.upcase}_KEY" unless env_name.upcase == env_name
262
+ env_name.match?(/^RAILS_/) ? env_name : "RAILS_#{env_name.upcase}_KEY"
263
+ end
264
+
265
+ def parse_credentials(path, key_path: path, env_name: path.split("/").last)
266
+ @current_credentials = path
267
+ env_name = parse_env_name(env_name)
150
268
  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)
269
+ options = {key_path: key_path.to_s}
270
+ options[:env_key] = ENV[env_name].present? ? env_name : "__RAVIOLI__#{SecureRandom.hex(6)}"
153
271
 
154
272
  path = path_to_config_file_path(path, extnames: "yml.enc")
155
- credentials = Rails.application.encrypted(path, options)&.config || {}
156
- credentials
273
+ (Rails.application.encrypted(path, **options)&.config || {}).tap do
274
+ @current_credentials = nil
275
+ end
157
276
  end
158
277
 
159
278
  def parse_json_config_file(path)
@@ -162,10 +281,9 @@ module Ravioli
162
281
  end
163
282
 
164
283
  def parse_yaml_config_file(path)
165
- require "erb"
166
284
  contents = File.read(path)
167
285
  erb = ERB.new(contents).tap { |renderer| renderer.filename = path.to_s }
168
- YAML.safe_load(erb.result, aliases: true)
286
+ YAML.safe_load(erb.result, [Symbol], aliases: true)
169
287
  end
170
288
 
171
289
  def path_to_config_file_path(path, extnames: EXTNAMES, quiet: false)
@@ -192,18 +310,19 @@ module Ravioli
192
310
  path
193
311
  end
194
312
 
195
- def warn(message, error = $!)
196
- message = "[#{Ravioli::NAME}] #{message}"
313
+ def warn(message, error = $!, critical: true)
314
+ message = "[Ravioli] #{message}"
197
315
  message = "#{message}:\n\n#{error.cause.inspect}" if error&.cause.present?
198
- if @strict
316
+ if @strict && critical
199
317
  raise BuildError.new(message, error)
200
318
  else
201
- Rails.logger.warn(message) if defined? Rails
319
+ Rails.logger.try(:warn, message) if defined? Rails
202
320
  $stderr.write message # rubocop:disable Rails/Output
203
321
  end
204
322
  end
205
323
  end
206
324
 
325
+ # Error raised when Ravioli is in strict mode. Includes the original error for context.
207
326
  class BuildError < StandardError
208
327
  def initialize(message, cause = nil)
209
328
  super message
@@ -214,5 +333,7 @@ module Ravioli
214
333
  @cause || super
215
334
  end
216
335
  end
336
+
337
+ # Error raised when Ravioli encounters a problem parsing a file
217
338
  class ParseError < StandardError; end
218
339
  end
@@ -5,20 +5,17 @@ require "ostruct"
5
5
 
6
6
  module Ravioli
7
7
  class Configuration < OpenStruct
8
- attr_reader :key_path
9
-
10
8
  def initialize(attributes = {})
11
9
  super({})
12
10
  @key_path = attributes.delete(:key_path)
13
11
  append(attributes)
14
12
  end
15
13
 
16
- # def ==(other)
17
- # other = other.table if other.respond_to?(:table)
18
- # other == table
19
- # end
20
-
14
+ # Convert a hash to accessors and nested {Ravioli::Configuration} instances.
15
+ #
16
+ # @param [Hash, #each] key-value pairs to be converted to accessors
21
17
  def append(attributes = {})
18
+ return unless attributes.respond_to?(:each)
22
19
  attributes.each do |key, value|
23
20
  self[key.to_sym] = cast(key.to_sym, value)
24
21
  end
@@ -40,6 +37,10 @@ module Ravioli
40
37
  fetch(*keys) { raise KeyMissingError.new("Could not find value at key path #{keys.inspect}") }
41
38
  end
42
39
 
40
+ def delete(key)
41
+ table.delete(key.to_s)
42
+ end
43
+
43
44
  def fetch(*keys)
44
45
  dig(*keys) || yield
45
46
  end
@@ -54,6 +55,8 @@ module Ravioli
54
55
 
55
56
  private
56
57
 
58
+ attr_reader :key_path
59
+
57
60
  def build(keys, attributes = {})
58
61
  attributes[:key_path] = key_path_for(keys)
59
62
  child = self.class.new(attributes)
@@ -1,18 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "./staging_inquirer"
4
+ Rails.env.class.prepend Ravioli::StagingInquirer
5
+
3
6
  module Ravioli
4
7
  class Engine < ::Rails::Engine
5
8
  # 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)
9
+ initializer "ravioli", before: :load_environment_config do |app|
10
+ Rails.extend Ravioli::RailsConfig unless Rails.respond_to?(:config)
8
11
  end
9
12
  end
10
13
 
11
- module Config
14
+ private
15
+
16
+ module RailsConfig
12
17
  def config
13
18
  Ravioli.default || Ravioli.build(namespace: Rails.application&.class&.module_parent, strict: Rails.env.production?) do |config|
14
19
  config.add_staging_flag!
15
- config.auto_load_config_files!
20
+ config.auto_load_files!
16
21
  config.auto_load_credentials!
17
22
  end
18
23
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ravioli
4
+ # A module that we mix in to the `Rails.env` inquirer class to add some extra staging-related
5
+ # metadata
6
+ module StagingInquirer
7
+ # Add a `name` method to `Rails.env` that will return "staging" for staging environments, and
8
+ # otherwise the string's value
9
+ def name
10
+ staging? ? "staging" : to_s
11
+ end
12
+
13
+ # Add a `strict:` keyword to reduce `Rails.env.production && !Rails.env.staging` calls
14
+ def production?(strict: false)
15
+ is_production = super()
16
+ return is_production unless strict && is_production
17
+
18
+ is_production && !staging?
19
+ end
20
+
21
+ # Override staging inquiries to check against the current configuration
22
+ def staging?
23
+ Rails.try(:config)&.staging?
24
+ end
25
+ end
26
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ravioli
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.5"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ravioli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Flip Sasser
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-02-23 00:00:00.000000000 Z
11
+ date: 2021-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -30,6 +30,48 @@ dependencies:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: 6.0.3.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: guard
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: guard-rspec
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: guard-rubocop
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
33
75
  - !ruby/object:Gem::Dependency
34
76
  name: pry
35
77
  requirement: !ruby/object:Gem::Requirement
@@ -90,16 +132,16 @@ dependencies:
90
132
  name: rubocop
91
133
  requirement: !ruby/object:Gem::Requirement
92
134
  requirements:
93
- - - "~>"
135
+ - - ">="
94
136
  - !ruby/object:Gem::Version
95
- version: '0.8'
137
+ version: '1.0'
96
138
  type: :development
97
139
  prerelease: false
98
140
  version_requirements: !ruby/object:Gem::Requirement
99
141
  requirements:
100
- - - "~>"
142
+ - - ">="
101
143
  - !ruby/object:Gem::Version
102
- version: '0.8'
144
+ version: '1.0'
103
145
  - !ruby/object:Gem::Dependency
104
146
  name: rubocop-ordered_methods
105
147
  requirement: !ruby/object:Gem::Requirement
@@ -118,44 +160,44 @@ dependencies:
118
160
  name: rubocop-performance
119
161
  requirement: !ruby/object:Gem::Requirement
120
162
  requirements:
121
- - - "~>"
163
+ - - ">="
122
164
  - !ruby/object:Gem::Version
123
- version: 1.5.2
165
+ version: '1.5'
124
166
  type: :development
125
167
  prerelease: false
126
168
  version_requirements: !ruby/object:Gem::Requirement
127
169
  requirements:
128
- - - "~>"
170
+ - - ">="
129
171
  - !ruby/object:Gem::Version
130
- version: 1.5.2
172
+ version: '1.5'
131
173
  - !ruby/object:Gem::Dependency
132
174
  name: rubocop-rails
133
175
  requirement: !ruby/object:Gem::Requirement
134
176
  requirements:
135
- - - "~>"
177
+ - - ">="
136
178
  - !ruby/object:Gem::Version
137
- version: 2.5.2
179
+ version: 2.5.0
138
180
  type: :development
139
181
  prerelease: false
140
182
  version_requirements: !ruby/object:Gem::Requirement
141
183
  requirements:
142
- - - "~>"
184
+ - - ">="
143
185
  - !ruby/object:Gem::Version
144
- version: 2.5.2
186
+ version: 2.5.0
145
187
  - !ruby/object:Gem::Dependency
146
188
  name: rubocop-rspec
147
189
  requirement: !ruby/object:Gem::Requirement
148
190
  requirements:
149
- - - "~>"
191
+ - - ">="
150
192
  - !ruby/object:Gem::Version
151
- version: '1.39'
193
+ version: '2.0'
152
194
  type: :development
153
195
  prerelease: false
154
196
  version_requirements: !ruby/object:Gem::Requirement
155
197
  requirements:
156
- - - "~>"
198
+ - - ">="
157
199
  - !ruby/object:Gem::Version
158
- version: '1.39'
200
+ version: '2.0'
159
201
  - !ruby/object:Gem::Dependency
160
202
  name: simplecov
161
203
  requirement: !ruby/object:Gem::Requirement
@@ -190,14 +232,14 @@ dependencies:
190
232
  requirements:
191
233
  - - "~>"
192
234
  - !ruby/object:Gem::Version
193
- version: '0.4'
235
+ version: 0.13.0
194
236
  type: :development
195
237
  prerelease: false
196
238
  version_requirements: !ruby/object:Gem::Requirement
197
239
  requirements:
198
240
  - - "~>"
199
241
  - !ruby/object:Gem::Version
200
- version: '0.4'
242
+ version: 0.13.0
201
243
  - !ruby/object:Gem::Dependency
202
244
  name: yard
203
245
  requirement: !ruby/object:Gem::Requirement
@@ -229,6 +271,7 @@ files:
229
271
  - lib/ravioli/builder.rb
230
272
  - lib/ravioli/configuration.rb
231
273
  - lib/ravioli/engine.rb
274
+ - lib/ravioli/staging_inquirer.rb
232
275
  - lib/ravioli/version.rb
233
276
  homepage: https://github.com/flipsasser/ravioli
234
277
  licenses:
@@ -236,7 +279,7 @@ licenses:
236
279
  metadata:
237
280
  homepage_uri: https://github.com/flipsasser/ravioli
238
281
  source_code_uri: https://github.com/flipsasser/ravioli
239
- post_install_message:
282
+ post_install_message:
240
283
  rdoc_options: []
241
284
  require_paths:
242
285
  - lib
@@ -244,15 +287,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
244
287
  requirements:
245
288
  - - ">="
246
289
  - !ruby/object:Gem::Version
247
- version: 2.3.0
290
+ version: 2.4.0
248
291
  required_rubygems_version: !ruby/object:Gem::Requirement
249
292
  requirements:
250
293
  - - ">="
251
294
  - !ruby/object:Gem::Version
252
295
  version: '0'
253
296
  requirements: []
254
- rubygems_version: 3.0.3
255
- signing_key:
297
+ rubygems_version: 3.2.3
298
+ signing_key:
256
299
  specification_version: 4
257
300
  summary: Grab a fork and twist all your configuration spaghetti into a single, delicious
258
301
  bundle