ConstConf

ConstConf: A robust, thread-safe configuration management library for Ruby applications.

Description

ConstConf provides a clean DSL for defining configuration settings with environment variable support, default values, required validation, decoding logic, and descriptive metadata. It offers file-based configuration plugins, XDG Base Directory Specification compliance, and seamless Rails integration.

Installation

To install ConstConf, you can use the following methods:

  1. Type
gem install const_conf

in your terminal.

  1. Or add the line
gem 'const_conf'

to your Gemfile and run bundle install in your terminal.

Usage

Define configuration in a module:

require 'const_conf'

module AppConfig
  include ConstConf

  plugin ConstConf::FilePlugin # Use FilePlugin
  plugin ConstConf::DirPlugin  # Use DirPlugin

  description 'Application Configuration'
  prefix '' # Use empty prefix for flat environment variable names

  # Simple environment variable setting
  DATABASE_URL = set do
    description 'Database connection string'
    required true
    sensitive true
  end

  # File-based configuration
  API_KEY = set do
    prefix 'CONFIG'
    description 'API key from file'
    default file('config/api.key', required: true)
    sensitive true
    decode(&:chomp)
  end

  # Directory-based configuration with XDG support
  CONFIG_FROM_DIR = set do
    description 'Configuration directory path'
    default dir('myapp', 'config.yaml', env_var_name: 'XDG_CONFIG_HOME')
    decode { require 'yaml'; YAML.load(it) }
  end
end

# Access settings
puts AppConfig::DATABASE_URL     # From ENV['DATABASE_URL']
puts AppConfig::API_KEY          # Default file config/api.key, or ENV['CONFIG_API_KEY']
puts AppConfig::CONFIG_FROM_DIR  # From directory structure at ENV['XDG_CONFIG_HOME'] + "/myapp"
# =>
# postgresql://user:pass@localhost/myapp
# sk-1234567890abcdef1234567890abcdef
# {"host" => "localhost", "port" => 3000}

In Rails it is best to require your configuration in config/app_config.rb as soon as the bundled gems were loaded, so subsequent initiallizers can access the created setting constants: init

Bundler.require(*Rails.groups)
require_relative 'app_config.rb'

module MyApp

end

Note that Predicate Methods (?) are defined

If a setting is active?, the predicate method returns a truthy value, which then can be used like this:

# Check if active?
if database_url = AppConfig::DATABASE_URL?
  connect_to_database with: database_url
else
  STDERR.puts "No DATABASE_URL configured for app!"
end

Or nil is returned, which can then be handled accordingly.

Note that Getter Methods (!) are also defined

In addition to predicate methods, ConstConf automatically defines ! methods that return the actual ConstConf::Setting object. These are useful for:

  • Accessing setting metadata and properties directly
  • Calling methods like setting.view for detailed configuration inspection of a single setting.
  • Testing specific setting characteristics beyond just their values

For example:

# Returns the Setting object itself
db_setting = AppConfig::DATABASE_URL!

# Useful for debugging/inspection
db_setting.view  # Shows detailed configuration information

# Access setting properties directly
puts db_setting.description
puts db_setting.required?
puts db_setting.sensitive?

This complements the ? methods which return the actual configuration value when active, while the ! methods provide access to the setting object for more advanced use cases.

Configuration View Explanation

The AppConfig.view output shows the complete configuration hierarchy with detailed metadata for each setting. Sensitive values are displayed as 🤫 to protect confidential information like passwords and API keys.

Key indicators in the view:

Yes No Purpose
🔒 Sensitive data (e.g., passwords, tokens)
🤫 Suppressed output of value if sensitive
🔴 Required settings that must be configured
🔧 Configured values from ENV
🙈 ENV var was ignored
🟢 Setting is active (? method returns truty value)
⚙️ Decoding logic applied to transform raw input
✅ ┊ ☑️ Validation checks have passed or failed

The view helps developers understand how configuration is resolved from multiple sources and validate that settings are properly configured while protecting sensitive data.

