# 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`:
```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:

```toml
[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:

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

### Feature-Specific Usage

#### With `file-system` disabled:
```rust
// ❌ 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:
```rust
// ❌ 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

```rust
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:

```rust
// 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:

```rust
// 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:

```rust
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:

```rust
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

```rust
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:

```rust
// 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:

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

### Basic Schema Validation

```rust
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

```rust
// 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

```rust
// 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

```rust
#[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

```rust
// 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

```rust
#[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**
```rust
// 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**
```rust
// 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**
```rust
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**
```rust
#[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](../../LICENSE) for details.
