runger_config 2.6.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.
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).