AppConfig # Application Configuration
├─ prefix ""
├─ 3 settings
├─ AppConfig::DATABASE_URL # Database connection string
│  ├─ prefix          ""
│  ├─ env var name    DATABASE_URL
│  ├─ env var (orig.) 🤫
│  ├─ default         🤫
│  ├─ value           🤫
│  ├─ sensitive       🔒
│  ├─ required        🔴
│  ├─ configured      🔧
│  ├─ ignored         ⚪
│  ├─ active          🟢
│  ├─ decoding        ⚪
│  └─ checked         ☑️
├─ AppConfig::API_KEY # API key from file
│  ├─ prefix          "CONFIG"
│  ├─ env var name    CONFIG_API_KEY
│  ├─ env var (orig.) 🤫
│  ├─ default         🤫
│  ├─ value           🤫
│  ├─ sensitive       🔒
│  ├─ required        ⚪
│  ├─ configured      ⚪
│  ├─ ignored         ⚪
│  ├─ active          🟢
│  ├─ decoding        ⚙️
│  └─ checked         ☑️
└─ AppConfig::CONFIG_FROM_DIR # Configuration directory path
   ├─ prefix          ""
   ├─ env var name    CONFIG_FROM_DIR
   ├─ env var (orig.) nil
   ├─ default         "host: 'localhost'\nport: 3000\n"
   ├─ value           {"host" => "localhost", "port" => 3000}
   ├─ sensitive       ⚪
   ├─ required        ⚪
   ├─ configured      ⚪
   ├─ ignored         ⚪
   ├─ active          🟢
   ├─ decoding        ⚙️
   └─ checked         ☑️

Use of Plugins

ConstConf provides extensible plugin architecture for adding configuration sources and behaviors, as can be seen in the example above. The library includes two main built-in plugins:

graph TD
    A[ConstConf Module] --> B[set do Block]
    A --> C[Plugin System]

    C --> D[FilePlugin]
    C --> E[DirPlugin]
    C --> F[JSONPlugin]
    C --> G[YAMLPlugin]
    C --> H[Custom Plugins]

    B --> I[DSL Methods]
    I --> J[file method]
    I --> K[dir method]
    I --> L[json method]
    I --> M[yaml method]
    I --> N[Core DSL methods]

    D --> O[File-based Configuration]
    E --> P[Directory-based Configuration]
    F --> Q[JSON Configuration]
    G --> R[YAML Configuration]
    H --> S[Custom Configuration Sources]

    O --> T[File Reading Logic]
    P --> U[Directory Traversal Logic]
    Q --> V[JSON Parsing Logic]
    R --> W[YAML Parsing Logic]
    S --> X[Custom Logic]

    I --> Y[Configuration Resolution Process]
    Y --> Z[ENV Variable Lookup]
    Y --> AA[Default Value Handling]
    Y --> AB[Validation & Processing]

    style A fill:#e1f5fe
    style B fill:#f3e5f5
    style C fill:#e8f5e9
    style D fill:#fff3e0
    style E fill:#fce4ec
    style F fill:#f1f8e9
    style G fill:#e0f2f1
    style H fill:#f1f8e9
    style I fill:#fafafa
    style Y fill:#ffebee

FilePlugin

Enables file-based configuration through the file() method:

API_KEY = set do
  default file('config/api.key', required: true)
  
end

DirPlugin

Enables directory-based configuration with XDG compliance through the dir() method:

CONFIG_FROM_DIR = set do
  default dir('myapp', 'config.yaml', env_var_name: 'XDG_CONFIG_HOME')
  
end

JSONPlugin

Enables JSON-based configuration through the json() method:

CONFIG = set do
  default json('config.json')
  # or with custom object class:
  # default json('config.json', object_class: MyCustomClass)
  
end

YAMLPlugin

Enables YAML-based configuration through the yaml() method:

CONFIG = set do
  default yaml('config.yaml')
  # or with environment-specific loading:
  # default yaml('config.yaml', env: true)
  # or with explicit environment:
  # default yaml('config.yaml', env: 'production')
  
end

Plugins are registered using the plugin method at the module level. Multiple plugins can be combined to provide flexible configuration sources.

When a plugin is loaded, it extends the DSL with additional methods and behaviors that can be used within set do blocks. The plugin system allows for easy extension of ConstConf's capabilities without modifying core functionality.

Configuration Concepts Explained

