A unified configuration management library for Rust that seamlessly integrates environment variables, configuration files, and CLI arguments with a clean, intuitive API.
- 🎯 Multiple Configuration Sources: Environment variables, config files (JSON/YAML/TOML), and CLI arguments
- 🔧 Flexible Prefix Management: Configure environment variable prefixes at struct and field levels
- 🚀 Derive Macro Support: Easy configuration with
#[derive(Gonfig)] - 🔀 Merge Strategies: Deep merge, replace, or append configurations
- 🛡️ Type Safety: Fully type-safe configuration with serde
- ✅ Validation: Built-in validation support for your configurations
- ⚙️ Granular Control: Enable/disable sources at struct or field level
- 🚫 Skip Support: Exclude sensitive or runtime fields from configuration
- 📊 Structured Logging: Built-in tracing support with fine-grained control via
RUST_LOG
Add to your Cargo.toml:
[dependencies]
gonfig = "0.1.9"
serde = { version = "1.0", features = ["derive"] }use gonfig::Gonfig;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Gonfig)]
#[Gonfig(env_prefix = "APP")]
struct Config {
// Environment variable: APP_DATABASE_URL
database_url: String,
// Environment variable: APP_PORT
port: u16,
// Skip this field from configuration
#[skip]
runtime_client: Option<DatabaseClient>,
}
fn main() -> gonfig::Result<()> {
std::env::set_var("APP_DATABASE_URL", "postgres://localhost/myapp");
std::env::set_var("APP_PORT", "8080");
let config = Config::from_gonfig()?;
tracing::info!("Database: {}", config.database_url);
tracing::info!("Port: {}", config.port);
Ok(())
}use gonfig::{Gonfig, ConfigBuilder, MergeStrategy};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Gonfig)]
#[Gonfig(allow_cli, env_prefix = "MD")]
struct Mongo {
// Environment variable: MD_MONGO_USERNAME
// CLI argument: --mongo-username
username: String,
// Environment variable: MD_MONGO_PASSWORD
// CLI argument: --mongo-password
password: String,
}
#[derive(Debug, Serialize, Deserialize, Gonfig)]
struct Application {
// Environment variable: MD_APP_USERNAME
username: String,
// Environment variable: MD_APP_PASSWORD
password: String,
#[skip]
client: Option<HttpClient>, // Excluded from configuration
}
#[derive(Debug, Serialize, Deserialize, Gonfig)]
#[Gonfig(env_prefix = "MD")]
pub struct Config {
mongo: Mongo,
app: Application,
}
fn main() -> gonfig::Result<()> {
// Option 1: Use derive macro (simple)
let config = Config::from_gonfig()?;
// Option 2: Use builder (advanced)
let config = ConfigBuilder::new()
.with_merge_strategy(MergeStrategy::Deep)
.with_env("MD")
.with_file_optional("config.toml")?
.with_cli()
.validate_with(|value| {
// Custom validation logic
if let Some(port) = value.get("port").and_then(|p| p.as_u64()) {
if port > 65535 {
return Err(gonfig::Error::Validation("Invalid port".into()));
}
}
Ok(())
})
.build::<Config>()?;
Ok(())
}Environment variables follow a hierarchical naming pattern:
#[derive(Gonfig)]
#[Gonfig(env_prefix = "MD")]
struct Config {
mongo: MongoConfig, // MD_MONGO_*
app: AppConfig, // MD_APP_*
}
struct MongoConfig {
username: String, // → MD_MONGO_USERNAME
password: String, // → MD_MONGO_PASSWORD
}struct Config {
#[gonfig(env_name = "DATABASE_URL")]
db_url: String, // → DATABASE_URL (ignores prefix)
port: u16, // → MD_CONFIG_PORT (uses prefix)
}| Attribute | Description | Example |
|---|---|---|
env_prefix = "PREFIX" |
Set environment variable prefix | #[Gonfig(env_prefix = "APP")] |
allow_cli |
Enable CLI argument support | #[Gonfig(allow_cli)] |
allow_config |
Enable config file support | #[Gonfig(allow_config)] |
| Attribute | Description | Example |
|---|---|---|
env_name = "NAME" |
Override environment variable name | #[gonfig(env_name = "DB_URL")] |
cli_name = "name" |
Override CLI argument name | #[gonfig(cli_name = "database-url")] |
#[skip] |
Skip field from all sources | #[skip] |
#[skip_gonfig] |
Alternative skip syntax | #[skip_gonfig] |
Use skip attributes to exclude fields from configuration:
#[derive(Gonfig)]
struct Config {
database_url: String, // ✅ Included in configuration
#[skip]
runtime_client: Option<Client>, // ❌ Excluded from configuration
#[skip_gonfig]
internal_state: Vec<String>, // ❌ Excluded from configuration
}- Non-serializable types: Database connections, thread pools
- Runtime state: Caches, temporary data
- Sensitive data: API keys loaded from secure vaults
- Computed fields: Values calculated from other config
- Implementation details: Internal buffers, state machines
CLI arguments use kebab-case naming:
#[derive(Gonfig)]
#[Gonfig(allow_cli)]
struct Config {
database_url: String, // → --database-url
max_connections: u32, // → --max-connections
#[gonfig(cli_name = "db-port")]
port: u16, // → --db-port
}Usage: cargo run -- --database-url postgres://localhost --max-connections 100
Sources are merged with the following priority (higher number wins):
- Default values (Priority: 0)
- Config files (Priority: 1)
- Environment variables (Priority: 2)
- CLI arguments (Priority: 3)
use gonfig::MergeStrategy;
ConfigBuilder::new()
.with_merge_strategy(MergeStrategy::Deep) // Merge nested objects
.with_merge_strategy(MergeStrategy::Replace) // Replace entire values
.with_merge_strategy(MergeStrategy::Append) // Append arraysAdd custom validation logic:
ConfigBuilder::new()
.validate_with(|config| {
if let Some(port) = config.get("port").and_then(|p| p.as_u64()) {
if port == 0 || port > 65535 {
return Err(gonfig::Error::Validation(
"Port must be between 1 and 65535".into()
));
}
}
Ok(())
})
.build::<Config>()?;Gonfig supports multiple config file formats:
# config.toml
database_url = "postgres://localhost/prod"
port = 8080
[mongo]
username = "admin"
password = "secret"# config.yaml
database_url: postgres://localhost/prod
port: 8080
mongo:
username: admin
password: secret{
"database_url": "postgres://localhost/prod",
"port": 8080,
"mongo": {
"username": "admin",
"password": "secret"
}
}Gonfig uses the tracing crate for structured logging. Control logging output using the RUST_LOG environment variable:
# Show all logs (default level: INFO)
RUST_LOG=info cargo run --example simple
# Show only errors
RUST_LOG=error cargo run --example simple
# Show debug information
RUST_LOG=debug cargo run --example simple
# Show trace-level details
RUST_LOG=trace cargo run --example simpleAll examples include tracing initialization:
use tracing_subscriber::EnvFilter;
fn main() -> gonfig::Result<()> {
// Initialize tracing subscriber
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::from_default_env()
.add_directive(tracing::Level::INFO.into())
)
.init();
// Your code here...
}This gives you fine-grained control over logging output without cluttering your application logs.
Gonfig provides detailed error types:
use gonfig::Error;
match config_result {
Err(Error::Environment(msg)) => tracing::error!("Environment error: {}", msg),
Err(Error::Config(msg)) => tracing::error!("Config file error: {}", msg),
Err(Error::Cli(msg)) => tracing::error!("CLI error: {}", msg),
Err(Error::Validation(msg)) => tracing::error!("Validation error: {}", msg),
Err(Error::Serialization(msg)) => tracing::error!("Serialization error: {}", msg),
Ok(config) => tracing::info!("Config loaded successfully: {:?}", config),
}Contributions are welcome! Please feel free to submit a Pull Request.
- MIT license (LICENSE or https://opensource.org/licenses/MIT)