anyway_config 2.0.0.pre2 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +350 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +444 -119
  5. data/lib/.rbnext/2.7/anyway/auto_cast.rb +33 -0
  6. data/lib/.rbnext/2.7/anyway/config.rb +404 -0
  7. data/lib/.rbnext/2.7/anyway/option_parser_builder.rb +31 -0
  8. data/lib/.rbnext/2.7/anyway/tracing.rb +187 -0
  9. data/lib/anyway/auto_cast.rb +33 -0
  10. data/lib/anyway/config.rb +235 -58
  11. data/lib/anyway/dynamic_config.rb +1 -1
  12. data/lib/anyway/env.rb +29 -19
  13. data/lib/anyway/ext/hash.rb +14 -6
  14. data/lib/anyway/loaders.rb +79 -0
  15. data/lib/anyway/loaders/base.rb +23 -0
  16. data/lib/anyway/loaders/env.rb +16 -0
  17. data/lib/anyway/loaders/yaml.rb +46 -0
  18. data/lib/anyway/option_parser_builder.rb +6 -3
  19. data/lib/anyway/optparse_config.rb +10 -6
  20. data/lib/anyway/rails.rb +16 -0
  21. data/lib/anyway/rails/config.rb +2 -69
  22. data/lib/anyway/rails/loaders.rb +5 -0
  23. data/lib/anyway/rails/loaders/credentials.rb +64 -0
  24. data/lib/anyway/rails/loaders/secrets.rb +39 -0
  25. data/lib/anyway/rails/loaders/yaml.rb +19 -0
  26. data/lib/anyway/rails/settings.rb +58 -0
  27. data/lib/anyway/railtie.rb +14 -2
  28. data/lib/anyway/settings.rb +29 -0
  29. data/lib/anyway/tracing.rb +187 -0
  30. data/lib/anyway/version.rb +1 -1
  31. data/lib/anyway_config.rb +27 -21
  32. data/lib/generators/anyway/app_config/USAGE +9 -0
  33. data/lib/generators/anyway/app_config/app_config_generator.rb +17 -0
  34. data/lib/generators/anyway/config/USAGE +13 -0
  35. data/lib/generators/anyway/config/config_generator.rb +46 -0
  36. data/lib/generators/anyway/config/templates/config.rb.tt +9 -0
  37. data/lib/generators/anyway/config/templates/config.yml.tt +13 -0
  38. data/lib/generators/anyway/install/USAGE +4 -0
  39. data/lib/generators/anyway/install/install_generator.rb +43 -0
  40. data/lib/generators/anyway/install/templates/application_config.rb.tt +17 -0
  41. metadata +75 -10
  42. data/lib/anyway/ext/string_serialize.rb +0 -38
  43. data/lib/anyway/loaders/env_loader.rb +0 -0
  44. data/lib/anyway/loaders/secrets_loader.rb +0 -0
  45. data/lib/anyway/loaders/yaml_loader.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 540aa91321939a31eb3982143a12cc17621bdcb23c61d2d24686f999ae28a144
4
- data.tar.gz: 3215663dd3722892f0635d13163dc02521f515ce31f5c310402893f304387b2d
3
+ metadata.gz: 0c4661661a2d8cbce7e35a4eef45520a290855af228b1d20d69367708b11be3f
4
+ data.tar.gz: 97f07abb3e194f490069d6dc1ef7076a88f3775f5b06a3cc99d15314da845c54
5
5
  SHA512:
