Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration DSL with Compile-Time Validation

Problem Statement

Build a type-safe configuration system using declarative macros that parses TOML/YAML-like syntax at compile time, validates configuration structure, generates type-safe accessor code, and provides environment variable overrides with compile-time defaults. The system should catch configuration errors at compile time (missing required fields, type mismatches, invalid values) and generate zero-cost runtime accessors.

Use Cases

  • Application configuration - Server ports, database URLs, feature flags
  • Build-time configuration - Compile flags, optimization levels, target platforms
  • Service discovery - Endpoints, timeouts, retry policies
  • Multi-environment configs - Development, staging, production settings
  • Plugin configuration - Extensible config for modular systems
  • CLI tools - Command-line argument parsing with defaults
  • Game configuration - Asset paths, difficulty settings, key bindings

Why It Matters

Runtime vs Compile-Time Parsing: Traditional config systems parse YAML/TOML at runtime—file not found = runtime panic in production. Type mismatch (expected int, got string) = runtime error. Missing required field discovered after deployment. Macro-based configs parse at compile time: invalid config = compilation error, caught in CI before production.

Type Safety: Runtime configs use config.get("database.host") returning Option<String>—typo in key name returns None, bug discovered at runtime. Macro DSL generates config.database().host() with compile-time field validation—typo = compiler error. Type system ensures port() returns u16, not String.

Zero Runtime Overhead: Runtime parsers read config file every startup (or cache with memory overhead). Macro configs compile to constants: DATABASE_HOST is a &'static str, zero parsing at runtime. Config access is direct field read, not HashMap lookup.

Environment Override Pattern: Production needs environment variables to override config (for secrets, container orchestration). Macro generates const DEFAULT_PORT: u16 = 8080; let port = env::var("PORT").unwrap_or(DEFAULT_PORT); at compile time—best of both worlds.

Example config validation:

Runtime YAML:          Compile-time macro:
port: "8080"          port: 8080,
(type error at        (compile error if wrong type)
runtime)

missing_field         required_field: value,
(runtime None)        (compile error if missing)

Parse time: 5ms       Parse time: 0ms (compile-time)
Memory: 1KB cached    Memory: 0 bytes (constants)

Milestone 1: Basic Configuration Definition

Introduction

Before building complex configuration systems, you need to understand how macros parse structured data (nested blocks, key-value pairs) and generate corresponding Rust structs. This milestone teaches parsing configuration syntax and generating type-safe accessor code.

Why Start Here: Configuration syntax has structure: section { key: value }. Learning to parse nested patterns and generate structs is foundational. You’ll match braces for sections, identifiers for keys, and expressions for values.

Architecture

Macros:

  • config! - Defines configuration structure

    • Pattern: config! { section { key: value, key: value } }
    • Expands to: Nested structs with const fields
    • Role: Main configuration interface
  • define_config! - Generates config struct

    • Pattern: define_config! { Config { field: Type = default } }
    • Expands to: Struct definition with defaults
    • Role: Type-safe config structure

Key Structs:

  • Configuration structs generated by macros
    • Fields: User-defined via macro invocation
    • Methods: Accessor functions (getters)
    • Role: Compile-time config data

Checkpoint Tests

#![allow(unused)]
fn main() {
#[test]
fn test_simple_config() {
    config! {
        AppConfig {
            server {
                host: "localhost",
                port: 8080,
            }
        }
    }

    let config = AppConfig::new();
    assert_eq!(config.server.host, "localhost");
    assert_eq!(config.server.port, 8080);
}

#[test]
fn test_nested_config() {
    config! {
        DatabaseConfig {
            database {
                connection {
                    host: "localhost",
                    port: 5432,
                    name: "mydb",
                }
                pool {
                    max_connections: 10,
                    timeout_seconds: 30,
                }
            }
        }
    }

    let config = DatabaseConfig::new();
    assert_eq!(config.database.connection.host, "localhost");
    assert_eq!(config.database.pool.max_connections, 10);
}

#[test]
fn test_config_with_types() {
    config! {
        TypedConfig {
            settings {
                enabled: true,
                count: 100u32,
                ratio: 0.5f64,
                name: "test",
            }
        }
    }

    let config = TypedConfig::new();
    assert_eq!(config.settings.enabled, true);
    assert_eq!(config.settings.count, 100);
    assert_eq!(config.settings.ratio, 0.5);
}

#[test]
fn test_config_accessors() {
    config! {
        AppConfig {
            server {
                host: "0.0.0.0",
                port: 3000,
            }
        }
    }

    let config = AppConfig::new();

    // Test accessor methods (if generated)
    assert_eq!(config.server().host(), "0.0.0.0");
    assert_eq!(config.server().port(), 3000);
}
}

Starter Code

//================================================
// Milestone 1: Basic configuration definition
//================================================

// TODO: Implement config! macro that generates nested structs
macro_rules! config {
    (
        $config_name:ident {
            $section_name:ident {
                $(
                    $key:ident: $value:expr
                ),* $(,)?
            }
        }
    ) => {
        // TODO: Generate nested struct definitions
        // Outer struct: $config_name
        // Inner struct: named after $section_name
        // Fields: $key with values inferred from $value

        // Hint: Start with simple struct, then add nested sections
        pub struct $config_name {
            pub $section_name: [<$section_name:camel Config>],
        }

        pub struct [<$section_name:camel Config>] {
            $(
                pub $key: _, // TODO: Infer type from $value
            )*
        }

        impl $config_name {
            pub fn new() -> Self {
                Self {
                    $section_name: [<$section_name:camel Config>] {
                        $(
                            $key: $value,
                        )*
                    },
                }
            }
        }

        todo!("Implement config! macro")
    };
}

