{"id":13879534,"url":"https://github.com/palkan/anyway_config","last_synced_at":"2025-05-13T19:05:40.841Z","repository":{"id":26082103,"uuid":"29526026","full_name":"palkan/anyway_config","owner":"palkan","description":"Configuration library for Ruby gems and applications","archived":false,"fork":false,"pushed_at":"2025-04-01T17:08:57.000Z","size":576,"stargazers_count":825,"open_issues_count":0,"forks_count":55,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-04-25T18:19:51.961Z","etag":null,"topics":["configuration","hacktoberfest","rails","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/palkan.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"palkan"}},"created_at":"2015-01-20T11:13:03.000Z","updated_at":"2025-04-17T17:22:26.000Z","dependencies_parsed_at":"2025-03-12T03:00:31.578Z","dependency_job_id":"bcf3374a-ab9a-4787-8ce4-f3e433109c60","html_url":"https://github.com/palkan/anyway_config","commit_stats":{"total_commits":347,"total_committers":29,"mean_commits":11.96551724137931,"dds":0.3717579250720461,"last_synced_commit":"8f4e3f5fb8e0320a7c9300bbb402005781b89292"},"previous_names":[],"tags_count":49,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fanyway_config","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fanyway_config/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fanyway_config/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/palkan%2Fanyway_config/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/palkan","download_url":"https://codeload.github.com/palkan/anyway_config/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250869707,"owners_count":21500370,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["configuration","hacktoberfest","rails","ruby"],"created_at":"2024-08-06T08:02:24.202Z","updated_at":"2025-04-25T18:20:18.096Z","avatar_url":"https://github.com/palkan.png","language":"Ruby","funding_links":["https://github.com/sponsors/palkan"],"categories":["Ruby","Configuration"],"sub_categories":[],"readme":"[![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](https://cultofmartians.com/tasks/anyway-config-options-parse.html#task)\n[![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)\n[![JRuby Build](https://github.com/palkan/anyway_config/workflows/JRuby%20Build/badge.svg)](https://github.com/palkan/anyway_config/actions)\n[![TruffleRuby Build](https://github.com/palkan/anyway_config/workflows/TruffleRuby%20Build/badge.svg)](https://github.com/palkan/anyway_config/actions)\n\n# Anyway Config\n\n\u003e One configuration to rule all data sources\n\nAnyway Config is a configuration library for Ruby gems and applications.\n\nAs a library author, you can benefit from using Anyway Config by providing a better UX for your end-users:\n\n- **Zero-code configuration** — no more boilerplate initializers.\n- **Per-environment and local** settings support out-of-the-box.\n\nFor application developers, Anyway Config could be useful to:\n\n- **Keep configuration organized** and use _named configs_ instead of bloated `.env`/`settings.yml`/whatever.\n- **Free code of ENV/credentials/secrets dependency** and use configuration classes instead—your code should not rely on configuration data sources.\n\n**NOTE:** this readme shows documentation for 2.x version.\nFor version 1.x see the [1-4-stable branch](https://github.com/palkan/anyway_config/tree/1-4-stable).\n\n\u003ca href=\"https://evilmartians.com/?utm_source=anyway_config\"\u003e\n\u003cimg src=\"https://evilmartians.com/badges/sponsored-by-evil-martians.svg\" alt=\"Sponsored by Evil Martians\" width=\"236\" height=\"54\"\u003e\u003c/a\u003e\n\n## Links\n\n- [Anyway Config: Keep your Ruby configuration sane](https://evilmartians.com/chronicles/anyway-config-keep-your-ruby-configuration-sane?utm_source=anyway_config)\n\n## Table of contents\n\n- [Main concepts](#main-concepts)\n- [Installation](#installation)\n- [Usage](#usage)\n  - [Configuration classes](#configuration-classes)\n  - [Dynamic configuration](#dynamic-configuration)\n  - [Validation \u0026 Callbacks](#validation-and-callbacks)\n- [Using with Rails applications](#using-with-rails)\n  - [Data population](#data-population)\n  - [Organizing configs](#organizing-configs)\n  - [Generators](#generators)\n- [Using with Ruby applications](#using-with-ruby)\n- [Environment variables](#environment-variables)\n- [Type coercion](#type-coercion)\n- [Local configuration](#local-files)\n- [Data loaders](#data-loaders)\n  - [Doppler integration](#doppler-integration)\n  - [EJSON support](#ejson-support)\n  - [Custom loaders](#custom-loaders)\n- [Source tracing](#tracing)\n- [Pattern matching](#pattern-matching)\n- [Test helpers](#test-helpers)\n- [OptionParser integration](#optionparser-integration)\n- [RBS support](#rbs-support)\n\n## Main concepts\n\nAnyway 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):\n\n```ruby\nmodule Influxer\n  class Config \u003c Anyway::Config\n    attr_config(\n      host: \"localhost\",\n      username: \"root\",\n      password: \"root\"\n    )\n  end\nend\n```\n\nUsing Ruby classes to represent configuration allows you to add helper methods and computed parameters easily, makes the configuration **testable**.\n\nThe `anyway_config` gem takes care of loading parameters from **different sources** (YAML, credentials, environment variables, etc.). Internally, we use a _pipeline pattern_ and provide the [Loaders API](#data-loaders) to manage and [extend](#custom-loaders) its functionality.\n\nCheck out the libraries using Anyway Config for more examples:\n\n- [Influxer](https://github.com/palkan/influxer)\n- [AnyCable](https://github.com/anycable/anycable)\n- [Sniffer](https://github.com/aderyabin/sniffer)\n- [Blood Contracts](https://github.com/sclinede/blood_contracts)\n- [and others](https://github.com/palkan/anyway_config/network/dependents).\n\n## Installation\n\nAdding to a gem:\n\n```ruby\n# my-cool-gem.gemspec\nGem::Specification.new do |spec|\n  # ...\n  spec.add_dependency \"anyway_config\", \"\u003e= 2.0.0\"\n  # ...\nend\n```\n\nOr adding to your project:\n\n```ruby\n# Gemfile\ngem \"anyway_config\", \"~\u003e 2.0\"\n```\n\n### Supported Ruby versions\n\n- Ruby (MRI) \u003e= 2.5.0\n- JRuby \u003e= 9.2.9\n\n## Usage\n\n### Configuration classes\n\nUsing configuration classes allows you to make configuration data a bit more than a bag of values:\nyou can define a schema for your configuration, provide defaults, add validations and additional helper methods.\n\nAnyway Config provides a base class to inherit from with a few DSL methods:\n\n```ruby\nrequire \"anyway_config\"\n\nmodule MyCoolGem\n  class Config \u003c Anyway::Config\n    attr_config user: \"root\", password: \"root\", host: \"localhost\"\n  end\nend\n```\n\nHere `attr_config` creates accessors and populates the default values. If you don't need default values you can write:\n\n```ruby\nattr_config :user, :password, host: \"localhost\", options: {}\n```\n\n**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.\n\nThen, create an instance of the config class and use it:\n\n```ruby\nMyCoolGem::Config.new.user #=\u003e \"root\"\n```\n\n**Bonus:**: if you define attributes with boolean default values (`false` or `true`), Anyway Config would automatically add a corresponding predicate method. For example:\n\n```ruby\nattr_config :user, :password, debug: false\n\nMyCoolGem::Config.new.debug? #=\u003e false\nMyCoolGem::Config.new(debug: true).debug? #=\u003e true\n```\n\n**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:\n\n```ruby\nclass MyConfig \u003c Anyway::Config\n  attr_config :host, :port, :url, :meta\n\n  # override writer to handle type coercion\n  def meta=(val)\n    super(JSON.parse(val))\n  end\n\n  # or override reader to handle missing values\n  def url\n    super || (self.url = \"#{host}:#{port}\")\n  end\n\n  # untill v2.1, it will still be possible to read instance variables,\n  # i.e. the following code would also work\n  def url\n    @url ||= \"#{host}:#{port}\"\n  end\nend\n```\n\nWe recommend to add a feature check and support both v1.x and v2.0 in gems for the time being:\n\n```ruby\n# Check for the class method added in 2.0, e.g., `.on_load`\nif respond_to?(:on_load)\n  def url\n    super || (self.url = \"#{host}:#{port}\")\n  end\nelse\n  def url\n    @url ||= \"#{host}:#{port}\"\n  end\nend\n```\n\n#### Config name\n\nAnyway Config relies on the notion of _config name_ to populate data.\n\nBy default, Anyway Config uses the config class name to infer the config name using the following rules:\n\n- if the class name has a form of `\u003cModule\u003e::Config` then use the module name (`SomeModule::Config =\u003e \"somemodule\"`)\n- if the class name has a form of `\u003cSomething\u003eConfig` then use the class name prefix (`SomeConfig =\u003e \"some\"`)\n\n**NOTE:** in both cases, the config name is a **downcased** module/class prefix, not underscored.\n\nYou can also specify the config name explicitly (it's required in cases when your class name doesn't match any of the patterns above):\n\n```ruby\nmodule MyCoolGem\n  class Config \u003c Anyway::Config\n    config_name :cool\n    attr_config user: \"root\", password: \"root\", host: \"localhost\", options: {}\n  end\nend\n```\n\n#### Customize env variable names prefix\n\nBy default, Anyway Config uses upper-cased config name as a prefix for env variable names (e.g.\n`config_name :my_app` will result to parsing `MY_APP_` prefix).\n\nYou can set env prefix explicitly:\n\n```ruby\nmodule MyCoolGem\n  class Config \u003c Anyway::Config\n    config_name :cool_gem\n    env_prefix :really_cool # now variables, starting wih `REALLY_COOL_`, will be parsed\n    attr_config user: \"root\", password: \"root\", host: \"localhost\", options: {}\n  end\nend\n```\n\n#### Explicit values\n\nSometimes it's useful to set some parameters explicitly during config initialization.\nYou can do that by passing a Hash into `.new` method:\n\n```ruby\nconfig = MyCoolGem::Config.new(\n  user: \"john\",\n  password: \"rubyisnotdead\"\n)\n\n# The value would not be overridden from other sources (such as YML file, env)\nconfig.user == \"john\"\n```\n\n#### Reload configuration\n\nThere are `#clear` and `#reload` methods that do exactly what they state.\n\n**NOTE**: `#reload` also accepts an optional Hash for [explicit values](#explicit-values).\n\n### Dynamic configuration\n\nYou can also fetch configuration without pre-defined schema:\n\n```ruby\n# load data from config/my_app.yml,\n# credentials.my_app, secrets.my_app (if using Rails), ENV[\"MY_APP_*\"]\n#\n# Given MY_APP_VALUE=42\nconfig = Anyway::Config.for(:my_app)\nconfig[\"value\"] #=\u003e 42\n\n# you can specify the config file path or env prefix\nconfig = Anyway::Config.for(:my_app, config_path: \"my_config.yml\", env_prefix: \"MYAPP\")\n```\n\nThis feature is similar to `Rails.application.config_for` but more powerful:\n\n| Feature | Rails | Anyway Config |\n| ------------- |-------------:| -----:|\n| Load data from `config/app.yml` | ✅ | ✅ |\n| Load data from `credentials` | ❌ | ✅ |\n| Load data from environment | ❌ | ✅ |\n| Load data from [other sources](#data-loaders) | ❌ | ✅ |\n| Local config files | ❌ | ✅ |\n| Type coercion | ❌ | ✅ |\n| [Source tracing](#tracing) | ❌ | ✅ |\n| Return Hash with indifferent access | ❌ | ✅ |\n| Support ERB\\* within `config/app.yml` | ✅ | ✅ |\n| Raise if file doesn't exist | ✅ | ❌ |\n| Works without Rails | 😀 | ✅ |\n\n\\* Make sure that ERB is loaded\n\n### Validation and callbacks\n\nAnyway Config provides basic ways of ensuring that the configuration is valid.\n\nThere is a built-in `required` class method to define the list of parameters that must be present in the\nconfiguration after loading (where present means non-`nil` and non-empty for strings):\n\n```ruby\nclass MyConfig \u003c Anyway::Config\n  attr_config :api_key, :api_secret, :debug\n\n  required :api_key, :api_secret\nend\n\nMyConfig.new(api_secret: \"\") #=\u003e raises Anyway::Config::ValidationError\n```\n\n`Required` method supports additional `env` parameter which indicates necessity to run validations under specified\nenvironments. `Env` parameter could be present in symbol, string, array or hash formats:\n\n**NOTE:** You can suppress the validation of the required parameters (it can be useful for CI) via the `ANYWAY_SUPPRESS_VALIDATIONS` environment variable or by setting it explicitly in the code: `Anyway::Settings.suppress_required_validations = true`.\nIf you are using Anyway Config with Rails and have already specified `SECRET_KEY_BASE_DUMMY` for asset pre compilation, validation will be skipped by default.\n\n```ruby\nclass EnvConfig \u003c Anyway::Config\n  required :password, env: \"production\"\n  required :maps_api_key, env: :production\n  required :smtp_host, env: %i[production staging]\n  required :aws_bucket, env: %w[production staging]\n  required :anycable_rpc_host, env: {except: :development}\n  required :anycable_redis_url, env: {except: %i[development test]}\n  required :anycable_broadcast_adapter, env: {except: %w[development test]}\nend\n```\n\nIf your current `Anyway::Settings.current_environment` is mismatch keys that specified\n`Anyway::Config::ValidationError` error will be raised.\n\nIf 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:\n\n```ruby\nclass MyConfig \u003c Anyway::Config\n  attr_config :api_key, :api_secret, :mode\n\n  # on_load macro accepts symbol method names\n  on_load :ensure_mode_is_valid\n\n  # or block\n  on_load do\n    # the block is evaluated in the context of the config\n    raise_validation_error(\"API key and/or secret could be blank\") if\n      api_key.blank? || api_secret.blank?\n  end\n\n  def ensure_mode_is_valid\n    unless %w[production test].include?(mode)\n      raise_validation_error \"Unknown mode; #{mode}\"\n    end\n  end\nend\n```\n\n## Using with Rails\n\n**NOTE:** version 2.x supports Rails \u003e= 5.0; for Rails 4.x use version 1.x of the gem.\n\nWe recommend going through [Data population](#data-population) and [Organizing configs](#organizing-configs) sections first,\nand then use [Rails generators](#generators) to make your application Anyway Config-ready.\n\n### Data population\n\nYour config is filled up with values from the following sources (ordered by priority from low to high):\n\n1) **YAML configuration files**: `RAILS_ROOT/config/my_cool_gem.yml`.\n\nRails environment is used as the namespace (required); supports `ERB`:\n\n```yml\ntest:\n  host: localhost\n  port: 3002\n\ndevelopment:\n  host: localhost\n  port: 3000\n```\n\n**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\"`.\n\nYou can also configure additional Ruby classes that you want to deserialized from YAML (`permitted_classes` option). For example:\n\n```ruby\nAnyway::Loaders::YAML.permitted_classes \u003c\u003c Date\n```\n\n### Multi-env configuration\n\n_⚡️ 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`._\n\nIf 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:\n\n```yml\nhost: localhost\nport: 3002\n# These values will be active in all environments\n```\n\nTo extend the list of known environments, use the setting in the relevant part of your Rails code:\n\n```ruby\nRails.application.config.anyway_config.known_environments \u003c\u003c \"staging\"\n```\n\nIf 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:\n\n```yml\nstaging:\n  host: localhost # This value will be loaded when Rails.env.staging? is true\n\nport: 3002 # This value will not be loaded at all\n```\n\nTo 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:\n\n```ruby\nconfig.anyway_config.default_environmental_key = \"default\"\n```\n\nAfter that, Anyway Config will start reading settings under the `\"default\"` key and then merge environmental settings into them.\n\n```yml\ndefault:\n  server: # This values will be loaded in all environments by default\n    host: localhost\n    port: 3002\n\nstaging:\n  server:\n    host: staging.example.com # This value will override the defaults when Rails.env.staging? is true\n    # port will be set to the value from the defaults — 3002\n```\n\nYou can specify the lookup path for YAML files in one of the following ways:\n\n- By setting `config.anyway_config.default_config_path` to a target directory path:\n\n```ruby\nconfig.anyway_config.default_config_path = \"/etc/configs\"\nconfig.anyway_config.default_config_path = Rails.root.join(\"etc\", \"configs\")\n```\n\n- By setting `config.anyway_config.default_config_path` to a Proc, which accepts a config name and returns the path:\n\n```ruby\nconfig.anyway_config.default_config_path = -\u003e(name) { Rails.root.join(\"data\", \"configs\", \"#{name}.yml\") }\n```\n\n- By overriding a specific config YML file path via the `\u003cNAME\u003e_CONF` env variable, e.g., `MYCOOLGEM_CONF=path/to/cool.yml`\n\n2) (Rails \u003c7.1) **Rails secrets**: `Rails.application.secrets.my_cool_gem` (if `secrets.yml` present).\n\n```yml\n# config/secrets.yml\ndevelopment:\n  my_cool_gem:\n    port: 4444\n```\n\n**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`.\n\n3) **Rails credentials**: `Rails.application.credentials.my_cool_gem` (if supported):\n\n```yml\nmy_cool_gem:\n  host: secret.host\n```\n\n**NOTE:** You can backport Rails 6 per-environment credentials to Rails 5.2 app using [this patch](https://gist.github.com/palkan/e27e4885535ff25753aefce45378e0cb).\n\n4) **Environment variables**: `ENV['MYCOOLGEM_*']`.\n\nSee [environment variables](#environment-variables).\n\n### Organizing configs\n\nYou can store application-level config classes in `app/configs` folder just like any other Rails entities.\n\nHowever, in that case you won't be able to use them during the application initialization (i.e., in `config/**/*.rb` files).\n\nSince that's a pretty common scenario, we provide a way to do that via a custom autoloader for `config/configs` folder.\nThat means, that you can put your configuration classes into `config/configs` folder, use them anywhere in your code without explicitly requiring them.\n\nConsider an example: setting the Action Mailer hostname for Heroku review apps.\n\nWe have the following config to fetch the Heroku provided [metadata](https://devcenter.heroku.com/articles/dyno-metadata):\n\n```ruby\n# This data is provided by Heroku Dyno Metadadata add-on.\nclass HerokuConfig \u003c Anyway::Config\n  attr_config :app_id, :app_name,\n    :dyno_id, :release_version,\n    :slug_commit\n\n  def hostname\n    \"#{app_name}.herokuapp.com\"\n  end\nend\n```\n\nThen in `config/application.rb` you can do the following:\n\n```ruby\nconfig.action_mailer.default_url_options = {host: HerokuConfig.new.hostname}\n```\n\nYou can configure the configs folder path:\n\n```ruby\n# The path must be relative to Rails root\nconfig.anyway_config.autoload_static_config_path = \"path/to/configs\"\n```\n\n**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`.\nOr you can store everything in `app/configs` by setting `config.anyway_config.autoload_static_config_path = \"app/configs\"`.\n\n**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:\n\n```ruby\n# config/application.rb\n\n# ...\n\nrequire_relative \"initializers/inflections\"\n\nmodule SomeApp\n  class Application \u003c Rails::Application\n    # ...\n  end\nend\n```\n\n### Generators\n\nAnyway Config provides Rails generators to create new config classes:\n\n- `rails g anyway:install`—creates an `ApplicationConfig` class (the base class for all config classes) and updates `.gitignore`\n\nYou can specify the static configs path via the `--configs-path` option:\n\n```sh\nrails g anyway:install --configs-path=config/settings\n\n# or to keep everything in app/configs\nrails g anyway:install --configs-path=app/configs\n```\n\n- `rails g anyway:config \u003cname\u003e param1 param2 ...`—creates a named configuration class and optionally the corresponding YAML file; creates `application_config.rb` is missing.\n\nThe generator command for the Heroku example above would be:\n\n```sh\n$ rails g anyway:config heroku app_id app_name dyno_id release_version slug_commit\n\n    generate  anyway:install\n       rails  generate anyway:install\n      create  config/configs/application_config.rb\n      append  .gitignore\n      create  config/configs/heroku_config.rb\nWould you like to generate a heroku.yml file? (Y/n) n\n```\n\nYou can also specify the `--app` option to put the newly created class into `app/configs` folder.\nAlternatively, you can call `rails g anyway:app_config name param1 param2 ...`.\n\n**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).\n\n### Loading Anyway Config before Rails\n\nAnyway 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).\n\nBy 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).\n\nYou 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`.\n\n## Using with Ruby\n\nThe default data loading mechanism for non-Rails applications is the following (ordered by priority from low to high):\n\n1) **YAML configuration files**: `./config/\u003cconfig-name\u003e.yml`.\n\nIn pure Ruby apps, we also can load data under specific _environments_ (`test`, `development`, `production`, etc.).\nIf you want to enable this feature you must specify `Anyway::Settings.current_environment` variable for load config under specific environment.\n\n```ruby\nAnyway::Settings.current_environment = \"development\"\n```\n\nYou can also specify the `ANYWAY_ENV=development` environment variable to set the current environment for configuration.\n\nYAML files should be in this format:\n\n```yml\ndevelopment:\n  host: localhost\n  port: 3000\n```\n\nIf `Anyway::Settings.current_environment` is missed we assume that the YAML contains values for a single environment:\n\n```yml\nhost: localhost\nport: 3000\n```\n\n`ERB` is supported if `erb` is loaded (thus, you need to call `require \"erb\"` somewhere before loading configuration).\n\nYou can specify the lookup path for YAML files in one of the following ways:\n\n- By setting `Anyway::Settings.default_config_path` to a target directory path:\n\n```ruby\nAnyway::Settings.default_config_path = \"/etc/configs\"\n```\n\n- By setting `Anyway::Settings.default_config_path` to a Proc, which accepts a config name and returns the path:\n\n```ruby\nAnyway::Settings.default_config_path = -\u003e(name) { Rails.root.join(\"data\", \"configs\", \"#{name}.yml\") }\n```\n\n- By overriding a specific config YML file path via the `\u003cNAME\u003e_CONF` env variable, e.g., `MYCOOLGEM_CONF=path/to/cool.yml`\n\n2) **Environment variables**: `ENV['MYCOOLGEM_*']`.\n\nSee [environment variables](#environment-variables).\n\n## Environment variables\n\nEnvironmental variables for your config should start with your config name, upper-cased.\n\nFor example, if your config name is \"mycoolgem\", then the env var \"MYCOOLGEM_PASSWORD\" is used as `config.password`.\n\nBy default, environment variables are automatically type cast (rules are case-insensitive):\n\n- `\"true\"`, `\"t\"`, `\"yes\"` and `\"y\"` to `true`;\n- `\"false\"`, `\"f\"`, `\"no\"` and `\"n\"` to `false`;\n- `\"nil\"` and `\"null\"` to `nil` (do you really need it?);\n- `\"123\"` to `123` and `\"3.14\"` to `3.14`.\n\nType coercion can be [customized or disabled](#type-coercion).\n\n*Anyway Config* supports nested (_hashed_) env variables—just separate keys with double-underscore.\n\nFor example, \"MYCOOLGEM_OPTIONS__VERBOSE\" is parsed as `config.options[\"verbose\"]`.\n\nArray values are also supported:\n\n```ruby\n# Suppose ENV[\"MYCOOLGEM_IDS\"] = '1,2,3'\nconfig.ids #=\u003e [1,2,3]\n```\n\nIf you want to provide a text-like env variable which contains commas then wrap it into quotes:\n\n```ruby\nMYCOOLGEM = \"Nif-Nif, Naf-Naf and Nouf-Nouf\"\n```\n\n## Type coercion\n\n\u003e 🆕 v2.2.0\n\nYou can define custom type coercion rules to convert string data to config values. To do that, use `.coerce_types` method:\n\n```ruby\nclass CoolConfig \u003c Anyway::Config\n  config_name :cool\n  attr_config port: 8080,\n    host: \"localhost\",\n    user: {name: \"admin\", password: \"admin\"}\n\n  coerce_types port: :string, user: {dob: :date}\nend\n\nENV[\"COOL_USER__DOB\"] = \"1989-07-01\"\n\nconfig = CoolConfig.new\nconfig.port == \"8080\" # Even though we defined the default value as int, it's converted into a string\nconfig.user[\"dob\"] == Date.new(1989, 7, 1) #=\u003e true\n```\n\nType coercion is especially useful to deal with array values:\n\n```ruby\n# To define an array type, provide a hash with two keys:\n#  - type — elements type\n#  - array: true — mark the parameter as array\ncoerce_types list: {type: :string, array: true}\n```\n\nYou can use `type: nil` in case you don't want to coerce values, just convert a value into an array:\n\n```ruby\n# From AnyCable config (sentinels could be represented via strings or hashes)\ncoerce_types redis_sentinels: {type: nil, array: true}\n```\n\nIt's also could be useful to explicitly define non-array types (to avoid confusion):\n\n```ruby\ncoerce_types non_list: :string\n```\n\nFinally, it's possible to disable auto-casting for a particular config completely:\n\n```ruby\nclass CoolConfig \u003c Anyway::Config\n  attr_config port: 8080,\n    host: \"localhost\",\n    user: {name: \"admin\", password: \"admin\"}\n\n  disable_auto_cast!\nend\n\nENV[\"COOL_PORT\"] = \"443\"\n\nCoolConfig.new.port == \"443\" #=\u003e true\n```\n\n**IMPORTANT**: Values provided explicitly (via attribute writers) are not coerced. Coercion is only happening during the load phase.\n\nThe following types are supported out-of-the-box: `:string`, `:integer`, `:integer!` (strict integer), `:float`, `:date`, `:datetime`, `:uri`, `:boolean`.\n\nYou can use custom deserializers by passing a callable object instead of a type name:\n\n```ruby\nCOLOR_TO_HEX = lambda do |raw|\n  case raw\n  when \"red\"\n    \"#ff0000\"\n  when \"green\"\n    \"#00ff00\"\n  when \"blue\"\n    \"#0000ff\"\n  end\nend\n\nclass CoolConfig \u003c Anyway::Config\n  attr_config :color\n\n  coerce_types color: COLOR_TO_HEX\nend\n\nCoolConfig.new({color: \"red\"}).color #=\u003e \"#ff0000\"\n```\n\n## Local files\n\nIt's useful to have a personal, user-specific configuration in development, which extends the project-wide one.\n\nWe support this by looking at _local_ files when loading the configuration data:\n\n- `\u003cconfig_name\u003e.local.yml` files (next to\\* the _global_ `\u003cconfig_name\u003e.yml`)\n- `config/credentials/local.yml.enc` (for Rails \u003e= 7.1, generate it via `rails local_credentials:edit`; for Rails \u003e= 6.0, \u003c7.1 you can also use `rails credentials:edit -e local`).\n\n\\* If the YAML config path is not a default one (i.e., set via `\u003cCONFIG_NAME\u003e_CONF`), we look up the local\nconfig at this location, too.\n\nLocal 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\"`).\n\n**NOTE:** in Rails apps you can use `Rails.application.configuration.anyway_config.use_local_files`.\n\nDon't forget to add `*.local.yml` (and `config/credentials/local.*`) to your `.gitignore`.\n\n**NOTE:** local YAML configs for a Rails app must be environment-free (i.e., you shouldn't have top-level `development:` key).\n\n## Data loaders\n\n### Doppler integration\n\nAnyway 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)).\n\nYou can also configure Doppler loader manually if needed:\n\n```ruby\n# Add loader\nAnyway.loaders.append :Doppler, Anyway::Loaders::Doppler\n\n# Configure API URL and token (defaults are shown)\nAnyway::Loaders::Doppler.download_url = \"https://api.doppler.com/v3/configs/config/secrets/download\"\nAnyway::Loaders::Doppler.token = ENV[\"DOPPLER_TOKEN\"]\n```\n\n**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).\n\n### EJSON support\n\nAnyway Config allows you to keep your configuration also in encrypted `.ejson` files. More information\nabout EJSON format you can read [here](https://github.com/Shopify/ejson).\n\nConfiguration will be loaded only if you have `ejson` executable in your PATH. Easiest way to do this - install `ejson` as a gem into project:\n\n```ruby\n# Gemfile\ngem \"ejson\"\n```\n\nLoading order of configuration is next:\n\n- `config/secrets.local.ejson` (see [Local files](#local-files) for more information)\n- `config/\u003cenvironment\u003e/secrets.ejson` (if you have any multi-environment setup, e.g Rails environments)\n- `config/secrets.ejson`\n\nExample of `config/secrets.ejson` file content for your `MyConfig`:\n\n```json\n{\n  \"_public_key\": \"0843d33f0eee994adc66b939fe4ef569e4c97db84e238ff581934ee599e19d1a\",\n  \"my\":\n    {\n      \"_username\": \"root\",\n      \"password\": \"EJ[1:IC1d347GkxLXdZ0KrjGaY+ljlsK1BmK7CobFt6iOLgE=:Z55OYS1+On0xEBvxUaIOdv/mE2r6lp44:T7bE5hkAbazBnnH6M8bfVcv8TOQJAgUDQffEgw==]\"\n    }\n}\n```\n\nTo debug any problems with loading configurations from `.ejson` files you can directly call `ejson decrypt`:\n\n```sh\nejson decrypt config/secrets.ejson\n```\n\nYou can customize the JSON namespace under which a loader searches for configuration via `loader_options`:\n\n```ruby\nclass MyConfig \u003c Anyway::Config\n  # To look under the key \"foo\" instead of the default key of \"my\"\n  loader_options ejson_namespace: \"foo\"\n\n  # Or to disable namespacing entirely, and instead search in the root object\n  loader_options ejson_namespace: false\nend\n```\n\n### Custom loaders\n\nYou can provide your own data loaders or change the existing ones using the Loaders API (which is very similar to Rack middleware builder):\n\n```ruby\n# remove env loader =\u003e do not load params from ENV\nAnyway.loaders.delete :env\n\n# add custom loader before :env (it's better to keep the ENV loader the last one)\nAnyway.loaders.insert_before :env, :my_loader, MyLoader\n```\n\nLoader is a _callable_ Ruby object (module/class responding to `.call` or lambda/proc), which `call` method\naccepts the following keyword arguments:\n\n```ruby\ndef call(\n  name:, # config name\n  env_prefix:, # prefix for env vars if any\n  config_path:, # path to YML config\n  local:, # true|false, whether to load local configuration\n  **options # custom options can be passed via Anyway::Config.loader_options example: \"custom\", option: \"blah\"\n)\n  #=\u003e must return Hash with configuration data\nend\n```\n\nYou can use `Anyway::Loaders::Base` as a base class for your loader and define a `#call` method.\nFor example, the [Chamber](https://github.com/thekompanee/chamber) loader could be written as follows:\n\n```ruby\nclass ChamberConfigLoader \u003c Base\n  def call(name:, **_opts)\n    Chamber.to_hash[name] || {}\n  rescue Chamber::Errors::DecryptionFailure =\u003e e\n    warn \"Couldn't decrypt Chamber settings: #{e.message}\"\n    {}\n  end\nend\n\n# Don't forget to register it\nAnyway.loaders.insert_before :env, :chamber, ChamberConfigLoader\n```\n\nIn order to support [source tracing](#tracing), you need to wrap the resulting Hash via the `#trace!` method with metadata:\n\n```ruby\ndef call(name:, **_opts)\n  trace!(:chamber) do\n    Chamber.to_hash[name] || {}\n  rescue Chamber::Errors::DecryptionFailure =\u003e e\n    warn \"Couldn't decrypt Chamber settings: #{e.message}\"\n    {}\n  end\nend\n```\n\n## Tracing\n\nSince Anyway Config loads data from multiple source, it could be useful to know where a particular value came from.\n\nEach `Anyway::Config` instance contains _tracing information_ which you can access via `#to_source_trace` method:\n\n```ruby\nconf = ExampleConfig.new\nconf.to_source_trace\n\n# returns the following hash\n{\n  \"host\" =\u003e {value: \"test.host\", source: {type: :yml, path: \"config/example.yml\"}},\n  \"user\" =\u003e {\n    \"name\" =\u003e {value: \"john\", source: {type: :env, key: \"EXAMPLE_USER__NAME\"}},\n    \"password\" =\u003e {value: \"root\", source: {type: :credentials, store: \"config/credentials/production.enc.yml\"}}\n  },\n  \"port\" =\u003e {value: 9292, source: {type: :defaults}}\n}\n\n# if you change the value manually in your code,\n# that would be reflected in the trace\n\nconf.host = \"anyway.host\"\nconf.to_source_trace[\"host\"]\n#=\u003e {type: :user, called_from: \"/path/to/caller.rb:15\"}\n```\n\nYou can disable tracing functionality by setting `Anyway::Settings.tracing_enabled = false` or `config.anyway_config.tracing_enabled = false` in Rails.\n\n### Pretty print\n\nYou can use `pp` to print a formatted information about the config including the sources trace.\n\nExample:\n\n```ruby\npp CoolConfig.new\n\n# #\u003cCoolConfig\n#   config_name=\"cool\"\n#   env_prefix=\"COOL\"\n#   values:\n#     port =\u003e 3334 (type=load),\n#     host =\u003e \"test.host\" (type=yml path=./config/cool.yml),\n#     user =\u003e\n#       name =\u003e \"john\" (type=env key=COOL_USER__NAME),\n#       password =\u003e \"root\" (type=yml path=./config/cool.yml)\u003e\n```\n\n## Pattern matching\n\nYou can use config instances in Ruby 2.7+ pattern matching:\n\n```ruby\ncase AWSConfig.new\nin bucket:, region: \"eu-west-1\"\n  setup_eu_storage(bucket)\nin bucket:, region: \"us-east-1\"\n  setup_us_storage(bucket)\nend\n```\n\nIf the attribute wasn't populated, the key won't be returned for pattern matching, i.e. you can do something line:\n\n```ruby\naws_configured =\n  case AWSConfig.new\n  in access_key_id:, secret_access_key:\n    true\n  else\n    false\n  end\n```\n\n## Test helpers\n\nWe provide the `with_env` test helper to test code in the context of the specified environment variables values:\n\n```ruby\ndescribe HerokuConfig, type: :config do\n  subject { described_class.new }\n\n  specify do\n    # Ensure that the env vars are set to the specified\n    # values within the block and reset to the previous values\n    # outside of it.\n    with_env(\n      \"HEROKU_APP_NAME\" =\u003e \"kin-web-staging\",\n      \"HEROKU_APP_ID\" =\u003e \"abc123\",\n      \"HEROKU_DYNO_ID\" =\u003e \"ddyy\",\n      \"HEROKU_RELEASE_VERSION\" =\u003e \"v0\",\n      \"HEROKU_SLUG_COMMIT\" =\u003e \"3e4d5a\"\n    ) do\n      is_expected.to have_attributes(\n        app_name: \"kin-web-staging\",\n        app_id: \"abc123\",\n        dyno_id: \"ddyy\",\n        release_version: \"v0\",\n        slug_commit: \"3e4d5a\"\n      )\n    end\n  end\nend\n```\n\nIf you want to delete the env var, pass `nil` as the value.\n\nThis 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/...`.\n\nYou can add it manually by requiring `\"anyway/testing/helpers\"` and including the `Anyway::Testing::Helpers` module (into RSpec configuration or Minitest test class).\n\n## OptionParser integration\n\nIt's possible to use config as option parser (e.g., for CLI apps/libraries). It uses\n[`optparse`](https://ruby-doc.org/stdlib-2.5.1/libdoc/optparse/rdoc/OptionParser.html) under the hood.\n\nExample usage:\n\n```ruby\nclass MyConfig \u003c Anyway::Config\n  attr_config :host, :log_level, :concurrency, :debug, server_args: {}\n\n  # specify which options shouldn't be handled by option parser\n  ignore_options :server_args\n\n  # provide description for options\n  describe_options(\n    concurrency: \"number of threads to use\"\n  )\n\n  # mark some options as flag\n  flag_options :debug\n\n  # extend an option parser object (i.e. add banner or version/help handlers)\n  extend_options do |parser, config|\n    parser.banner = \"mycli [options]\"\n\n    parser.on(\"--server-args VALUE\") do |value|\n      config.server_args = JSON.parse(value)\n    end\n\n    parser.on_tail \"-h\", \"--help\" do\n      puts parser\n    end\n  end\nend\n\nconfig = MyConfig.new\n\nconfig.parse_options!(%w[--host localhost --port 3333 --log-level debug])\n\nconfig.host # =\u003e \"localhost\"\nconfig.port # =\u003e 3333\nconfig.log_level # =\u003e \"debug\"\n\n# Get the instance of OptionParser\nconfig.option_parser\n```\n\n**NOTE:** values are automatically type cast using the same rules as for [environment variables](#environment-variables).\nIf you want to specify the type explicitly, you can do that using `describe_options`:\n\n```ruby\ndescribe_options(\n  # In this case, you should specify a hash with `type`\n  # and (optionally) `desc` keys\n  concurrency: {\n    desc: \"number of threads to use\",\n    type: String\n  }\n)\n```\n\n## RBS support\n\nAnyway Config comes with Ruby type signatures (RBS).\n\nTo use them with Steep, add the following your `Steepfile`:\n\n```ruby\nlibrary \"pathname\"\nlibrary \"optparse\"\nlibrary \"anyway_config\"\n```\n\nWe also provide an API to generate a type signature for your config class:\n\n```ruby\nclass MyGem::Config \u003c Anyway::Config\n  attr_config :host, port: 8080, tags: [], debug: false\n\n  coerce_types host: :string, port: :integer,\n    tags: {type: :string, array: true}\n\n  required :host\nend\n```\n\nThen calling `MyGem::Config.to_rbs` will return the following signature:\n\n```rbs\nmodule MyGem\n  interface _Config\n    def host: () -\u003e String\n    def host=: (String) -\u003e void\n    def port: () -\u003e String?\n    def port=: (String) -\u003e void\n    def tags: () -\u003e Array[String]?\n    def tags=: (Array[String]) -\u003e void\n    def debug: () -\u003e bool\n    def debug?: () -\u003e bool\n    def debug=: (bool) -\u003e void\n  end\n\n  class Config \u003c Anyway::Config\n    include _Config\n  end\nend\n```\n\n### Handling `on_load`\n\nWhen 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:\n\n```ruby\nclass MyConfig \u003c Anyway::Config\n  on_load do\n    # @type self : MyConfig\n    raise_validation_error(\"host is invalid\") if host.start_with?(\"localhost\")\n  end\nend\n```\n\nYeah, a lot of annotations 😞 Welcome to the type-safe world!\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at [https://github.com/palkan/anyway_config](https://github.com/palkan/anyway_config).\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpalkan%2Fanyway_config","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpalkan%2Fanyway_config","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpalkan%2Fanyway_config/lists"}