Description

  • Purpose: Human-readable explanation of what the setting is for (📝 is always required to be provided)
  • Implementation: The description accessor stores a string explaining the setting's purpose
  • Usage: description 'Database connection string'
  • Indicator: Shows in view output as descriptive text next to each setting

Prefix

  • Purpose: Namespace prefix used to construct environment variable names
  • Implementation: The prefix accessor determines how environment variables are named
  • Usage: prefix 'APP' makes API_KEY become APP_API_KEY
  • Indicator: Shows as prefix value in view output

Decoding (decoding?)

  • Purpose: Transforms raw input values using a Proc
  • Implementation: The decode accessor stores a Proc that processes the value
  • Usage: decode(&:chomp) removes whitespace, decode { YAML.load(it) } parses YAML
  • Indicator: Shows ⚙️ in view output when active

Required (required?)

  • Purpose: Determines if a setting must have a valid value
  • Implementation: Can be boolean (true/false) or Proc for conditional validation
  • Usage: required true (always required) or required { !Rails.env.test? } (conditional)
  • Indicator: Shows 🔴 in view output when required and not satisfied

Configured (configured?)

  • Purpose: Indicates whether a value was actually provided (not just default)
  • Implementation: Checks if environment variable is set or default is used
  • Usage: configured? returns true when ENV['VAR_NAME'] exists or default is non-nil
  • Indicator: Shows 🔧 in view output when value is explicitly provided

Checked (checked?)

  • Purpose: Validates that custom validation logic passes
  • Implementation: The check accessor stores validation logic as Proc
  • Usage: check ->(setting) { setting.value >= 1024 } for port validation
  • Indicator Logic:
    • ☑️ = No custom check defined (passes by default - :unchecked_true)
    • = Custom check explicitly passes (returns true)
    • = Custom check explicitly fails (returns false)

Active (active?)

  • Purpose: Determines if the setting should be considered usable/active and determines if the result of the AppConfig::FOOBAR? method is the AppConfig::FOOBAR value if true.
  • Implementation: Evaluates the activated property (defaults to :present?)
  • Usage: Can be true, false, Symbol, or Proc for custom activation logic
  • Indicator: Shows 🟢 in view output when active

Ignored (ignored?)

  • Purpose: Skips processing this setting during configuration resolution
  • Implementation: The ignored accessor prevents reading from ENV variables
  • Usage: Set to true to skip environment variable lookup for this setting
  • Indicator: Shows 🙈 in view output when ignored

These concepts work together to provide a comprehensive configuration management system that tracks the complete lifecycle and status of each setting from definition through validation and usage.

Advanced Usage Examples

Nested Configuration Modules

ConstConf supports elegant nested module organization where prefixes are automatically inherited and combined. When you define nested modules, the library automatically constructs full environment variable names by combining parent module prefixes with child module names. This creates a clean, hierarchical configuration structure that's both maintainable and predictable.

require 'const_conf'

module AppConfig # default prefix 'APP_CONFIG'
  include ConstConf

  description 'Application Configuration'

  module Database # default prefix 'APP_CONFIG_DATABASE'

    description 'Database settings'

    URL = set do # from ENV['APP_CONFIG_DATABASE_URL'] via default prefix
      description 'Database connection URL'
    end
  end
end

To access the nested configuration values in Ruby, you would reference them through the full module path:

# Access the database URL setting
db_url = AppConfig::Database::URL

# Use it in your application
connect_to_database(db_url)

# Check if it's active/available
if db_url = AppConfig::Database::URL?
  # Use the database connection
  connect_to_database(db_url)
end

This hierarchical approach makes configuration organization intuitive while maintaining clear access patterns through Ruby's constant resolution mechanism.

Validation and Checks

ConstConf provides powerful validation capabilities through custom check blocks that allow you to enforce business logic and data integrity rules. These checks are evaluated during configuration resolution and appear in the view output with visual indicators.

require 'const_conf'

module AppConfig
  include ConstConf

  description 'Application Configuration'
  prefix '' # Use empty prefix for flat environment variable names

  PORT = set do # from ENV['PORT']
    description 'Server port'
    default 3000
    decode(&:to_i)
    check { value >= 1024 } # Port must be >= 1024
  end

  HOST = set do # from ENV['APP_HOST']
    description 'Host name'
    prefix 'APP'
    required { !Rails.env.test? }
    check -> setting { setting.value.present? } # Must not be blank
  end