// Simpler version without type inference
macro_rules! define_config {
    (
        $config_name:ident {
            $(
                $field:ident: $type:ty = $default:expr
            ),* $(,)?
        }
    ) => {
        // TODO: Generate struct with explicitly typed fields
        #[derive(Debug, Clone)]
        pub struct $config_name {
            $(
                pub $field: $type,
            )*
        }

        impl $config_name {
            pub fn new() -> Self {
                Self {
                    $(
                        $field: $default,
                    )*
                }
            }

            // TODO: Generate accessor methods
            // Example: pub fn field(&self) -> &Type { &self.field }
        }

        todo!("Implement define_config! macro")
    };
}

fn main() {
    println!("Configuration DSL example");

    // Example usage
    define_config! {
        ServerConfig {
            host: String = "localhost".to_string(),
            port: u16 = 8080,
            workers: usize = 4,
        }
    }

    let config = ServerConfig::new();
    println!("Server: {}:{}", config.host, config.port);
}

Implementation Hints:

  1. Use paste crate for identifier manipulation: paste::paste! { [<$name Config>] }
  2. Type inference impossible in declarative macros—require explicit types or use expression type
  3. Nested sections require recursive macro invocation
  4. impl block generates new() constructor with default values
  5. Accessor methods can be generated with paste::paste! { pub fn $field(&self) -> &$type }

Milestone 2: Environment Variable Overrides

Introduction

Why Milestone 1 Isn’t Enough: Hard-coded defaults work for development, but production needs runtime overrides. Secrets (API keys, passwords) shouldn’t be in source code. Container orchestration (Docker, Kubernetes) provides config via environment variables.

The Improvement: Generate code that checks environment variables at runtime, falling back to compile-time defaults. port: env_or!(PORT, 8080) expands to env::var("PORT").ok().and_then(|s| s.parse().ok()).unwrap_or(8080).

Optimization (Security): Environment variables allow secrets injection without recompilation. Deploy same binary to dev/staging/prod with different env vars. Separates config from code (12-factor app principle).

Architecture

New Macros:

  • env_or! - Environment variable with default

    • Pattern: env_or!(VAR_NAME, default_value)
    • Expands to: Runtime env lookup with fallback
    • Role: Runtime configuration override
  • env_required! - Mandatory environment variable

    • Pattern: env_required!(VAR_NAME)
    • Expands to: Panic if env var missing
    • Role: Required runtime configuration
  • config_with_env! - Config with env support

    • Pattern: config_with_env! { field: env("VAR") or default }
    • Expands to: Lazy initialization with env check
    • Role: Hybrid compile/runtime config

Checkpoint Tests

#![allow(unused)]
fn main() {
#[test]
fn test_env_or_with_default() {
    // Without env var, uses default
    let port: u16 = env_or!(TEST_PORT_1, 8080);
    assert_eq!(port, 8080);
}

#[test]
fn test_env_or_with_env_set() {
    std::env::set_var("TEST_PORT_2", "3000");

    let port: u16 = env_or!(TEST_PORT_2, 8080);
    assert_eq!(port, 3000);

    std::env::remove_var("TEST_PORT_2");
}

#[test]
fn test_env_or_type_conversion() {
    std::env::set_var("TEST_ENABLED", "true");

    let enabled: bool = env_or!(TEST_ENABLED, false);
    assert_eq!(enabled, true);

    std::env::remove_var("TEST_ENABLED");
}

#[test]
#[should_panic(expected = "Required environment variable")]
fn test_env_required_missing() {
    let _api_key: String = env_required!(MISSING_API_KEY);
}

#[test]
fn test_env_required_present() {
    std::env::set_var("TEST_API_KEY", "secret123");

    let api_key: String = env_required!(TEST_API_KEY);
    assert_eq!(api_key, "secret123");

    std::env::remove_var("TEST_API_KEY");
}

#[test]
fn test_config_with_env_overrides() {
    std::env::set_var("APP_PORT", "9000");

    config_with_env! {
        AppConfig {
            server {
                host: env("APP_HOST") or "localhost",
                port: env("APP_PORT") or 8080,
            }
        }
    }

    let config = AppConfig::new();
    assert_eq!(config.server.host, "localhost"); // Not overridden
    assert_eq!(config.server.port, 9000); // Overridden

    std::env::remove_var("APP_PORT");
}
}

Starter Code

use std::env;
use std::str::FromStr;

// TODO: Implement env_or! macro for optional env vars
macro_rules! env_or {
    ($var_name:ident, $default:expr) => {
        {
            // TODO: Read environment variable, parse, or use default
            // Hint: env::var(stringify!($var_name))
            //       .ok()
            //       .and_then(|s| s.parse().ok())
            //       .unwrap_or($default)

            todo!("Implement env_or!")
        }
    };
}

