use gonfig::{ConfigBuilder, ConfigFormat, Environment, MergeStrategy};
use serde::{Deserialize, Serialize};
use std::io::Write;
use tempfile::NamedTempFile;
#[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");
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
);
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:");
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");
}
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.");
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(())
}