config
config là một library mạnh mẽ để quản lý configuration trong Rust applications. Nó hỗ trợ đọc config từ nhiều nguồn khác nhau (files, environment variables, command line) và merge chúng lại theo thứ tự ưu tiên.
Đặc điểm
- 📁 Multi-format - JSON, TOML, YAML, INI, RON, JSON5
- 🔄 Layered configs - Merge từ nhiều nguồn
- 🌍 Environment variables - Override config với env vars
- 🎯 Type-safe - Deserialize vào Rust structs
- ⚙️ Flexible - Custom sources và formats
Cài đặt
[dependencies]
config = "0.14"
serde = { version = "1.0", features = ["derive"] }
Hoặc:
cargo add config
cargo add serde --features derive
Ví dụ cơ bản
File config: config.toml
[database]
host = "localhost"
port = 5432
username = "admin"
password = "secret"
[server]
host = "0.0.0.0"
port = 8080
workers = 4
[logging]
level = "info"
Code
use config::{Config, ConfigError, File}; use serde::Deserialize; #[derive(Debug, Deserialize)] struct Database { host: String, port: u16, username: String, password: String, } #[derive(Debug, Deserialize)] struct Server { host: String, port: u16, workers: u8, } #[derive(Debug, Deserialize)] struct Logging { level: String, } #[derive(Debug, Deserialize)] struct Settings { database: Database, server: Server, logging: Logging, } fn main() -> Result<(), ConfigError> { let settings = Config::builder() .add_source(File::with_name("config")) .build()?; let settings: Settings = settings.try_deserialize()?; println!("{:#?}", settings); Ok(()) }
Nhiều file config (Environments)
use config::{Config, ConfigError, File, Environment}; use serde::Deserialize; #[derive(Debug, Deserialize)] struct Settings { debug: bool, database: DatabaseSettings, } #[derive(Debug, Deserialize)] struct DatabaseSettings { url: String, } fn main() -> Result<(), ConfigError> { let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "development".into()); let settings = Config::builder() // Start with default config .add_source(File::with_name("config/default")) // Layer on environment-specific values .add_source(File::with_name(&format!("config/{}", run_mode)).required(false)) // Layer on local config (ignored by git) .add_source(File::with_name("config/local").required(false)) // Add environment variables (with a prefix of APP and separator of __) .add_source(Environment::with_prefix("APP").separator("__")) .build()?; let settings: Settings = settings.try_deserialize()?; println!("{:#?}", settings); Ok(()) }
File structure:
config/
├── default.toml # Base config
├── development.toml # Development overrides
├── production.toml # Production overrides
└── local.toml # Local overrides (gitignored)
config/default.toml:
debug = false
[database]
url = "postgres://localhost/myapp"
config/development.toml:
debug = true
[database]
url = "postgres://localhost/myapp_dev"
config/production.toml:
[database]
url = "postgres://prod-server/myapp"
Environment Variables Override
# Override config với environment variables
export APP__DEBUG=true
export APP__DATABASE__URL="postgres://custom-host/db"
cargo run
Pattern: PREFIX__SECTION__KEY
Nhiều format config
JSON
{
"server": {
"port": 8080,
"host": "localhost"
}
}
#![allow(unused)] fn main() { let settings = Config::builder() .add_source(File::with_name("config.json")) .build()?; }
YAML
server:
port: 8080
host: localhost
#![allow(unused)] fn main() { let settings = Config::builder() .add_source(File::with_name("config.yaml")) .build()?; }
Hoặc auto-detect format:
#![allow(unused)] fn main() { // Sẽ tự động detect .json, .yaml, .toml, etc. let settings = Config::builder() .add_source(File::with_name("config")) .build()?; }
Set default values
use config::{Config, ConfigError}; fn main() -> Result<(), ConfigError> { let settings = Config::builder() .set_default("server.port", 8080)? .set_default("server.host", "127.0.0.1")? .set_default("debug", false)? .add_source(File::with_name("config").required(false)) .build()?; Ok(()) }
Nested configuration
#![allow(unused)] fn main() { use serde::Deserialize; #[derive(Debug, Deserialize)] struct Settings { database: DatabaseSettings, cache: CacheSettings, api: ApiSettings, } #[derive(Debug, Deserialize)] struct DatabaseSettings { primary: ConnectionSettings, replica: Option<ConnectionSettings>, } #[derive(Debug, Deserialize)] struct ConnectionSettings { host: String, port: u16, pool_size: u32, } #[derive(Debug, Deserialize)] struct CacheSettings { redis_url: String, ttl: u64, } #[derive(Debug, Deserialize)] struct ApiSettings { key: String, timeout: u64, } }
config.toml:
[database.primary]
host = "db-primary.example.com"
port = 5432
pool_size = 20
[database.replica]
host = "db-replica.example.com"
port = 5432
pool_size = 10
[cache]
redis_url = "redis://localhost:6379"
ttl = 3600
[api]
key = "your-api-key"
timeout = 30
Validation
use config::{Config, ConfigError}; use serde::Deserialize; #[derive(Debug, Deserialize)] struct Settings { port: u16, workers: u8, } impl Settings { pub fn new() -> Result<Self, ConfigError> { let settings = Config::builder() .add_source(File::with_name("config")) .build()?; let mut settings: Settings = settings.try_deserialize()?; // Validate if settings.workers == 0 { return Err(ConfigError::Message("workers must be > 0".into())); } if settings.port < 1024 { return Err(ConfigError::Message("port must be >= 1024".into())); } Ok(settings) } } fn main() -> Result<(), ConfigError> { let settings = Settings::new()?; println!("{:#?}", settings); Ok(()) }
Ví dụ thực tế: Web application config
use config::{Config, ConfigError, File, Environment}; use serde::Deserialize; #[derive(Debug, Deserialize)] pub struct Settings { pub server: ServerSettings, pub database: DatabaseSettings, pub redis: RedisSettings, pub logging: LoggingSettings, } #[derive(Debug, Deserialize)] pub struct ServerSettings { pub host: String, pub port: u16, pub workers: usize, pub keep_alive: u64, } #[derive(Debug, Deserialize)] pub struct DatabaseSettings { pub url: String, pub max_connections: u32, pub min_connections: u32, pub connect_timeout: u64, } #[derive(Debug, Deserialize)] pub struct RedisSettings { pub url: String, pub pool_size: u32, } #[derive(Debug, Deserialize)] pub struct LoggingSettings { pub level: String, pub format: String, } impl Settings { pub fn new() -> Result<Self, ConfigError> { let env = std::env::var("APP_ENV").unwrap_or_else(|_| "development".into()); let config = Config::builder() // Defaults .set_default("server.host", "127.0.0.1")? .set_default("server.port", 8080)? .set_default("server.workers", 4)? .set_default("server.keep_alive", 75)? .set_default("logging.level", "info")? .set_default("logging.format", "json")? // Base config .add_source(File::with_name("config/default")) // Environment-specific .add_source(File::with_name(&format!("config/{}", env)).required(false)) // Local overrides .add_source(File::with_name("config/local").required(false)) // Environment variables .add_source(Environment::with_prefix("APP").separator("__")) .build()?; config.try_deserialize() } } fn main() -> Result<(), ConfigError> { let settings = Settings::new()?; println!("Server: {}:{}", settings.server.host, settings.server.port); println!("Database: {}", settings.database.url); println!("Redis: {}", settings.redis.url); println!("Logging: {} ({})", settings.logging.level, settings.logging.format); Ok(()) }
config/default.toml:
[database]
max_connections = 100
min_connections = 10
connect_timeout = 30
[redis]
url = "redis://127.0.0.1:6379"
pool_size = 10
config/production.toml:
[server]
host = "0.0.0.0"
workers = 8
[database]
url = "postgres://prod-db:5432/myapp"
max_connections = 200
[logging]
level = "warn"
Integration với web frameworks
Actix-web
use actix_web::{web, App, HttpServer}; use config::{Config, ConfigError}; use serde::Deserialize; #[derive(Debug, Deserialize, Clone)] struct Settings { server: ServerConfig, } #[derive(Debug, Deserialize, Clone)] struct ServerConfig { host: String, port: u16, } #[actix_web::main] async fn main() -> std::io::Result<()> { let config = Config::builder() .add_source(config::File::with_name("config")) .build() .unwrap(); let settings: Settings = config.try_deserialize().unwrap(); let server_config = settings.server.clone(); HttpServer::new(move || { App::new() .app_data(web::Data::new(settings.clone())) }) .bind((server_config.host, server_config.port))? .run() .await }
Best Practices
1. Separate configs by environment
config/
├── default.toml # Shared defaults
├── development.toml # Dev settings
├── production.toml # Prod settings
└── local.toml # Local overrides (gitignore)
2. Use environment variables for secrets
#![allow(unused)] fn main() { // ❌ Không lưu secrets trong file [database] password = "secret123" // ✅ Dùng environment variable export APP__DATABASE__PASSWORD="secret123" }
3. Validate config at startup
#![allow(unused)] fn main() { impl Settings { pub fn new() -> Result<Self, ConfigError> { let settings = /* load config */; // Validate early settings.validate()?; Ok(settings) } fn validate(&self) -> Result<(), ConfigError> { if self.server.workers == 0 { return Err(ConfigError::Message("workers must be > 0".into())); } Ok(()) } } }
4. Document your config
#![allow(unused)] fn main() { /// Application settings #[derive(Debug, Deserialize)] pub struct Settings { /// Server configuration pub server: ServerSettings, /// Database configuration pub database: DatabaseSettings, } /// Server settings #[derive(Debug, Deserialize)] pub struct ServerSettings { /// Bind address (default: 127.0.0.1) pub host: String, /// Bind port (default: 8080) pub port: u16, } }
Tổng kết
config-rs là lựa chọn tốt cho configuration management:
- ✅ Multi-format support (JSON, TOML, YAML...)
- ✅ Layered configuration (defaults → files → env vars)
- ✅ Type-safe với serde
- ✅ Environment-specific configs
- ✅ Easy to test
Best practices:
- Separate by environment
- Use env vars for secrets
- Validate early
- Provide sensible defaults
- Document your config structure