// TODO: Implement env_required! macro for mandatory env vars
macro_rules! env_required {
    ($var_name:ident) => {
        {
            // TODO: Read env var or panic with helpful message
            // Hint: env::var(stringify!($var_name))
            //       .expect(&format!("Required environment variable {} not set", stringify!($var_name)))

            todo!("Implement env_required!")
        }
    };
}

// TODO: Implement config_with_env! for configs with env support
macro_rules! config_with_env {
    (
        $config_name:ident {
            $section_name:ident {
                $(
                    $key:ident: env($env_var:expr) or $default:expr
                ),* $(,)?
            }
        }
    ) => {
        // TODO: Generate config struct with lazy env loading
        // Each field checks env var at runtime, falls back to default

        pub struct $config_name {
            // TODO: Store fields or use lazy initialization
        }

        impl $config_name {
            pub fn new() -> Self {
                // TODO: Initialize with env checks
                todo!("Implement config_with_env!")
            }
        }
    };
}

// Alternative: generate const defaults and runtime overrides
macro_rules! config_constants {
    (
        $(
            const $name:ident: $type:ty = env($env_var:expr) or $default:expr;
        )*
    ) => {
        $(
            pub fn $name() -> $type {
                env::var($env_var)
                    .ok()
                    .and_then(|s| s.parse().ok())
                    .unwrap_or($default)
            }
        )*
    };
}

fn main() {
    // Example usage
    config_constants! {
        const server_port: u16 = env("PORT") or 8080;
        const database_host: String = env("DB_HOST") or "localhost".to_string();
    }

    println!("Server port: {}", server_port());
    println!("Database host: {}", database_host());
}

Implementation Hints:

  1. Use stringify!($var_name) to convert identifier to string for env::var
  2. Parse with str::parse::<T>() which requires type inference or turbofish
  3. FromStr trait required for parse—ensure types implement it
  4. Use expect() with descriptive message for required env vars
  5. Consider lazy_static! for one-time initialization of env-backed configs

Milestone 3: Validation and Type Constraints

Introduction

Why Milestone 2 Isn’t Enough: Environment variables are strings—no type checking. User sets PORT=invalid → runtime parse error. Port 70000 parses as u32 but invalid (max port 65535). Need compile-time schema + runtime validation.

The Improvement: Add validation rules to config macro. port: u16 in 1..=65535 generates runtime check. host: String matching r"^[a-z0-9.-]+$" validates format. Compile-time schema ensures validators applied.

Optimization (Fail-Fast): Validation at config load time fails fast with clear errors. Application startup fails immediately if config invalid, before serving requests. Better than discovering invalid port when first connection attempted.

Architecture

New Macros:

  • validated_config! - Config with validation rules

    • Pattern: field: Type in range
    • Expands to: Struct with validation in constructor
    • Role: Runtime validation with compile-time schema
  • validate! - Standalone validation macro

    • Pattern: validate!(value, rule, "error message")
    • Expands to: Conditional check with panic
    • Role: Reusable validation logic

Validation Rules:

  • in range - Numeric bounds checking
  • matching regex - String pattern validation
  • one_of [values] - Enum-like validation
  • min_length n - String/collection length
  • custom closure - Arbitrary validation function

Checkpoint Tests

#![allow(unused)]
fn main() {
#[test]
fn test_validated_port() {
    validated_config! {
        ServerConfig {
            port: u16 in 1..=65535 = 8080,
        }
    }

    let config = ServerConfig::new();
    assert_eq!(config.port, 8080);
}

#[test]
#[should_panic(expected = "port must be in range")]
fn test_invalid_port() {
    validated_config! {
        ServerConfig {
            port: u16 in 1..=65535 = 70000,
        }
    }

    ServerConfig::new(); // Should panic
}

#[test]
fn test_string_pattern_validation() {
    validated_config! {
        DatabaseConfig {
            host: String matching r"^[a-z0-9.-]+$" = "localhost",
        }
    }

    let config = DatabaseConfig::new();
    assert_eq!(config.host, "localhost");
}

#[test]
#[should_panic(expected = "host must match pattern")]
fn test_invalid_hostname() {
    validated_config! {
        DatabaseConfig {
            host: String matching r"^[a-z0-9.-]+$" = "INVALID HOST!",
        }
    }

    DatabaseConfig::new();
}

#[test]
fn test_one_of_validation() {
    validated_config! {
        LogConfig {
            level: String one_of ["debug", "info", "warn", "error"] = "info",
        }
    }

    let config = LogConfig::new();
    assert_eq!(config.level, "info");
}

#[test]
#[should_panic(expected = "level must be one of")]
fn test_invalid_log_level() {
    validated_config! {
        LogConfig {
            level: String one_of ["debug", "info", "warn", "error"] = "trace",
        }
    }

    LogConfig::new();
}

#[test]
fn test_custom_validation() {
    validated_config! {
        Config {
            workers: usize custom |n| n > 0 && n <= 128 = 4,
        }
    }

    let config = Config::new();
    assert_eq!(config.workers, 4);
}
}

Starter Code

