runger_config 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +562 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +1121 -0
  5. data/lib/anyway/auto_cast.rb +53 -0
  6. data/lib/anyway/config.rb +473 -0
  7. data/lib/anyway/dynamic_config.rb +31 -0
  8. data/lib/anyway/ejson_parser.rb +40 -0
  9. data/lib/anyway/env.rb +73 -0
  10. data/lib/anyway/ext/deep_dup.rb +48 -0
  11. data/lib/anyway/ext/deep_freeze.rb +44 -0
  12. data/lib/anyway/ext/flatten_names.rb +37 -0
  13. data/lib/anyway/ext/hash.rb +40 -0
  14. data/lib/anyway/ext/string_constantize.rb +24 -0
  15. data/lib/anyway/loaders/base.rb +21 -0
  16. data/lib/anyway/loaders/doppler.rb +63 -0
  17. data/lib/anyway/loaders/ejson.rb +89 -0
  18. data/lib/anyway/loaders/env.rb +18 -0
  19. data/lib/anyway/loaders/yaml.rb +84 -0
  20. data/lib/anyway/loaders.rb +79 -0
  21. data/lib/anyway/option_parser_builder.rb +29 -0
  22. data/lib/anyway/optparse_config.rb +92 -0
  23. data/lib/anyway/rails/autoload.rb +42 -0
  24. data/lib/anyway/rails/config.rb +23 -0
  25. data/lib/anyway/rails/loaders/credentials.rb +64 -0
  26. data/lib/anyway/rails/loaders/secrets.rb +37 -0
  27. data/lib/anyway/rails/loaders/yaml.rb +9 -0
  28. data/lib/anyway/rails/loaders.rb +5 -0
  29. data/lib/anyway/rails/settings.rb +83 -0
  30. data/lib/anyway/rails.rb +24 -0
  31. data/lib/anyway/railtie.rb +28 -0
  32. data/lib/anyway/rbs.rb +92 -0
  33. data/lib/anyway/settings.rb +111 -0
  34. data/lib/anyway/testing/helpers.rb +36 -0
  35. data/lib/anyway/testing.rb +13 -0
  36. data/lib/anyway/tracing.rb +188 -0
  37. data/lib/anyway/type_casting.rb +144 -0
  38. data/lib/anyway/utils/deep_merge.rb +21 -0
  39. data/lib/anyway/utils/which.rb +18 -0
  40. data/lib/anyway/version.rb +5 -0
  41. data/lib/anyway.rb +3 -0
  42. data/lib/anyway_config.rb +54 -0
  43. data/lib/generators/anyway/app_config/USAGE +9 -0
  44. data/lib/generators/anyway/app_config/app_config_generator.rb +17 -0
  45. data/lib/generators/anyway/config/USAGE +13 -0
  46. data/lib/generators/anyway/config/config_generator.rb +51 -0
  47. data/lib/generators/anyway/config/templates/config.rb.tt +12 -0
  48. data/lib/generators/anyway/config/templates/config.yml.tt +13 -0
  49. data/lib/generators/anyway/install/USAGE +4 -0
  50. data/lib/generators/anyway/install/install_generator.rb +47 -0
  51. data/lib/generators/anyway/install/templates/application_config.rb.tt +17 -0
  52. data/sig/anyway_config.rbs +149 -0
  53. data/sig/manifest.yml +6 -0
  54. metadata +202 -0
