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