#![allow(unused)]
fn main() {
use regex::Regex;

// TODO: Implement validate! macro for assertions
macro_rules! validate {
    ($value:expr, in $range:expr, $error:expr) => {
        {
            if !$range.contains(&$value) {
                panic!("{}: {} not in range {:?}", $error, $value, $range);
            }
        }
    };

    ($value:expr, matching $pattern:expr, $error:expr) => {
        {
            let re = Regex::new($pattern).unwrap();
            if !re.is_match(&$value) {
                panic!("{}: '{}' does not match pattern {}", $error, $value, $pattern);
            }
        }
    };

    ($value:expr, one_of [$($option:expr),*], $error:expr) => {
        {
            let valid = vec![$($option),*];
            if !valid.contains(&$value) {
                panic!("{}: '{}' must be one of {:?}", $error, $value, valid);
            }
        }
    };

    ($value:expr, custom $validator:expr, $error:expr) => {
        {
            if !$validator(&$value) {
                panic!("{}: validation failed for {}", $error, stringify!($value));
            }
        }
    };
}

// TODO: Implement validated_config! with validation rules
macro_rules! validated_config {
    (
        $config_name:ident {
            $(
                $field:ident: $type:ty in $range:expr = $default:expr
            ),* $(,)?
        }
    ) => {
        pub struct $config_name {
            $(
                pub $field: $type,
            )*
        }

        impl $config_name {
            pub fn new() -> Self {
                $(
                    let $field = $default;
                    validate!($field, in $range, concat!("Invalid ", stringify!($field)));
                )*

                Self {
                    $(
                        $field,
                    )*
                }
            }

            // TODO: Add method to create from env with validation
            pub fn from_env() -> Self {
                // TODO: Read from env, validate, use defaults
                todo!("Implement from_env with validation")
            }
        }
    };

    // Pattern for string matching
    (
        $config_name:ident {
            $(
                $field:ident: String matching $pattern:expr = $default:expr
            ),* $(,)?
        }
    ) => {
        pub struct $config_name {
            $(
                pub $field: String,
            )*
        }

        impl $config_name {
            pub fn new() -> Self {
                $(
                    let $field = $default.to_string();
                    validate!($field, matching $pattern, concat!("Invalid ", stringify!($field)));
                )*

                Self {
                    $(
                        $field,
                    )*
                }
            }
        }
    };

    // Pattern for one_of validation
    (
        $config_name:ident {
            $(
                $field:ident: String one_of [$($option:expr),*] = $default:expr
            ),* $(,)?
        }
    ) => {
        // TODO: Implement one_of pattern
        todo!("Implement one_of validation pattern")
    };

    // Pattern for custom validation
    (
        $config_name:ident {
            $(
                $field:ident: $type:ty custom $validator:expr = $default:expr
            ),* $(,)?
        }
    ) => {
        // TODO: Implement custom validator pattern
        todo!("Implement custom validation pattern")
    };
}
}

Implementation Hints:

  1. Validation happens in new() constructor—fails fast at config creation
  2. Use panic! for validation failures (alternative: return Result)
  3. regex crate for pattern matching: Regex::new(pattern)?.is_match(value)
  4. Range validation uses Range::contains(&value)
  5. Multiple validation patterns require multiple macro arms

Milestone 4: Configuration Merging and Profiles

Introduction

Why Milestone 3 Isn’t Enough: Applications need different configs for dev/staging/prod. Repeating entire config per environment duplicates values. Need base config + environment-specific overrides.

The Improvement: Support config profiles that merge: base { port: 8080 } + production { port: 80 } = production config with port 80. Generate code that merges at compile time or runtime based on environment.

Optimization (DRY Principle): Base config has 50 settings, only 5 differ per environment. Without merging: 150 lines of config (50×3 environments). With merging: 50 base + 5×3 overrides = 65 lines. Changes to base automatically apply to all environments.

Architecture

New Macros:

  • config_profiles! - Define base + profile overrides

    • Pattern: config_profiles! { base {...}, dev {...}, prod {...} }
    • Expands to: Struct with profile selection method
    • Role: Multi-environment configuration
  • merge_configs! - Combine two configs

    • Pattern: merge_configs!(base, override)
    • Expands to: New config with override taking precedence
    • Role: Config composition
  • select_profile! - Choose config by name

    • Pattern: select_profile!(env("PROFILE") or "dev")
    • Expands to: Profile selection logic
    • Role: Dynamic profile selection

Checkpoint Tests

#![allow(unused)]
fn main() {
#[test]
fn test_config_profiles_dev() {
    config_profiles! {
        AppConfig {
            base {
                host: "localhost",
                port: 8080,
                debug: true,
            },
            dev {
                debug: true,
            },
            prod {
                host: "0.0.0.0",
                port: 80,
                debug: false,
            },
        }
    }

    let config = AppConfig::dev();
    assert_eq!(config.host, "localhost"); // From base
    assert_eq!(config.port, 8080); // From base
    assert_eq!(config.debug, true); // From dev (overrides base if needed)
}

#[test]
fn test_config_profiles_prod() {
    config_profiles! {
        AppConfig {
            base {
                host: "localhost",
                port: 8080,
                debug: true,
            },
            prod {
                host: "0.0.0.0",
                port: 80,
                debug: false,
            },
        }
    }

    let config = AppConfig::prod();
    assert_eq!(config.host, "0.0.0.0"); // Overridden
    assert_eq!(config.port, 80); // Overridden
    assert_eq!(config.debug, false); // Overridden
}

#[test]
fn test_merge_configs() {
    define_config! {
        BaseConfig {
            host: String = "localhost".to_string(),
            port: u16 = 8080,
            workers: usize = 4,
        }
    }

    define_config! {
        OverrideConfig {
            host: String = "0.0.0.0".to_string(),
            port: u16 = 80,
            workers: usize = 4,
        }
    }

    let base = BaseConfig::new();
    let override_cfg = OverrideConfig::new();

    // TODO: Implement actual merge logic
    // let merged = merge_configs!(base, override_cfg);
    // assert_eq!(merged.host, "0.0.0.0");
    // assert_eq!(merged.workers, 4);
}

#[test]
fn test_select_profile_from_env() {
    std::env::set_var("APP_PROFILE", "production");

    config_profiles! {
        AppConfig {
            base { port: 8080 },
            development { port: 8080 },
            production { port: 80 },
        }
    }

    let config = AppConfig::from_env_profile();
    assert_eq!(config.port, 80);

    std::env::remove_var("APP_PROFILE");
}
}