end

The output of AppConfig.view is:

AppConfig # Application Configuration
├─ prefix ""
├─ 2 settings
├─ AppConfig::PORT # Server port
│  ├─ prefix          ""
│  ├─ env var name    PORT
│  ├─ env var (orig.) nil
│  ├─ default         3000
│  ├─ value           3000
│  ├─ sensitive       ⚪
│  ├─ required        ⚪
│  ├─ configured      ⚪
│  ├─ ignored         ⚪
│  ├─ active          🟢
│  ├─ decoding        ⚙️
│  └─ checked         ✅
└─ AppConfig::HOST # Host name
   ├─ prefix          "APP"
   ├─ env var name    APP_HOST
   ├─ env var (orig.) "www.example.com"
   ├─ default         nil
   ├─ value           "www.example.com"
   ├─ sensitive       ⚪
   ├─ required        🔴
   ├─ configured      🔧
   ├─ ignored         ⚪
   ├─ active          🟢
   ├─ decoding        ⚪
   └─ checked         ✅
How Validation Works:
  • PORT: The check { value >= 1024 } ensures that if a PORT environment variable is set, it must be at least 1024 (a privileged port range)
  • HOST: The required { !Rails.env.test? } makes this setting required only in non-test environments, and the check validates that the value isn't blank

The validation system ensures that your configuration meets both technical requirements (like port ranges) and business rules (like non-empty hostnames), making it much harder to deploy applications with invalid configurations.

When validations fail during confirmation, ConstConf raises specific exceptions:

  • ConstConf::RequiredValueNotConfigured for missing required values
  • ConstConf::SettingCheckFailed for failed custom checks
  • These errors are raised immediately during configuration loading to prevent runtime issues

Complex Settings Examples

require 'const_conf'

module AppConfig
  include ConstConf

  description 'Application Configuration'

  # Version validation
  REVISION = set do # from ENV['REVISION']
    description 'Current software revision'
    prefix ''
    required { Rails.env.production? }
    check    { value.blank? || value.to_s =~ /\A\h{7}\z/ }
  end

  # Host validation with regex
  HOST = set do # from ENV['APP_CONFIG_HOST']
    description 'HOST name the application can be reached under'
    required { Rails.env.production? }
    check    { value.blank? || value =~ /\A[a-z\-]+\.[a-z\-\.]+\z/ && value.size <= 253 }
  end

  # Multi-value validation
  HOSTS_ALLOWED = set do # from ENV['APP_CONFIG_HOSTS_ALLOWED']
    description 'Connections under these hostnames are allowed in Rails.'
    default ''
    decode { it.split(?,).map(&:strip) }
    check  { value.all? { |host| host =~ /\A[a-z\-]+\.[a-z\-\.]+\z/ && host.size <= 253 } }
  end

  # Sensitive configuration with validation
  GITHUB_PERSONAL_ACCESS_TOKEN = set do # from ENV['GITHUB_PERSONAL_ACCESS_TOKEN']
    description 'GitHub Personal Access Token for repo access'
    prefix ''
    required { !Rails.env.test? }
    sensitive true
    check { value.to_s =~ /\Aghp_[A-Za-z0-9]{36}\z/ }
  end

  # URI validation
  REDIS_URL = set do # from ENV['REDIS_URL']
    description 'Redis server URL'
    prefix ''
    default 'redis://localhost:6379/1'
    sensitive true
    check { URI.parse(value).scheme == 'redis' rescue false }
  end
end

The output of AppConfig.view is:

