use serde::{Deserialize, Serialize};
use crate::error::{ForgeError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
#[serde(default)]
pub url: String,
#[serde(default = "default_pool_size")]
pub pool_size: u32,
#[serde(default = "default_pool_timeout")]
pub pool_timeout_secs: u64,
#[serde(default = "default_statement_timeout")]
pub statement_timeout_secs: u64,
#[serde(default)]
pub replica_urls: Vec<String>,
#[serde(default)]
pub read_from_replica: bool,
#[serde(default)]
pub min_pool_size: u32,
#[serde(default = "default_true")]
pub test_before_acquire: bool,
#[serde(default)]
pub pools: PoolsConfig,
}
impl Default for DatabaseConfig {
fn default() -> Self {
Self {
url: String::new(),
pool_size: default_pool_size(),
pool_timeout_secs: default_pool_timeout(),
statement_timeout_secs: default_statement_timeout(),
replica_urls: Vec::new(),
read_from_replica: false,
min_pool_size: 0,
test_before_acquire: true,
pools: PoolsConfig::default(),
}
}
}
impl DatabaseConfig {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
..Default::default()
}
}
pub fn url(&self) -> &str {
&self.url
}
pub fn validate(&self) -> Result<()> {
if self.url.is_empty() {
return Err(ForgeError::Config(
"database.url is required. \
Set database.url to a PostgreSQL connection string \
(e.g., \"postgres://user:pass@localhost/mydb\")."
.into(),
));
}
Ok(())
}
}
fn default_pool_size() -> u32 {
50
}
fn default_pool_timeout() -> u64 {
30
}
fn default_statement_timeout() -> u64 {
30
}
use super::default_true;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PoolsConfig {
#[serde(default)]
pub default: Option<PoolConfig>,
#[serde(default)]
pub jobs: Option<PoolConfig>,
#[serde(default)]
pub observability: Option<PoolConfig>,
#[serde(default)]
pub analytics: Option<PoolConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoolConfig {
pub size: u32,
#[serde(default = "default_pool_timeout")]
pub timeout_secs: u64,
pub statement_timeout_secs: Option<u64>,
#[serde(default)]
pub min_size: u32,
#[serde(default = "default_true")]
pub test_before_acquire: bool,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
mod tests {
use super::*;
#[test]
fn test_default_database_config() {
let config = DatabaseConfig::default();
assert_eq!(config.pool_size, 50);
assert_eq!(config.pool_timeout_secs, 30);
assert!(config.url.is_empty());
}
#[test]
fn test_new_config() {
let config = DatabaseConfig::new("postgres://localhost/test");
assert_eq!(config.url(), "postgres://localhost/test");
}
#[test]
fn test_parse_config() {
let toml = r#"
url = "postgres://localhost/test"
pool_size = 100
replica_urls = ["postgres://replica1/test", "postgres://replica2/test"]
read_from_replica = true
"#;
let config: DatabaseConfig = toml::from_str(toml).unwrap();
assert_eq!(config.pool_size, 100);
assert_eq!(config.url(), "postgres://localhost/test");
assert_eq!(config.replica_urls.len(), 2);
assert!(config.read_from_replica);
}
#[test]
fn test_validate_with_url() {
let config = DatabaseConfig::new("postgres://localhost/test");
assert!(config.validate().is_ok());
}
#[test]
fn test_validate_empty_url() {
let config = DatabaseConfig::default();
let result = config.validate();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("database.url is required"));
}
}