Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
name = "gonfig"
version = "0.1.7"
edition = "2021"
authors = ["Vasanthkumar Kalaiselvan<itsparser@gmail.com>"]
authors = ["Vasanthkumar Kalaiselvan<0xvasanth@gmail.com>"]
description = "A unified configuration management library for Rust that seamlessly integrates environment variables, config files, and CLI arguments"
license = "MIT OR Apache-2.0"
repository = "https://github.com/itsparser/gonfig"
repository = "https://github.com/0xvasanth/gonfig"
documentation = "https://docs.rs/gonfig"
homepage = "https://github.com/itsparser/gonfig"
homepage = "https://github.com/0xvasanth/gonfig"
readme = "README.md"
keywords = ["config", "configuration", "cli", "environment", "settings"]
categories = ["config", "command-line-interface"]
Expand Down
233 changes: 233 additions & 0 deletions examples/issue_18_verification.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
use gonfig::{ConfigBuilder, ConfigFormat, Environment, MergeStrategy};
use serde::{Deserialize, Serialize};
use std::io::Write;
use tempfile::NamedTempFile;

/// Multi-level nested configuration structure
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct AppConfig {
service: ServiceConfig,
http: HttpConfig,
database: DatabaseConfig,
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct ServiceConfig {
name: String,
version: String,
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct HttpConfig {
host: String,
port: u16,
timeout: u32,
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct DatabaseConfig {
host: String,
port: u16,
name: String,
pool: PoolConfig,
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct PoolConfig {
minsize: u32,
maxsize: u32,
}

fn main() -> gonfig::Result<()> {
println!("=== Issue #18 Verification: Nested Environment Variable Override ===\n");

// Create a temporary config file with nested values
let mut config_file = NamedTempFile::new().expect("Failed to create temp file");
writeln!(
config_file,
r#"
service:
name: "MyApp"
version: "1.0.0"

http:
host: "127.0.0.1"
port: 3000
timeout: 30

database:
host: "localhost"
port: 5432
name: "production_db"
pool:
minsize: 5
maxsize: 20
"#
)
.expect("Failed to write config");
config_file.flush().expect("Failed to flush");

println!("1. Loading configuration FROM FILE ONLY:");
println!(" Config file path: {:?}", config_file.path());

let config_file_only: AppConfig = ConfigBuilder::new()
.with_merge_strategy(MergeStrategy::Deep)
.with_file_format(config_file.path(), ConfigFormat::Yaml)?
.build()?;

println!(
" Service: {} v{}",
config_file_only.service.name, config_file_only.service.version
);
println!(
" HTTP: {}:{} (timeout: {}s)",
config_file_only.http.host, config_file_only.http.port, config_file_only.http.timeout
);
println!(
" Database: {}:{}/{}",
config_file_only.database.host,
config_file_only.database.port,
config_file_only.database.name
);
println!(
" Pool: min={}, max={}",
config_file_only.database.pool.minsize, config_file_only.database.pool.maxsize
);

// Verify file values
assert_eq!(
config_file_only.http.port, 3000,
"File config should have port 3000"
);
assert_eq!(
config_file_only.database.pool.maxsize, 20,
"File config should have maxsize 20"
);

println!("\n2. Setting ENVIRONMENT VARIABLES to override nested values:");
std::env::set_var("APP_HTTP_PORT", "9000");
std::env::set_var("APP_HTTP_TIMEOUT", "60");
std::env::set_var("APP_DATABASE_POOL_MAXSIZE", "50");
std::env::set_var("APP_DATABASE_NAME", "staging_db");

println!(" APP_HTTP_PORT=9000");
println!(" APP_HTTP_TIMEOUT=60");
println!(" APP_DATABASE_POOL_MAXSIZE=50");
println!(" APP_DATABASE_NAME=staging_db");

println!("\n3. Loading configuration WITH NESTED ENV OVERRIDE:");
let config_with_env: AppConfig = ConfigBuilder::new()
.with_merge_strategy(MergeStrategy::Deep)
.with_file_format(config_file.path(), ConfigFormat::Yaml)?
.with_env_custom(Environment::new().with_prefix("APP").nested(true))
.build()?;

println!(
" Service: {} v{}",
config_with_env.service.name, config_with_env.service.version
);
println!(
" HTTP: {}:{} (timeout: {}s)",
config_with_env.http.host, config_with_env.http.port, config_with_env.http.timeout
);
println!(
" Database: {}:{}/{}",
config_with_env.database.host, config_with_env.database.port, config_with_env.database.name
);
println!(
" Pool: min={}, max={}",
config_with_env.database.pool.minsize, config_with_env.database.pool.maxsize
);

println!("\n4. VERIFICATION RESULTS:");

// Critical assertion: env vars should override file values
if config_with_env.http.port == 9000 {
println!(" ✅ PASS: HTTP port overridden by env (9000)");
} else {
println!(
" ❌ FAIL: HTTP port NOT overridden (expected 9000, got {})",
config_with_env.http.port
);
panic!("Issue #18 NOT FIXED: Environment variable failed to override nested config value");
}

if config_with_env.http.timeout == 60 {
println!(" ✅ PASS: HTTP timeout overridden by env (60)");
} else {
println!(
" ❌ FAIL: HTTP timeout NOT overridden (expected 60, got {})",
config_with_env.http.timeout
);
panic!("Issue #18 NOT FIXED: Environment variable failed to override nested config value");
}

if config_with_env.database.pool.maxsize == 50 {
println!(" ✅ PASS: Database pool maxsize overridden by env (50)");
} else {
println!(
" ❌ FAIL: Database pool maxsize NOT overridden (expected 50, got {})",
config_with_env.database.pool.maxsize
);
panic!("Issue #18 NOT FIXED: Environment variable failed to override deeply nested config value");
}

if config_with_env.database.name == "staging_db" {
println!(" ✅ PASS: Database name overridden by env (staging_db)");
} else {
println!(
" ❌ FAIL: Database name NOT overridden (expected staging_db, got {})",
config_with_env.database.name
);
panic!("Issue #18 NOT FIXED: Environment variable failed to override nested config value");
}

// Verify non-overridden values remain from file
assert_eq!(
config_with_env.http.host, "127.0.0.1",
"Non-overridden values should remain from file"
);
assert_eq!(
config_with_env.service.name, "MyApp",
"Non-overridden values should remain from file"
);
println!(" ✅ PASS: Non-overridden values preserved from config file");

println!("\n5. Testing WITHOUT nested mode (backward compatibility):");
let config_flat: Result<AppConfig, _> = ConfigBuilder::new()
.with_merge_strategy(MergeStrategy::Deep)
.with_file_format(config_file.path(), ConfigFormat::Yaml)?
.with_env_custom(Environment::new().with_prefix("APP").nested(false))
.build();

match config_flat {
Ok(cfg) => {
println!(" Config loaded with nested=false");
println!(
" HTTP port: {} (should be from file: 3000)",
cfg.http.port
);
if cfg.http.port == 3000 {
println!(
" ✅ PASS: Backward compatibility maintained - nested=false keeps flat keys"
);
}
}
Err(e) => {
println!(" Note: Config might fail without nested mode (expected): {e}");
}
}

println!("\n=== CONCLUSION ===");
println!("✅ Issue #18 is FIXED!");
println!(" Environment variables now properly override nested config file values");
println!(" when using .nested(true) with Deep merge strategy.");

// Clean up
std::env::remove_var("APP_HTTP_PORT");
std::env::remove_var("APP_HTTP_TIMEOUT");
std::env::remove_var("APP_DATABASE_POOL_MAXSIZE");
std::env::remove_var("APP_DATABASE_NAME");

Ok(())
}
Loading
Loading