AppConfig # Application Configuration
├─ prefix "APP_CONFIG"
├─ 5 settings
├─ AppConfig::REVISION # Current software revision
│  ├─ prefix          ""
│  ├─ env var name    REVISION
│  ├─ env var (orig.) "b781318"
│  ├─ default         nil
│  ├─ value           "b781318"
│  ├─ sensitive       ⚪
│  ├─ required        ⚪
│  ├─ configured      🔧
│  ├─ ignored         ⚪
│  ├─ active          🟢
│  ├─ decoding        ⚪
│  └─ checked         ✅
├─ AppConfig::HOST # HOST name the application can be reached under
│  ├─ prefix          "APP_CONFIG"
│  ├─ env var name    APP_CONFIG_HOST
│  ├─ env var (orig.) "www.example.com"
│  ├─ default         nil
│  ├─ value           "www.example.com"
│  ├─ sensitive       ⚪
│  ├─ required        ⚪
│  ├─ configured      🔧
│  ├─ ignored         ⚪
│  ├─ active          🟢
│  ├─ decoding        ⚪
│  └─ checked         ✅
├─ AppConfig::HOSTS_ALLOWED # Connections under these hostnames are allowed in Rails.
│  ├─ prefix          "APP_CONFIG"
│  ├─ env var name    APP_CONFIG_HOSTS_ALLOWED
│  ├─ env var (orig.) "www.example.com,example.com…
│  ├─ default         ""
│  ├─ value           ["www.example.com", "example…
│  ├─ sensitive       ⚪
│  ├─ required        ⚪
│  ├─ configured      🔧
│  ├─ ignored         ⚪
│  ├─ active          🟢
│  ├─ decoding        ⚙️
│  └─ checked         ✅
├─ AppConfig::GITHUB_PERSONAL_ACCESS_TOKEN # GitHub Personal Access Token for repo access
│  ├─ prefix          ""
│  ├─ env var name    GITHUB_PERSONAL_ACCESS_TOKEN
│  ├─ env var (orig.) 🤫
│  ├─ default         🤫
│  ├─ value           🤫
│  ├─ sensitive       🔒
│  ├─ required        🔴
│  ├─ configured      🔧
│  ├─ ignored         ⚪
│  ├─ active          🟢
│  ├─ decoding        ⚪
│  └─ checked         ✅
└─ AppConfig::REDIS_URL # Redis server URL
   ├─ prefix          ""
   ├─ env var name    REDIS_URL
   ├─ env var (orig.) 🤫
   ├─ default         🤫
   ├─ value           🤫
   ├─ sensitive       🔒
   ├─ required        ⚪
   ├─ configured      🔧
   ├─ ignored         ⚪
   ├─ active          🟢
   ├─ decoding        ⚪
   └─ checked         ✅
1. Version Validation (REVISION)
REVISION = set do
  description 'Current software revision'
  prefix ''
  required { Rails.env.production? }
  check    { value.blank? || value.to_s =~ /\A\h{7}\z/ }
end

Explanation:

  • Purpose: Validates that the revision is either blank (not set) or a valid 7-character hexadecimal string
  • Conditional Required: Only required in production environment
  • Validation Logic: Uses regex /\A\h{7}\z/ to match exactly 7 hexadecimal characters (0-9, a-f)
  • Indicator: Shows 🔴 when required and not satisfied, when valid
2. Host Validation with Regex (HOST)
HOST = set do
  description 'HOST name the application can be reached under'
  required { Rails.env.production? }
  check    { value.blank? || value =~ /\A[a-z\-]+\.[a-z\-\.]+\z/ && value.size <= 253 }
end

Explanation:

  • Purpose: Validates domain names with proper format and length constraints
  • Conditional Required: Production-only requirement
  • Regex Pattern:
    • /\A[a-z\-]+\.[a-z\-\.]+\z/ - Matches lowercase letters, hyphens, dots in hostnames
    • Ensures valid DNS format (e.g., "example.com")
  • Length Check: Maximum 253 characters per RFC 1035
  • Indicator: Shows validation status with visual cues
3. Multi-value Validation (HOSTS_ALLOWED)
HOSTS_ALLOWED = set do
  description 'Connections under these hostnames are allowed in Rails.'
  default ''
  decode { it.split(?,).map(&:strip) }
  check  { value.all? { |host| host =~ /\A[a-z\-]+\.[a-z\-\.]+\z/ && host.size <= 253 } }
end

Explanation:

  • Purpose: Validates a comma-separated list of hostnames for allowed connections
  • Default Value: Empty string (no hosts allowed by default)
  • Decoding Logic: Splits on commas and strips whitespace from each hostname
  • Validation: Ensures ALL hosts in the list pass the same regex validation as single hosts
  • Indicator: Shows ⚙️ for decoding, for valid multi-value
4. Sensitive Configuration with Validation (GITHUB_PERSONAL_ACCESS_TOKEN)
GITHUB_PERSONAL_ACCESS_TOKEN = set do
  description 'GitHub Personal Access Token for repo access'
  prefix ''
  required { !Rails.env.test? }
  sensitive true
  check { value.to_s =~ /\Aghp_[A-Za-z0-9]{36}\z/ }
end

Explanation:

  • Purpose: Validates GitHub personal access tokens with strict format requirements
  • Conditional Required: Not required in test environment
  • Sensitive Flag: Masks the actual value in views (🤫)
  • Token Validation:
    • Must start with ghp_
    • Followed by exactly 36 alphanumeric characters
    • Matches GitHub's token format specification
  • Indicator: Shows both 🔒 (sensitive) and (validated)
5. URI Validation (REDIS_URL)
REDIS_URL = set do
  description 'Redis server URL'
  prefix ''
  default 'redis://localhost:6379/1'
  sensitive true
  check { URI.parse(value).scheme == 'redis' rescue false }
end

Explanation:

  • Purpose: Validates Redis connection URLs are properly formatted
  • Default Value: Safe localhost configuration
  • Sensitive Flag: Masks the URL in views
  • Validation Logic: Attempts to parse as URI and checks for 'redis' scheme
  • Error Handling: Uses rescue false to gracefully handle invalid URLs
  • Indicator: Shows when valid, handles malformed inputs gracefully
Key Technical Details

Conditional Validation: The use of Procs like { Rails.env.production? } allows dynamic validation rules based on runtime conditions.

Safe Error Handling: The URI parsing example demonstrates proper error handling with rescue clauses to prevent configuration failures due to malformed inputs.

Regex Patterns: All validations use precise regex patterns that match RFC standards or specific format requirements, ensuring data integrity.

These examples showcase how ConstConf can handle complex business logic while maintaining clean, readable configuration definitions. The combination of conditional requirements, multi-value validation, and sensitive data protection makes it suitable for production environments with strict security requirements.

Rails Integration

ConstConf automatically integrates with Rails:

  • Configuration is reloaded when the application prepares configuration
  • Works seamlessly with Rails environment variables

Testing with ConstConf

ConstConf provides built-in testing utilities to make it easy to test your configuration in isolation.

Using const_conf_as Helper

The ConstConf::ConstConfHelper provides a convenient way to temporarily override constant values during testing:

# In your test file
require 'const_conf/spec'

RSpec.describe "MyApp" do
  include ConstConf::ConstConfHelper

  it "works with overridden configuration" do
    const_conf_as(
      'AppConfig::DATABASE_URL' => 'postgresql://test:pass@localhost/test',
      'AppConfig::API_KEY' => 'test-key-123'
    )

    # Now your tests can use the overridden values
    expect(AppConfig::DATABASE_URL).to eq('postgresql://test:pass@localhost/test')
    expect(AppConfig::API_KEY).to eq('test-key-123')
  end
end

To make the helper available throughout your test suite, add this to your spec/spec_helper.rb or rails_helper.rb:

require 'const_conf/spec'

# spec/spec_helper.rb or spec/rails_helper.rb
RSpec.configure do |config|
  config.include ConstConf::ConstConfHelper
end

Testing Nested Modules

The helper also works with nested configuration modules:

const_conf_as(
  'AppConfig::Database::URL' => 'postgresql://test:pass@localhost/test',
  'AppConfig::Database::ENABLED' => true
)

This automatically sets up the predicate methods (?) for nested constants, so you can test both the values and their active status:

expect(AppConfig::Database::URL?).to be_truthy
expect(AppConfig::Database::ENABLED?).to be true

The helper ensures that:

  • Constants exist before attempting to override them
  • Parent modules are properly handled for nested constants
  • Predicate methods are correctly mocked for boolean values
  • Proper error handling is in place for missing constants

This approach makes testing configuration-dependent code much easier and more reliable than relying on environment variables or manual setup.

Debugging and Inspection

View configuration hierarchies:

# Show all configuration settings
AppConfig.view

# Show specific setting details
AppConfig::DATABASE_URL!.view

Download

The homepage of this library is located at

Author

ConstConf was written by Florian Frank Florian Frank

License

MIT License