use std::path::PathBuf; use secrecy::{ExposeSecret, SecretString}; use crate::config::helpers::{optional_env, parse_optional_env}; use crate::error::ConfigError; /// Which database backend to use. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum DatabaseBackend { /// PostgreSQL via deadpool-postgres (default). #[default] Postgres, /// libSQL/Turso embedded database. LibSql, } impl std::fmt::Display for DatabaseBackend { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Postgres => write!(f, "postgres"), Self::LibSql => write!(f, "libsql"), } } } impl std::str::FromStr for DatabaseBackend { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "postgres" | "postgresql" | "pg" => Ok(Self::Postgres), "libsql" | "turso" | "sqlite" => Ok(Self::LibSql), _ => Err(format!( "invalid database backend '{}', expected 'postgres' or 'libsql'", s )), } } } /// Database configuration. #[derive(Debug, Clone)] pub struct DatabaseConfig { /// Which backend to use (default: Postgres). pub backend: DatabaseBackend, // -- PostgreSQL fields -- pub url: SecretString, pub pool_size: usize, // -- libSQL fields -- /// Path to local libSQL database file (default: ~/.ironclaw/ironclaw.db). pub libsql_path: Option, /// Turso cloud URL for remote sync (optional). pub libsql_url: Option, /// Turso auth token (required when libsql_url is set). pub libsql_auth_token: Option, } impl DatabaseConfig { pub(crate) fn resolve() -> Result { let backend: DatabaseBackend = if let Some(b) = optional_env("DATABASE_BACKEND")? { b.parse().map_err(|e| ConfigError::InvalidValue { key: "DATABASE_BACKEND".to_string(), message: e, })? } else { DatabaseBackend::default() }; // PostgreSQL URL is required only when using the postgres backend. // For libsql backend, default to an empty placeholder. // DATABASE_URL is loaded from ~/.ironclaw/.env via dotenvy early in startup. let url = optional_env("DATABASE_URL")? .or_else(|| { if backend == DatabaseBackend::LibSql { Some("unused://libsql".to_string()) } else { None } }) .ok_or_else(|| ConfigError::MissingRequired { key: "DATABASE_URL".to_string(), hint: "Run 'ironclaw onboard' or set DATABASE_URL environment variable".to_string(), })?; let pool_size = parse_optional_env("DATABASE_POOL_SIZE", 10)?; let libsql_path = optional_env("LIBSQL_PATH")?.map(PathBuf::from).or_else(|| { if backend == DatabaseBackend::LibSql { Some(default_libsql_path()) } else { None } }); let libsql_url = optional_env("LIBSQL_URL")?; let libsql_auth_token = optional_env("LIBSQL_AUTH_TOKEN")?.map(SecretString::from); if libsql_url.is_some() && libsql_auth_token.is_none() { return Err(ConfigError::MissingRequired { key: "LIBSQL_AUTH_TOKEN".to_string(), hint: "LIBSQL_AUTH_TOKEN is required when LIBSQL_URL is set".to_string(), }); } Ok(Self { backend, url: SecretString::from(url), pool_size, libsql_path, libsql_url, libsql_auth_token, }) } /// Get the database URL (exposes the secret). pub fn url(&self) -> &str { self.url.expose_secret() } } /// Default libSQL database path (~/.ironclaw/ironclaw.db). pub fn default_libsql_path() -> PathBuf { dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) .join(".ironclaw") .join("ironclaw.db") }