anyway_config 2.9.0 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 910bf1014d5b1b2a7952b2e6da131a341722998f3b891d096f4d5764006fac23
4
- data.tar.gz: 6f9233771e022ccb6d3ea3d03ee2b15bee8e2e3d322aa3434cd5d08b8794cf14
3
+ metadata.gz: 5b4393c06a6078956ade1727f37879c8a38003806b59c8c5a7e9c330708a42fd
4
+ data.tar.gz: 98e3e7d9c0a50e2a18f98a917818ab4fa088314471102d6d6f9e67c08b059670
5
5
  SHA512:
6
- metadata.gz: 11f0d4f61ebabda431abba36837438c767405a0432e2f8f8ae1d2460b35e8e14438b9ed2acb6cb9d2d1b6889111e61cfecdf7abed2e2d4bc40eccba416c1d6f5
7
- data.tar.gz: 3bf99f8766ed1a38ea3d9b6f114bf59c694c492ecd0f635067b97750750c2afea3e4d3c476e4d7bfa70c6e02fb478e36f273a4b32613712c79e10dc7c9fed400
6
+ metadata.gz: c9a771b5fd116be880f4aa3271a5389c6023500452eb8ea98ff8cefc5452ad0f24e3eab97bc02d39daab5231a070e4441efed4a285331d64f7f329073bfbab38
7
+ data.tar.gz: 8af2c0fdc71913929d78ef07f84db3b440cfa0a324834958f3bce0b77dd90e5a9c6995a61c3ba5637008171f6e02d7c7a429f46470b2d5a15984bcc524698737
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 2.9.0 (2026-10-21)
6
+
7
+ - Add an agent skill (`skills/anyway-config`) with guidance on organizing configuration in Rails apps. ([@palkan][])
8
+
9
+ The skill follows the [skills.sh](https://www.skills.sh) layout and installs automatically with [Rails Hyperdrive](https://github.com/rails-hyperdrive/rails-hyperdrive).
10
+
5
11
  ## 2.9.0 (2026-10-20)
6
12
 
7
13
  - Add a new `enforce_config_schema` to warn or raise on unknown attributes. ([@palkan][])
@@ -24,6 +30,7 @@ config.anyway_config.enforce_config_schema = true
24
30
  - Guard against a missing `PATH` environment variable in `Anyway::Utils.which`. ([@SAY-5][])
25
31
 
26
32
  - Do not require optional loader dependencies to speed up loading. ([@ardecvz][])
33
+ - Use `to_s` on values that support it during validation for required attributes. This provides more consistent behaviour for the `:uri` coercer, which will otherwise produce a `#<URI::Generic >` rather than a validation failure. ([@phil-janeapp][])
27
34
 
28
35
  ## 2.8.0 (2026-06-24)
29
36
 
data/README.md CHANGED
@@ -34,6 +34,7 @@ For version 1.x see the [1-4-stable branch](https://github.com/palkan/anyway_con
34
34
 
35
35
  - [Main concepts](#main-concepts)
36
36
  - [Installation](#installation)
37
+ - [Adding agent skills](#adding-agent-skills)
37
38
  - [Usage](#usage)
38
39
  - [Configuration classes](#configuration-classes)
39
40
  - [Dynamic configuration](#dynamic-configuration)
@@ -105,6 +106,18 @@ Or adding to your project:
105
106
  gem "anyway_config", "~> 2.0"
106
107
  ```
107
108
 
109
+ ### Adding agent skills
110
+
111
+ The gem ships a skill for coding agents in `skills/anyway-config`. It guides an agent when it adds or changes configuration in a Rails app: where to put a config class, which source to use for a value, how to replace direct `ENV` reads, and how to test configs. It follows the [skills.sh](https://www.skills.sh) layout, so it works with any agent that reads `SKILL.md`.
112
+
113
+ With [Rails Hyperdrive](https://github.com/rails-hyperdrive/rails-hyperdrive), the skill installs automatically: run `bin/rails hyperdrive:init` the first time, then `bin/rails hyperdrive:sync`.
114
+
115
+ Without it, install the skill from the repository with the [skills.sh](https://www.skills.sh) CLI, or copy `skills/anyway-config` into your agent's skills directory:
116
+
117
+ ```sh
118
+ npx skills add palkan/anyway_config
119
+ ```
120
+
108
121
  ### Supported Ruby versions
109
122
 
110
123
  - Ruby (MRI) >= 2.7.0
@@ -471,7 +471,7 @@ module Anyway # :nodoc:
471
471
 
472
472
  self.class.required_attributes.select do |name|
473
473
  val = values.dig(*name.to_s.split(".").map(&:to_sym))
474
- val.nil? || (val.is_a?(String) && val.empty?)
474
+ val.nil? || (val.respond_to?(:to_s) && val.to_s.empty?)
475
475
  end.then do |missing|
476
476
  next if missing.empty?
477
477
  raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
@@ -471,7 +471,7 @@ module Anyway # :nodoc:
471
471
 
472
472
  self.class.required_attributes.select do |name|
473
473
  val = values.dig(*name.to_s.split(".").map(&:to_sym))
474
- val.nil? || (val.is_a?(String) && val.empty?)
474
+ val.nil? || (val.respond_to?(:to_s) && val.to_s.empty?)
475
475
  end.then do |missing|
476
476
  next if missing.empty?
477
477
  raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
@@ -471,7 +471,7 @@ module Anyway # :nodoc:
471
471
 
472
472
  self.class.required_attributes.select do |name|
473
473
  val = values.dig(*name.to_s.split(".").map(&:to_sym))
474
- val.nil? || (val.is_a?(String) && val.empty?)
474
+ val.nil? || (val.respond_to?(:to_s) && val.to_s.empty?)
475
475
  end.then do |missing|
476
476
  next if missing.empty?
477
477
  raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
@@ -471,7 +471,7 @@ module Anyway # :nodoc:
471
471
 
472
472
  self.class.required_attributes.select do |name|
473
473
  val = values.dig(*name.to_s.split(".").map(&:to_sym))
474
- val.nil? || (val.is_a?(String) && val.empty?)
474
+ val.nil? || (val.respond_to?(:to_s) && val.to_s.empty?)
475
475
  end.then do |missing|
476
476
  next if missing.empty?
477
477
  raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
data/lib/anyway/config.rb CHANGED
@@ -471,7 +471,7 @@ module Anyway # :nodoc:
471
471
 
472
472
  self.class.required_attributes.select do |name|
473
473
  val = values.dig(*name.to_s.split(".").map(&:to_sym))
474
- val.nil? || (val.is_a?(String) && val.empty?)
474
+ val.nil? || (val.respond_to?(:to_s) && val.to_s.empty?)
475
475
  end.then do |missing|
476
476
  next if missing.empty?
477
477
  raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Anyway # :nodoc:
4
- VERSION = "2.9.0"
4
+ VERSION = "2.9.1"
5
5
  end
@@ -0,0 +1,223 @@
1
+ ---
2
+ name: anyway-config
3
+ description: Add, organize, refactor and test application configuration in Rails apps with Anyway Config (`Anyway::Config`). Use when a change needs a new setting, secret, API key, token, URL, env variable, feature toggle or third-party integration credentials; when code reads `ENV[...]`, `Rails.application.credentials`, `config_for` or branches on `Rails.env.production?`; when the user asks where configuration should live or how to stub it in tests.
4
+ ---
5
+
6
+ # Anyway Config: configuration in Rails apps
7
+
8
+ A config class is the only code that knows where a value comes from.
9
+ Application code depends on the config object. It never reads `ENV`,
10
+ credentials or YAML directly, and it does not branch on `Rails.env`.
11
+
12
+ This file is the decision procedure. Read these files for details:
13
+
14
+ - `references/organizing.md`: class placement, sources, boot integration, examples.
15
+ - `references/testing.md`: test the config class and the code that uses it.
16
+ - `references/docs.md`: the full map of README sections (§10 has the short version). Read the README
17
+ (or the gem source, `bundle info anyway_config --path`) for the API. Do not guess it.
18
+
19
+ ## 1. Look at the project first
20
+
21
+ 1. Find the existing configs: `config/configs/`, `app/configs/`, and `*/config.rb` next to services.
22
+ 2. Read `ApplicationConfig`. Most apps make it a singleton (`AppConfig.host`).
23
+ 3. Read the `config.anyway_config.*` lines in `config/application.rb`.
24
+ Note `autoload_static_config_path`, `default_environmental_key` and `enforce_config_schema`.
25
+ 4. Read `.gitignore`. Check that `config/*.local.yml` and `config/credentials/local.*` are ignored.
26
+ 5. Follow the conventions that you find. This skill gives the defaults for a new project.
27
+
28
+ If the gem is not set up yet, run `bin/rails g anyway:install`.
29
+
30
+ ## 2. Decide where the new value goes
31
+
32
+ ```
33
+ Is the value a config parameter (it changes between deployments or environments)?
34
+ ├── no → a constant in the code. Stop.
35
+ └── yes
36
+ ├── Is it infrastructure that Rails or a tool reads itself
37
+ │ (PORT, RAILS_MAX_THREADS, DATABASE_URL, RAILS_LOG_LEVEL)?
38
+ │ └── yes → keep it where the tool expects it (puma.rb, database.yml). Stop.
39
+ ├── Does an existing config class own this concern (same service, same subsystem)?
40
+ │ └── yes → add an attribute to that class. Go to §4.
41
+ └── no → create a new config class. Go to §3.
42
+ ```
43
+
44
+ Group parameters by concern: one class per integration or subsystem
45
+ (`StripeConfig`, `SMTPConfig`, `FlyDeploy::Config`).
46
+ Do not make one class per parameter. Do not put everything into `AppConfig`.
47
+ `AppConfig` holds app-wide values only: host, port, asset host, global toggles.
48
+
49
+ ## 3. Create a config class
50
+
51
+ Choose the location:
52
+
53
+ | The config is used… | Put it in | Notes |
54
+ |---|---|---|
55
+ | at boot (`config/application.rb`, initializers, routes, `puma.rb`), or app-wide | `config/configs/<name>_config.rb` | Static: not reloaded in development. |
56
+ | by one module, service or pack only | next to that code, e.g. `app/services/fly_deploy/config.rb` | Set `config_name` explicitly. Not available before autoloading. |
57
+ | at runtime only, and the project keeps such configs in `app/configs/` | `app/configs/<name>_config.rb` | Reloaded in development. |
58
+
59
+ Generate a class with `bin/rails g anyway:config <name> param1 param2`.
60
+ Add `--app` for `app/configs/`. For an in-place class, write it by hand:
61
+
62
+ ```ruby
63
+ # app/services/fly_deploy/config.rb
64
+ module FlyDeploy
65
+ class Config < ApplicationConfig
66
+ # Without it, the name is "flydeploy" (downcased, not underscored)
67
+ config_name :fly_deploy
68
+
69
+ attr_config :api_token, :org_slug,
70
+ rest_api_url: "https://api.machines.dev/v1",
71
+ region: "iad"
72
+
73
+ required :api_token, env: :production
74
+
75
+ def fake_api? = rest_api_url.include?("fake_fly")
76
+ end
77
+ end
78
+ ```
79
+
80
+ The config name sets all source keys: `config/fly_deploy.yml`,
81
+ `credentials.fly_deploy`, and `FLY_DEPLOY_*` env variables.
82
+ If a gem or tool already uses that name (for example, Litestream reads `LITESTREAM_*`),
83
+ choose a different name: `config_name :litestream_rails`.
84
+
85
+ ## 4. Choose the source for each value
86
+
87
+ | Value | Source |
88
+ |---|---|
89
+ | Non-secret default, same everywhere | `attr_config name: default` |
90
+ | Non-secret value that differs per environment | `config/<name>.yml`, per-environment keys |
91
+ | Fake value for development and test (stub API URL, test token) | `development:` / `test:` keys in `config/<name>.yml` |
92
+ | Secret (API key, password, signing secret) | Rails credentials under `<name>:`, or ENV `<NAME>_<ATTR>` from the deployment platform |
93
+ | Personal developer override | `config/<name>.local.yml` or `config/credentials/local.yml.enc` (git-ignored) |
94
+ | Value that an operator changes without a deploy | ENV `<NAME>_<ATTR>` |
95
+
96
+ Load priority, from low to high: attribute defaults → YAML (plus `.local.yml`) →
97
+ credentials → ENV → explicit `Config.new(key: value)`.
98
+
99
+ Do not write `<%= ENV["STRIPE_API_KEY"] %>` in YAML. The ENV loader already maps
100
+ `STRIPE_API_KEY` to `StripeConfig#api_key`.
101
+
102
+ ## 5. Write the attributes
103
+
104
+ - Give a default to each attribute that has a safe value. A `true`/`false` default adds a `?` predicate.
105
+ - Mark mandatory values with `required`. Limit it with `env:` (`env: :production`,
106
+ `env: {except: %i[development test]}`) when development can run without the value.
107
+ - Add `coerce_types` when ENV must produce an array (`{type: :string, array: true}`)
108
+ or must stay a string (`oauth_scope: :string`, a numeric-looking token).
109
+ - Put logic on the config, not in callers:
110
+ - Presence predicate: `def configured? = api_key.present? && secret.present?`
111
+ - Derived value: `def asset_host = super || "#{ssl? ? "https" : "http"}://#{host}"`
112
+ - Adapter for a library: `def to_settings = {address:, port:, user_name: username}.compact`
113
+ - Add a comment to an attribute when its purpose or format is not obvious.
114
+ - Do not use reserved names (`values`, `to_h`, `clear`, `config_name`, …).
115
+ With the singleton `ApplicationConfig`, also avoid names of class methods (README, issue #120).
116
+ - If the project type-checks with RBS (`sig/`, `Steepfile`), add a signature for the new config.
117
+ `MyConfig.to_rbs` generates it from `attr_config`, `coerce_types` and `required`.
118
+
119
+ ## 6. Use the config
120
+
121
+ - Call the config from application code: `StripeConfig.secret_key`, `AppConfig.analytics_enabled?`.
122
+ - Pass the config as a default argument when a class depends on it. Tests can then inject another one:
123
+ `def initialize(config: FlyDeploy::Config)`.
124
+ - Turn integrations on by the presence of their settings, not by the environment:
125
+ `provider :github, ... if GithubConfig.oauth_configured?`.
126
+ - Configure Rails and gems from configs in `config/application.rb` and initializers.
127
+ See `references/organizing.md` §4.
128
+ - A gem that reads only `ENV`: copy the value once in its initializer,
129
+ `ENV["STRIPE_PRIVATE_KEY"] ||= StripeConfig.secret_key`.
130
+
131
+ ## 7. Replace direct reads
132
+
133
+ | You see | Replace with |
134
+ |---|---|
135
+ | `ENV["FOO_API_KEY"]` in app code | `FooConfig.api_key` |
136
+ | `ENV.fetch("FOO_ENABLED", "false") == "true"` | `attr_config enabled: false` → `FooConfig.enabled?` |
137
+ | `Rails.application.credentials.foo[:key]` | `FooConfig.key` (same `foo:` credentials, no change in data) |
138
+ | `Rails.application.config_for(:foo)` | `FooConfig` (same `config/foo.yml`) |
139
+ | `if Rails.env.production?` to select a value | a per-environment YAML value |
140
+ | `if Rails.env.production?` to turn a feature on | a boolean attribute or `configured?` predicate |
141
+ | `ENV["X"] \|\| "default"` | `attr_config x: "default"` |
142
+
143
+ Keep `Rails.env` checks that are about the framework itself (for example, test-only view helpers).
144
+
145
+ ## 8. Test
146
+
147
+ Read `references/testing.md`. In short:
148
+
149
+ - Put the test values in the `test:` section of `config/<name>.yml`. CI then needs no secrets.
150
+ - Unit-test the config class with `described_class.new(key: value)` and `with_env`.
151
+ - For code that uses a config, inject a config instance or stub the singleton method.
152
+ - Do not set `ENV` directly in tests.
153
+
154
+ ## 9. Debug a wrong value
155
+
156
+ When a config value is not what you expect, do not guess. Ask the config where the value came from:
157
+
158
+ ```sh
159
+ bin/rails runner 'pp StripeConfig.new'
160
+ ```
161
+
162
+ ```
163
+ #<StripeConfig
164
+ config_name="stripe"
165
+ env_prefix="STRIPE"
166
+ values:
167
+ currency => "eur" (type=env key=STRIPE_CURRENCY),
168
+ api_key => "sk_test_..." (type=credentials store=config/credentials/development.yml.enc),
169
+ webhook_secret => "whsec_..." (type=yml path=./config/stripe.yml)>
170
+ ```
171
+
172
+ `StripeConfig.new.to_source_trace` returns the same data as a hash.
173
+ Use `.new`: the singleton keeps the values it loaded when the process started.
174
+ The output shows secrets in clear text. Do not copy it into commits, issues or reports.
175
+
176
+ If the trace does not explain the value, check these in order:
177
+
178
+ 1. Priority: defaults → YAML (+ `.local.yml`) → credentials (+ local) → ENV → explicit values.
179
+ A higher source wins, so a stale ENV variable hides a YAML change.
180
+ 2. Names: `env_prefix` and `config_name` in the output. `FlyDeploy::Config` without
181
+ `config_name` reads `FLYDEPLOY_*` and `config/flydeploy.yml`.
182
+ 3. Environment: YAML with environment keys needs a section for the current `Rails.env`.
183
+ Local files load in development only.
184
+ 4. Typos: a misspelled key in YAML or credentials is ignored silently.
185
+ Turn on `enforce_config_schema` to raise on unknown keys.
186
+ 5. Types: ENV values are auto-cast (`"0123"` → `123`). The trace shows the raw string,
187
+ the reader returns the cast value. Fix with `coerce_types key: :string`.
188
+
189
+ ## 10. Gem features at a glance
190
+
191
+ The README documents each feature. `references/docs.md` maps tasks to README sections.
192
+
193
+ | Feature | Use it for |
194
+ |---|---|
195
+ | `attr_config`, `required`, `on_load`, `coerce_types` | Schema, validation, types |
196
+ | Local files (`*.local.yml`, local credentials) | Personal development overrides |
197
+ | `enforce_config_schema` (2.9+) | Raise or warn on unknown keys in YAML and credentials |
198
+ | Source tracing (`pp`, `to_source_trace`) | Find where a value came from |
199
+ | Generators (`anyway:install`, `anyway:config`) | New base class and config classes |
200
+ | `with_env` test helper | Set ENV in a block, with the Anyway ENV cache reset |
201
+ | Doppler, EJSON, custom loaders | More sources; ENV stays the last loader |
202
+ | `Anyway::Config.for(:name)` | Read a config without a class (prefer a class) |
203
+ | `to_rbs`, bundled signatures | Type-check configs with RBS and Steep |
204
+ | OptionParser integration | Build CLI options from a config (plain Ruby tools) |
205
+
206
+ ## 11. Do not
207
+
208
+ - Do not read `ENV` or credentials outside config classes, except for infrastructure (§2).
209
+ - Do not commit secrets to `config/<name>.yml`. Development and test fakes are fine.
210
+ - Do not add a config class for one value that another config already covers.
211
+ - Do not mutate a config at runtime to change behavior. Pass a new instance instead.
212
+ - Do not reference a config from `app/configs/` or an in-place class during boot.
213
+ Move it to `config/configs/`.
214
+ - Do not add an env-specific `if` to a config method when a YAML value can express the difference.
215
+
216
+ ## 12. Report
217
+
218
+ When you finish, list:
219
+
220
+ - each config class you added or changed, and its location;
221
+ - each new parameter with its source (YAML, credentials, ENV) and its ENV variable name;
222
+ - the values that operators must set before a deployment (new required secrets);
223
+ - the direct `ENV` / credentials / `Rails.env` reads that you removed.
@@ -0,0 +1,30 @@
1
+ # Where to find the API
2
+
3
+ The README is the API reference: https://github.com/palkan/anyway_config#readme.
4
+ The installed copy is at `$(bundle info anyway_config --path)/README.md`.
5
+ Read the section that you need. The source is in `lib/anyway/` of the same directory.
6
+
7
+ | Task | README section |
8
+ |---|---|
9
+ | Declare attributes, defaults, boolean predicates, override readers with `super` | [Configuration classes](https://github.com/palkan/anyway_config#configuration-classes) |
10
+ | Change the config name or the ENV prefix (`config_name`, `env_prefix`) | [Config name](https://github.com/palkan/anyway_config#config-name), [Customize env variable names prefix](https://github.com/palkan/anyway_config#customize-env-variable-names-prefix) |
11
+ | Pass explicit values, reload a config | [Explicit values](https://github.com/palkan/anyway_config#explicit-values), [Reload configuration](https://github.com/palkan/anyway_config#reload-configuration) |
12
+ | Load a config without a class (`Anyway::Config.for(:name)`) | [Dynamic configuration](https://github.com/palkan/anyway_config#dynamic-configuration) |
13
+ | `required` (with `env:`), `on_load`, `raise_validation_error`, `ANYWAY_SUPPRESS_VALIDATIONS` | [Validation and callbacks](https://github.com/palkan/anyway_config#validation-and-callbacks) |
14
+ | Detect typos in YAML and credentials (`enforce_config_schema`, `rescue_unknown_attributes_with`) | [Enforcing config schema](https://github.com/palkan/anyway_config#enforcing-config-schema) |
15
+ | Source priority in Rails, YAML lookup path, `<NAME>_CONF`, credentials | [Data population](https://github.com/palkan/anyway_config#data-population) |
16
+ | `default:` key, environment-free YAML, `known_environments` | [Multi-env configuration](https://github.com/palkan/anyway_config#multi-env-configuration) |
17
+ | `config/configs` vs `app/configs`, static configs, inflections | [Organizing configs](https://github.com/palkan/anyway_config#organizing-configs) |
18
+ | `anyway:install`, `anyway:config`, `anyway:app_config` generators | [Generators](https://github.com/palkan/anyway_config#generators) |
19
+ | Use configs in `config/puma.rb` or before Rails loads | [Loading Anyway Config before Rails](https://github.com/palkan/anyway_config#loading-anyway-config-before-rails) |
20
+ | Plain Ruby apps and gems | [Using with Ruby](https://github.com/palkan/anyway_config#using-with-ruby) |
21
+ | ENV names, nested keys (`__`), auto-casting, arrays | [Environment variables](https://github.com/palkan/anyway_config#environment-variables) |
22
+ | `coerce_types`, built-in types, custom deserializers, `disable_auto_cast!` | [Type coercion](https://github.com/palkan/anyway_config#type-coercion) |
23
+ | `*.local.yml`, local credentials | [Local files](https://github.com/palkan/anyway_config#local-files) |
24
+ | Doppler, EJSON, custom loaders, loader order | [Data loaders](https://github.com/palkan/anyway_config#data-loaders) |
25
+ | Find where a value came from (`to_source_trace`, `pp config`) | [Tracing](https://github.com/palkan/anyway_config#tracing) |
26
+ | Pattern matching on configs | [Pattern matching](https://github.com/palkan/anyway_config#pattern-matching) |
27
+ | `with_env` and other test helpers | [Test helpers](https://github.com/palkan/anyway_config#test-helpers) |
28
+ | Build a CLI from a config (`OptionParser`) | [OptionParser integration](https://github.com/palkan/anyway_config#optionparser-integration) |
29
+ | RBS signatures for config classes | [RBS support](https://github.com/palkan/anyway_config#rbs-support) |
30
+
@@ -0,0 +1,341 @@
1
+ # Organizing configuration in a Rails app
2
+
3
+ ## Contents
4
+
5
+ 1. Layers
6
+ 2. File layout
7
+ 3. Base class and access
8
+ 4. Boot integration
9
+ 5. Sources in practice
10
+ 6. In-place config classes
11
+ 7. Shared config names
12
+ 8. Recommended settings
13
+
14
+ ## 1. Layers
15
+
16
+ ```
17
+ Application code (models, services, controllers, initializers)
18
+ │ depends on
19
+ ▼
20
+ Config classes (schema: names, defaults, types, validation, predicates)
21
+ │ loaded from
22
+ ▼
23
+ Sources (YAML, credentials, ENV, Doppler, EJSON, custom loaders)
24
+ ```
25
+
26
+ Sources are infrastructure. You can move a value from YAML to credentials to ENV,
27
+ and the application code does not change.
28
+
29
+ ## 2. File layout
30
+
31
+ ```
32
+ config/
33
+ ├── application.rb # assigns configs to Rails.application.config
34
+ ├── configs/ # static configs: available at boot
35
+ │ ├── application_config.rb
36
+ │ ├── app_config.rb
37
+ │ ├── smtp_config.rb
38
+ │ └── stripe_config.rb
39
+ ├── app.yml # non-secret values, per environment
40
+ ├── smtp.yml
41
+ ├── app.local.yml # personal overrides (git-ignored)
42
+ └── credentials/
43
+ ├── production.yml.enc # secrets
44
+ ├── development.yml.enc
45
+ └── local.yml.enc # personal secrets (git-ignored)
46
+ app/services/fly_deploy/
47
+ ├── client.rb
48
+ └── config.rb # in-place config: FlyDeploy::Config
49
+ ```
50
+
51
+ `config/configs/` is the default value of `config.anyway_config.autoload_static_config_path`.
52
+ Anyway Config autoloads it before the initializers run. Rails does not reload it in development.
53
+
54
+ `app/configs/` is a normal autoload path. Its classes reload in development,
55
+ but they are not available in `config/application.rb` or before the app finishes initialization.
56
+
57
+ Set `autoload_static_config_path = "app/configs"` to keep all configs in `app/configs/`.
58
+
59
+ ### Custom inflections
60
+
61
+ Static configs load before `config/initializers/inflections.rb`.
62
+ If a class name needs an acronym (`SMTPConfig`), require the inflections first (README, issue #81):
63
+
64
+ ```ruby
65
+ # config/application.rb
66
+ require_relative "boot"
67
+ require "rails/all"
68
+ require_relative "initializers/inflections"
69
+ ```
70
+
71
+ ## 3. Base class and access
72
+
73
+ The generator creates a singleton base class:
74
+
75
+ ```ruby
76
+ class ApplicationConfig < Anyway::Config
77
+ class << self
78
+ delegate_missing_to :instance
79
+
80
+ private
81
+
82
+ def instance
83
+ @instance ||= new
84
+ end
85
+ end
86
+ end
87
+ ```
88
+
89
+ Callers write `StripeConfig.secret_key`. The instance loads once per process.
90
+
91
+ - Use the class methods in application code.
92
+ - Use `StripeConfig.new` when you need a fresh read (a script, a test, an initializer that must not
93
+ keep the singleton).
94
+ - Use `StripeConfig.new(key: value)` to build a config with explicit values. Other sources do not
95
+ override explicit values.
96
+
97
+ ## 4. Boot integration
98
+
99
+ Expose the configs that Rails itself needs on `Rails.application.config`.
100
+ Then configure the framework from them in one place:
101
+
102
+ ```ruby
103
+ # config/application.rb
104
+ module MyApp
105
+ class Application < Rails::Application
106
+ config.app = AppConfig
107
+ config.mailer = MailerConfig
108
+ config.smtp = SMTPConfig
109
+
110
+ if config.app.host
111
+ config.action_controller.default_url_options =
112
+ config.action_mailer.default_url_options = {
113
+ host: config.app.host,
114
+ port: config.app.port,
115
+ protocol: config.app.ssl? ? "https" : "http"
116
+ }
117
+ config.action_controller.asset_host = config.app.asset_host
118
+ end
119
+
120
+ config.action_mailer.default_options = config.mailer.to_default_options
121
+
122
+ if config.smtp.configured?
123
+ config.action_mailer.delivery_method = :smtp
124
+ config.action_mailer.smtp_settings = config.smtp.to_settings
125
+ end
126
+ end
127
+ end
128
+ ```
129
+
130
+ The config class owns the shape (`to_settings`, `to_default_options`).
131
+ `config/application.rb` only connects it to Rails.
132
+
133
+ ### Initializers
134
+
135
+ Turn an integration on when its settings are present. Do not check the environment:
136
+
137
+ ```ruby
138
+ # config/initializers/omniauth.rb
139
+ Rails.application.config.middleware.use OmniAuth::Builder do
140
+ if GithubConfig.oauth_configured?
141
+ provider :github, GithubConfig.oauth_key, GithubConfig.oauth_secret,
142
+ scope: GithubConfig.oauth_scope
143
+ end
144
+ end
145
+ ```
146
+
147
+ ```ruby
148
+ # config/initializers/litestream.rb
149
+ Rails.application.configure do
150
+ litestream = LitestreamConfig.new
151
+ next unless litestream.configured?
152
+
153
+ config.litestream.replica_bucket = litestream.bucket
154
+ config.litestream.replica_key_id = litestream.access_key_id
155
+ config.litestream.replica_access_key = litestream.secret_access_key
156
+ end
157
+ ```
158
+
159
+ ### Gems that read only ENV
160
+
161
+ Copy the value from the config once, in the gem's initializer:
162
+
163
+ ```ruby
164
+ # config/initializers/pay.rb
165
+ # Pay reads Stripe keys from ENV, but we keep them in StripeConfig
166
+ ENV["STRIPE_PRIVATE_KEY"] ||= StripeConfig.secret_key
167
+ ```
168
+
169
+ ### Routes
170
+
171
+ Configs can gate routes, too: `mount Litestream::Engine, at: "/litestream" if LitestreamConfig.configured?`.
172
+
173
+ ## 5. Sources in practice
174
+
175
+ ### YAML: non-secret values per environment
176
+
177
+ ```yaml
178
+ # config/app.yml
179
+ default: &default
180
+ host: localhost
181
+ port: 3000
182
+
183
+ development:
184
+ <<: *default
185
+
186
+ test:
187
+ <<: *default
188
+ port: 3002
189
+
190
+ production:
191
+ host: example.com
192
+ port: 443
193
+ ```
194
+
195
+ - With `config.anyway_config.default_environmental_key = "default"`, Anyway Config deep-merges
196
+ `default:` into each environment. Then you do not need YAML anchors.
197
+ - With `config.anyway_config.future.use :unwrap_known_environments`, a file without environment keys
198
+ applies to all environments.
199
+ - A section can be absent. For example, `smtp.yml` can define `production:` only.
200
+ - Fake values for development and test belong here: a stub API URL, `api_token: "test-token"`.
201
+ - A production section that omits a secret makes `required` fail fast if the secret is missing
202
+ from credentials and ENV.
203
+
204
+ ### Credentials: secrets
205
+
206
+ ```yaml
207
+ # bin/rails credentials:edit --environment production
208
+ stripe:
209
+ secret_key: sk_live_...
210
+ signing_secret: whsec_...
211
+ smtp:
212
+ username: ...
213
+ password: ...
214
+ ```
215
+
216
+ The top-level key is the config name. The `stripe:` keys map to `StripeConfig` attributes.
217
+
218
+ ### ENV: deployment-time values
219
+
220
+ `STRIPE_SECRET_KEY` sets `StripeConfig#secret_key`. ENV has the highest priority
221
+ of the standard sources, so an operator can override any value without a deploy.
222
+
223
+ - Nested keys use a double underscore: `APP_SMTP__PORT` → `app.smtp["port"]`.
224
+ - Values are auto-cast: `"true"` → `true`, `"42"` → `42`, `"a,b"` → `["a", "b"]`.
225
+ Use `coerce_types` to change or fix the type.
226
+
227
+ ### Local files: personal development overrides
228
+
229
+ `config/app.local.yml` (no environment keys) and `config/credentials/local.yml.enc`
230
+ load in development only. Use them for a personal token, a fork's repository name,
231
+ or a real API in place of the fake one. They must be git-ignored.
232
+
233
+ A local YAML file supports ERB. Use it to switch between a fake and a real service:
234
+
235
+ ```yaml
236
+ # config/fly_deploy.local.yml
237
+ <% if ENV["REAL_FLY"] %>
238
+ rest_api_url: https://api.machines.dev/v1
239
+ org_slug: my-org
240
+ <% end %>
241
+ ```
242
+
243
+ ### Data that stays outside configs
244
+
245
+ - Process and infrastructure settings: `PORT`, `RAILS_MAX_THREADS`, `WEB_CONCURRENCY`,
246
+ `DATABASE_URL`, `RAILS_LOG_LEVEL`. Rails, Puma and the database adapter read them directly.
247
+ - Values that a third-party binary reads from its own ENV variables.
248
+
249
+ ## 6. In-place config classes
250
+
251
+ Put a config next to the code that uses it when only one module needs it.
252
+ This keeps a module (or a packwerk pack, or an engine) self-contained.
253
+
254
+ ```ruby
255
+ # app/services/fly_deploy/client.rb
256
+ module FlyDeploy
257
+ class Client
258
+ attr_reader :config
259
+
260
+ delegate :api_token, :rest_api_url, to: :config
261
+
262
+ def initialize(config = FlyDeploy::Config)
263
+ @config = config
264
+ end
265
+ end
266
+ end
267
+ ```
268
+
269
+ - Set `config_name` explicitly. For `FlyDeploy::Config`, the inferred name is `flydeploy`.
270
+ - The class autoloads with the rest of `app/`. Do not use it in `config/application.rb` outside
271
+ a `config.after_initialize` or `server do` block.
272
+ - If the module grows and boot code needs the config, move the class to `config/configs/`.
273
+
274
+ A config can also be nested inside the model that uses it:
275
+
276
+ ```ruby
277
+ class License::GithubAccess
278
+ class Config < ApplicationConfig
279
+ config_name :github
280
+
281
+ attr_config :token
282
+
283
+ def enabled? = token.present?
284
+ end
285
+
286
+ def initialize(repo:, config: Config)
287
+ @repo = repo
288
+ @config = config
289
+ end
290
+
291
+ def grant(user)
292
+ unless config.enabled?
293
+ Rails.logger.warn("GitHub access is not configured")
294
+ return
295
+ end
296
+
297
+ GithubClient.new(config.token).add_collaborator(@repo, user.github_login)
298
+ end
299
+ end
300
+ ```
301
+
302
+ ## 7. Shared config names
303
+
304
+ Two classes can read the same config name. In the example above, `License::GithubAccess::Config`
305
+ and `GithubConfig` both read `config/github.yml`, `credentials.github` and `GITHUB_*`.
306
+ Each class sees only its own attributes.
307
+
308
+ With `enforce_config_schema` on, each class reports the other class's keys as unknown.
309
+ Allow them with `rescue_unknown_attributes_with`:
310
+
311
+ ```ruby
312
+ Anyway::Settings.rescue_unknown_attributes_with = ->(config, _key) do
313
+ config.config_name == "github"
314
+ end
315
+ ```
316
+
317
+ ## 8. Recommended settings
318
+
319
+ Put the settings at the top of the application class, before any config is referenced.
320
+ Settings in `config/environments/*.rb` apply too late for configs that `application.rb` already loaded.
321
+
322
+ ```ruby
323
+ # config/application.rb
324
+ module MyApp
325
+ class Application < Rails::Application
326
+ # Deep-merge the `default:` section into each environment
327
+ config.anyway_config.default_environmental_key = "default"
328
+ # Pass all future features in a single `use` call: each call resets the list
329
+ config.anyway_config.future.use :unwrap_known_environments, :reject_unknown_overrides
330
+ # Raise on unknown keys in YAML and credentials (typos). Requires 2.9+. Use :warn to only log
331
+ config.anyway_config.enforce_config_schema = true
332
+ config.anyway_config.known_environments << "staging"
333
+
334
+ config.app = AppConfig
335
+ # ...
336
+ end
337
+ end
338
+ ```
339
+
340
+ Asset precompilation in CI or Docker runs without secrets. `required` validations skip
341
+ automatically when `SECRET_KEY_BASE_DUMMY` is set. Otherwise, set `ANYWAY_SUPPRESS_VALIDATIONS=1`.
@@ -0,0 +1,139 @@
1
+ # Testing configuration
2
+
3
+ ## Contents
4
+
5
+ 1. Test values live in YAML
6
+ 2. Test the config class
7
+ 3. Test code that uses a config
8
+ 4. ENV in tests
9
+ 5. CI and asset precompilation
10
+
11
+ ## 1. Test values live in YAML
12
+
13
+ Put deterministic values in the `test:` section of `config/<name>.yml`:
14
+
15
+ ```yaml
16
+ # config/fly_deploy.yml
17
+ test:
18
+ rest_api_url: http://localhost:3002/fake_fly
19
+ api_token: test-token
20
+ org_slug: test-org
21
+ ```
22
+
23
+ - The test suite then needs no secrets and no credentials key.
24
+ - Integrations that use a `configured?` predicate stay off in test when their secrets are absent.
25
+ - Use obviously fake values (`test-token`, `sk_test_fake`). Never copy real secrets.
26
+
27
+ ## 2. Test the config class
28
+
29
+ Test the logic that you added: predicates, derived values, adapters, validations.
30
+ Do not test that Anyway Config reads YAML.
31
+
32
+ Build an instance with explicit values. Other sources do not override them:
33
+
34
+ ```ruby
35
+ # spec/configs/smtp_config_spec.rb
36
+ RSpec.describe SMTPConfig do
37
+ subject(:config) { described_class.new(address: "smtp.example.com", port: 587, username: "u") }
38
+
39
+ it { is_expected.to be_configured }
40
+
41
+ specify do
42
+ expect(config.to_settings).to include(address: "smtp.example.com", user_name: "u")
43
+ end
44
+
45
+ context "without address" do
46
+ subject(:config) { described_class.new(address: nil) }
47
+
48
+ it { is_expected.not_to be_configured }
49
+ end
50
+ end
51
+ ```
52
+
53
+ Check the ENV mapping and type coercion with `with_env`.
54
+ The helper is included automatically for specs in `spec/configs/` or with `type: :config`:
55
+
56
+ ```ruby
57
+ RSpec.describe AnycableProConfig, type: :config do
58
+ it "parses a comma-separated safelist" do
59
+ with_env("ANYCABLE_PRO_GITHUB_SAFELIST" => "alice,bob") do
60
+ expect(described_class.new.github_safelist).to eq(%w[alice bob])
61
+ end
62
+ end
63
+ end
64
+ ```
65
+
66
+ `with_env` sets the variables, clears the Anyway ENV cache and restores both after the block.
67
+ Pass `nil` to delete a variable.
68
+ Elsewhere, require `"anyway/testing/helpers"` and include `Anyway::Testing::Helpers`
69
+ (this also works for Minitest).
70
+
71
+ Check `required` validations:
72
+
73
+ ```ruby
74
+ it "requires the magic link key" do
75
+ expect { described_class.new(magic_link_key: "") }
76
+ .to raise_error(Anyway::Config::ValidationError, /magic_link_key/)
77
+ end
78
+ ```
79
+
80
+ `required ..., env:` checks the environment once, when the class loads.
81
+ A stub in a test cannot change it, so do not test environment-scoped `required` in unit tests.
82
+
83
+ Check an environment-dependent default:
84
+
85
+ ```ruby
86
+ it "enables analytics in production by default" do
87
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
88
+
89
+ expect(described_class.new(analytics_enabled: nil)).to be_analytics_enabled
90
+ end
91
+ ```
92
+
93
+ ## 3. Test code that uses a config
94
+
95
+ ### Inject a config
96
+
97
+ If the class takes the config as an argument, pass an instance with explicit values.
98
+ This is the preferred way:
99
+
100
+ ```ruby
101
+ let(:config) { FlyDeploy::Config.new(api_token: "t", rest_api_url: "http://fake") }
102
+ let(:client) { FlyDeploy::Client.new(config) }
103
+ ```
104
+
105
+ ### Stub the singleton
106
+
107
+ If the code calls the singleton (`AppConfig.cables_quota`), stub the class method.
108
+ `delegate_missing_to` defines `respond_to_missing?`, so verified partial doubles accept the stub:
109
+
110
+ ```ruby
111
+ before { allow(AppConfig).to receive(:analytics_enabled?).and_return(true) }
112
+ ```
113
+
114
+ Read expected values from the config instead of hard-coding them:
115
+
116
+ ```ruby
117
+ expect(account.cables_quota).to eq(AppConfig.cables_quota)
118
+ ```
119
+
120
+ ### Do not mutate the singleton
121
+
122
+ Do not assign attributes on the shared instance (`AppConfig.cables_quota = 10`).
123
+ The value leaks into other tests. Stub the method or inject an instance.
124
+
125
+ ## 4. ENV in tests
126
+
127
+ - The singleton loads once per process. `with_env` does not change it.
128
+ Use `with_env` together with `described_class.new`, not with `AppConfig.some_value`.
129
+ - Do not assign `ENV[...]` directly. Anyway Config caches parsed ENV,
130
+ and a missed restore leaks into other tests. Use `with_env`.
131
+ - A gem that reads `ENV` itself (for example, Pay) is outside Anyway Config.
132
+ Set and restore its variable in an `around` hook.
133
+
134
+ ## 5. CI and asset precompilation
135
+
136
+ - `bin/rails assets:precompile` with `SECRET_KEY_BASE_DUMMY=1` skips `required` validations.
137
+ - In other jobs without secrets, set `ANYWAY_SUPPRESS_VALIDATIONS=1`.
138
+ - Turn on `config.anyway_config.enforce_config_schema = true` so that the test suite
139
+ fails on a mistyped key in YAML or credentials.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anyway_config
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.0
4
+ version: 2.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-20 00:00:00.000000000 Z
11
+ date: 2026-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-next-core
@@ -197,6 +197,10 @@ files:
197
197
  - lib/rails/commands/local_credentials/local_credentials_command.rb
198
198
  - sig/anyway_config.rbs
199
199
  - sig/manifest.yaml
200
+ - skills/anyway-config/SKILL.md
201
+ - skills/anyway-config/references/docs.md
202
+ - skills/anyway-config/references/organizing.md
203
+ - skills/anyway-config/references/testing.md
200
204
  homepage: http://github.com/palkan/anyway_config
201
205
  licenses:
202
206
  - MIT
@@ -207,6 +211,8 @@ metadata:
207
211
  homepage_uri: http://github.com/palkan/anyway_config
208
212
  source_code_uri: http://github.com/palkan/anyway_config
209
213
  funding_uri: https://github.com/sponsors/palkan
214
+ hyperdrive_targets: railties
215
+ hyperdrive_artifacts: skill
210
216
  post_install_message:
211
217
  rdoc_options: []
212
218
  require_paths: