Files
pawctioneer-bot/src/config.rs
Dylan Knutson db04de9442 cargo clippy
2025-08-29 10:17:57 -07:00

135 lines
4.5 KiB
Rust

use anyhow::{Context, Result};
use sqlx::SqlitePool;
use std::{env, str::FromStr};
#[derive(Debug, Clone)]
pub struct Config {
/// Telegram bot token (required)
pub telegram_token: String,
/// Database URL (required)
pub database_url: String,
/// Admin user ID for administrative commands
pub admin_user_id: Option<i64>,
/// Port for the web interface (future feature)
pub web_port: u16,
}
/// Database connection pool type alias for convenience
pub type DatabasePool = sqlx::SqlitePool;
impl Config {
/// Load and validate configuration from environment variables
///
/// This function expects a .env file to be present or environment variables to be set.
/// Required variables: TELOXIDE_TOKEN
/// Optional variables: DATABASE_URL, ADMIN_USER_ID, WEB_PORT
///
/// The configuration is automatically validated during construction.
pub fn from_env() -> Result<Self> {
dotenvy::dotenv()?;
env_logger::init();
let telegram_token = env::var("TELOXIDE_TOKEN")
.context("TELOXIDE_TOKEN environment variable is required")?;
let database_url =
env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite:pawctioneer_bot.db".to_string());
let admin_user_id = env::var("ADMIN_USER_ID")
.ok()
.and_then(|s| s.parse::<i64>().ok());
let web_port = env::var("WEB_PORT")
.unwrap_or_else(|_| "3000".to_string())
.parse::<u16>()
.context("WEB_PORT must be a valid port number")?;
let config = Config {
telegram_token,
database_url,
admin_user_id,
web_port,
};
// Automatically validate before returning
config.validate()?;
Ok(config)
}
/// Internal validation method called automatically by from_env()
///
/// This method validates the configuration and logs important settings.
/// It's called internally and doesn't need to be called manually.
fn validate(&self) -> Result<()> {
if self.telegram_token.is_empty() {
anyhow::bail!("Telegram token cannot be empty");
}
if self.database_url.is_empty() {
anyhow::bail!("Database URL cannot be empty");
}
// Log configuration (without sensitive data)
log::info!("Configuration loaded:");
log::info!(" Database URL: {}", self.database_url);
log::info!(" Web Port: {}", self.web_port);
if let Some(admin_id) = self.admin_user_id {
log::info!(" Admin User ID: {admin_id}");
} else {
log::info!(" Admin User ID: Not set");
}
log::info!(" Telegram Token: [CONFIGURED]");
Ok(())
}
/// Create a database connection pool using the configured database URL
///
/// This establishes a connection pool to the SQLite database and runs any pending migrations.
pub async fn create_database_pool(&self) -> Result<DatabasePool> {
log::info!("Connecting to database: {}", self.database_url);
// Create connection pool with sensible defaults for SQLite
// For SQLite, we need to ensure the database file can be created
let pool = SqlitePool::connect_with(
sqlx::sqlite::SqliteConnectOptions::from_str(&self.database_url)?
.create_if_missing(true)
.pragma("foreign_keys", "ON"), // Enable foreign key constraints
)
.await
.with_context(|| format!("Failed to connect to database: {}", self.database_url))?;
// Run database migrations
log::info!("Running database migrations...");
sqlx::migrate!("./migrations")
.run(&pool)
.await
.context("Failed to run database migrations")?;
log::info!("Database migrations completed successfully");
// Run health check
Self::health_check(&pool)
.await
.context("Database health check failed after connection")?;
log::info!("Database connection pool created successfully");
Ok(pool)
}
/// Perform a health check on the database connection
///
/// This verifies the database is accessible and responding to queries.
pub async fn health_check(pool: &DatabasePool) -> Result<()> {
sqlx::query("SELECT 1")
.execute(pool)
.await
.context("Database health check query failed")?;
log::debug!("Database health check passed");
Ok(())
}
}