Skip to content

Commit

Permalink
feat: Implement robust configuration management for To-Do application
Browse files Browse the repository at this point in the history
  • Loading branch information
johns10 committed Sep 1, 2024
1 parent 2dc3908 commit e39e765
Show file tree
Hide file tree
Showing 10 changed files with 562 additions and 25 deletions.
437 changes: 437 additions & 0 deletions .aider.chat.history.md

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions .aider.input.history
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,46 @@

# 2024-09-01 13:21:51.279894
+drop *

# 2024-09-01 13:23:53.488509
+/add config/default.toml config/development.toml config/production.toml config/test.toml src/config.rs src/main.rs .env.example documentation/design/architecture.md documentation/design/code_standards.md Cargo.toml

# 2024-09-01 13:24:10.607129
+https://docs.rs/config/latest/config/, https://www.sea-ql.org/SeaORM/docs/install-and-config/connection/, https://docs.rs/axum/latest/axum/ Review and improve the existing configuration file structure for the To-Do application. The current structure includes separate TOML files for development, testing, and production environments, as well as a default configuration file. The task involves refining this structure and implementing best practices for Rust configuration management.
+
+Specifically:
+
+1. Review the existing configuration files in the `config` directory: `default.toml`, `development.toml`, `test.toml`, and `production.toml`. Ensure they cover all necessary settings, including database connection parameters, server settings, and logging levels.
+
+2. Refine the `src/config.rs` file to efficiently load and manage the configuration using the `config` crate. Implement the following:
+
+ a. Create a `Config` struct that represents all possible configuration options.
+ b. Implement a function to load the configuration, merging the default settings with environment-specific overrides.
+ c. Use the builder pattern from the `config` crate to load configurations in the following order: default, environment-specific file, environment variables.
+ d. Implement environment variable overrides for all configuration options, following the naming convention `APP_SECTION__KEY`.
+
+3. Implement configuration validation to ensure all required fields are present and have valid values.
+
+4. For sensitive information like database passwords or API keys, ensure they are not stored directly in the TOML files. Instead, use environment variables or a separate `.env` file for these values.
+
+5. In the `Config` struct, use appropriate Rust types for different configuration values (e.g., `String` for text, `u16` for ports, `Vec<String>` for lists).
+
+6. Implement methods on the `Config` struct to easily access configuration values, such as `get_database_url()` or `get_server_port()`.
+
+7. Use the `once_cell` crate to create a global, lazily-initialized configuration instance that can be easily accessed throughout the application.
+
+8. Update `src/main.rs` to initialize the configuration early in the application startup process.
+
+9. Ensure that the configuration loading process handles errors gracefully, providing meaningful error messages if a configuration file is missing or invalid.
+
+10. Add comments to the `config.rs` file explaining the configuration loading process and how to use the `Config` struct.
+
+11. Update the `.env.example` file to include all possible environment variable overrides, with placeholder values.
+
+Please implement these changes, ensuring that the code follows the project's established code standards and best practices for Rust. The configuration structure should be flexible, secure, and easy to use throughout the application.

# 2024-09-01 13:25:16.910531
+/run cargo update

# 2024-09-01 13:25:28.140585
+/test cargo test
23 changes: 15 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
# Application Configuration
APP_RUN_MODE=development

# Database Configuration
DATABASE_URL=postgres://username:password@localhost/todo_app
DATABASE_MAX_CONNECTIONS=100
DATABASE_MIN_CONNECTIONS=5
DATABASE_CONNECT_TIMEOUT=10
DATABASE_IDLE_TIMEOUT=300
DATABASE_MAX_LIFETIME=1800
APP_DATABASE__URL=postgres://username:password@localhost/todo_app
APP_DATABASE__MAX_CONNECTIONS=100
APP_DATABASE__MIN_CONNECTIONS=5
APP_DATABASE__CONNECT_TIMEOUT=10
APP_DATABASE__IDLE_TIMEOUT=300
APP_DATABASE__MAX_LIFETIME=1800

# Application Configuration
RUN_MODE=development
# Server Configuration
APP_SERVER__HOST=127.0.0.1
APP_SERVER__PORT=8080

