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 min_pool_size: u32,
35
36 #[serde(default = "default_true")]
39 pub test_before_acquire: bool,
40
41 #[serde(default)]
43 pub pools: PoolsConfig,
44}
45
46impl Default for DatabaseConfig {
47 fn default() -> Self {
48 Self {
49 url: String::new(),
50 pool_size: default_pool_size(),
51 pool_timeout_secs: default_pool_timeout(),
52 statement_timeout_secs: default_statement_timeout(),
53 replica_urls: Vec::new(),
54 read_from_replica: false,
55 min_pool_size: 0,
56 test_before_acquire: true,
57 pools: PoolsConfig::default(),
58 }
59 }
60}
61
62impl DatabaseConfig {
63 pub fn new(url: impl Into<String>) -> Self {
65 Self {
66 url: url.into(),
67 ..Default::default()
68 }
69 }
70
71 pub fn url(&self) -> &str {
73 &self.url
74 }
75
76 pub fn validate(&self) -> Result<()> {
78 if self.url.is_empty() {
79 return Err(ForgeError::Config(
80 "database.url is required. \
81 Set database.url to a PostgreSQL connection string \
82 (e.g., \"postgres://user:pass@localhost/mydb\")."
83 .into(),
84 ));
85 }
86 Ok(())
87 }
88}
89
90fn default_pool_size() -> u32 {
91 50
92}
93
94fn default_pool_timeout() -> u64 {
95 30
96}
97
98fn default_statement_timeout() -> u64 {
99 30
100}
101
102fn default_true() -> bool {
103 true
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, Default)]
108pub struct PoolsConfig {
109 #[serde(default)]
111 pub default: Option<PoolConfig>,
112
113 #[serde(default)]
115 pub jobs: Option<PoolConfig>,
116
117 #[serde(default)]
119 pub observability: Option<PoolConfig>,
120
121 #[serde(default)]
123 pub analytics: Option<PoolConfig>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct PoolConfig {
129 pub size: u32,
131
132 #[serde(default = "default_pool_timeout")]
134 pub timeout_secs: u64,
135
136 pub statement_timeout_secs: Option<u64>,
138
139 #[serde(default)]
141 pub min_size: u32,
142
143 #[serde(default = "default_true")]
145 pub test_before_acquire: bool,
146}
147
148#[cfg(test)]
149#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn test_default_database_config() {
155 let config = DatabaseConfig::default();
156 assert_eq!(config.pool_size, 50);
157 assert_eq!(config.pool_timeout_secs, 30);
158 assert!(config.url.is_empty());
159 }
160
161 #[test]
162 fn test_new_config() {
163 let config = DatabaseConfig::new("postgres://localhost/test");
164 assert_eq!(config.url(), "postgres://localhost/test");
165 }
166
167 #[test]
168 fn test_parse_config() {
169 let toml = r#"
170 url = "postgres://localhost/test"
171 pool_size = 100
172 replica_urls = ["postgres://replica1/test", "postgres://replica2/test"]
173 read_from_replica = true
174 "#;
175
176 let config: DatabaseConfig = toml::from_str(toml).unwrap();
177 assert_eq!(config.pool_size, 100);
178 assert_eq!(config.url(), "postgres://localhost/test");
179 assert_eq!(config.replica_urls.len(), 2);
180 assert!(config.read_from_replica);
181 }
182
183 #[test]
184 fn test_validate_with_url() {
185 let config = DatabaseConfig::new("postgres://localhost/test");
186 assert!(config.validate().is_ok());
187 }
188
189 #[test]
190 fn test_validate_empty_url() {
191 let config = DatabaseConfig::default();
192 let result = config.validate();
193 assert!(result.is_err());
194 let err_msg = result.unwrap_err().to_string();
195 assert!(err_msg.contains("database.url is required"));
196 }
197}