rialo-modular-config

Crates.iorialo-modular-config
lib.rsrialo-modular-config
version0.1.10
created_at2025-11-14 16:47:43.622736+00
updated_at2025-12-09 18:32:35.698729+00
descriptionRialo Modular Config
homepage
repository
max_upload_size
id1933139
size179,734
(subzerolabs-eng-ops)

documentation

README

Rialo Modular Configuration

A flexible layered configuration system for Rust applications. Build robust configuration systems with multiple sources, precedence handling, and optional JSON Schema validation.

Features

๐Ÿ—๏ธ Composable Layers

  • Build configuration from multiple sources with custom precedence
  • Mix and match different layer types: files, environment variables, CLI arguments, in-memory defaults
  • Flexible precedence system with optional weights

๐Ÿ“ Multiple File Formats

  • TOML, JSON, YAML support with automatic format detection
  • Nested structure support with dot-notation access
  • File discovery and optional file loading

๐Ÿ”’ Security & Isolation

  • Final layers block inheritance to prevent data leaks
  • Environment isolation for test/demo environments
  • Sensitive data protection from user configurations

๐Ÿ›ก๏ธ JSON Schema Validation (Optional)

  • Type safety with comprehensive schema validation
  • Enhanced error reporting showing which layers contribute to failures
  • Development-time validation catches configuration errors early

๐Ÿ’พ Smart Configuration Saving

  • Layer-aware saving to appropriate writable layers
  • Automatic directory creation for configuration files
  • Merge-friendly updates that preserve existing values

Quick Start

Add to your Cargo.toml:

[dependencies]
rialo-modular-config = { version = "0.1", features = ["schema"] }  # Optional schema validation

Features

The crate uses a modular feature system to minimize dependencies and enable WASM compatibility:

Available Features

Feature Description Dependencies Default
file-system File-based configuration layers (FileLayer) dirs, toml, serde_yaml โœ…
environment-vars Environment variable layers (EnvironmentLayer) None โœ…
schema JSON Schema validation support jsonschema โŒ

WASM Compatibility

For WASM targets where file system access is limited, disable the file-system feature:

[dependencies]
rialo-modular-config = { version = "0.1", default-features = false, features = ["environment-vars"] }

For minimal WASM builds with only in-memory and CLI argument layers:

[dependencies]
rialo-modular-config = { version = "0.1", default-features = false }

Feature-Specific Usage

With file-system disabled:

// โŒ FileLayer not available without file-system feature
// let layer = FileLayer::new("config.toml").build()?;

// โœ… Use InMemoryLayer and CliArgsLayer instead
let config = LayeredConfigBuilder::new()
    .with_layer(
        InMemoryLayer::new(ConfigLayerSource::Default)
            .with_value("setting", "default")
            .build()
    )
    .with_layer(
        CliArgsLayer::new()
            .with_arg("setting", "override")
            .build()
    )
    .build()?;

With environment-vars disabled:

// โŒ EnvironmentLayer not available without environment-vars feature  
// let layer = EnvironmentLayer::new().build();

// โœ… Use other layer types
let config = LayeredConfigBuilder::new()
    .with_layer(default_layer)
    .with_layer(file_layer)
    .build()?;

Basic Usage

use rialo_modular_config::*;
use std::collections::HashMap;

// Create layered configuration
let config = LayeredConfigBuilder::new()
    // 1. Default values (lowest precedence)
    .with_layer(
        InMemoryLayer::new(ConfigLayerSource::Default)
            .with_value("database.host", "localhost")
            .with_value("database.port", 5432i64)
            .with_value("debug", false)
            .build()
    )
    // 2. User configuration file
    .with_layer(
        FileLayer::new("/home/user/.myapp/config.toml")
            .required(false)  // File may not exist
            .build()?
    )
    // 3. Environment variables (higher precedence)
    .with_layer(
        EnvironmentLayer::new()
            .with_prefix("MYAPP_")  // Only MYAPP_* variables
            .build()
    )
    // 4. CLI arguments (highest precedence)
    .with_layer({
        let mut cli_args = HashMap::new();
        cli_args.insert("debug".to_string(), "true".to_string());
        CliArgsLayer::new().with_args(cli_args).build()
    })
    .build()?;

// Access configuration with automatic type conversion
let host: String = config.get("database.host").unwrap();
let port: i64 = config.get("database.port").unwrap(); 
let debug: bool = config.get("debug").unwrap();  // true from CLI override

println!("Connecting to {}:{} (debug: {})", host, port, debug);

Layer Types

๐Ÿ“„ File Layers

Load configuration from TOML, JSON, or YAML files:

// Auto-detect format from extension
let layer = FileLayer::new("config.toml").build()?;

// Explicit format
let layer = FileLayer::new("config.conf")
    .with_format(FileFormat::Toml)
    .required(false)  // Optional file
    .build()?;