# Logging Configuration
APP_LOGGING__LEVEL=info

# Add any other environment variables your application might need
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ tokio = { version = "1.28.2", features = ["full"] }
config = "0.13.3"
once_cell = "1.18.0"
tracing = "0.1.37"
tracing-subscriber = "0.3.17"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
anyhow = "1.0.72"
serde = { version = "1.0", features = ["derive"] }
7 changes: 7 additions & 0 deletions config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ min_connections = 5
connect_timeout = 10
idle_timeout = 300
max_lifetime = 1800

[server]
host = "127.0.0.1"
port = 8080

[logging]
level = "info"
3 changes: 3 additions & 0 deletions config/development.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[database]
url = "postgres://username:password@localhost/todo_app_dev"

[logging]
level = "debug"
6 changes: 6 additions & 0 deletions config/production.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@
url = "postgres://username:password@production_host/todo_app_prod"
max_connections = 200
min_connections = 10

[server]
host = "0.0.0.0"

[logging]
level = "warn"
3 changes: 3 additions & 0 deletions config/test.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[database]
url = "postgres://username:password@localhost/todo_app_test"

[logging]
level = "debug"
51 changes: 40 additions & 11 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use config::{Config, ConfigError, Environment, File};
use once_cell::sync::Lazy;
use serde::Deserialize;
use std::env;
use std::sync::Arc;

#[derive(Debug, Deserialize)]
pub struct DatabaseConfig {
Expand All @@ -12,31 +14,58 @@ pub struct DatabaseConfig {
pub max_lifetime: u64,
}

#[derive(Debug, Deserialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
}

#[derive(Debug, Deserialize)]
pub struct LoggingConfig {
pub level: String,
}

#[derive(Debug, Deserialize)]
pub struct AppConfig {
pub database: DatabaseConfig,
pub server: ServerConfig,
pub logging: LoggingConfig,
}

impl AppConfig {
pub fn new() -> Result<Self, ConfigError> {
let run_mode = env::var("RUN_MODE").unwrap_or_else(|_| "development".into());

let s = Config::builder()
// Start off by merging in the "default" configuration file
.add_source(File::with_name("config/default"))
// Add in the current environment file
// Default to 'development' env
// Note that this file is _optional_
.add_source(File::with_name(&format!("config/{}", run_mode)).required(false))
// Add in a local configuration file
// This file shouldn't be checked in to git
.add_source(File::with_name("config/local").required(false))
// Add in settings from the environment (with a prefix of APP)
// Eg.. `APP_DEBUG=1 ./target/app` would set the `debug` key
.add_source(Environment::with_prefix("app"))
.add_source(Environment::with_prefix("APP").separator("__"))
.build()?;

// You can deserialize (and thus freeze) the entire configuration as
s.try_deserialize()
let config: AppConfig = s.try_deserialize()?;
config.validate()?;
Ok(config)
}

fn validate(&self) -> Result<(), ConfigError> {
// Add validation logic here
// For example:
if self.database.max_connections < self.database.min_connections {
return Err(ConfigError::Message("max_connections must be greater than or equal to min_connections".into()));
}
Ok(())
}

pub fn get_database_url(&self) -> &str {
&self.database.url
}

pub fn get_server_address(&self) -> String {
format!("{}:{}", self.server.host, self.server.port)
}
}

pub static CONFIG: Lazy<Arc<AppConfig>> = Lazy::new(|| {
Arc::new(AppConfig::new().expect("Failed to load configuration"))
});
12 changes: 7 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ use tracing::info;

#[tokio::main]
async fn main() -> Result<()> {
// Initialize tracing
tracing_subscriber::fmt::init();

// Load configuration
let config = config::AppConfig::new()?;
let config = &config::CONFIG;

// Initialize tracing
tracing_subscriber::fmt()
.with_max_level(config.logging.level.parse()?)
.init();

// Initialize database connection
db::init_db(&config).await?;
db::init_db(config).await?;

info!("Application started successfully");

Expand Down

0 comments on commit e39e765

Please sign in to comment.