Starter Code

#![allow(unused)]
fn main() {
// TODO: Implement config_profiles! macro
macro_rules! config_profiles {
    (
        $config_name:ident {
            base {
                $(
                    $base_key:ident: $base_value:expr
                ),* $(,)?
            },
            $($profile_name:ident {
                $(
                    $profile_key:ident: $profile_value:expr
                ),* $(,)?
            }),* $(,)?
        }
    ) => {
        pub struct $config_name {
            $(
                pub $base_key: _,  // TODO: Infer types
            )*
        }

        impl $config_name {
            // Base configuration
            fn base() -> Self {
                Self {
                    $(
                        $base_key: $base_value,
                    )*
                }
            }

            // Generate method for each profile
            $(
                pub fn $profile_name() -> Self {
                    let mut config = Self::base();

                    // TODO: Apply profile-specific overrides
                    // This is tricky—need to identify which fields to override
                    // May need to track field names and match them

                    $(
                        config.$profile_key = $profile_value;
                    )*

                    config
                }
            )*

            pub fn from_env_profile() -> Self {
                // TODO: Read PROFILE env var and call appropriate method
                let profile = std::env::var("APP_PROFILE")
                    .unwrap_or_else(|_| "development".to_string());

                match profile.as_str() {
                    $(
                        stringify!($profile_name) => Self::$profile_name(),
                    )*
                    _ => Self::base(),
                }
            }
        }

        todo!("Implement config_profiles! macro")
    };
}

// TODO: Implement merge_configs! macro
macro_rules! merge_configs {
    ($base:expr, $override:expr) => {
        {
            // TODO: Create new config with fields from both
            // Override takes precedence where defined
            // Challenge: declarative macros can't introspect struct fields
            // Solution: user specifies which fields to merge, or use all

            todo!("Implement merge_configs!")
        }
    };
}

// Helper: select profile by name
macro_rules! select_profile {
    ($config_type:ty, $profile_name:expr) => {
        {
            match $profile_name {
                "dev" | "development" => <$config_type>::dev(),
                "prod" | "production" => <$config_type>::prod(),
                "staging" => <$config_type>::staging(),
                _ => <$config_type>::base(),
            }
        }
    };
}
}

Implementation Hints:

  1. Profiles implemented as different constructors on same struct type
  2. Each profile method starts with base() and applies overrides
  3. Field override syntax: config.$field = $value; after base initialization
  4. Profile selection uses match on profile name string
  5. Consider Default trait for base configuration

Milestone 5: Nested Sections and References

Introduction

Why Milestone 4 Isn’t Enough: Real configs have deep nesting: database.connection.pool.max_size. Sections reference other sections: logging.path = "${base_dir}/logs". Need structured data and variable interpolation.

The Improvement: Support arbitrarily nested sections and cross-references. server.host references global.default_host. Macros resolve references at compile time when possible, runtime when needed.

Optimization (Code Organization): Nested configs group related settings. Flat database_connection_pool_max_size vs hierarchical database.connection.pool.max_size. Namespacing prevents collisions. References avoid duplication: define base_path once, reference everywhere.

Architecture

New Macros:

  • nested_config! - Deeply nested structures

    • Pattern: section { subsection { subsubsection { ... } } }
    • Expands to: Nested struct definitions
    • Role: Hierarchical configuration
  • config_with_refs! - Config with references

    • Pattern: field: ref(other.field)
    • Expands to: Field access or lazy evaluation
    • Role: Cross-config references

Reference Syntax:

  • ${section.field} - String interpolation
  • ref(path) - Direct value reference
  • env_ref(VAR, fallback) - Env var with reference fallback

Checkpoint Tests