// Different source types
let user_config = FileLayer::new("/home/user/.myapp.toml")
    .with_source(ConfigLayerSource::User(path.clone()))
    .build()?;

let project_config = FileLayer::new("./.myapp-config.toml") 
    .with_source(ConfigLayerSource::Project(path.clone()))
    .build()?;

๐ŸŒ Environment Layers

Extract configuration from environment variables:

// All environment variables
let layer = EnvironmentLayer::new().build();

// Prefix filtering with key transformation
let layer = EnvironmentLayer::new()
    .with_prefix("MYAPP_")           // Only MYAPP_* variables
    .transform_keys(true)            // MYAPP_DB_HOST -> database.host
    .build();

// Raw keys without transformation  
let layer = EnvironmentLayer::new()
    .with_prefix("APP_")
    .transform_keys(false)           // Keep original key names
    .build();

๐Ÿ’พ In-Memory Layers

Programmatic configuration for defaults and testing:

let layer = InMemoryLayer::new(ConfigLayerSource::Default)
    .with_value("api.timeout", 30000)
    .with_value("api.retries", 3)
    .with_value("features.auth", true)
    .build();

// Bulk loading
let mut values = HashMap::new();
values.insert("key1".to_string(), "value1".to_string());
let layer = InMemoryLayer::new(ConfigLayerSource::Default)
    .with_values(values)
    .build();

โŒจ๏ธ CLI Argument Layers

Command-line argument overrides:

let mut args = HashMap::new();
args.insert("verbose".to_string(), "true".to_string());
args.insert("output.format".to_string(), "json".to_string());

let layer = CliArgsLayer::new()
    .with_args(args)
    .build();

// Single arguments
let layer = CliArgsLayer::new()
    .with_arg("debug", "true")
    .with_arg("port", "8080")
    .build();

Precedence System

Default Layer Order

Layers are resolved with increasing precedence:

  1. Default - In-memory defaults
  2. EnvironmentBasic - System environment variables
  3. User - User configuration files (~/.config/app/)
  4. Project - Project configuration files (.appconfig)
  5. ActiveEnvironment - Environment-specific configs (dev/staging/prod)
  6. EnvironmentOverrides - Prefixed environment variables (APP_*)
  7. CommandLineArgs - Command-line arguments (highest)

Custom Precedence with Weights

let config = LayeredConfigBuilder::new()
    .with_layer(
        InMemoryLayer::new(ConfigLayerSource::Default)
            .with_weight(1)  // Low priority
            .with_value("setting", "default")
            .build()
    )
    .with_layer(
        FileLayer::new("important.toml")
            .with_weight(100)  // High priority  
            .build()?
    )
    .build()?;

Security with Final Layers

Prevent sensitive configuration leakage with final layers:

// Production user configuration
let user_layer = FileLayer::new("/home/user/.myapp.toml")
    .with_source(ConfigLayerSource::User(user_path.clone()))
    .required(false)
    .build()?;

// Test environment isolation
let test_layer = InMemoryLayer::new(
    ConfigLayerSource::ActiveEnvironment("/tmp/test-env".into())
)
    .with_value("final", true)        // ๐Ÿ”’ Block inheritance
    .with_value("database.url", "test-db")
    .with_value("debug", true)
    .build();

let config = LayeredConfigBuilder::new()
    .with_layer(user_layer)           // Contains production secrets
    .with_layer(test_layer)           // Final layer blocks access
    .build()?;

// โœ… Only test layer values accessible
let db_url: String = config.get("database.url").unwrap(); // "test-db"
let debug: bool = config.get("debug").unwrap();           // true

// โŒ Production secrets are blocked
let api_key: Option<String> = config.get("api_key");     // None

JSON Schema Validation

Enable type-safe configuration with JSON Schema validation:

[dependencies]
rialo-modular-config = { version = "0.1", features = ["schema"] }

Basic Schema Validation

use serde_json::json;

let schema = json!({
    "type": "object",
    "properties": {
        "database": {
            "type": "object", 
            "properties": {
                "host": { "type": "string" },
                "port": { "type": "integer", "minimum": 1, "maximum": 65535 }
            },
            "required": ["host", "port"]
        },
        "debug": { "type": "boolean" }
    },
    "required": ["database"]
});

let config = LayeredConfigBuilder::new()
    .with_layer(default_layer)
    .with_layer(user_layer)
    .with_schema(schema)              // ๐Ÿ›ก๏ธ Automatic validation
    .build()?;

// Manual validation anytime
config.validate()?;

Enhanced Error Reporting

Schema validation provides detailed debugging information:

