README.md in anyway_config-2.1.0 vs README.md in anyway_config-2.2.0
- old
+ new
@@ -40,16 +40,18 @@
- [Data population](#data-population)
- [Organizing configs](#organizing-configs)
- [Generators](#generators)
- [Using with Ruby applications](#using-with-ruby)
- [Environment variables](#environment-variables)
+- [Type coercion](#type-coercion)
- [Local configuration](#local-files)
- [Data loaders](#data-loaders)
- [Source tracing](#tracing)
- [Pattern matching](#pattern-matching)
- [Test helpers](#test-helpers)
- [OptionParser integration](#optionparser-integration)
+- [RBS support](#rbs-support)
## Main concepts
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):
@@ -92,11 +94,11 @@
Or adding to your project:
```ruby
# Gemfile
-gem "anyway_config", "~> 2.0.0"
+gem "anyway_config", "~> 2.0"
```
### Supported Ruby versions
- Ruby (MRI) >= 2.5.0
@@ -267,10 +269,11 @@
| Load data from `secrets` | ❌ | ✅ |
| Load data from `credentials` | ❌ | ✅ |
| Load data from environment | ❌ | ✅ |
| Load data from [custom sources](#data-loaders) | ❌ | ✅ |
| Local config files | ❌ | ✅ |
+| Type coercion | ❌ | ✅ |
| [Source tracing](#tracing) | ❌ | ✅ |
| Return Hash with indifferent access | ❌ | ✅ |
| Support ERB\* within `config/app.yml` | ✅ | ✅ |
| Raise if file doesn't exist | ✅ | ❌ |
| Works without Rails | 😀 | ✅ |
@@ -329,11 +332,11 @@
Your config is filled up with values from the following sources (ordered by priority from low to high):
1) **YAML configuration files**: `RAILS_ROOT/config/my_cool_gem.yml`.
-Recognizes Rails environment, supports `ERB`:
+Rails environment is used as the namespace (required); supports `ERB`:
```yml
test:
host: localhost
port: 3002
@@ -449,10 +452,26 @@
```
**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`.
Or you can store everything in `app/configs` by setting `config.anyway_config.autoload_static_config_path = "app/configs"`.
+**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:
+
+```ruby
+# config/application.rb
+
+# ...
+
+require_relative "initializers/inflections"
+
+module SomeApp
+ class Application < Rails::Application
+ # ...
+ end
+end
+```
+
### Generators
Anyway Config provides Rails generators to create new config classes:
- `rails g anyway:install`—creates an `ApplicationConfig` class (the base class for all config classes) and updates `.gitignore`
@@ -523,17 +542,19 @@
Environmental variables for your config should start with your config name, upper-cased.
For example, if your config name is "mycoolgem", then the env var "MYCOOLGEM_PASSWORD" is used as `config.password`.
-Environment variables are automatically type cast:
+By default, environment variables are automatically type cast\*:
- `"True"`, `"t"` and `"yes"` to `true`;
- `"False"`, `"f"` and `"no"` to `false`;
- `"nil"` and `"null"` to `nil` (do you really need it?);
- `"123"` to 123 and `"3.14"` to 3.14.
+\* See below for coercion customization.
+
*Anyway Config* supports nested (_hashed_) env variables—just separate keys with double-underscore.
For example, "MYCOOLGEM_OPTIONS__VERBOSE" is parsed as `config.options["verbose"]`.
Array values are also supported:
@@ -547,10 +568,91 @@
```ruby
MYCOOLGEM = "Nif-Nif, Naf-Naf and Nouf-Nouf"
```
+## Type coercion
+
+> 🆕 v2.2.0
+
+You can define custom type coercion rules to convert string data to config values. To do that, use `.coerce_types` method:
+
+```ruby
+class CoolConfig < Anyway::Config
+ config_name :cool
+ attr_config port: 8080,
+ host: "localhost",
+ user: {name: "admin", password: "admin"}
+
+ coerce_types port: :string, user: {dob: :date}
+end
+
+ENV["COOL_USER__DOB"] = "1989-07-01"
+
+config = CoolConfig.new
+config.port == "8080" # Even though we defined the default value as int, it's converted into a string
+config.user["dob"] == Date.new(1989, 7, 1) #=> true
+```
+
+Type coercion is especially useful to deal with array values:
+
+```ruby
+# To define an array type, provide a hash with two keys:
+# - type — elements type
+# - array: true — mark the parameter as array
+coerce_types list: {type: :string, array: true}
+```
+
+It's also could be useful to explicitly define non-array types (to avoid confusion):
+
+```ruby
+coerce_types non_list: :string
+```
+
+Finally, it's possible to disable auto-casting for a particular config completely:
+
+```ruby
+class CoolConfig < Anyway::Config
+ attr_config port: 8080,
+ host: "localhost",
+ user: {name: "admin", password: "admin"}
+
+ disable_auto_cast!
+end
+
+ENV["COOL_PORT"] = "443"
+
+CoolConfig.new.port == "443" #=> true
+```
+
+**IMPORTANT**: Values provided explicitly (via attribute writers) are not coerced. Coercion is only happening during the load phase.
+
+The following types are supported out-of-the-box: `:string`, `:integer`, `:float`, `:date`, `:datetime`, `:uri`, `:boolean`.
+
+You can use custom deserializers by passing a callable object instead of a type name:
+
+```ruby
+COLOR_TO_HEX = lambda do |raw|
+ case raw
+ when "red"
+ "#ff0000"
+ when "green"
+ "#00ff00"
+ when "blue"
+ "#0000ff"
+ end
+end
+
+class CoolConfig < Anyway::Config
+ attr_config :color
+
+ coerce_types color: COLOR_TO_HEX
+end
+
+CoolConfig.new({color: "red"}).color #=> "#ff0000"
+```
+
## Local files
It's useful to have a personal, user-specific configuration in development, which extends the project-wide one.
We support this by looking at _local_ files when loading the configuration data:
@@ -608,11 +710,11 @@
In order to support [source tracing](#tracing), you need to wrap the resulting Hash via the `#trace!` method with metadata:
```ruby
def call(name:, **_opts)
- trace!(source: :chamber) do
+ trace!(:chamber) do
Chamber.env.to_h[name] || {}
end
end
```
@@ -787,9 +889,65 @@
desc: "number of threads to use",
type: String
}
)
```
+
+## RBS support
+
+Anyway Config comes with Ruby type signatures (RBS).
+
+To use them with Steep, add `library "anyway_config"` to your Steepfile.
+
+We also provide an API to generate a type signature for your config class:
+
+```ruby
+class MyGem::Config < Anyway::Config
+ attr_config :host, port: 8080, tags: [], debug: false
+
+ coerce_types host: :string, port: :integer,
+ tags: {type: :string, array: true}
+
+ required :host
+end
+```
+
+Then calling `MyGem::Config.to_rbs` will return the following signature:
+
+```rbs
+module MyGem
+ interface _Config
+ def host: () -> String
+ def host=: (String) -> void
+ def port: () -> String?
+ def port=: (String) -> void
+ def tags: () -> Array[String]?
+ def tags=: (Array[String]) -> void
+ def debug: () -> bool
+ def debug?: () -> bool
+ def debug=: (bool) -> void
+ end
+
+ class Config < Anyway::Config
+ include _Config
+ end
+end
+```
+
+### Handling `on_load`
+
+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:
+
+```ruby
+class MyConfig < Anyway::Config
+ on_load do
+ # @type self : MyConfig
+ raise_validation_error("host is invalid") if host.start_with?("localhost")
+ end
+end
+```
+
+Yeah, a lot of annotations 😞 Welcome to the type-safe world!
## Contributing
Bug reports and pull requests are welcome on GitHub at [https://github.com/palkan/anyway_config](https://github.com/palkan/anyway_config).