| name | anyway-config-coder |
| description | Implement type-safe configuration with anyway_config gem. Use when creating configuration classes, replacing ENV access, or managing application settings. Triggers on configuration, environment variables, settings, secrets, or ENV patterns. |
Anyway Config Coder
Implement type-safe configuration management using the anyway_config gem. Never access ENV directly - wrap all configuration in typed classes.
Core Principle
Never use ENV directly or Rails credentials. Create typed configuration classes instead.
# WRONG - scattered ENV access
api_key = ENV["GEMINI_API_KEY"]
timeout = ENV.fetch("API_TIMEOUT", 30).to_i
# RIGHT - typed configuration class
class GeminiConfig < Anyway::Config
attr_config :api_key,
timeout: 30
required :api_key
end
# Usage
GeminiConfig.new.api_key
Setup
# Gemfile
gem "anyway_config", "~> 2.6"
Configuration Class Structure
Basic Configuration
# config/configs/gemini_config.rb
class GeminiConfig < Anyway::Config
# Define attributes with defaults
attr_config :api_key,
model: "gemini-pro",
timeout: 30,
max_retries: 3
# Mark required attributes
required :api_key
# Computed helpers
def configured?
api_key.present?
end
def base_url
"https://generativelanguage.googleapis.com/v1beta"
end
end
Environment Variable Mapping
Anyway Config automatically maps environment variables:
class GeminiConfig < Anyway::Config
attr_config :api_key # GEMINI_API_KEY
attr_config :model # GEMINI_MODEL
attr_config :timeout # GEMINI_TIMEOUT
end
# Custom prefix
class StripeConfig < Anyway::Config
config_name :payment # Uses PAYMENT_* prefix instead of STRIPE_*
attr_config :api_key, # PAYMENT_API_KEY
:webhook_secret
end
Nested Configuration
class AppConfig < Anyway::Config
attr_config :name,
:environment,
database: {
host: "localhost",
port: 5432,
pool: 5
},
redis: {
url: "redis://localhost:6379"
}
end
# Access nested values
AppConfig.new.database.host
AppConfig.new.redis.url
Directory Structure
config/
├── configs/
│ ├── gemini_config.rb
│ ├── stripe_config.rb
│ ├── storage_config.rb
│ └── app_config.rb
└── settings/ # YAML files (optional)
├── gemini.yml
└── storage.yml
YAML Configuration Files
# config/settings/gemini.yml
default: &default
model: gemini-pro
timeout: 30
development:
<<: *default
api_key: <%= ENV["GEMINI_API_KEY"] %>
test:
<<: *default
api_key: test-key
production:
<<: *default
timeout: 60
Using Configurations
Direct Instantiation
config = GeminiConfig.new
config.api_key
config.timeout
Singleton Pattern (Recommended)
class GeminiConfig < Anyway::Config
attr_config :api_key, :model
class << self
def instance
@instance ||= new
end
end
end
# Usage anywhere
GeminiConfig.instance.api_key
Memoized Helper Method
# app/models/concerns/gemini_client.rb
module GeminiClient
extend ActiveSupport::Concern
private
def gemini_config
@gemini_config ||= GeminiConfig.new
end
end
In Jobs/Services
class Cloud::CardGenerator
def initialize(cloud)
@cloud = cloud
@config = GeminiConfig.new
end
def generate
return unless @config.configured?
client = Gemini::Client.new(
api_key: @config.api_key,
timeout: @config.timeout
)
# ...
end
end
Validation
class StorageConfig < Anyway::Config
attr_config :bucket,
:region,
:access_key_id,
:secret_access_key
# Required attributes
required :bucket, :region
# Conditional requirements
required :access_key_id, :secret_access_key, env: :production
# Custom validation
def validate!
super
raise_validation_error("Invalid region") unless valid_regions.include?(region)
end
private
def valid_regions
%w[us-east-1 us-west-2 eu-west-1]
end
end
Type Coercion
class ApiConfig < Anyway::Config
# Automatic coercion
attr_config timeout: 30 # Integer
attr_config enabled: true # Boolean
attr_config rate: 1.5 # Float
# Coerce arrays from comma-separated strings
coerce_types allowed_origins: {
type: :string,
array: true
}
# ALLOWED_ORIGINS="example.com,other.com" => ["example.com", "other.com"]
end
Testing Configurations
# spec/configs/gemini_config_spec.rb
RSpec.describe GeminiConfig do
subject(:config) { described_class.new }
describe "defaults" do
it "has default timeout" do
expect(config.timeout).to eq(30)
end
it "has default model" do
expect(config.model).to eq("gemini-pro")
end
end
describe "validation" do
it "requires api_key" do
expect { described_class.new(api_key: nil) }
.to raise_error(Anyway::Config::ValidationError)
end
end
describe "#configured?" do
context "with api_key" do
subject(:config) { described_class.new(api_key: "test") }
it "returns true" do
expect(config.configured?).to be true
end
end
end
end
Override in Tests
# spec/support/anyway_config.rb
RSpec.configure do |config|
config.around(:each) do |example|
# Override config for test
with_env(
"GEMINI_API_KEY" => "test-key",
"GEMINI_TIMEOUT" => "5"
) do
example.run
end
end
end
Common Patterns
Feature Flags
class FeaturesConfig < Anyway::Config
attr_config dark_mode: false,
beta_features: false,
maintenance_mode: false
def maintenance?
maintenance_mode
end
def beta?
beta_features
end
end
External API Client
class OpenAIConfig < Anyway::Config
attr_config :api_key,
:organization_id,
model: "gpt-4",
max_tokens: 1000,
temperature: 0.7
required :api_key
def client_options
{
access_token: api_key,
organization_id: organization_id
}.compact
end
end
Multi-Environment Storage
class StorageConfig < Anyway::Config
attr_config provider: "local",
bucket: nil,
endpoint: nil,
credentials: {}
def s3?
provider == "s3"
end
def r2?
provider == "r2"
end
def local?
provider == "local"
end
def service_options
case provider
when "s3" then s3_options
when "r2" then r2_options
else local_options
end
end
end
Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|---|---|
Direct ENV["KEY"] |
No type safety, scattered | Config class |
ENV.fetch everywhere |
Duplication, no validation | Centralized config |
| Rails credentials | Complex, hard to test | anyway_config classes |
| Hardcoded secrets | Security risk | Environment variables |
| Magic strings | Typos, no IDE support | Config constants |
Quick Reference
# Create config class
class MyConfig < Anyway::Config
attr_config :required_key, # Required
optional: "default" # With default
required :required_key
end
# Environment variables
MY_REQUIRED_KEY=value # Mapped automatically
MY_OPTIONAL=override # Overrides default
# Usage
config = MyConfig.new
config.required_key # => "value"
config.optional # => "override"
Detailed References
references/advanced-patterns.md- Dynamic configs, callbacks, inheritance