Schema validation failed for resolved configuration:
  - /database/port: 999999 is greater than the maximum of 65535
  Contributing layers: User("/home/user/.myapp.toml")
  
  - <root>: "api_key" is a required property  
  Contributing layers: Default, User("/home/user/.myapp.toml"), CommandLineArgs

Key Benefits:

  • ๐ŸŽฏ Pinpoint errors: See exactly which layers contribute to validation failures
  • ๐Ÿ“ Path-specific tracking: Map schema errors to configuration sources
  • ๐Ÿ“ File context: Know which files need to be corrected
  • ๐Ÿ”’ Security-aware: Respects final layer boundaries in error reporting

Configuration Management

Reading Configuration

// Type-safe access with automatic conversion
let timeout: u64 = config.get("api.timeout").unwrap_or(30);
let enabled: bool = config.get("features.auth").unwrap_or(false);
let hosts: Vec<String> = config.get("database.replicas").unwrap_or_default();

// Optional values
let optional: Option<String> = config.get("optional.setting");

// Get all resolved values
let all_config = config.get_resolved_values();
for (key, value) in all_config {
    println!("{} = {:?}", key, value);
}

Writing Configuration

// Save to highest precedence writable layer
config.set("new_setting", "value")?;

// Save to specific layer type  
let user_layer = config.find_user_layer().unwrap();
config.set_in_layer("user_preference", true, user_layer)?;

let project_layer = config.find_project_layer().unwrap();
config.set_in_layer("project_setting", "local", project_layer)?;

// Layer discovery
let writable_layers = config.writable_layers();
for layer in writable_layers {
    println!("Can write to: {:?}", layer.source());
}

Advanced Features

Custom Layer Sources

#[derive(Debug, Clone)]
enum MyConfigSource {
    Database(String),
    Api(String), 
    Cache,
}

let layer = InMemoryLayer::new(ConfigLayerSource::Custom("database".to_string()))
    .with_values(load_from_database()?)
    .build();

Configuration Debugging

// Debug resolution path for a specific key
let path = config.resolver.debug_resolution_path("database.host");
for (source, value, precedence) in path {
    println!("Source: {:?}, Value: {:?}, Weight: {}", source, value, precedence);
}

// Inspect layers
for layer in config.layers() {
    println!("Layer: {:?}", layer.source());
    println!("Writable: {}", layer.is_writable());
    println!("Values: {:?}", layer.values());
}

Environment-Specific Builds

#[cfg(debug_assertions)]
let debug_layer = InMemoryLayer::new(ConfigLayerSource::Default)
    .with_value("log.level", "debug")
    .with_value("dev_mode", true)
    .build();

#[cfg(not(debug_assertions))] 
let debug_layer = InMemoryLayer::new(ConfigLayerSource::Default)
    .with_value("log.level", "info")
    .with_value("dev_mode", false)
    .build();

let config = LayeredConfigBuilder::new()
    .with_layer(debug_layer)
    .with_layer(file_layer)
    .build()?;

Best Practices

๐Ÿ—๏ธ Layer Organization

// Recommended layer structure
LayeredConfigBuilder::new()
    .with_layer(create_defaults())      // 1. Sensible defaults
    .with_layer(load_user_config()?)    // 2. User preferences  
    .with_layer(load_project_config()?) // 3. Project settings
    .with_layer(load_env_config()?)     // 4. Environment-specific
    .with_layer(parse_cli_args())       // 5. Runtime overrides
    .build()?

๐Ÿ”’ Security

// Use final layers for environment isolation
let test_config = test_layer
    .with_value("final", true)          // Block production data
    .with_value("database.url", "test-db")
    .build();

// Validate schemas in development
#[cfg(debug_assertions)]
let config = builder.with_schema(load_schema()).build()?;

#[cfg(not(debug_assertions))]  
let config = builder.build()?;  // Skip validation in production

๐Ÿ“ Error Handling

let config = LayeredConfigBuilder::new()
    .with_layer(create_defaults())
    .with_layer(
        FileLayer::new("config.toml")
            .required(false)            // Gracefully handle missing files
            .build()?
    )
    .build()
    .context("Failed to build configuration")?;

// Validate early and provide helpful errors
if let Err(e) = config.validate() {
    eprintln!("Configuration validation failed:\n{}", e);
    process::exit(1);
}

๐Ÿงช Testing

#[cfg(test)]
fn create_test_config() -> LayeredConfig {
    LayeredConfigBuilder::new()
        .with_layer(
            InMemoryLayer::new(ConfigLayerSource::ActiveEnvironment("/tmp/test".into()))
                .with_value("final", true)    // Isolate test environment
                .with_value("database.url", "sqlite::memory:")
                .with_value("log.level", "debug")
                .build()
        )
        .build()
        .unwrap()
}

License

Licensed under the Apache License, Version 2.0. See LICENSE for details.

Commit count: 0

cargo fmt