data/README.md ADDED
@@ -0,0 +1,1121 @@
1
+ [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](https://cultofmartians.com/tasks/anyway-config-options-parse.html#task)
2
+ [![Gem Version](https://badge.fury.io/rb/anyway_config.svg)](https://rubygems.org/gems/anyway_config) [![Build](https://github.com/palkan/anyway_config/workflows/Build/badge.svg)](https://github.com/palkan/anyway_config/actions)
3
+ [![JRuby Build](https://github.com/palkan/anyway_config/workflows/JRuby%20Build/badge.svg)](https://github.com/palkan/anyway_config/actions)
4
+ [![TruffleRuby Build](https://github.com/palkan/anyway_config/workflows/TruffleRuby%20Build/badge.svg)](https://github.com/palkan/anyway_config/actions)
5
+
6
+ # Anyway Config
7
+
8
+ > One configuration to rule all data sources
9
+
10
+ Anyway Config is a configuration library for Ruby gems and applications.
11
+
12
+ As a library author, you can benefit from using Anyway Config by providing a better UX for your end-users:
13
+
14
+ - **Zero-code configuration** — no more boilerplate initializers.
15
+ - **Per-environment and local** settings support out-of-the-box.
16
+
17
+ For application developers, Anyway Config could be useful to:
18
+
19
+ - **Keep configuration organized** and use _named configs_ instead of bloated `.env`/`settings.yml`/whatever.
20
+ - **Free code of ENV/credentials/secrets dependency** and use configuration classes instead—your code should not rely on configuration data sources.
21
+
22
+ **NOTE:** this readme shows documentation for 2.x version.
23
+ For version 1.x see the [1-4-stable branch](https://github.com/palkan/anyway_config/tree/1-4-stable).
24
+
25
+ <a href="https://evilmartians.com/?utm_source=anyway_config">
26
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
27
+
28
+ ## Links
29
+
30
+ - [Anyway Config: Keep your Ruby configuration sane](https://evilmartians.com/chronicles/anyway-config-keep-your-ruby-configuration-sane?utm_source=anyway_config)
31
+
32
+ ## Table of contents
33
+
34
+ - [Main concepts](#main-concepts)
35
+ - [Installation](#installation)
36
+ - [Usage](#usage)
37
+ - [Configuration classes](#configuration-classes)
38
+ - [Dynamic configuration](#dynamic-configuration)
39
+ - [Validation & Callbacks](#validation-and-callbacks)
40
+ - [Using with Rails applications](#using-with-rails)
41
+ - [Data population](#data-population)
42
+ - [Organizing configs](#organizing-configs)
43
+ - [Generators](#generators)
44
+ - [Using with Ruby applications](#using-with-ruby)
45
+ - [Environment variables](#environment-variables)
46
+ - [Type coercion](#type-coercion)
47
+ - [Local configuration](#local-files)
48
+ - [Data loaders](#data-loaders)
49
+ - [Doppler integration](#doppler-integration)
50
+ - [EJSON support](#ejson-support)
51
+ - [Custom loaders](#custom-loaders)
52
+ - [Source tracing](#tracing)
53
+ - [Pattern matching](#pattern-matching)
54
+ - [Test helpers](#test-helpers)
55
+ - [OptionParser integration](#optionparser-integration)
56
+ - [RBS support](#rbs-support)
57
+
58
+ ## Main concepts
59
+
60
+ Anyway Config abstractize the configuration layer by introducing **configuration classes** which describe available parameters and their defaults. For [example](https://github.com/palkan/influxer/blob/master/lib/influxer/config.rb):
61
+
62
+ ```ruby
63
+ module Influxer
64
+ class Config < Anyway::Config
65
+ attr_config(
66
+ host: "localhost",
67
+ username: "root",
68
+ password: "root"
69
+ )
70
+ end
71
+ end
72
+ ```
73
+
74
+ Using Ruby classes to represent configuration allows you to add helper methods and computed parameters easily, makes the configuration **testable**.
75
+
76
+ The `anyway_config` gem takes care of loading parameters from **different sources** (YAML, credentials/secrets, environment variables, etc.). Internally, we use a _pipeline pattern_ and provide the [Loaders API](#data-loaders) to manage and [extend](#custom-loaders) its functionality.
77
+
78
+ Check out the libraries using Anyway Config for more examples:
79
+
80
+ - [Influxer](https://github.com/palkan/influxer)
81
+ - [AnyCable](https://github.com/anycable/anycable)
82
+ - [Sniffer](https://github.com/aderyabin/sniffer)
83
+ - [Blood Contracts](https://github.com/sclinede/blood_contracts)
84
+ - [and others](https://github.com/palkan/anyway_config/network/dependents).
85
+
86
+ ## Installation
87
+
88
+ Adding to a gem:
89
+
90
+ ```ruby
91
+ # my-cool-gem.gemspec
92
+ Gem::Specification.new do |spec|
93
+ # ...
94
+ spec.add_dependency "anyway_config", ">= 2.0.0"
95
+ # ...
96
+ end
97
+ ```
98
+
99
+ Or adding to your project:
100
+
101
+ ```ruby
102
+ # Gemfile
103
+ gem "anyway_config", "~> 2.0"
104
+ ```
105
+
106
+ ### Supported Ruby versions
107
+
108
+ - Ruby (MRI) >= 2.5.0
109
+ - JRuby >= 9.2.9
110
+
111
+ ## Usage
112
+
113
+ ### Configuration classes
114
+
115
+ Using configuration classes allows you to make configuration data a bit more than a bag of values:
116
+ you can define a schema for your configuration, provide defaults, add validations and additional helper methods.
117
+
118
+ Anyway Config provides a base class to inherit from with a few DSL methods:
119
+
120
+ ```ruby
121
+ require "anyway_config"
122
+
123
+ module MyCoolGem
124
+ class Config < Anyway::Config
125
+ attr_config user: "root", password: "root", host: "localhost"
126
+ end
127
+ end
128
+ ```
129
+
130
+ Here `attr_config` creates accessors and populates the default values. If you don't need default values you can write:
131
+
132
+ ```ruby
133
+ attr_config :user, :password, host: "localhost", options: {}
134
+ ```
135
+
136
+ **NOTE**: it's safe to use non-primitive default values (like Hashes or Arrays) without worrying about their mutation: the values would be deeply duplicated for each config instance.
137
+
138
+ Then, create an instance of the config class and use it:
139
+
140
+ ```ruby
141
+ MyCoolGem::Config.new.user #=> "root"
142
+ ```
143
+
144
+ **Bonus:**: if you define attributes with boolean default values (`false` or `true`), Anyway Config would automatically add a corresponding predicate method. For example:
145
+
146
+ ```ruby
147
+ attr_config :user, :password, debug: false
148
+
149
+ MyCoolGem::Config.new.debug? #=> false
150
+ MyCoolGem::Config.new(debug: true).debug? #=> true
151
+ ```
152
+
153
+ **NOTE**: since v2.0 accessors created by `attr_config` are not `attr_accessor`, i.e. they do not populate instance variables. If you used instance variables before to override readers, you must switch to using `super` or `values` store:
154
+
155
+ ```ruby
156
+ class MyConfig < Anyway::Config
157
+ attr_config :host, :port, :url, :meta
158
+
159
+ # override writer to handle type coercion
160
+ def meta=(val)
161
+ super JSON.parse(val)
162
+ end
163
+
164
+ # or override reader to handle missing values
165
+ def url
166
+ super || (self.url = "#{host}:#{port}")
167
+ end
168
+
169
+ # untill v2.1, it will still be possible to read instance variables,
170
+ # i.e. the following code would also work
171
+ def url
172
+ @url ||= "#{host}:#{port}"
173
+ end
174
+ end
175
+ ```
176
+
177
+ We recommend to add a feature check and support both v1.x and v2.0 in gems for the time being:
178
+
179
+ ```ruby
180
+ # Check for the class method added in 2.0, e.g., `.on_load`
181
+ if respond_to?(:on_load)
182
+ def url
183
+ super || (self.url = "#{host}:#{port}")
184
+ end
185
+ else
186
+ def url
187
+ @url ||= "#{host}:#{port}"
188
+ end
189
+ end
190
+ ```
191
+
192
+ #### Config name
193
+
194
+ Anyway Config relies on the notion of _config name_ to populate data.
195
+
196
+ By default, Anyway Config uses the config class name to infer the config name using the following rules:
197
+
198
+ - if the class name has a form of `<Module>::Config` then use the module name (`SomeModule::Config => "somemodule"`)
199
+ - if the class name has a form of `<Something>Config` then use the class name prefix (`SomeConfig => "some"`)
200
+
201
+ **NOTE:** in both cases, the config name is a **downcased** module/class prefix, not underscored.
202
+
203
+ You can also specify the config name explicitly (it's required in cases when your class name doesn't match any of the patterns above):
204
+
205
+ ```ruby
206
+ module MyCoolGem
207
+ class Config < Anyway::Config
208
+ config_name :cool
209
+ attr_config user: "root", password: "root", host: "localhost", options: {}
210
+ end
211
+ end
212
+ ```
213
+
214
+ #### Customize env variable names prefix
215
+
216
+ By default, Anyway Config uses upper-cased config name as a prefix for env variable names (e.g.
217
+ `config_name :my_app` will result to parsing `MY_APP_` prefix).
218
+
219
+ You can set env prefix explicitly:
220
+
221
+ ```ruby
222
+ module MyCoolGem
223
+ class Config < Anyway::Config
224
+ config_name :cool_gem
225
+ env_prefix :really_cool # now variables, starting wih `REALLY_COOL_`, will be parsed
226
+ attr_config user: "root", password: "root", host: "localhost", options: {}
227
+ end
228
+ end
229
+ ```
230
+
231
+ #### Explicit values
232
+
233
+ Sometimes it's useful to set some parameters explicitly during config initialization.
234
+ You can do that by passing a Hash into `.new` method:
235
+
236
+ ```ruby
237
+ config = MyCoolGem::Config.new(
238
+ user: "john",
239
+ password: "rubyisnotdead"
240
+ )
241
+
242
+ # The value would not be overridden from other sources (such as YML file, env)
243
+ config.user == "john"
244
+ ```
245
+
246
+ #### Reload configuration
247
+
248
+ There are `#clear` and `#reload` methods that do exactly what they state.
249
+
250
+ **NOTE**: `#reload` also accepts an optional Hash for [explicit values](#explicit-values).
251
+
252
+ ### Dynamic configuration
253
+
254
+ You can also fetch configuration without pre-defined schema:
255
+
256
+ ```ruby
257
+ # load data from config/my_app.yml,
258
+ # credentials.my_app, secrets.my_app (if using Rails), ENV["MY_APP_*"]
259
+ #
260
+ # Given MY_APP_VALUE=42
261
+ config = Anyway::Config.for(:my_app)
262
+ config["value"] #=> 42
263
+
264
+ # you can specify the config file path or env prefix
265
+ config = Anyway::Config.for(:my_app, config_path: "my_config.yml", env_prefix: "MYAPP")
266
+ ```
267
+
268
+ This feature is similar to `Rails.application.config_for` but more powerful:
269
+
270
+ | Feature | Rails | Anyway Config |
271
+ | ------------- |-------------:| -----:|
272
+ | Load data from `config/app.yml` | ✅ | ✅ |
273
+ | Load data from `secrets` | ❌ | ✅ |
274
+ | Load data from `credentials` | ❌ | ✅ |
275
+ | Load data from environment | ❌ | ✅ |
276
+ | Load data from [other sources](#data-loaders) | ❌ | ✅ |
277
+ | Local config files | ❌ | ✅ |
278
+ | Type coercion | ❌ | ✅ |
279
+ | [Source tracing](#tracing) | ❌ | ✅ |
280
+ | Return Hash with indifferent access | ❌ | ✅ |
281
+ | Support ERB\* within `config/app.yml` | ✅ | ✅ |
282
+ | Raise if file doesn't exist | ✅ | ❌ |
283
+ | Works without Rails | 😀 | ✅ |
284
+
285
+ \* Make sure that ERB is loaded
286
+
287
+ ### Validation and callbacks
288
+
289
+ Anyway Config provides basic ways of ensuring that the configuration is valid.
290
+
291
+ There is a built-in `required` class method to define the list of parameters that must be present in the
292
+ configuration after loading (where present means non-`nil` and non-empty for strings):
293
+
294
+ ```ruby
295
+ class MyConfig < Anyway::Config
296
+ attr_config :api_key, :api_secret, :debug
297
+
298
+ required :api_key, :api_secret
299
+ end
300
+
301
+ MyConfig.new(api_secret: "") #=> raises Anyway::Config::ValidationError
302
+ ```
303
+
304
+ `Required` method supports additional `env` parameter which indicates necessity to run validations under specified
305
+ environments. `Env` parameter could be present in symbol, string, array or hash formats:
306
+
307
+ ```ruby
308
+ class EnvConfig < Anyway::Config
309
+ required :password, env: "production"
310
+ required :maps_api_key, env: :production
311
+ required :smtp_host, env: %i[production staging]
312
+ required :aws_bucket, env: %w[production staging]
313
+ required :anycable_rpc_host, env: {except: :development}
314
+ required :anycable_redis_url, env: {except: %i[development test]}
315
+ required :anycable_broadcast_adapter, env: {except: %w[development test]}
316
+ end
317
+ ```
318
+
319
+ If your current `Anyway::Settings.current_environment` is mismatch keys that specified
320
+ `Anyway::Config::ValidationError` error will be raised.
321
+
322
+ If you need more complex validation or need to manipulate with config state right after it has been loaded, you can use _on load callbacks_ and `#raise_validation_error` method:
323
+
324
+ ```ruby
325
+ class MyConfig < Anyway::Config
326
+ attr_config :api_key, :api_secret, :mode
327
+
328
+ # on_load macro accepts symbol method names
329
+ on_load :ensure_mode_is_valid
330
+
331
+ # or block
332
+ on_load do
333
+ # the block is evaluated in the context of the config
334
+ raise_validation_error("API key and/or secret could be blank") if
335
+ api_key.blank? || api_secret.blank?
336
+ end
337
+
338
+ def ensure_mode_is_valid
339
+ unless %w[production test].include?(mode)
340
+ raise_validation_error "Unknown mode; #{mode}"
341
+ end
342
+ end
343
+ end
344
+ ```
345
+
346
+ ## Using with Rails
347
+
348
+ **NOTE:** version 2.x supports Rails >= 5.0; for Rails 4.x use version 1.x of the gem.
349
+
350
+ We recommend going through [Data population](#data-population) and [Organizing configs](#organizing-configs) sections first,
351
+ and then use [Rails generators](#generators) to make your application Anyway Config-ready.
352
+
353
+ ### Data population
354
+
355
+ Your config is filled up with values from the following sources (ordered by priority from low to high):
356
+
357
+ 1) **YAML configuration files**: `RAILS_ROOT/config/my_cool_gem.yml`.
358
+
359
+ Rails environment is used as the namespace (required); supports `ERB`:
360
+
361
+ ```yml
362
+ test:
363
+ host: localhost
364
+ port: 3002
365
+
366
+ development:
367
+ host: localhost
368
+ port: 3000
369
+ ```
370
+
371
+ **NOTE:** You can override the environment name for configuration files via the `ANYWAY_ENV` environment variable or by setting it explicitly in the code: `Anyway::Settings.current_environment = "some_other_env"`.
372
+
373
+ ### Multi-env configuration
374
+
375
+ _⚡️ This feature will be turned on by default in the future releases. You can turn it on now via `config.anyway_config.future.use :unwrap_known_environments`._
376
+
377
+ If the YML does not have keys that are one of the "known" Rails environments (development, production, test)—the same configuration will be available in all environments, similar to non-Rails behavior:
378
+
379
+ ```yml
380
+ host: localhost
381
+ port: 3002
382
+ # These values will be active in all environments
383
+ ```
384
+
385
+ To extend the list of known environments, use the setting in the relevant part of your Rails code:
386
+
387
+ ```ruby
388
+ Rails.application.config.anyway_config.known_environments << "staging"
389
+ ```
390
+
391
+ If your YML defines at least a single "environmental" top-level, you _have_ to separate all your settings per-environment. You can't mix and match:
392
+
393
+ ```yml
394
+ staging:
395
+ host: localhost # This value will be loaded when Rails.env.staging? is true
396
+
397
+ port: 3002 # This value will not be loaded at all
398
+ ```
399
+
400
+ To provide default values you can use YAML anchors, but they do not deep-merge settings, so Anyway Config provides a way to define a special top-level key for default values like this:
401
+
402
+ ```ruby
403
+ config.anyway_config.default_environmental_key = "default"
404
+ ```
405
+
406
+ After that, Anyway Config will start reading settings under the `"default"` key and then merge environmental settings into them.
407
+
408
+ ```yml
409
+ default:
410
+ server: # This values will be loaded in all environments by default
411
+ host: localhost
412
+ port: 3002
413
+
414
+ staging:
415
+ server:
416
+ host: staging.example.com # This value will override the defaults when Rails.env.staging? is true
417
+ # port will be set to the value from the defaults — 3002
418
+ ```
419
+
420
+ You can specify the lookup path for YAML files in one of the following ways:
421
+
422
+ - By setting `config.anyway_config.default_config_path` to a target directory path:
423
+
424
+ ```ruby
425
+ config.anyway_config.default_config_path = "/etc/configs"
426
+ config.anyway_config.default_config_path = Rails.root.join("etc", "configs")
427
+ ```
428
+
429
+ - By setting `config.anyway_config.default_config_path` to a Proc, which accepts a config name and returns the path:
430
+
431
+ ```ruby
432
+ config.anyway_config.default_config_path = ->(name) { Rails.root.join("data", "configs", "#{name}.yml") }
433
+ ```
434
+
435
+ - By overriding a specific config YML file path via the `<NAME>_CONF` env variable, e.g., `MYCOOLGEM_CONF=path/to/cool.yml`
436
+
437
+ 2) (Rails <7.1) **Rails secrets**: `Rails.application.secrets.my_cool_gem` (if `secrets.yml` present).
438
+
439
+ ```yml
440
+ # config/secrets.yml
441
+ development:
442
+ my_cool_gem:
443
+ port: 4444
444
+ ```
445
+
446
+ **NOTE:** If you want to use secrets with Rails 7.1 (still supported, but deprecated) you must add the corresponding loader manually: `Anyway.loaders.insert_after :yml, :secrets, Anyway::Rails::Loaders::Secrets`.
447
+
448
+ 3) **Rails credentials**: `Rails.application.credentials.my_cool_gem` (if supported):
449
+
450
+ ```yml
451
+ my_cool_gem:
452
+ host: secret.host
453
+ ```
454
+
455
+ **NOTE:** You can backport Rails 6 per-environment credentials to Rails 5.2 app using [this patch](https://gist.github.com/palkan/e27e4885535ff25753aefce45378e0cb).
456
+
457
+ 4) **Environment variables**: `ENV['MYCOOLGEM_*']`.
458
+
459
+ See [environment variables](#environment-variables).
460
+
461
+ ### Organizing configs
462
+
463
+ You can store application-level config classes in `app/configs` folder just like any other Rails entities.
464
+
465
+ However, in that case you won't be able to use them during the application initialization (i.e., in `config/**/*.rb` files).
466
+
467
+ Since that's a pretty common scenario, we provide a way to do that via a custom autoloader for `config/configs` folder.
468
+ That means, that you can put your configuration classes into `config/configs` folder, use them anywhere in your code without explicitly requiring them.
469
+
470
+ Consider an example: setting the Action Mailer hostname for Heroku review apps.
471
+
472
+ We have the following config to fetch the Heroku provided [metadata](https://devcenter.heroku.com/articles/dyno-metadata):
473
+
474
+ ```ruby
475
+ # This data is provided by Heroku Dyno Metadadata add-on.
476
+ class HerokuConfig < Anyway::Config
477
+ attr_config :app_id, :app_name,
478
+ :dyno_id, :release_version,
479
+ :slug_commit
480
+
481
+ def hostname
482
+ "#{app_name}.herokuapp.com"
483
+ end
484
+ end
485
+ ```
486
+
487
+ Then in `config/application.rb` you can do the following:
488
+
489
+ ```ruby
490
+ config.action_mailer.default_url_options = {host: HerokuConfig.new.hostname}
491
+ ```
492
+
493
+ You can configure the configs folder path:
494
+
495
+ ```ruby
496
+ # The path must be relative to Rails root
497
+ config.anyway_config.autoload_static_config_path = "path/to/configs"
498
+ ```
499
+
500
+ **NOTE:** Configs loaded from the `autoload_static_config_path` are **not reloaded in development**. We call them _static_. So, it makes sense to keep only configs necessary for initialization in this folder. Other configs, _dynamic_, could be stored in `app/configs`.
501
+ Or you can store everything in `app/configs` by setting `config.anyway_config.autoload_static_config_path = "app/configs"`.
502
+
503
+ **NOTE 2**: Since _static_ configs are loaded before initializers, it's not possible to use custom inflection Rules (usually defined in `config/initializers/inflections.rb`) to resolve constant names from files. If you rely on custom inflection rules (see, for example, [#81](https://github.com/palkan/anyway_config/issues/81)), we recommend configuration Rails inflector before initialization as well:
504
+
505
+ ```ruby
506
+ # config/application.rb
507
+
508
+ # ...
509
+
510
+ require_relative "initializers/inflections"
511
+
512
+ module SomeApp
513
+ class Application < Rails::Application
514
+ # ...
515
+ end
516
+ end
517
+ ```
518
+
519
+ ### Generators
520
+
521
+ Anyway Config provides Rails generators to create new config classes:
522
+
523
+ - `rails g anyway:install`—creates an `ApplicationConfig` class (the base class for all config classes) and updates `.gitignore`
524
+
525
+ You can specify the static configs path via the `--configs-path` option:
526
+
527
+ ```sh
528
+ rails g anyway:install --configs-path=config/settings
529
+
530
+ # or to keep everything in app/configs
531
+ rails g anyway:install --configs-path=app/configs
532
+ ```
533
+
534
+ - `rails g anyway:config <name> param1 param2 ...`—creates a named configuration class and optionally the corresponding YAML file; creates `application_config.rb` is missing.
535
+
536
+ The generator command for the Heroku example above would be:
537
+
538
+ ```sh
539
+ $ rails g anyway:config heroku app_id app_name dyno_id release_version slug_commit
540
+
541
+ generate anyway:install
542
+ rails generate anyway:install
543
+ create config/configs/application_config.rb
544
+ append .gitignore
545
+ create config/configs/heroku_config.rb
546
+ Would you like to generate a heroku.yml file? (Y/n) n
547
+ ```
548
+
549
+ You can also specify the `--app` option to put the newly created class into `app/configs` folder.
550
+ Alternatively, you can call `rails g anyway:app_config name param1 param2 ...`.
551
+
552
+ **NOTE:** The generated `ApplicationConfig` class uses a singleton pattern along with `delegate_missing_to` to re-use the same instance across the application. However, the delegation can lead to unexpected behaviour and break Anyway Config internals if you have attributes named as `Anyway::Config` class methods. See [#120](https://github.com/palkan/anyway_config/issues/120).
553
+
554
+ ### Loading Anyway Config before Rails
555
+
556
+ Anyway Config activates Rails-specific features automatically on the gem load only if Rails has been already required (we check for the `Rails::VERSION` constant presence). However, in some cases you may want to use Anyway Config before Rails initialization (e.g., in `config/puma.rb` when starting a Puma web server).
557
+
558
+ By default, Anyway Config sets up a hook (via TracePoint API) and waits for Rails to be loaded to require the Rails extensions (`require "anyway/rails"`). In case you load Rails after Anyway Config, you will see a warning telling you about that. Note that config classes loaded before Rails are not populated from Rails-specific data sources (e.g., credentials).
559
+
560
+ You can disable the warning by setting `Anyway::Rails.disable_postponed_load_warning = true` in your application. Also, you can disable the _hook_ completely by calling `Anyway::Rails.tracer.disable`.
561
+
562
+ ## Using with Ruby
563
+
564
+ The default data loading mechanism for non-Rails applications is the following (ordered by priority from low to high):
565
+
566
+ 1) **YAML configuration files**: `./config/<config-name>.yml`.
567
+
568
+ In pure Ruby apps, we also can load data under specific _environments_ (`test`, `development`, `production`, etc.).
569
+ If you want to enable this feature you must specify `Anyway::Settings.current_environment` variable for load config under specific environment.
570
+
571
+ ```ruby
572
+ Anyway::Settings.current_environment = "development"
573
+ ```
574
+
575
+ You can also specify the `ANYWAY_ENV=development` environment variable to set the current environment for configuration.
576
+
577
+ YAML files should be in this format:
578
+
579
+ ```yml
580
+ development:
581
+ host: localhost
582
+ port: 3000
583
+ ```
584
+
585
+ If `Anyway::Settings.current_environment` is missed we assume that the YAML contains values for a single environment:
586
+
587
+ ```yml
588
+ host: localhost
589
+ port: 3000
590
+ ```
591
+
592
+ `ERB` is supported if `erb` is loaded (thus, you need to call `require "erb"` somewhere before loading configuration).
593
+
594
+ You can specify the lookup path for YAML files in one of the following ways:
595
+
596
+ - By setting `Anyway::Settings.default_config_path` to a target directory path:
597
+
598
+ ```ruby
599
+ Anyway::Settings.default_config_path = "/etc/configs"
600
+ ```
601
+
602
+ - By setting `Anyway::Settings.default_config_path` to a Proc, which accepts a config name and returns the path:
603
+
604
+ ```ruby
605
+ Anyway::Settings.default_config_path = ->(name) { Rails.root.join("data", "configs", "#{name}.yml") }
606
+ ```
607
+
608
+ - By overriding a specific config YML file path via the `<NAME>_CONF` env variable, e.g., `MYCOOLGEM_CONF=path/to/cool.yml`
609
+
610
+ 2) **Environment variables**: `ENV['MYCOOLGEM_*']`.
611
+
612
+ See [environment variables](#environment-variables).
613
+
614
+ ## Environment variables
615
+
616
+ Environmental variables for your config should start with your config name, upper-cased.
617
+
618
+ For example, if your config name is "mycoolgem", then the env var "MYCOOLGEM_PASSWORD" is used as `config.password`.
619
+
620
+ By default, environment variables are automatically type cast (rules are case-insensitive):
621
+
622
+ - `"true"`, `"t"`, `"yes"` and `"y"` to `true`;
623
+ - `"false"`, `"f"`, `"no"` and `"n"` to `false`;
624
+ - `"nil"` and `"null"` to `nil` (do you really need it?);
625
+ - `"123"` to `123` and `"3.14"` to `3.14`.
626
+
627
+ Type coercion can be [customized or disabled](#type-coercion).
628
+
629
+ *Anyway Config* supports nested (_hashed_) env variables—just separate keys with double-underscore.
630
+
631
+ For example, "MYCOOLGEM_OPTIONS__VERBOSE" is parsed as `config.options["verbose"]`.
632
+
633
+ Array values are also supported:
634
+
635
+ ```ruby
636
+ # Suppose ENV["MYCOOLGEM_IDS"] = '1,2,3'
637
+ config.ids #=> [1,2,3]
638
+ ```
639
+
640
+ If you want to provide a text-like env variable which contains commas then wrap it into quotes:
641
+
642
+ ```ruby
643
+ MYCOOLGEM = "Nif-Nif, Naf-Naf and Nouf-Nouf"
644
+ ```
645
+
646
+ ## Type coercion
647
+
648
+ > 🆕 v2.2.0
649
+
650
+ You can define custom type coercion rules to convert string data to config values. To do that, use `.coerce_types` method:
651
+
652
+ ```ruby
653
+ class CoolConfig < Anyway::Config
654
+ config_name :cool
655
+ attr_config port: 8080,
656
+ host: "localhost",
657
+ user: {name: "admin", password: "admin"}
658
+
659
+ coerce_types port: :string, user: {dob: :date}
660
+ end
661
+
662
+ ENV["COOL_USER__DOB"] = "1989-07-01"
663
+
664
+ config = CoolConfig.new
665
+ config.port == "8080" # Even though we defined the default value as int, it's converted into a string
666
+ config.user["dob"] == Date.new(1989, 7, 1) #=> true
667
+ ```
668
+
669
+ Type coercion is especially useful to deal with array values:
670
+
671
+ ```ruby
672
+ # To define an array type, provide a hash with two keys:
673
+ # - type — elements type
674
+ # - array: true — mark the parameter as array
675
+ coerce_types list: {type: :string, array: true}
676
+ ```
677
+
678
+ You can use `type: nil` in case you don't want to coerce values, just convert a value into an array:
679
+
680
+ ```ruby
681
+ # From AnyCable config (sentinels could be represented via strings or hashes)
682
+ coerce_types redis_sentinels: {type: nil, array: true}
683
+ ```
684
+
685
+ It's also could be useful to explicitly define non-array types (to avoid confusion):
686
+
687
+ ```ruby
688
+ coerce_types non_list: :string
689
+ ```
690
+
691
+ Finally, it's possible to disable auto-casting for a particular config completely:
692
+
693
+ ```ruby
694
+ class CoolConfig < Anyway::Config
695
+ attr_config port: 8080,
696
+ host: "localhost",
697
+ user: {name: "admin", password: "admin"}
698
+
699
+ disable_auto_cast!
700
+ end
701
+
702
+ ENV["COOL_PORT"] = "443"
703
+
704
+ CoolConfig.new.port == "443" #=> true
705
+ ```
706
+
707
+ **IMPORTANT**: Values provided explicitly (via attribute writers) are not coerced. Coercion is only happening during the load phase.
708
+
709
+ The following types are supported out-of-the-box: `:string`, `:integer`, `:float`, `:date`, `:datetime`, `:uri`, `:boolean`.
710
+
711
+ You can use custom deserializers by passing a callable object instead of a type name:
712
+
713
+ ```ruby
714
+ COLOR_TO_HEX = lambda do |raw|
715
+ case raw
716
+ when "red"
717
+ "#ff0000"
718
+ when "green"
719
+ "#00ff00"
720
+ when "blue"
721
+ "#0000ff"
722
+ end
723
+ end
724
+
725
+ class CoolConfig < Anyway::Config
726
+ attr_config :color
727
+
728
+ coerce_types color: COLOR_TO_HEX
729
+ end
730
+
731
+ CoolConfig.new({color: "red"}).color #=> "#ff0000"
732
+ ```
733
+
734
+ ## Local files
735
+
736
+ It's useful to have a personal, user-specific configuration in development, which extends the project-wide one.
737
+
738
+ We support this by looking at _local_ files when loading the configuration data:
739
+
740
+ - `<config_name>.local.yml` files (next to\* the _global_ `<config_name>.yml`)
741
+ - `config/credentials/local.yml.enc` (for Rails >= 6, generate it via `rails credentials:edit --environment local`).
742
+
743
+ \* If the YAML config path is not a default one (i.e., set via `<CONFIG_NAME>_CONF`), we look up the local
744
+ config at this location, too.
745
+
746
+ Local configs are meant for using in development and only loaded if `Anyway::Settings.use_local_files` is `true` (which is true by default if `RACK_ENV` or `RAILS_ENV` env variable is equal to `"development"`).
747
+
748
+ **NOTE:** in Rails apps you can use `Rails.application.configuration.anyway_config.use_local_files`.
749
+
750
+ Don't forget to add `*.local.yml` (and `config/credentials/local.*`) to your `.gitignore`.
751
+
752
+ **NOTE:** local YAML configs for a Rails app must be environment-free (i.e., you shouldn't have top-level `development:` key).
753
+
754
+ ## Data loaders
755
+
756
+ ### Doppler integration
757
+
758
+ Anyway Config can pull configuration data from [Doppler](https://www.doppler.com/). All you need is to specify the `DOPPLER_TOKEN` environment variable with the **service token**, associated with the specific content (read more about [service tokens](https://docs.doppler.com/docs/service-tokens)).
759
+
760
+ You can also configure Doppler loader manually if needed:
761
+
762
+ ```ruby
763
+ # Add loader
764
+ Anyway.loaders.append :Doppler, Anyway::Loaders::Doppler
765
+
766
+ # Configure API URL and token (defaults are shown)
767
+ Anyway::Loaders::Doppler.download_url = "https://api.doppler.com/v3/configs/config/secrets/download"
768
+ Anyway::Loaders::Doppler.token = ENV["DOPPLER_TOKEN"]
769
+ ```
770
+
771
+ **NOTE:** You can opt-out from Doppler loader by specifying the`ANYWAY_CONFIG_DISABLE_DOPPLER=true` env var (in case you have the `DOPPLER_TOKEN` env var, but don't want to use it with Anyway Config).
772
+
773
+ ### EJSON support
774
+
775
+ Anyway Config allows you to keep your configuration also in encrypted `.ejson` files. More information
776
+ about EJSON format you can read [here](https://github.com/Shopify/ejson).
777
+
778
+ Configuration will be loaded only if you have `ejson` executable in your PATH. Easiest way to do this - install `ejson` as a gem into project:
779
+
780
+ ```ruby
781
+ # Gemfile
782
+ gem "ejson"
783
+ ```
784
+
785
+ Loading order of configuration is next:
786
+
787
+ - `config/secrets.local.ejson` (see [Local files](#local-files) for more information)
788
+ - `config/<environment>/secrets.ejson` (if you have any multi-environment setup, e.g Rails environments)
789
+ - `config/secrets.ejson`
790
+
791
+ Example of `config/secrets.ejson` file content for your `MyConfig`:
792
+
793
+ ```json
794
+ {
795
+ "_public_key": "0843d33f0eee994adc66b939fe4ef569e4c97db84e238ff581934ee599e19d1a",
796
+ "my":
797
+ {
798
+ "_username": "root",
799
+ "password": "EJ[1:IC1d347GkxLXdZ0KrjGaY+ljlsK1BmK7CobFt6iOLgE=:Z55OYS1+On0xEBvxUaIOdv/mE2r6lp44:T7bE5hkAbazBnnH6M8bfVcv8TOQJAgUDQffEgw==]"
800
+ }
801
+ }
802
+ ```
803
+
804
+ To debug any problems with loading configurations from `.ejson` files you can directly call `ejson decrypt`:
805
+
806
+ ```sh
807
+ ejson decrypt config/secrets.ejson
808
+ ```
809
+
810
+ You can customize the JSON namespace under which a loader searches for configuration via `loader_options`:
811
+
812
+ ```ruby
813
+ class MyConfig < Anyway::Config
814
+ # To look under the key "foo" instead of the default key of "my"
815
+ loader_options ejson_namespace: "foo"
816
+
817
+ # Or to disable namespacing entirely, and instead search in the root object
818
+ loader_options ejson_namespace: false
819
+ end
820
+ ```
821
+
822
+ ### Custom loaders
823
+
824
+ You can provide your own data loaders or change the existing ones using the Loaders API (which is very similar to Rack middleware builder):
825
+
826
+ ```ruby
827
+ # remove env loader => do not load params from ENV
828
+ Anyway.loaders.delete :env
829
+
830
+ # add custom loader before :env (it's better to keep the ENV loader the last one)
831
+ Anyway.loaders.insert_before :env, :my_loader, MyLoader
832
+ ```
833
+
834
+ Loader is a _callable_ Ruby object (module/class responding to `.call` or lambda/proc), which `call` method
835
+ accepts the following keyword arguments:
836
+
837
+ ```ruby
838
+ def call(
839
+ name:, # config name
840
+ env_prefix:, # prefix for env vars if any
841
+ config_path:, # path to YML config
842
+ local:, # true|false, whether to load local configuration
843
+ **options # custom options can be passed via Anyway::Config.loader_options example: "custom", option: "blah"
844
+ )
845
+ #=> must return Hash with configuration data
846
+ end
847
+ ```
848
+
849
+ You can use `Anyway::Loaders::Base` as a base class for your loader and define a `#call` method.
850
+ For example, the [Chamber](https://github.com/thekompanee/chamber) loader could be written as follows:
851
+
852
+ ```ruby
853
+ class ChamberConfigLoader < Base
854
+ def call(name:, **_opts)
855
+ Chamber.to_hash[name] || {}
856
+ rescue Chamber::Errors::DecryptionFailure => e
857
+ warn "Couldn't decrypt Chamber settings: #{e.message}"
858
+ {}
859
+ end
860
+ end
861
+
862
+ # Don't forget to register it
863
+ Anyway.loaders.insert_before :env, :chamber, ChamberConfigLoader
864
+ ```
865
+
866
+ In order to support [source tracing](#tracing), you need to wrap the resulting Hash via the `#trace!` method with metadata:
867
+
868
+ ```ruby
869
+ def call(name:, **_opts)
870
+ trace!(:chamber) do
871
+ Chamber.to_hash[name] || {}
872
+ rescue Chamber::Errors::DecryptionFailure => e
873
+ warn "Couldn't decrypt Chamber settings: #{e.message}"
874
+ {}
875
+ end
876
+ end
877
+ ```
878
+
879
+ ## Tracing
880
+
881
+ Since Anyway Config loads data from multiple source, it could be useful to know where a particular value came from.
882
+
883
+ Each `Anyway::Config` instance contains _tracing information_ which you can access via `#to_source_trace` method:
884
+
885
+ ```ruby
886
+ conf = ExampleConfig.new
887
+ conf.to_source_trace
888
+
889
+ # returns the following hash
890
+ {
891
+ "host" => {value: "test.host", source: {type: :yml, path: "config/example.yml"}},
892
+ "user" => {
893
+ "name" => {value: "john", source: {type: :env, key: "EXAMPLE_USER__NAME"}},
894
+ "password" => {value: "root", source: {type: :credentials, store: "config/credentials/production.enc.yml"}}
895
+ },
896
+ "port" => {value: 9292, source: {type: :defaults}}
897
+ }
898
+
899
+ # if you change the value manually in your code,
900
+ # that would be reflected in the trace
901
+
902
+ conf.host = "anyway.host"
903
+ conf.to_source_trace["host"]
904
+ #=> {type: :user, called_from: "/path/to/caller.rb:15"}
905
+ ```
906
+
907
+ You can disable tracing functionality by setting `Anyway::Settings.tracing_enabled = false` or `config.anyway_config.tracing_enabled = false` in Rails.
908
+
909
+ ### Pretty print
910
+
911
+ You can use `pp` to print a formatted information about the config including the sources trace.
912
+
913
+ Example:
914
+
915
+ ```ruby
916
+ pp CoolConfig.new
917
+
918
+ # #<CoolConfig
919
+ # config_name="cool"
920
+ # env_prefix="COOL"
921
+ # values:
922
+ # port => 3334 (type=load),
923
+ # host => "test.host" (type=yml path=./config/cool.yml),
924
+ # user =>
925
+ # name => "john" (type=env key=COOL_USER__NAME),
926
+ # password => "root" (type=yml path=./config/cool.yml)>
927
+ ```
928
+
929
+ ## Pattern matching
930
+
931
+ You can use config instances in Ruby 2.7+ pattern matching:
932
+
933
+ ```ruby
934
+ case AWSConfig.new
935
+ in bucket:, region: "eu-west-1"
936
+ setup_eu_storage(bucket)
937
+ in bucket:, region: "us-east-1"
938
+ setup_us_storage(bucket)
939
+ end
940
+ ```
941
+
942
+ If the attribute wasn't populated, the key won't be returned for pattern matching, i.e. you can do something line:
943
+
944
+ ```ruby
945
+ aws_configured =
946
+ case AWSConfig.new
947
+ in access_key_id:, secret_access_key:
948
+ true
949
+ else
950
+ false
951
+ end
952
+ ```
953
+
954
+ ## Test helpers
955
+
956
+ We provide the `with_env` test helper to test code in the context of the specified environment variables values:
957
+
958
+ ```ruby
959
+ describe HerokuConfig, type: :config do
960
+ subject { described_class.new }
961
+
962
+ specify do
963
+ # Ensure that the env vars are set to the specified
964
+ # values within the block and reset to the previous values
965
+ # outside of it.
966
+ with_env(
967
+ "HEROKU_APP_NAME" => "kin-web-staging",
968
+ "HEROKU_APP_ID" => "abc123",
969
+ "HEROKU_DYNO_ID" => "ddyy",
970
+ "HEROKU_RELEASE_VERSION" => "v0",
971
+ "HEROKU_SLUG_COMMIT" => "3e4d5a"
972
+ ) do
973
+ is_expected.to have_attributes(
974
+ app_name: "kin-web-staging",
975
+ app_id: "abc123",
976
+ dyno_id: "ddyy",
977
+ release_version: "v0",
978
+ slug_commit: "3e4d5a"
979
+ )
980
+ end
981
+ end
982
+ end
983
+ ```
984
+
985
+ If you want to delete the env var, pass `nil` as the value.
986
+
987
+ This helper is automatically included to RSpec if `RAILS_ENV` or `RACK_ENV` env variable is equal to "test". It's only available for the example with the tag `type: :config` or with the path `spec/configs/...`.
988
+
989
+ You can add it manually by requiring `"anyway/testing/helpers"` and including the `Anyway::Testing::Helpers` module (into RSpec configuration or Minitest test class).
990
+
991
+ ## OptionParser integration
992
+
993
+ It's possible to use config as option parser (e.g., for CLI apps/libraries). It uses
994
+ [`optparse`](https://ruby-doc.org/stdlib-2.5.1/libdoc/optparse/rdoc/OptionParser.html) under the hood.
995
+
996
+ Example usage:
997
+
998
+ ```ruby
999
+ class MyConfig < Anyway::Config
1000
+ attr_config :host, :log_level, :concurrency, :debug, server_args: {}
1001
+
1002
+ # specify which options shouldn't be handled by option parser
1003
+ ignore_options :server_args
1004
+
1005
+ # provide description for options
1006
+ describe_options(
1007
+ concurrency: "number of threads to use"
1008
+ )
1009
+
1010
+ # mark some options as flag
1011
+ flag_options :debug
1012
+
1013
+ # extend an option parser object (i.e. add banner or version/help handlers)
1014
+ extend_options do |parser, config|
1015
+ parser.banner = "mycli [options]"
1016
+
1017
+ parser.on("--server-args VALUE") do |value|
1018
+ config.server_args = JSON.parse(value)
1019
+ end
1020
+
1021
+ parser.on_tail "-h", "--help" do
1022
+ puts parser
1023
+ end
1024
+ end
1025
+ end
1026
+
1027
+ config = MyConfig.new
1028
+
1029
+ config.parse_options!(%w[--host localhost --port 3333 --log-level debug])
1030
+
1031
+ config.host # => "localhost"
1032
+ config.port # => 3333
1033
+ config.log_level # => "debug"
1034
+
1035
+ # Get the instance of OptionParser
1036
+ config.option_parser
1037
+ ```
1038
+
1039
+ **NOTE:** values are automatically type cast using the same rules as for [environment variables](#environment-variables).
1040
+ If you want to specify the type explicitly, you can do that using `describe_options`:
1041
+
1042
+ ```ruby
1043
+ describe_options(
1044
+ # In this case, you should specify a hash with `type`
1045
+ # and (optionally) `desc` keys
1046
+ concurrency: {
1047
+ desc: "number of threads to use",
1048
+ type: String
1049
+ }
1050
+ )
1051
+ ```
1052
+
1053
+ ## RBS support
1054
+
1055
+ Anyway Config comes with Ruby type signatures (RBS).
1056
+
1057
+ To use them with Steep, add the following your `Steepfile`:
1058
+
1059
+ ```ruby
1060
+ library "pathname"
1061
+ library "optparse"
1062
+ library "anyway_config"
1063
+ ```
1064
+
1065
+ We also provide an API to generate a type signature for your config class:
1066
+
1067
+ ```ruby
1068
+ class MyGem::Config < Anyway::Config
1069
+ attr_config :host, port: 8080, tags: [], debug: false
1070
+
1071
+ coerce_types host: :string, port: :integer,
1072
+ tags: {type: :string, array: true}
1073
+
1074
+ required :host
1075
+ end
1076
+ ```
1077
+
1078
+ Then calling `MyGem::Config.to_rbs` will return the following signature:
1079
+
1080
+ ```rbs
1081
+ module MyGem
1082
+ interface _Config
1083
+ def host: () -> String
1084
+ def host=: (String) -> void
1085
+ def port: () -> String?
1086
+ def port=: (String) -> void
1087
+ def tags: () -> Array[String]?
1088
+ def tags=: (Array[String]) -> void
1089
+ def debug: () -> bool
1090
+ def debug?: () -> bool
1091
+ def debug=: (bool) -> void
1092
+ end
1093
+
1094
+ class Config < Anyway::Config
1095
+ include _Config
1096
+ end
1097
+ end
1098
+ ```
1099
+
1100
+ ### Handling `on_load`
1101
+
1102
+ When we use `on_load` callback with a block, we switch the context (via `instance_eval`), and we need to provide type hints for the type checker. Here is an example:
1103
+
1104
+ ```ruby
1105
+ class MyConfig < Anyway::Config
1106
+ on_load do
1107
+ # @type self : MyConfig
1108
+ raise_validation_error("host is invalid") if host.start_with?("localhost")
1109
+ end
1110
+ end
1111
+ ```
1112
+
1113
+ Yeah, a lot of annotations 😞 Welcome to the type-safe world!
1114
+
1115
+ ## Contributing
1116
+
1117
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/palkan/anyway_config](https://github.com/palkan/anyway_config).
1118
+
1119
+ ## License
1120
+
1121
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).