#![allow(unused)]
fn main() {
#[test]
fn test_deeply_nested_config() {
    nested_config! {
        AppConfig {
            server {
                http {
                    host: "0.0.0.0",
                    port: 8080,
                },
                https {
                    host: "0.0.0.0",
                    port: 8443,
                    cert_path: "/etc/ssl/cert.pem",
                },
            },
            database {
                primary {
                    host: "localhost",
                    port: 5432,
                },
                replica {
                    host: "localhost",
                    port: 5433,
                },
            },
        }
    }

    let config = AppConfig::new();
    assert_eq!(config.server.http.port, 8080);
    assert_eq!(config.server.https.port, 8443);
    assert_eq!(config.database.primary.port, 5432);
}

#[test]
fn test_config_with_string_interpolation() {
    config_with_refs! {
        PathConfig {
            base_dir: "/var/app",
            log_dir: "${base_dir}/logs",
            data_dir: "${base_dir}/data",
        }
    }

    let config = PathConfig::new();
    assert_eq!(config.log_dir, "/var/app/logs");
    assert_eq!(config.data_dir, "/var/app/data");
}

#[test]
fn test_cross_section_references() {
    config_with_refs! {
        AppConfig {
            defaults {
                host: "localhost",
                timeout: 30,
            },
            server {
                host: ref(defaults.host),
                port: 8080,
                timeout: ref(defaults.timeout),
            },
            client {
                host: ref(defaults.host),
                timeout: ref(defaults.timeout),
            },
        }
    }

    let config = AppConfig::new();
    assert_eq!(config.server.host, "localhost");
    assert_eq!(config.client.timeout, 30);
}

#[test]
fn test_env_with_ref_fallback() {
    nested_config! {
        AppConfig {
            server {
                port: env_or!(PORT, 8080),
            },
            admin {
                port: env_or!(ADMIN_PORT, 9000),
            },
        }
    }

    let config = AppConfig::new();
    assert_eq!(config.server.port, 8080);
    assert_eq!(config.admin.port, 9000);
}
}

Starter Code

#![allow(unused)]
fn main() {
// TODO: Implement nested_config! macro with arbitrary depth
macro_rules! nested_config {
    // Base case: leaf section with fields
    (
        $config_name:ident {
            $(
                $field:ident: $value:expr
            ),* $(,)?
        }
    ) => {
        pub struct $config_name {
            $(
                pub $field: _,
            )*
        }

        impl $config_name {
            pub fn new() -> Self {
                Self {
                    $(
                        $field: $value,
                    )*
                }
            }
        }
    };

    // Recursive case: section with subsections
    (
        $config_name:ident {
            $section_name:ident {
                $($section_content:tt)*
            }
            $(
                $rest_section:ident {
                    $($rest_content:tt)*
                }
            )*
        }
    ) => {
        // TODO: Generate nested struct for each section
        // Recursively process subsections
        // Challenge: need to distinguish between fields and subsections

        nested_config! {
            [<$section_name:camel Section>] {
                $($section_content)*
            }
        }

        $(
            nested_config! {
                [<$rest_section:camel Section>] {
                    $($rest_content)*
                }
            }
        )*

        pub struct $config_name {
            pub $section_name: [<$section_name:camel Section>],
            $(
                pub $rest_section: [<$rest_section:camel Section>],
            )*
        }

        todo!("Implement recursive nested config")
    };
}

// TODO: Implement config_with_refs! for references
macro_rules! config_with_refs {
    (
        $config_name:ident {
            $(
                $field:ident: $value:expr
            ),* $(,)?
        }
    ) => {
        pub struct $config_name {
            $(
                pub $field: String,
            )*
        }

        impl $config_name {
            pub fn new() -> Self {
                // First pass: initialize fields with literals
                $(
                    let $field = $value;
                )*

                // Second pass: resolve references
                // This is where string interpolation happens
                // TODO: Parse ${...} syntax and substitute

                Self {
                    $(
                        $field: Self::resolve_value(stringify!($value), &[
                            // TODO: Pass context for reference resolution
                        ]),
                    )*
                }
            }

            fn resolve_value(template: &str, context: &[(&str, &str)]) -> String {
                // TODO: Implement ${var} substitution
                // Use regex or manual parsing
                let mut result = template.to_string();

                for (key, value) in context {
                    let pattern = format!("${{{}}}", key);
                    result = result.replace(&pattern, value);
                }

                result
            }
        }

        todo!("Implement config_with_refs!")
    };
}

// Helper: reference another field
macro_rules! ref_field {
    ($section:ident.$field:ident) => {
        {
            // This is a marker that gets replaced during config construction
            // In practice, need to track references and resolve in correct order
            concat!("${", stringify!($section), ".", stringify!($field), "}")
        }
    };
}
}

Implementation Hints:

  1. Nested sections require recursive macro matching with base case
  2. Use token tree (tt) for flexible content matching
  3. String interpolation: "${var}" → replace with actual value
  4. References need two-pass initialization: compute values, then resolve refs
  5. Use paste crate for struct name generation from section names

Milestone 6: Code Generation and Builder Pattern

Introduction

Why Milestone 5 Isn’t Enough: Large configs are verbose even with DSL. Users want builder pattern for programmatic construction. Need fluent API: Config::builder().host("localhost").port(8080).build().

The Improvement: Auto-generate builder from config schema. Macro sees host: String, port: u16 and generates .host(value: String), .port(value: u16) methods. Type-safe, discoverable, minimal code.

Optimization (API Ergonomics): Builder pattern common in Rust (reqwest, tokio, serde). Users familiar with .method() chaining. Auto-generation ensures builders stay in sync with schema—add field, builder method appears automatically.

Architecture

New Macros:

  • config_with_builder! - Config + builder generation

    • Pattern: Same as config! but generates builder
    • Expands to: Config struct + Builder struct + methods
    • Role: Ergonomic config construction
  • derive_builder! - Standalone builder generator

    • Pattern: derive_builder!(Config)
    • Expands to: Builder implementation for existing struct
    • Role: Add builder to any struct