6
- metadata.gz: a629cdb9ba784714cff8df41814a6520745b9f1a6fdce272da2a56fd9d6635d46365767c4ce1289c93fdb0f3c5e4d9167b9b617830ff719c555a2a5a6e19f915
7
- data.tar.gz: '009fd89edf49b814bde0fa2c7d846fa3f086fe606386c067bc447f4f8c990853a580257cb943e07692b57b49674912fd041f29cc9dbbb9eb652aaa3ee3857776'
6
+ metadata.gz: 9897240812d5ae7522b99c0d4ac2ec1c3e2ec5759ef481ff3db6d9fbeb1c46312a895ed64fd94fc6b3c1a416a3ccee9325458c1aeacd0ff7dbc9a10593fabf89
7
+ data.tar.gz: c590c11acb69e99ff0ada09fa7ebb4ae44f777da626f2f9dd053935d9d201fa6b3d6296642271e7d56260b9258d0baca65ade78fd10c9397e5205a03d8551b1a
data/CHANGELOG.md ADDED
@@ -0,0 +1,350 @@
1
+ # Change log
2
+
3
+ ## master
4
+
5
+ ## 2.0.0.rc1 (2020-03-31)
6
+
7
+ - Add predicate methods for attributes with boolean defaults. ([@palkan][])
8
+
9
+ For example:
10
+
11
+ ```ruby
12
+ class MyConfig < Anyway::Config
13
+ attr_config :key, :secret, debug: false
14
+ end
15
+
16
+ MyConfig.new.debug? #=> false
17
+ MyConfig.new(debug: true).debug? #=> true
18
+ ```
19
+
20
+ - Add `Config#deconstruct_keys`. ([@palkan][])
21
+
22
+ Now you can use configs in pattern matching:
23
+
24
+ ```ruby
25
+ case AWSConfig.new
26
+ in bucket:, region: "eu-west-1"
27
+ setup_eu_storage(bucket)
28
+ in bucket:, region: "us-east-1"
29
+ setup_us_storage(bucket)
30
+ end
31
+ ```
32
+
33
+ - Add pretty_print support. ([@palkan][])
34
+
35
+ Whenever you use `pp`, the output would contain pretty formatted config information
36
+ including the sources.
37
+
38
+ Example:
39
+
40
+ ```ruby
41
+ pp CoolConfig.new
42
+
43
+ # #<CoolConfig
44
+ # config_name="cool"
45
+ # env_prefix="COOL"
46
+ # values:
47
+ # port => 3334 (type=load),
48
+ # host => "test.host" (type=yml path=./config/cool.yml),
49
+ # user =>
50
+ # name => "john" (type=env key=COOL_USER__NAME),
51
+ # password => "root" (type=yml path=./config/cool.yml)>
52
+ ```
53
+
54
+ - Add source tracing support. ([@palkan][])
55
+
56
+ You can get the information on where a particular parameter value came from
57
+ (which loader) through via `#to_source_trace` method:
58
+
59
+ ```ruby
60
+ conf = MyConfig.new
61
+ conf.to_source_trace
62
+ # {
63
+ # "host" => {value: "test.host", source: {type: :user, called_from: "/rails/root/config/application.rb:21"}},
64
+ # "user" => {
65
+ # "name" => {value: "john", source: {type: :env, key: "COOL_USER__NAME"}},
66
+ # "password" => {value: "root", source: {type: :yml, path: "config/cool.yml"}}
67
+ # },
68
+ # "port" => {value: 9292, source: {type: :defaults}}
69
+ # }
70
+ ```
71
+
72
+ - Change the way Rails configs autoloading works. ([@palkan][])
73
+
74
+ In Rails 6, autoloading before initialization is [deprecated](https://github.com/rails/rails/commit/3e66ba91d511158e22f90ff96b594d61f40eda01). We can still
75
+ make it work by using our own autoloading mechanism (custom Zeitwerk loader).
76
+
77
+ This forces us to use a custom directory (not `app/`) for configs required at the boot time.
78
+ By default, we put _static_ configs into `config/configs` but you can still use `app/configs` for
79
+ _dynamic_ (runtime) configs.
80
+
81
+ **NOTE:** if you used `app/configs` with 2.0.0.preX and relied on configs during initialization,
82
+ you can set static configs path to `app/configs`:
83
+
84
+ ```ruby
85
+ config.anyway_config.autoload_static_config_path = "app/configs"
86
+ ```
87
+
88
+ You can do this by running the generator:
89
+
90
+ ```sh
91
+ rails g anyway:install --configs-path=app/configs
92
+ ```
93
+
94
+ - Add Rails generators. ([@palkan][])
95
+
96
+ You can create config classes with the predefined attributes like this:
97
+
98
+ ```sh
99
+ rails generate config aws access_key_id secret_access_key region
100
+ ```
101
+
102
+ - **BREAKING** The accessors generated by `attr_config` are not longer `attr_accessor`-s. ([@palkan][])
103
+
104
+ You cannot rely on instance variables anymore. Instead, you can use `super` when overriding accessors or
105
+ `values[name]`:
106
+
107
+ ```ruby
108
+ attr_config :host, :port, :url, :meta
109
+
110
+ # override writer to handle type coercion
111
+ def meta=(val)
112
+ super JSON.parse(val)
113
+ end
114
+
115
+ # or override reader to handle missing values
116
+ def url
117
+ values[:url] ||= "#{host}:#{port}"
118
+ end
119
+
120
+ # in <2.1 it's still possible to read instance variables,
121
+ # i.e. the following would also work
122
+ def url
123
+ @url ||= "#{host}:#{port}"
124
+ end
125
+ ```
126
+
127
+ **NOTE**: we still set instance variables in writers (for backward compatibility), but that would
128
+ be removed in 2.1.
129
+
130
+ - Add `Config#dig` method. ([@palkan][])
131
+
132
+ - Add ability to specify types for OptionParser options. ([@palkan][])
133
+
134
+ ```ruby
135
+ describe_options(
136
+ concurrency: {
137
+ desc: "number of threads to use",
138
+ type: String
139
+ }
140
+ )
141
+ ```
142
+
143
+ - Add param presence validation. ([@palkan][])
144
+
145
+ You can specify some params as required, and the validation
146
+ error would be raised if they're missing or empty (only for strings):
147
+
148
+ ```ruby
149
+ class MyConfig < Anyway::Config
150
+ attr_config :api_key, :api_secret, :debug
151
+
152
+ required :api_key, :api_secret
153
+ end
154
+
155
+ MyConfig.new(api_secret: "") #=> raises Anyway::Config::ValidationError
156
+ ```
157
+
158
+ You can change the validation behaviour by overriding the `#validate!` method in your config class.
159
+
160
+ - Validate config attribute names. ([@palkan][])
161
+
162
+ Do not allow using reserved names (`Anyway::Config` method names).
163
+ Allow only alphanumeric names (matching `/^[a-z_]([\w]+)?$/`).
164
+
165
+ - Add Loaders API. ([@palkan][])
166
+
167
+ All config sources have standardized via _loaders_ API. It's possible to define
168
+ custom loaders or change the sources order.
169
+
170
+ ## 2.0.0.pre2 (2019-04-26)
171
+
172
+ - Fix bug with loading from credentials when local credentials are missing. ([@palkan][])
173
+
174
+ ## 2.0.0.pre (2019-04-26)
175
+
176
+ - **BREAKING** Changed the way of providing explicit values. ([@palkan][])
177
+
178
+ ```ruby
179
+ # BEFORE
180
+ Config.new(overrides: data)
181
+
182
+ # AFTER
183
+ Config.new(data)
184
+ ```
185
+
186
+ - Add Railtie. ([@palkan][])
187
+
188
+ `Anyway::Railtie` provides `Anyway::Settings` access via `Rails.applicaiton.configuration.anyway_config`.
189
+
190
+ It also adds `app/configs` path to autoload paths (low-level, `ActiveSupport::Dependencies`) to
191
+ make it possible to use configs in the app configuration files.
192
+
193
+ - Add test helpers. ([@palkan][])
194
+
195
+ Added `with_env` helper to test code in the context of the specified
196
+ environment variables.
197
+
198
+ Included automatically in RSpec for examples with the `type: :config` meta
199
+ or with the `spec/configs` path.
200
+
201
+ - Add support for _local_ files. ([@palkan][])
202
+
203
+ Now users can store their personal configurations in _local_ files:
204
+ - `<config_name>.local.yml`
205
+ - `config/credentials/local.yml.enc` (for Rails 6).
206
+
207
+ Local configs are meant for using in development and only loaded if
208
+ `Anyway::Settings.use_local_files` is `true` (which is true by default if
209
+ `RACK_ENV` or `RAILS_ENV` env variable is equal to `"development"`).
210
+
211
+ - Add Rails credentials support. ([@palkan][])
212
+
213
+ The data from credentials is loaded after the data from YAML config and secrets,
214
+ but before environmental variables (i.e. env variables are _stronger_)
215
+
216
+ - Update config name inference logic. ([@palkan][])
217
+
218
+ Config name is automatically inferred only if:
219
+ - the class name has a form of `<Module>::Config` (`SomeModule::Config => "some_module"`)
220
+ - the class name has a form of `<Something>Config` (`SomeConfig => "some"`)
221
+
222
+ - Fix config classes inheritance. ([@palkan][])
223
+
224
+ Previously, inheritance didn't work due to the lack of proper handling of class-level
225
+ configuration (naming, option parses settings, defaults).
226
+
227
+ Now it's possible to extend config classes without breaking the original classes functionality.
228
+
229
+ Noticeable features:
230
+ - if `config_name` is explicitly defined on class, it's inherited by subclasses:
231
+
232
+ ```ruby
233
+ class MyAppBaseConfig < Anyway::Config
234
+ config_name :my_app
235
+ end
236
+
237
+ class MyServiceConfig < MyAppBaseConfig
238
+ end
239
+
240
+ MyServiceConfig.config_name #=> "my_app"
241
+ ```
242
+
243
+ - defaults are merged leaving the parent class defaults unchanged
244
+ - option parse extension are not overriden, but added to the parent class extensions
245
+
246
+ - **Require Ruby >= 2.5.0.**
247
+
248
+ ## 1.4.3 (2019-02-04)
249
+
250
+ - Add a temporary fix for JRuby regression [#5550](https://github.com/jruby/jruby/issues/5550). ([@palkan][])
251
+
252
+ ## 1.4.2 (2018-01-05)
253
+
254
+ - Fix: detect Rails by presence of `Rails::VERSION` (instead of just `Rails`). ([@palkan][])
255
+
256
+ ## 1.4.1 (2018-10-30)
257
+
258
+ - Add `.flag_options` to mark some params as flags (value-less) for OptionParse. ([@palkan][])
259
+
260
+ ## 1.4.0 (2018-10-29)
261
+
262
+ - Add OptionParse integration ([@jastkand][])
263
+
264
+ See more [PR#18](https://github.com/palkan/anyway_config/pull/18).
265
+
266
+ - Use underscored config name as an env prefix. ([@palkan][])
267
+
268
+ For a config class:
269
+
270
+ ```ruby
271
+ class MyApp < Anyway::Config
272
+ end
273
+ ```
274
+
275
+ Before this change we use `MYAPP_` prefix, now it's `MY_APP_`.
276
+
277
+ You can specify the prefix explicitly:
278
+
279
+ ```ruby
280
+ class MyApp < Anyway::Config
281
+ env_prefix "MYAPP_"
282
+ end
283
+ ```
284
+
285
+ ## 1.3.0 (2018-06-15)
286
+
287
+ - Ruby 2.2 is no longer supported.
288
+
289
+ - `Anyway::Config.env_prefix` method is introduced. ([@charlie-wasp][])
290
+
291
+ ## 1.2.0 (2018-02-19)
292
+
293
+ Now works on JRuby 9.1+.
294
+
295
+ ## 1.1.3 (2017-12-20)
296
+
297
+ - Allow to pass raw hash with explicit values to `Config.new`. ([@dsalahutdinov][])
298
+
299
+ Example:
300
+
301
+ ```ruby
302
+ Sniffer::Config.new(
303
+ overrides: {
304
+ enabled: true,
305
+ storage: {capacity: 500}
306
+ }
307
+ )
308
+ ```
309
+
310
+ See more [PR#10](https://github.com/palkan/anyway_config/pull/10).
311
+
312
+ ## 1.1.2 (2017-11-19)
313
+
314
+ - Enable aliases for YAML. ([@onemanstartup][])
315
+
316
+ ## 1.1.1 (2017-10-21)
317
+
318
+ - Return deep duplicate of a Hash in `Env#fetch`. ([@palkan][])
319
+
320
+ ## 1.1.0 (2017-10-06)
321
+
322
+ - Add `#to_h` method. ([@palkan][])
323
+
324
+ See [#4](https://github.com/palkan/anyway_config/issues/4).
325
+
326
+ - Make it possible to extend configuration parameters. ([@palkan][])
327
+
328
+ ## 1.0.0 (2017-06-20)
329
+
330
+ - Lazy load and parse ENV configuration. ([@palkan][])
331
+
332
+ - Add support for ERB in YML configuration. ([@palkan][])
333
+
334
+ ## 0.5.0 (2017-01-20)
335
+
336
+ - Drop `active_support` dependency. ([@palkan][])
337
+
338
+ Use custom refinements instead of requiring `active_support`.
339
+
340
+ No we're dependency-free!
341
+
342
+ ## 0.1.0 (2015-01-20)
343
+
344
+ Initial version.
345
+
346
+ [@palkan]: https://github.com/palkan
347
+ [@onemanstartup]: https://github.com/onemanstartup
348
+ [@dsalahutdinov]: https://github.com/dsalahutdinov
349
+ [@charlie-wasp]: https://github.com/charlie-wasp
350
+ [@jastkand]: https://github.com/jastkand
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2015-2019 Vladimir Dementyev
1
+ Copyright (c) 2015-2020 Vladimir Dementyev
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -4,23 +4,70 @@
4
4
 
5
5
  # Anyway Config
6
6
 
7
- **NOTE:** this readme shows doc for the upcoming 2.0 version (`2.0.0.pre` is available on RubyGems).
7
+ > One configuration to rule all data sources
8
+
9
+ Anyway Config is a configuration library for Ruby gems and applications.
10
+
11
+ As a library author, you can benefit from using Anyway Config by providing a better UX for your end-users:
12
+
13
+ - **Zero-code configuration** — no more boilerplate initializers.
14
+ - **Per-environment and local** settings support out-of-the-box.
15
+
16
+ For application developers, Anyway Config could be useful to:
17
+
18
+ - **Keep configuration organized** and use _named configs_ instead of bloated `.env`/`settings.yml`/whatever.
19
+ - **Free code of ENV/credentials/secrets dependency** and use configuration classes instead—your code should not rely on configuration data sources.
20
+
21
+ **NOTE:** this readme shows documentation for the upcoming 2.0 version (`2.0.0.rc1` is available on RubyGems).
8
22
  For version 1.x see [1-4-stable branch](https://github.com/palkan/anyway_config/tree/1-4-stable).
9
23
 
10
- Rails/Ruby plugin/application configuration tool which allows you to load parameters from different sources: YAML, Rails secrets/credentials, environment.
24
+ ## Table of contents
25
+
26
+ - [Main concepts](#main-concepts)
27
+ - [Installation](#installation)
28
+ - [Usage](#usage)
29
+ - [Configuration classes](#configuration-classes)
30
+ - [Dynamic configuration](#dynamic-configuration)
31
+ - [Validation & Callbacks](#validation-and-callbacks)
32
+ - [Using with Rails applications](#using-with-rails)
33
+ - [Data population](#data-population)
34
+ - [Organizing configs](#organizing-configs)
35
+ - [Generators](#generators)
36
+ - [Using with Ruby applications](#using-with-ruby)
37
+ - [Environment variables](#environment-variables)
38
+ - [Local configuration](#local-files)
39
+ - [Data loaders](#data-loaders)
40
+ - [Source tracing](#tracing)
41
+ - [Pattern matching](#pattern-matching)
42
+ - [Test helpers](#test-helpers)
43
+ - [OptionParser integration](#optionparser-integration)
44
+
45
+ ## Main concepts
46
+
47
+ 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):
11
48
 
12
- Allows you to easily follow the [twelve-factor application](https://12factor.net/config) principles and adds zero complexity to your development process.
49
+ ```ruby
50
+ module Influxer
51
+ class Config < Anyway::Config
52
+ attr_config(
53
+ host: "localhost",
54
+ username: "root",
55
+ password: "root"
56
+ )
57
+ end
58
+ end
59
+ ```
13
60
 
14
- Libraries using Anyway Config:
61
+ Using Ruby classes to represent configuration allows you to add helper methods and computed parameters easily, makes the configuration **testable**.
15
62
 
16
- - [Influxer](https://github.com/palkan/influxer)
63
+ 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.
17
64
 
18
- - [AnyCable](https://github.com/anycable/anycable)
65
+ Check out the libraries using Anyway Config for more examples:
19
66
 
67
+ - [Influxer](https://github.com/palkan/influxer)
68
+ - [AnyCable](https://github.com/anycable/anycable)
20
69
  - [Sniffer](https://github.com/aderyabin/sniffer)
21
-
22
70
  - [Blood Contracts](https://github.com/sclinede/blood_contracts)
23
-
24
71
  - [and others](https://github.com/palkan/anyway_config/network/dependents).
25
72
 
26
73
  ## Installation
@@ -31,7 +78,7 @@ Adding to a gem:
31
78
  # my-cool-gem.gemspec
32
79
  Gem::Specification.new do |spec|
33
80
  # ...
34
- spec.add_dependency "anyway_config", "2.0.0.pre"
81
+ spec.add_dependency "anyway_config", "2.0.0.rc1"
35
82
  # ...
36
83
  end
37
84
  ```
@@ -40,23 +87,25 @@ Or adding to your project:
40
87
 
41
88
  ```ruby
42
89
  # Gemfile
43
- gem "anyway_config", "2.0.0.pre"
90
+ gem "anyway_config", "2.0.0.rc1"
44
91
  ```
45
92
 
46
- ## Supported Ruby versions
93
+ ### Supported Ruby versions
47
94
 
48
95
  - Ruby (MRI) >= 2.5.0
49
-
50
- - JRuby >= 9.2.7
96
+ - JRuby >= 9.2.9
51
97
 
52
98
  ## Usage
53
99
 
54
- ### Pre-defined configuration
100
+ ### Configuration classes
101
+
102
+ Using configuration classes allows you to make configuration data a bit more than a bag of values:
103
+ you can define a schema for your configuration, provide defaults, add validations and additional helper methods.
55
104
 
56
- Create configuration class:
105
+ Anyway Config provides a base class to inherit from with a few DSL methods:
57
106
 
58
107
  ```ruby
59
- require "anyway"
108
+ require "anyway_config"
60
109
 
61
110
  module MyCoolGem
62
111
  class Config < Anyway::Config
@@ -65,22 +114,66 @@ module MyCoolGem
65
114
  end
66
115
  ```
67
116
 
68
- `attr_config` creates accessors and default values. If you don't need default values just write:
117
+ Here `attr_config` creates accessors and populates the default values. If you don't need default values you can write:
69
118
 
70
119
  ```ruby
71
120
  attr_config :user, :password, host: "localhost", options: {}
72
121
  ```
73
122
 
74
- Then create an instance of the config class and use it:
123
+ **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.
124
+
125
+ Then, create an instance of the config class and use it:
75
126
 
76
127
  ```ruby
77
- module MyCoolGem
78
- def self.config
79
- @config ||= Config.new
128
+ MyCoolGem::Config.new.user #=> "root"
129
+ ```
130
+
131
+ **Bonus:**: if you define attributes with boolean default values (`false` or `true`), Anyway Config would automatically add a corresponding predicate method. For example:
132
+
133
+ ```ruby
134
+ attr_config :user, :password, debug: false
135
+
136
+ MyCoolGem::Config.new.debug? #=> false
137
+ MyCoolGem::Config.new(debug: true).debug? #=> true
138
+ ```
139
+
140
+ **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:
141
+
142
+ ```ruby
143
+ class MyConfig < Anyway::Config
144
+ attr_config :host, :port, :url, :meta
145
+
146
+ # override writer to handle type coercion
147
+ def meta=(val)
148
+ super JSON.parse(val)
149
+ end
150
+
151
+ # or override reader to handle missing values
152
+ def url
153
+ super || (self.url = "#{host}:#{port}")
154
+ end
155
+
156
+ # untill v2.1, it will still be possible to read instance variables,
157
+ # i.e. the following code would also work
158
+ def url
159
+ @url ||= "#{host}:#{port}"
80
160
  end
81
161
  end
162
+ ```
163
+
164
+ We recommend to add a feature check and support both v1.x and v2.0 in gems for the time being:
82
165
 
83
- MyCoolGem.config.user #=> "root"
166
+ ```ruby
167
+ # Check for the class method added in 2.0, e.g., `.on_load`
168
+ if respond_to?(:on_load)
169
+ def url
170
+ super || (self.url = "#{host}:#{port}")
171
+ end
172
+ else
173
+ def url
174
+ @url ||= "#{host}:#{port}"
175
+ end
176
+ end
84
177
  ```
85
178
 
86
179
  #### Config name
@@ -92,9 +185,9 @@ By default, Anyway Config uses the config class name to infer the config name us
92
185
  - if the class name has a form of `<Module>::Config` then use the module name (`SomeModule::Config => "somemodule"`)
93
186
  - if the class name has a form of `<Something>Config` then use the class name prefix (`SomeConfig => "some"`)
94
187
 
95
- **NOTE:** in both cases the config name is a **downcased** module/class prefix, not underscored.
188
+ **NOTE:** in both cases, the config name is a **downcased** module/class prefix, not underscored.
96
189
 
97
- You can also specify the config name explicitly (it's required in cases when you class name doesn't match any of the patterns above):
190
+ 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):
98
191
 
99
192
  ```ruby
100
193
  module MyCoolGem
@@ -122,7 +215,7 @@ module MyCoolGem
122
215
  end
123
216
  ```
124
217
 
125
- #### Provide explicit values
218
+ #### Explicit values
126
219
 
127
220
  Sometimes it's useful to set some parameters explicitly during config initialization.
128
221
  You can do that by passing a Hash into `.new` method:
@@ -133,17 +226,25 @@ config = MyCoolGem::Config.new(
133
226
  password: "rubyisnotdead"
134
227
  )
135
228
 
136
- # The value would not be overriden from other sources (such as YML file, env)
229
+ # The value would not be overridden from other sources (such as YML file, env)
137
230
  config.user == "john"
138
231
  ```
139
232
 
233
+ #### Reload configuration
234
+
235
+ There are `#clear` and `#reload` methods that do exactly what they state.
236
+
237
+ **NOTE**: `#reload` also accepts an optional Hash for [explicit values](#explicit-values).
238
+
140
239
  ### Dynamic configuration
141
240
 
142
- You can also create configuration objects without pre-defined schema (just like `Rails.application.config_for` but more [powerful](#railsapplicationconfig_for-vs-anywayconfigfor)):
241
+ You can also fetch configuration without pre-defined schema:
143
242
 
144
243
  ```ruby
145
- # load data from config/my_app.yml, secrets.my_app (if using Rails), ENV["MY_APP_*"]
146
- # MY_APP_VALUE=42
244
+ # load data from config/my_app.yml,
245
+ # credentials.my_app, secrets.my_app (if using Rails), ENV["MY_APP_*"]
246
+ #
247
+ # Given MY_APP_VALUE=42
147
248
  config = Anyway::Config.for(:my_app)
148
249
  config["value"] #=> 42
149
250
 
@@ -151,11 +252,75 @@ config["value"] #=> 42
151
252
  config = Anyway::Config.for(:my_app, config_path: "my_config.yml", env_prefix: "MYAPP")
152
253
  ```
153
254
 
154
- ### Using with Rails
255
+ This feature is similar to `Rails.application.config_for` but more powerful:
256
+
257
+ | Feature | Rails | Anyway Config |
258
+ | ------------- |-------------:| -----:|
259
+ | Load data from `config/app.yml` | ✅ | ✅ |
260
+ | Load data from `secrets` | ❌ | ✅ |
261
+ | Load data from `credentials` | ❌ | ✅ |
262
+ | Load data from environment | ❌ | ✅ |
263
+ | Load data from [custom sources](#data-loaders) | ❌ | ✅ |
264
+ | Local config files | ❌ | ✅ |
265
+ | [Source tracing](#tracing) | ❌ | ✅ |
266
+ | Return Hash with indifferent access | ❌ | ✅ |
267
+ | Support ERB\* within `config/app.yml` | ✅ | ✅ |
268
+ | Raise if file doesn't exist | ✅ | ❌ |
269
+ | Works without Rails | 😀 | ✅ |
270
+
271
+ \* Make sure that ERB is loaded
272
+
273
+ ### Validation and callbacks
274
+
275
+ Anyway Config provides basic ways of ensuring that the configuration is valid.
276
+
277
+ There is a built-in `required` class method to define the list of parameters that must be present in the
278
+ configuration after loading (where present means non-`nil` and non-empty for strings):
279
+
280
+ ```ruby
281
+ class MyConfig < Anyway::Config
282
+ attr_config :api_key, :api_secret, :debug
283
+
284
+ required :api_key, :api_secret
285
+ end
286
+
287
+ MyConfig.new(api_secret: "") #=> raises Anyway::Config::ValidationError
288
+ ```
289
+
290
+ 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:
291
+
292
+ ```ruby
293
+ class MyConfig < Anyway::Config
294
+ attr_config :api_key, :api_secret, :mode
295
+
296
+ # on_load macro accepts symbol method names
297
+ on_load :ensure_mode_is_valid
298
+
299
+ # or block
300
+ on_load do
301
+ # the block is evaluated in the context of the config
302
+ raise_validation_error("API key and/or secret could be blank") if
303
+ api_key.blank? || api_secret.blank?
304
+ end
305
+
306
+ def ensure_mode_is_valid
307
+ unless %w[production test].include?(mode)
308
+ raise_validation_error "Unknown mode; #{mode}"
309
+ end
310
+ end
311
+ end
312
+ ```
313
+
314
+ ## Using with Rails
155
315
 
156
316
  **NOTE:** version 2.x supports Rails >= 5.0; for Rails 4.x use version 1.x of the gem.
157
317
 
158
- Your config will be filled up with values from the following sources (ordered by priority from low to high):
318
+ We recommend going through [Data population](#data-population) and [Organizing configs](#organizing-configs) sections first,
319
+ and then use [Rails generators](#generators) to make your application Anyway Config-ready.
320
+
321
+ ### Data population
322
+
323
+ Your config is filled up with values from the following sources (ordered by priority from low to high):
159
324
 
160
325
  - `RAILS_ROOT/config/my_cool_gem.yml` (for the current `RAILS_ENV`, supports `ERB`):
161
326
 
@@ -180,7 +345,7 @@ development:
180
345
  port: 4444
181
346
  ```
182
347
 
183
- - `Rails.application.credentials` (if supported):
348
+ - `Rails.application.credentials.my_cool_gem` (if supported):
184
349
 
185
350
  ```yml
186
351
  my_cool_gem:
@@ -191,14 +356,18 @@ my_cool_gem:
191
356
 
192
357
  - `ENV['MYCOOLGEM_*']`.
193
358
 
194
- #### `app/configs`
359
+ See [environment variables](#environment-variables).
360
+
361
+ ### Organizing configs
362
+
363
+ You can store application-level config classes in `app/configs` folder just like any other Rails entities.
195
364
 
196
- You can store application-level config classes in `app/configs` folder.
365
+ However, in that case you won't be able to use them during the application initialization (i.e., in `config/**/*.rb` files).
197
366
 
198
- Anyway Config automatically adds this folder to Rails autoloading system to make it possible to
199
- autoload configs even during the configuration phase.
367
+ Since that's a pretty common scenario, we provide a way to do that via a custom autoloader for `config/configs` folder.
368
+ That means, that you can put your configuration classes into `config/configs` folder, use them anywhere in your code without explicitly requiring them.
200
369
 
201
- Consider an example: setting the Action Mailer host name for Heroku review apps.
370
+ Consider an example: setting the Action Mailer hostname for Heroku review apps.
202
371
 
203
372
  We have the following config to fetch the Heroku provided [metadata](https://devcenter.heroku.com/articles/dyno-metadata):
204
373
 
@@ -221,32 +390,108 @@ Then in `config/application.rb` you can do the following:
221
390
  config.action_mailer.default_url_options = {host: HerokuConfig.new.hostname}
222
391
  ```
223
392
 
224
- ### Using with Ruby
393
+ You can configure the configs folder path:
394
+
395
+ ```ruby
396
+ # The path must be relative to Rails root
397
+ config.anyway_config.autoload_static_config_path = "path/to/configs"
398
+ ```
399
+
400
+ **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`.
401
+ Or you can store everything in `app/configs` by setting `config.anyway_config.autoload_static_config_path = "app/configs"`.
402
+
403
+ ### Generators
404
+
405
+ Anyway Config provides Rails generators to create new config classes:
406
+
407
+ - `rails g anyway:install`—creates an `ApplicationConfig` class (the base class for all config classes) and updates `.gitignore`
408
+
409
+ You can specify the static configs path via the `--configs-path` option:
410
+
411
+ ```sh
412
+ rails g anyway:install --configs-path=config/settings
413
+
414
+ # or to keep everything in app/configs
415
+ rails g anyway:install --configs-path=app/configs
416
+ ```
417
+
418
+ - `rails g anyway:config <name> param1 param2 ...`—creates a named configuration class and optionally the corresponding YAML file; creates `application_config.rb` is missing.
419
+
420
+ The generator command for the Heroku example above would be:
421
+
422
+ ```sh
423
+ $ rails g anyway:config heroku app_id app_name dyno_id release_version slug_commit
424
+
425
+ generate anyway:install
426
+ rails generate anyway:install
427
+ create config/configs/application_config.rb
428
+ append .gitignore
429
+ create config/configs/heroku_config.rb
430
+ Would you like to generate a heroku.yml file? (Y/n) n
431
+ ```
432
+
433
+ You can also specify the `--app` option to put the newly created class into `app/configs` folder.
434
+ Alternatively, you can call `rails g anyway:app_config name param1 param2 ...`.
225
435
 
226
- When you're using Anyway Config in non-Rails environment, we're looking for a YAML config file
227
- at `./config/<config-name>.yml`.
436
+ ## Using with Ruby
228
437
 
229
- You can override this setting through special environment variable – 'MYCOOLGEM_CONF' containing the path to the YAML file.
438
+ The default data loading mechanism for non-Rails applications is the following (ordered by priority from low to high):
230
439
 
231
- **NOTE:** in pure Ruby apps we have no knowledge of _environments_ (`test`, `development`, `production`, etc.); thus we assume that the YAML contains values for a single environment:
440
+ - `./config/<config-name>.yml` (`ERB` is supported if `erb` is loaded)
441
+
442
+ In pure Ruby apps, we do not know about _environments_ (`test`, `development`, `production`, etc.); thus, we assume that the YAML contains values for a single environment:
232
443
 
233
444
  ```yml
234
445
  host: localhost
235
446
  port: 3000
236
447
  ```
237
448
 
238
- Environmental variables work the same way as with Rails.
449
+ **NOTE:** you can override the default YML lookup path by setting `MYCOOLGEM_CONF` env variable.
450
+
451
+ - `ENV['MYCOOLGEM_*']`.
452
+
453
+ See [environment variables](#environment-variables).
454
+
455
+ ## Environment variables
456
+
457
+ Environmental variables for your config should start with your config name, upper-cased.
458
+
459
+ For example, if your config name is "mycoolgem", then the env var "MYCOOLGEM_PASSWORD" is used as `config.password`.
460
+
461
+ Environment variables are automatically type cast:
462
+
463
+ - `"True"`, `"t"` and `"yes"` to `true`;
464
+ - `"False"`, `"f"` and `"no"` to `false`;
465
+ - `"nil"` and `"null"` to `nil` (do you really need it?);
466
+ - `"123"` to 123 and `"3.14"` to 3.14.
467
+
468
+ *Anyway Config* supports nested (_hashed_) env variables—just separate keys with double-underscore.
239
469
 
240
- ### Local files
470
+ For example, "MYCOOLGEM_OPTIONS__VERBOSE" is parsed as `config.options["verbose"]`.
241
471
 
242
- It's useful to have personal, user-specific configuration in development, which extends the project-wide one.
472
+ Array values are also supported:
473
+
474
+ ```ruby
475
+ # Suppose ENV["MYCOOLGEM_IDS"] = '1,2,3'
476
+ config.ids #=> [1,2,3]
477
+ ```
478
+
479
+ If you want to provide a text-like env variable which contains commas then wrap it into quotes:
480
+
481
+ ```ruby
482
+ MYCOOLGEM = "Nif-Nif, Naf-Naf and Nouf-Nouf"
483
+ ```
484
+
485
+ ## Local files
486
+
487
+ It's useful to have a personal, user-specific configuration in development, which extends the project-wide one.
243
488
 
244
489
  We support this by looking at _local_ files when loading the configuration data:
245
490
 
246
491
  - `<config_name>.local.yml` files (next to\* the _global_ `<config_name>.yml`)
247
492
  - `config/credentials/local.yml.enc` (for Rails >= 6, generate it via `rails credentials:edit --environment local`).
248
493
 
249
- \* If the YAML config path is not default (i.e. set via `<CONFIG_NAME>_CONF`), we lookup the local
494
+ \* If the YAML config path is not a default one (i.e., set via `<CONFIG_NAME>_CONF`), we look up the local
250
495
  config at this location, too.
251
496
 
252
497
  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"`).
@@ -255,110 +500,128 @@ Local configs are meant for using in development and only loaded if `Anyway::Set
255
500
 
256
501
  Don't forget to add `*.local.yml` (and `config/credentials/local.*`) to your `.gitignore`.
257
502
 
258
- **NOTE:** local YAML configs for Rails app must be environment-free (i.e. you shouldn't have top-level `development:` key).
259
-
260
- ### Reload configuration
503
+ **NOTE:** local YAML configs for a Rails app must be environment-free (i.e., you shouldn't have top-level `development:` key).
261
504
 
262
- There are `#clear` and `#reload` methods which do exactly what they state.
505
+ ## Data loaders
263
506
 
264
- Note: `#reload` also accepts `overrides` key to provide explicit values (see above).
265
-
266
- ### OptionParser integration
267
-
268
- It's possible to use config as option parser (e.g. for CLI apps/libraries). It uses
269
- [`optparse`](https://ruby-doc.org/stdlib-2.5.1/libdoc/optparse/rdoc/OptionParser.html) under the hood.
270
-
271
- Example usage:
507
+ You can provide your own data loaders or change the existing ones using the Loaders API (which is very similar to Rack middleware builder):
272
508
 
273
509
  ```ruby
274
- class MyConfig < Anyway::Config
275
- attr_config :host, :log_level, :concurrency, :debug, server_args: {}
510
+ # remove env loader => do not load params from ENV
511
+ Anyway.loaders.delete :env
276
512
 
277
- # specify which options shouldn't be handled by option parser
278
- ignore_options :server_args
279
-
280
- # provide description for options
281
- describe_options(
282
- concurrency: "number of threads to use"
283
- )
513
+ # add custom loader before :env (it's better to keep the ENV loader the last one)
514
+ Anyway.loaders.insert_before :env, :my_loader, MyLoader
515
+ ```
284
516
 
285
- # mark some options as flag
286
- flag_options :debug
517
+ Loader is a _callable_ Ruby object (module/class responding to `.call` or lambda/proc), which `call` method
518
+ accepts the following keyword arguments:
287
519
 
288
- # extend an option parser object (i.e. add banner or version/help handlers)
289
- extend_options do |parser, config|
290
- parser.banner = "mycli [options]"
520
+ ```ruby
521
+ def call(
522
+ name:, # config name
523
+ env_prefix:, # prefix for env vars if any
524
+ config_path:, # path to YML config
525
+ local: # true|false, whether to load local configuration
526
+ )
527
+ #=> must return Hash with configuration data
528
+ end
529
+ ```
291
530
 
292
- parser.on("--server-args VALUE") do |value|
293
- config.server_args = JSON.parse(value)
294
- end
531
+ You can use `Anyway::Loaders::Base` as a base class for your loader and define a `#call` method.
532
+ For example, the [Chamber](https://github.com/thekompanee/chamber) loader could be written as follows:
295
533
 
296
- parser.on_tail "-h", "--help" do
297
- puts parser
298
- end
534
+ ```ruby
535
+ class ChamberConfigLoader < Anyway::Loaders::Base
536
+ def call(name:, **_opts)
537
+ Chamber.env.to_h[name] || {}
299
538
  end
300
539
  end
540
+ ```
301
541
 
302
- config = MyConfig.new
303
-
304
- config.parse_options!(%w[--host localhost --port 3333 --log-level debug])
305
-
306
- config.host # => "localhost"
307
- config.port # => 3333
308
- config.log_level # => "debug"
542
+ In order to support [source tracing](#tracing), you need to wrap the resulting Hash via the `#trace!` method with metadata:
309
543
 
310
- # Get the instance of OptionParser
311
- config.option_parser
544
+ ```ruby
545
+ def call(name:, **_opts)
546
+ trace!(source: :chamber) do
547
+ Chamber.env.to_h[name] || {}
548
+ end
549
+ end
312
550
  ```
313
551
 
314
- ## `Rails.application.config_for` vs `Anyway::Config.for`
315
-
316
- Rails 4.2 introduced new feature: `Rails.application.config_for`. It looks very similar to
317
- `Anyway::Config.for`, but there are some differences:
552
+ ## Tracing
318
553
 
319
- | Feature | Rails | Anyway Config |
320
- | ------------- |-------------:| -----:|
321
- | load data from `config/app.yml` | yes | yes |
322
- | load data from `secrets` | no | yes |
323
- | load data from `credentials` | no | yes |
324
- | load data from environment | no | yes |
325
- | local config files | no | yes |
326
- | return Hash with indifferent access | no | yes |
327
- | support ERB within `config/app.yml` | yes | yes* |
328
- | raise errors if file doesn't exist | yes | no |
554
+ Since Anyway Config loads data from multiple source, it could be useful to know where a particular value came from.
329
555
 
330
- <sub><sup>*</sup>make sure that ERB is loaded</sub>
556
+ Each `Anyway::Config` instance contains _tracing information_ which you can access via `#to_source_trace` method:
331
557
 
332
- But the main advantage of Anyway::Config is that it can be used [without Rails](#using-with-ruby)!)
558
+ ```ruby
559
+ conf = ExampleConfig.new
560
+ conf.to_source_trace
561
+
562
+ # returns the following hash
563
+ {
564
+ "host" => {value: "test.host", source: {type: :yml, path: "config/example.yml"}},
565
+ "user" => {
566
+ "name" => {value: "john", source: {type: :env, key: "EXAMPLE_USER__NAME"}},
567
+ "password" => {value: "root", source: {type: :credentials, store: "config/credentials/production.enc.yml"}}
568
+ },
569
+ "port" => {value: 9292, source: {type: :defaults}}
570
+ }
571
+
572
+ # if you change the value manually in your code,
573
+ # that would be reflected in the trace
574
+
575
+ conf.host = "anyway.host"
576
+ conf.to_source_trace["host"]
577
+ #=> {type: :user, called_from: "/path/to/caller.rb:15"}
578
+ ```
333
579
 
334
- ## How to set env vars
580
+ You can disable tracing functionality by setting `Anyway::Settings.tracing_enabled = false` or `config.anyway_config.tracing_enabled = false` in Rails.
335
581
 
336
- Environmental variables for your config should start with your config name, upper-cased.
582
+ ### Pretty print
337
583
 
338
- For example, if your config name is "mycoolgem" then the env var "MYCOOLGEM_PASSWORD" is used as `config.password`.
584
+ You can use `pp` to print a formatted information about the config including the sources trace.
339
585
 
340
- Environment variables are automatically serialized:
586
+ Example:
341
587
 
342
- - `"True"`, `"t"` and `"yes"` to `true`;
343
- - `"False"`, `"f"` and `"no"` to `false`;
344
- - `"nil"` and `"null"` to `nil` (do you really need it?);
345
- - `"123"` to 123 and `"3.14"` to 3.14.
588
+ ```ruby
589
+ pp CoolConfig.new
590
+
591
+ # #<CoolConfig
592
+ # config_name="cool"
593
+ # env_prefix="COOL"
594
+ # values:
595
+ # port => 3334 (type=load),
596
+ # host => "test.host" (type=yml path=./config/cool.yml),
597
+ # user =>
598
+ # name => "john" (type=env key=COOL_USER__NAME),
599
+ # password => "root" (type=yml path=./config/cool.yml)>
600
+ ```
346
601
 
347
- *Anyway Config* supports nested (_hashed_) env variables. Just separate keys with double-underscore.
602
+ ## Pattern matching
348
603
 
349
- For example, "MYCOOLGEM_OPTIONS__VERBOSE" is parsed as `config.options["verbose"]`.
350
-
351
- Array values are also supported:
604
+ You can use config instances in Ruby 2.7+ pattern matching:
352
605
 
353
606
  ```ruby
354
- # Suppose ENV["MYCOOLGEM_IDS"] = '1,2,3'
355
- config.ids #=> [1,2,3]
607
+ case AWSConfig.new
608
+ in bucket:, region: "eu-west-1"
609
+ setup_eu_storage(bucket)
610
+ in bucket:, region: "us-east-1"
611
+ setup_us_storage(bucket)
612
+ end
356
613
  ```
357
614
 
358
- If you want to provide a text-like env variable which contains commas then wrap it into quotes:
615
+ If the attribute wasn't populated, the key won't be returned for pattern matching, i.e. you can do something line:
359
616
 
360
617
  ```ruby
361
- MYCOOLGEM = "Nif-Nif, Naf-Naf and Nouf-Nouf"
618
+ aws_configured =
619
+ case AWSConfig.new
620
+ in access_key_id:, secret_access_key:
621
+ true
622
+ else
623
+ false
624
+ end
362
625
  ```
363
626
 
364
627
  ## Test helpers
@@ -398,6 +661,68 @@ This helper is automatically included to RSpec if `RAILS_ENV` or `RACK_ENV` env
398
661
 
399
662
  You can add it manually by requiring `"anyway/testing/helpers"` and including the `Anyway::Test::Helpers` module (into RSpec configuration or Minitest test class).
400
663
 
664
+ ## OptionParser integration
665
+
666
+ It's possible to use config as option parser (e.g., for CLI apps/libraries). It uses
667
+ [`optparse`](https://ruby-doc.org/stdlib-2.5.1/libdoc/optparse/rdoc/OptionParser.html) under the hood.
668
+
669
+ Example usage:
670
+
671
+ ```ruby
672
+ class MyConfig < Anyway::Config
673
+ attr_config :host, :log_level, :concurrency, :debug, server_args: {}
674
+
675
+ # specify which options shouldn't be handled by option parser
676
+ ignore_options :server_args
677
+
678
+ # provide description for options
679
+ describe_options(
680
+ concurrency: "number of threads to use"
681
+ )
682
+
683
+ # mark some options as flag
684
+ flag_options :debug
685
+
686
+ # extend an option parser object (i.e. add banner or version/help handlers)
687
+ extend_options do |parser, config|
688
+ parser.banner = "mycli [options]"
689
+
690
+ parser.on("--server-args VALUE") do |value|
691
+ config.server_args = JSON.parse(value)
692
+ end
693
+
694
+ parser.on_tail "-h", "--help" do
695
+ puts parser
696
+ end
697
+ end
698
+ end
699
+
700
+ config = MyConfig.new
701
+
702
+ config.parse_options!(%w[--host localhost --port 3333 --log-level debug])
703
+
704
+ config.host # => "localhost"
705
+ config.port # => 3333
706
+ config.log_level # => "debug"
707
+
708
+ # Get the instance of OptionParser
709
+ config.option_parser
710
+ ```
711
+
712
+ **NOTE:** values are automatically type cast using the same rules as for [environment variables](#environment-variables).
713
+ If you want to specify the type explicitly, you can do that using `describe_options`:
714
+
715
+ ```ruby
716
+ describe_options(
717
+ # In this case, you should specify a hash with `type`
718
+ # and (optionally) `desc` keys
719
+ concurrency: {
720
+ desc: "number of threads to use",
721
+ type: String
722
+ }
723
+ )
724
+ ```
725
+
401
726
  ## Contributing
402
727
 
403
728
  Bug reports and pull requests are welcome on GitHub at [https://github.com/palkan/anyway_config](https://github.com/palkan/anyway_config).