Configuration Management Guide
This guide explains how to use the configuration system in Armature, inspired by NestJS's @nestjs/config.
Overview
Armature provides a comprehensive configuration management system through the armature-config module. It supports multiple configuration sources, environment variables, validation, and seamless dependency injection integration.
Features
โ
Environment Variables - Load from system environment
โ
.env File Support - Load from dotenv files
โ
Multiple Formats - JSON, TOML, and ENV files
โ
Type-Safe - Strongly typed configuration
โ
Validation - Built-in validation rules
โ
DI Integration - Injectable configuration service
โ
Prefix Support - Namespace environment variables
โ
Hierarchical - Nested configuration objects
Installation
Add the config feature to your Cargo.toml:
[dependencies]
armature-framework = { version = "0.1", features = ["config"] }
armature-config = "0.1"
Quick Start
1. Create Configuration Service
use armature_config::ConfigService;
let config = ConfigService::builder()
.with_prefix("APP".to_string())
.load_env()
.load_dotenv(None)
.build()?;
2. Access Configuration
// Get string value
let app_name = config.get_string("app.name")?;
// Get with default
let port = config.get_or("server.port", 3000);
// Get typed value
let debug: bool = config.get("app.debug")?;
3. Inject into Services
#[injectable]
#[derive(Clone)]
struct MyService {
config: ConfigService,
}
impl MyService {
fn do_something(&self) {
let api_key = self.config.get_string("api.key").unwrap();
// Use api_key...
}
}
Configuration Sources
Environment Variables
Load from system environment:
let config = ConfigService::builder()
.load_env()
.build()?;
// Access
let value = config.get_string("path")?;
With Prefix:
// Loads APP_DATABASE_HOST as "database.host"
let config = ConfigService::builder()
.with_prefix("APP".to_string())
.load_env()
.build()?;
.env Files
Create a .env file:
APP_NAME=My Application
APP_PORT=3000
DATABASE_HOST=localhost
DATABASE_PORT=5432
Load it:
let config = ConfigService::builder()
.load_dotenv(None) // Loads from .env
.build()?;
// Or specify path
let config = ConfigService::builder()
.load_dotenv(Some(".env.production".to_string()))
.build()?;
JSON Files
Create config.json:
{
"app": {
"name": "My App",
"port": 3000
},
"database": {
"host": "localhost",
"port": 5432
}
}
Load it:
use armature_config::FileFormat;
let config = ConfigService::builder()
.add_file("config.json".to_string(), FileFormat::Json)
.build()?;
TOML Files
Create config.toml:
[app]
name = "My App"
port = 3000
[database]
host = "localhost"
port = 5432
Load it:
let config = ConfigService::builder()
.add_file("config.toml".to_string(), FileFormat::Toml)
.build()?;
Multiple Sources
Load from multiple sources (later sources override earlier ones):
let config = ConfigService::builder()
.add_file("config.json".to_string(), FileFormat::Json)
.load_dotenv(None)
.load_env()
.build()?;
Type-Safe Configuration
Define Configuration Structs
use serde::{Deserialize, Serialize};
use armature_config::Validate;
#[derive(Debug, Deserialize, Serialize)]
struct AppConfig {
app: ApplicationConfig,
database: DatabaseConfig,
}
#[derive(Debug, Deserialize, Serialize)]
struct ApplicationConfig {
name: String,
port: u16,
debug: bool,
}
#[derive(Debug, Deserialize, Serialize)]
struct DatabaseConfig {
host: String,
port: u16,
username: String,
password: String,
}
Load and Validate
// Implement validation
impl Validate for AppConfig {
fn validate(&self) -> armature_config::Result<()> {
armature_config::ConfigValidator::not_empty(&self.app.name, "app.name")?;
armature_config::ConfigValidator::is_port(self.app.port, "app.port")?;
Ok(())
}
}
// Load and validate
let manager = config.manager();
let app_config: AppConfig = manager.load_validated()?;
Validation
Built-in Validators
use armature_config::ConfigValidator;
// Not empty
ConfigValidator::not_empty(&value, "field_name")?;
// Range check
ConfigValidator::in_range(port, 1, 65535, "port")?;
// One of allowed values
ConfigValidator::one_of(&env, &["dev", "prod"], "environment")?;
// URL validation
ConfigValidator::is_url(&api_url, "api_url")?;
// Email validation
ConfigValidator::is_email(&email, "email")?;
// Port validation
ConfigValidator::is_port(port, "port")?;
Custom Validation
impl Validate for MyConfig {
fn validate(&self) -> armature_config::Result<()> {
// Custom validation logic
if self.min_value > self.max_value {
return Err(ConfigError::ValidationError(
"min_value must be less than max_value".to_string()
));
}
// Use built-in validators
ConfigValidator::not_empty(&self.name, "name")?;
ConfigValidator::in_range(self.timeout, 0, 3600, "timeout")?;
Ok(())
}
}
Accessing Configuration
Basic Access
// Get value (returns Result)
let name: String = config.get("app.name")?;
// Get with default
let port = config.get_or("app.port", 3000);
// Check if key exists
if config.has("feature.enabled") {
// Key exists
}
Type-Specific Getters
// String
let name = config.get_string("app.name")?;
// Integer
let port = config.get_int("server.port")?;
// Boolean
let debug = config.get_bool("app.debug")?;
// Float
let ratio = config.get_float("app.ratio")?;
Nested Values
// Access nested configuration
let db_host = config.get_string("database.host")?;
let db_port = config.get_int("database.port")?;
// Or load as struct
#[derive(Deserialize)]
struct DatabaseConfig {
host: String,
port: i64,
}
let db_config: DatabaseConfig = config.get("database")?;
Dependency Injection
Register Config Service
#[injectable]
#[derive(Clone)]
struct ConfigService {
// ConfigService is itself injectable
}
#[module(
providers: [ConfigService],
controllers: [AppController]
)]
struct AppModule;
Inject into Services
#[injectable]
#[derive(Clone)]
struct UserService {
config: ConfigService,
}
impl UserService {
fn connect_database(&self) {
let host = self.config.get_string("database.host").unwrap();
let port = self.config.get_int("database.port").unwrap();
// Connect to database...
}
}
Inject into Controllers
#[controller("/api")]
#[derive(Clone)]
struct ApiController {
config: ConfigService,
}
impl ApiController {
fn get_version(&self) -> Result<Json<String>, Error> {
let version = self.config.get_string("app.version")
.unwrap_or_else(|_| "unknown".to_string());
Ok(Json(version))
}
}
Best Practices
1. Use Environment-Specific Files
.env.development
.env.staging
.env.production
let env = std::env::var("ENV").unwrap_or("development".to_string());
let env_file = format!(".env.{}", env);
let config = ConfigService::builder()
.load_dotenv(Some(env_file))
.load_env() // Override with system env vars
.build()?;
2. Provide Defaults
let port = config.get_or("server.port", 3000);
let host = config.get_or("server.host", "0.0.0.0".to_string());
3. Validate Early
// Validate at startup
let app_config: AppConfig = config.manager().load_validated()?;
// Use validated config
let app = Application::create::<AppModule>().await;
// Configuration is available via the DI container
4. Use Type-Safe Configuration
// Define strong types
#[derive(Deserialize)]
struct ServerConfig {
host: String,
port: u16,
#[serde(default = "default_workers")]
workers: usize,
}
fn default_workers() -> usize { 4 }
// Load typed config
let server_config: ServerConfig = config.get("server")?;
5. Namespace with Prefixes
// All env vars start with APP_
let config = ConfigService::builder()
.with_prefix("APP".to_string())
.load_env()
.build()?;
// APP_DATABASE_HOST becomes "database.host"
6. Keep Secrets Secure
// Load secrets from environment, not config files
let api_key = config.get_string("api.key")?;
let db_password = config.get_string("database.password")?;
// Don't commit .env files with secrets!
// Use .env.example instead
Configuration Patterns
Factory Pattern
struct DatabaseFactory;
impl DatabaseFactory {
fn create(config: &ConfigService) -> Result<Database> {
let host = config.get_string("database.host")?;
let port = config.get_int("database.port")?;
Database::connect(&host, port as u16)
}
}
Feature Flags
#[derive(Deserialize)]
struct FeatureFlags {
new_ui: bool,
beta_features: bool,
analytics: bool,
}
let features: FeatureFlags = config.get("features")?;
if features.new_ui {
// Use new UI
}
Multi-Environment Setup
#[derive(Deserialize)]
struct Environment {
name: String, // "development", "production"
debug: bool,
log_level: String,
}
let env: Environment = config.get("environment")?;
match env.name.as_str() {
"production" => setup_production(),
"development" => setup_development(),
_ => setup_default(),
}
Testing
Mock Configuration
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_with_mock_config() {
let config = ConfigService::new();
config.manager().set("test.value", "mock_value").unwrap();
let service = MyService { config };
assert_eq!(service.get_test_value(), "mock_value");
}
}
Test-Specific Config
#[tokio::test]
async fn test_api_endpoint() {
let config = ConfigService::builder()
.add_file("config.test.json".to_string(), FileFormat::Json)
.build()
.unwrap();
// Test with config
}
Error Handling
use armature_config::ConfigError;
match config.get_string("required.key") {
Ok(value) => println!("Value: {}", value),
Err(ConfigError::KeyNotFound(key)) => {
eprintln!("Missing required config: {}", key);
}
Err(ConfigError::ValidationError(msg)) => {
eprintln!("Invalid config: {}", msg);
}
Err(e) => {
eprintln!("Config error: {}", e);
}
}
Advanced Usage
Merge Configurations
let base_config = ConfigService::builder()
.add_file("config.base.json".to_string(), FileFormat::Json)
.build()?;
let env_config = ConfigService::builder()
.add_file("config.prod.json".to_string(), FileFormat::Json)
.build()?;
base_config.manager().merge(env_config.manager())?;
Dynamic Configuration
// Update configuration at runtime
config.manager().set("feature.enabled", true)?;
// Check and update
if !config.has("cache.ttl") {
config.manager().set("cache.ttl", 3600)?;
}
Configuration Watchers (Future)
// Watch for configuration changes
config.watch("config.json", |new_config| {
println!("Configuration updated!");
});
Comparison with NestJS
NestJS
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: '.env',
isGlobal: true,
}),
],
})
export class AppModule {}
@Injectable()
export class AppService {
constructor(private config: ConfigService) {}
getPort(): number {
return this.config.get<number>('PORT');
}
}
Armature
#[injectable]
#[derive(Clone)]
struct AppService {
config: ConfigService,
}
impl AppService {
fn get_port(&self) -> i64 {
self.config.get_int("port").unwrap()
}
}
#[module(
providers: [ConfigService, AppService],
controllers: []
)]
struct AppModule;
Summary
Armature's configuration system provides:
โ Multiple Sources - Env, .env, JSON, TOML โ Type-Safe - Strong typing and validation โ DI Integration - Seamlessly injectable โ Flexible - Hierarchical and namespace support โ Production Ready - Validation and error handling โ NestJS-Like - Familiar patterns for NestJS users
For complete examples, see examples/config_example.rs in the repository.