Builder Pattern:

  • Builder struct with Option<T> fields
  • .field(value) methods set options
  • .build() method validates and constructs final config

Checkpoint Tests

#![allow(unused)]
fn main() {
#[test]
fn test_builder_pattern() {
    config_with_builder! {
        ServerConfig {
            host: String = "localhost",
            port: u16 = 8080,
            workers: usize = 4,
        }
    }

    let config = ServerConfig::builder()
        .host("0.0.0.0".to_string())
        .port(3000)
        .build();

    assert_eq!(config.host, "0.0.0.0");
    assert_eq!(config.port, 3000);
    assert_eq!(config.workers, 4); // Default value
}

#[test]
fn test_builder_partial() {
    config_with_builder! {
        AppConfig {
            debug: bool = false,
            log_level: String = "info",
            max_connections: usize = 100,
        }
    }

    let config = AppConfig::builder()
        .debug(true)
        .build();

    assert_eq!(config.debug, true);
    assert_eq!(config.log_level, "info"); // Default
    assert_eq!(config.max_connections, 100); // Default
}

#[test]
fn test_builder_method_chaining() {
    config_with_builder! {
        DatabaseConfig {
            host: String = "localhost",
            port: u16 = 5432,
            database: String = "mydb",
            user: String = "admin",
        }
    }

    let config = DatabaseConfig::builder()
        .host("db.example.com".to_string())
        .port(5433)
        .database("production".to_string())
        .user("prod_user".to_string())
        .build();

    assert_eq!(config.host, "db.example.com");
    assert_eq!(config.database, "production");
}

#[test]
#[should_panic(expected = "required field not set")]
fn test_builder_required_field() {
    config_with_builder! {
        Config {
            required_field: String required,
            optional_field: String = "default",
        }
    }

    // Should panic because required_field not set
    Config::builder().build();
}
}

Starter Code

#![allow(unused)]
fn main() {
// TODO: Implement config_with_builder! macro
macro_rules! config_with_builder {
    (
        $config_name:ident {
            $(
                $field:ident: $type:ty = $default:expr
            ),* $(,)?
        }
    ) => {
        // Generate config struct
        #[derive(Debug, Clone)]
        pub struct $config_name {
            $(
                pub $field: $type,
            )*
        }

        // Generate builder struct
        paste::paste! {
            #[derive(Default)]
            pub struct [<$config_name Builder>] {
                $(
                    $field: Option<$type>,
                )*
            }

            impl [<$config_name Builder>] {
                // Generate setter method for each field
                $(
                    pub fn $field(mut self, value: $type) -> Self {
                        self.$field = Some(value);
                        self
                    }
                )*

                // Build method constructs final config
                pub fn build(self) -> $config_name {
                    $config_name {
                        $(
                            $field: self.$field.unwrap_or($default),
                        )*
                    }
                }
            }

            impl $config_name {
                pub fn builder() -> [<$config_name Builder>] {
                    [<$config_name Builder>]::default()
                }
            }
        }
    };

    // Pattern for required fields (no default)
    (
        $config_name:ident {
            $(
                $field:ident: $type:ty $(= $default:expr)? $(required)?
            ),* $(,)?
        }
    ) => {
        // TODO: Handle mix of required and optional fields
        // Required: panic if not set
        // Optional: use default or None
        todo!("Implement required/optional field handling")
    };
}

// Alternative: derive-style builder
macro_rules! derive_builder {
    ($struct_name:ident) => {
        paste::paste! {
            impl $struct_name {
                pub fn builder() -> [<$struct_name Builder>] {
                    [<$struct_name Builder>]::default()
                }
            }

            // TODO: Generate builder struct based on original struct fields
            // Challenge: declarative macros can't introspect structs
            // Solution: require user to specify fields again
        }
    };
}
}

Implementation Hints:

  1. Builder struct has Option<T> for each field to track if set
  2. Setter methods take self and return Self for chaining
  3. build() uses unwrap_or(default) for optional fields
  4. Required fields use expect("field X required") instead of unwrap_or
  5. Use paste crate to generate builder struct name from config name

Complete Working Example

Here’s a full implementation demonstrating all milestones:

use std::env;

// Helper macro for simple configs
macro_rules! define_config {
    (
        $config_name:ident {
            $(
                $field:ident: $type:ty = $default:expr
            ),* $(,)?
        }
    ) => {
        #[derive(Debug, Clone)]
        pub struct $config_name {
            $(
                pub $field: $type,
            )*
        }

        impl $config_name {
            pub fn new() -> Self {
                Self {
                    $(
                        $field: $default,
                    )*
                }
            }

            pub fn from_env() -> Self {
                Self {
                    $(
                        $field: env::var(concat!(stringify!($config_name), "_", stringify!($field)))
                            .ok()
                            .and_then(|s| s.parse().ok())
                            .unwrap_or($default),
                    )*
                }
            }
        }
    };
}

// Environment variable helpers
macro_rules! env_or {
    ($var_name:ident, $default:expr) => {
        env::var(stringify!($var_name))
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or($default)
    };
}

// Validation macro
macro_rules! validate {
    ($value:expr, in $range:expr, $field_name:expr) => {
        if !$range.contains(&$value) {
            panic!(
                "Configuration error: {} = {} is not in valid range {:?}",
                $field_name, $value, $range
            );
        }
    };
}

