forge_core/config/
database.rs1use serde::{Deserialize, Serialize};
2
3use crate::error::{ForgeError, Result};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct DatabaseConfig {
8 #[serde(default)]
10 pub url: String,
11
12 #[serde(default = "default_pool_size")]
14 pub pool_size: u32,
15
16 #[serde(default = "default_pool_timeout")]
18 pub pool_timeout_secs: u64,
19
20 #[serde(default = "default_statement_timeout")]
22 pub statement_timeout_secs: u64,
23
24 #[serde(default)]
26 pub replica_urls: Vec<String>,
27
28 #[serde(default)]
30 pub read_from_replica: bool,
31
32 #[serde(default)]
34 pub pools: PoolsConfig,
35}
36
37impl Default for DatabaseConfig {
38 fn default() -> Self {
39 Self {
40 url: String::new(),
41 pool_size: default_pool_size(),
42 pool_timeout_secs: default_pool_timeout(),
43 statement_timeout_secs: default_statement_timeout(),
44 replica_urls: Vec::new(),
45 read_from_replica: false,
46 pools: PoolsConfig::default(),
47 }
48 }
49}
50
51impl DatabaseConfig {
52 pub fn new(url: impl Into<String>) -> Self {
54 Self {
55 url: url.into(),
56 ..Default::default()
57 }
58 }
59
60 pub fn url(&self) -> &str {
62 &self.url
63 }
64
65 pub fn validate(&self) -> Result<()> {
67 if self.url.is_empty() {
68 return Err(ForgeError::Config(
69 "database.url is required. \
70 Set database.url to a PostgreSQL connection string \
71 (e.g., \"postgres://user:pass@localhost/mydb\")."
72 .into(),
73 ));
74 }
75 Ok(())
76 }
77}
78
79fn default_pool_size() -> u32 {
80 50
81}
82
83fn default_pool_timeout() -> u64 {
84 30
85}
86
87fn default_statement_timeout() -> u64 {
88 30
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, Default)]
93pub struct PoolsConfig {
94 #[serde(default)]
96 pub default: Option<PoolConfig>,
97
98 #[serde(default)]
100 pub jobs: Option<PoolConfig>,
101
102 #[serde(default)]
104 pub observability: Option<PoolConfig>,
105
106 #[serde(default)]
108 pub analytics: Option<PoolConfig>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct PoolConfig {
114 pub size: u32,
116
117 #[serde(default = "default_pool_timeout")]
119 pub timeout_secs: u64,
120
121 pub statement_timeout_secs: Option<u64>,
123}
124
125#[cfg(test)]
126#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_default_database_config() {
132 let config = DatabaseConfig::default();
133 assert_eq!(config.pool_size, 50);
134 assert_eq!(config.pool_timeout_secs, 30);
135 assert!(config.url.is_empty());
136 }
137
138 #[test]
139 fn test_new_config() {
140 let config = DatabaseConfig::new("postgres://localhost/test");
141 assert_eq!(config.url(), "postgres://localhost/test");
142 }
143
144 #[test]
145 fn test_parse_config() {
146 let toml = r#"
147 url = "postgres://localhost/test"
148 pool_size = 100
149 replica_urls = ["postgres://replica1/test", "postgres://replica2/test"]
150 read_from_replica = true
151 "#;
152
153 let config: DatabaseConfig = toml::from_str(toml).unwrap();
154 assert_eq!(config.pool_size, 100);
155 assert_eq!(config.url(), "postgres://localhost/test");
156 assert_eq!(config.replica_urls.len(), 2);
157 assert!(config.read_from_replica);
158 }
159
160 #[test]
161 fn test_validate_with_url() {
162 let config = DatabaseConfig::new("postgres://localhost/test");
163 assert!(config.validate().is_ok());
164 }
165
166 #[test]
167 fn test_validate_empty_url() {
168 let config = DatabaseConfig::default();
169 let result = config.validate();
170 assert!(result.is_err());
171 let err_msg = result.unwrap_err().to_string();
172 assert!(err_msg.contains("database.url is required"));
173 }
174}