// Config with profiles
macro_rules! config_profiles {
    (
        $config_name:ident {
            base {
                $($base_field:ident: $base_type:ty = $base_value:expr),* $(,)?
            },
            dev {
                $($dev_field:ident: $dev_value:expr),* $(,)?
            },
            prod {
                $($prod_field:ident: $prod_value:expr),* $(,)?
            }
        }
    ) => {
        #[derive(Debug, Clone)]
        pub struct $config_name {
            $(
                pub $base_field: $base_type,
            )*
        }

        impl $config_name {
            fn base() -> Self {
                Self {
                    $(
                        $base_field: $base_value,
                    )*
                }
            }

            pub fn dev() -> Self {
                let mut config = Self::base();
                $(
                    config.$dev_field = $dev_value;
                )*
                config
            }

            pub fn prod() -> Self {
                let mut config = Self::base();
                $(
                    config.$prod_field = $prod_value;
                )*
                config
            }

            pub fn from_profile(profile: &str) -> Self {
                match profile {
                    "dev" | "development" => Self::dev(),
                    "prod" | "production" => Self::prod(),
                    _ => Self::base(),
                }
            }
        }
    };
}

//======================
// Example usage
//======================

fn main() {
    println!("=== Configuration DSL Examples ===\n");

    // Example 1: Simple config with defaults
    println!("Example 1: Basic Configuration");
    define_config! {
        ServerConfig {
            host: String = "localhost".to_string(),
            port: u16 = 8080,
            workers: usize = 4,
            debug: bool = false,
        }
    }

    let server = ServerConfig::new();
    println!("Server: {}:{} (workers: {}, debug: {})",
        server.host, server.port, server.workers, server.debug);

    // Example 2: Environment variable overrides
    println!("\nExample 2: Environment Overrides");
    env::set_var("ServerConfig_port", "3000");

    let server_from_env = ServerConfig::from_env();
    println!("Server from env: {}:{}", server_from_env.host, server_from_env.port);

    // Example 3: Config profiles
    println!("\nExample 3: Configuration Profiles");
    config_profiles! {
        AppConfig {
            base {
                host: String = "localhost".to_string(),
                port: u16 = 8080,
                debug: bool = true,
                workers: usize = 4,
            },
            dev {
                debug: true,
            },
            prod {
                host: "0.0.0.0".to_string(),
                port: 80,
                debug: false,
                workers: 16,
            }
        }
    }

    let dev_config = AppConfig::dev();
    println!("Dev config: {}:{} (debug: {})",
        dev_config.host, dev_config.port, dev_config.debug);

    let prod_config = AppConfig::prod();
    println!("Prod config: {}:{} (debug: {}, workers: {})",
        prod_config.host, prod_config.port, prod_config.debug, prod_config.workers);

    // Example 4: Validation
    println!("\nExample 4: Validated Configuration");
    define_config! {
        ValidatedConfig {
            port: u16 = 8080,
            max_connections: usize = 100,
        }
    }

    let validated = ValidatedConfig::new();
    validate!(validated.port, in 1..=65535, "port");
    validate!(validated.max_connections, in 1..=10000, "max_connections");
    println!("Validated config: port={}, max_connections={}",
        validated.port, validated.max_connections);

    // Example 5: Nested configuration
    println!("\nExample 5: Nested Configuration");
    define_config! {
        DatabaseSection {
            host: String = "localhost".to_string(),
            port: u16 = 5432,
            database: String = "mydb".to_string(),
        }
    }

    define_config! {
        FullAppConfig {
            app_name: String = "MyApp".to_string(),
            version: String = "1.0.0".to_string(),
        }
    }

    let db_config = DatabaseSection::new();
    let app_config = FullAppConfig::new();

    println!("App: {} v{}", app_config.app_name, app_config.version);
    println!("Database: {}:{}/{}", db_config.host, db_config.port, db_config.database);

    println!("\n=== All examples completed ===");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_basic_config() {
        define_config! {
            TestConfig {
                value: i32 = 42,
            }
        }

        let config = TestConfig::new();
        assert_eq!(config.value, 42);
    }

    #[test]
    fn test_env_override() {
        env::set_var("TEST_VAR", "100");

        let value: i32 = env_or!(TEST_VAR, 42);
        assert_eq!(value, 100);

        env::remove_var("TEST_VAR");
    }

    #[test]
    fn test_profiles() {
        config_profiles! {
            ProfileConfig {
                base {
                    value: i32 = 10,
                },
                dev {
                    value: 20,
                },
                prod {
                    value: 30,
                }
            }
        }

        assert_eq!(ProfileConfig::dev().value, 20);
        assert_eq!(ProfileConfig::prod().value, 30);
    }

    #[test]
    #[should_panic(expected = "not in valid range")]
    fn test_validation_fails() {
        let port: u16 = 70000; // Invalid
        validate!(port, in 1..=65535, "port");
    }
}

This complete implementation demonstrates:

  1. Basic configuration - Structs with defaults
  2. Environment overrides - Runtime config from env vars
  3. Validation - Compile-time schema, runtime checks
  4. Profiles - Multi-environment configurations
  5. Type safety - All config access type-checked
  6. Zero runtime parsing - Configs are compile-time constants

The DSL provides type-safe, validated configuration with environment overrides—a production-ready foundation for robust application configuration management.