1use crate::error::{Error, Result};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
27pub struct PostgresConfig {
28 pub version: String,
30
31 pub database_name: String,
33
34 pub username: String,
36
37 pub password: String,
39
40 pub listen_addresses: String,
42
43 pub port: u16,
45
46 pub max_connections: Option<u32>,
48
49 pub shared_buffers: Option<String>,
51
52 pub effective_cache_size: Option<String>,
54
55 pub work_mem: Option<String>,
57
58 pub maintenance_work_mem: Option<String>,
60
61 pub wal_buffers: Option<String>,
63
64 pub checkpoint_completion_target: Option<f32>,
66
67 pub ssl: bool,
69
70 pub extra_config: std::collections::HashMap<String, String>,
72}
73
74impl PostgresConfig {
75 pub fn builder() -> PostgresConfigBuilder {
77 PostgresConfigBuilder::default()
78 }
79
80 pub fn validate(&self) -> Result<()> {
86 if self.version.is_empty() {
87 return Err(Error::MissingConfig("version".to_string()));
88 }
89
90 if !self.version.chars().all(|c| c.is_ascii_digit() || c == '.') {
92 return Err(Error::InvalidVersion(self.version.clone()));
93 }
94
95 if self.database_name.is_empty() {
96 return Err(Error::MissingConfig("database_name".to_string()));
97 }
98
99 if self.username.is_empty() {
100 return Err(Error::MissingConfig("username".to_string()));
101 }
102
103 if self.password.is_empty() {
104 return Err(Error::MissingConfig("password".to_string()));
105 }
106
107 if self.port == 0 {
108 return Err(Error::invalid_config("port", self.port.to_string()));
109 }
110
111 if let Some(target) = self.checkpoint_completion_target
112 && !(0.0..=1.0).contains(&target)
113 {
114 return Err(Error::invalid_config(
115 "checkpoint_completion_target",
116 target.to_string(),
117 ));
118 }
119
120 if let Some(ref shared_buffers) = self.shared_buffers {
122 crate::validation::parse_memory_size(shared_buffers)?;
123 }
124
125 if let Some(ref work_mem) = self.work_mem {
126 crate::validation::parse_memory_size(work_mem)?;
127 }
128
129 if let Some(ref maintenance_work_mem) = self.maintenance_work_mem {
130 crate::validation::parse_memory_size(maintenance_work_mem)?;
131 }
132
133 if let Some(ref effective_cache_size) = self.effective_cache_size {
134 crate::validation::parse_memory_size(effective_cache_size)?;
135 }
136
137 if let Some(ref wal_buffers) = self.wal_buffers {
138 crate::validation::parse_memory_size(wal_buffers)?;
139 }
140
141 crate::validation::validate_listen_addresses(&self.listen_addresses)?;
143
144 Ok(())
145 }
146
147 pub fn config_dir(&self) -> String {
149 format!("/etc/postgresql/{}/main", self.version)
150 }
151
152 pub fn postgresql_conf_path(&self) -> String {
154 format!("{}/postgresql.conf", self.config_dir())
155 }
156
157 pub fn pg_hba_conf_path(&self) -> String {
159 format!("{}/pg_hba.conf", self.config_dir())
160 }
161}
162
163#[derive(Debug, Default)]
185pub struct PostgresConfigBuilder {
186 version: Option<String>,
187 database_name: Option<String>,
188 username: Option<String>,
189 password: Option<String>,
190 listen_addresses: Option<String>,
191 port: Option<u16>,
192 max_connections: Option<u32>,
193 shared_buffers: Option<String>,
194 effective_cache_size: Option<String>,
195 work_mem: Option<String>,
196 maintenance_work_mem: Option<String>,
197 wal_buffers: Option<String>,
198 checkpoint_completion_target: Option<f32>,
199 ssl: Option<bool>,
200 extra_config: std::collections::HashMap<String, String>,
201}
202
203impl PostgresConfigBuilder {
204 pub fn version(mut self, version: impl Into<String>) -> Self {
206 self.version = Some(version.into());
207 self
208 }
209
210 pub fn database_name(mut self, name: impl Into<String>) -> Self {
212 self.database_name = Some(name.into());
213 self
214 }
215
216 pub fn username(mut self, username: impl Into<String>) -> Self {
218 self.username = Some(username.into());
219 self
220 }
221
222 pub fn password(mut self, password: impl Into<String>) -> Self {
224 self.password = Some(password.into());
225 self
226 }
227
228 pub fn listen_addresses(mut self, addresses: impl Into<String>) -> Self {
230 self.listen_addresses = Some(addresses.into());
231 self
232 }
233
234 pub fn port(mut self, port: u16) -> Self {
236 self.port = Some(port);
237 self
238 }
239
240 pub fn max_connections(mut self, max: u32) -> Self {
242 self.max_connections = Some(max);
243 self
244 }
245
246 pub fn shared_buffers(mut self, size: impl Into<String>) -> Self {
248 self.shared_buffers = Some(size.into());
249 self
250 }
251
252 pub fn effective_cache_size(mut self, size: impl Into<String>) -> Self {
254 self.effective_cache_size = Some(size.into());
255 self
256 }
257
258 pub fn work_mem(mut self, size: impl Into<String>) -> Self {
260 self.work_mem = Some(size.into());
261 self
262 }
263
264 pub fn maintenance_work_mem(mut self, size: impl Into<String>) -> Self {
266 self.maintenance_work_mem = Some(size.into());
267 self
268 }
269
270 pub fn wal_buffers(mut self, size: impl Into<String>) -> Self {
272 self.wal_buffers = Some(size.into());
273 self
274 }
275
276 pub fn checkpoint_completion_target(mut self, target: f32) -> Self {
278 self.checkpoint_completion_target = Some(target);
279 self
280 }
281
282 pub fn ssl(mut self, enabled: bool) -> Self {
284 self.ssl = Some(enabled);
285 self
286 }
287
288 pub fn add_config(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
290 self.extra_config.insert(key.into(), value.into());
291 self
292 }
293
294 pub fn build(self) -> Result<PostgresConfig> {
296 let config = PostgresConfig {
297 version: self.version.unwrap_or_else(|| "15".to_string()),
298 database_name: self
299 .database_name
300 .ok_or_else(|| Error::MissingConfig("database_name".to_string()))?,
301 username: self
302 .username
303 .ok_or_else(|| Error::MissingConfig("username".to_string()))?,
304 password: self
305 .password
306 .ok_or_else(|| Error::MissingConfig("password".to_string()))?,
307 listen_addresses: self
308 .listen_addresses
309 .unwrap_or_else(|| "0.0.0.0/0".to_string()),
310 port: self.port.unwrap_or(5432),
311 max_connections: self.max_connections,
312 shared_buffers: self.shared_buffers,
313 effective_cache_size: self.effective_cache_size,
314 work_mem: self.work_mem,
315 maintenance_work_mem: self.maintenance_work_mem,
316 wal_buffers: self.wal_buffers,
317 checkpoint_completion_target: self.checkpoint_completion_target,
318 ssl: self.ssl.unwrap_or(false),
319 extra_config: self.extra_config,
320 };
321
322 config.validate()?;
323 Ok(config)
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_builder_minimal() {
333 let config = PostgresConfig::builder()
334 .database_name("test_db")
335 .username("test_user")
336 .password("test_pass")
337 .build()
338 .unwrap();
339
340 assert_eq!(config.version, "15");
341 assert_eq!(config.database_name, "test_db");
342 assert_eq!(config.username, "test_user");
343 assert_eq!(config.password, "test_pass");
344 assert_eq!(config.port, 5432);
345 assert!(!config.ssl);
346 }
347
348 #[test]
349 fn test_builder_full() {
350 let config = PostgresConfig::builder()
351 .version("14")
352 .database_name("prod_db")
353 .username("prod_user")
354 .password("secure_pass")
355 .listen_addresses("10.0.0.0/8")
356 .port(5433)
357 .max_connections(200)
358 .shared_buffers("512MB")
359 .effective_cache_size("2GB")
360 .ssl(true)
361 .add_config("log_statement", "all")
362 .build()
363 .unwrap();
364
365 assert_eq!(config.version, "14");
366 assert_eq!(config.port, 5433);
367 assert_eq!(config.max_connections, Some(200));
368 assert!(config.ssl);
369 assert_eq!(
370 config.extra_config.get("log_statement"),
371 Some(&"all".to_string())
372 );
373 }
374
375 #[test]
376 fn test_missing_required_fields() {
377 let result = PostgresConfig::builder().build();
378 assert!(result.is_err());
379
380 let result = PostgresConfig::builder().database_name("db").build();
381 assert!(result.is_err());
382
383 let result = PostgresConfig::builder()
384 .database_name("db")
385 .username("user")
386 .build();
387 assert!(result.is_err());
388 }
389
390 #[test]
391 fn test_invalid_version() {
392 let result = PostgresConfig::builder()
393 .version("invalid-version")
394 .database_name("db")
395 .username("user")
396 .password("pass")
397 .build();
398
399 assert!(matches!(result, Err(Error::InvalidVersion(_))));
400 }
401
402 #[test]
403 fn test_config_paths() {
404 let config = PostgresConfig::builder()
405 .version("15")
406 .database_name("db")
407 .username("user")
408 .password("pass")
409 .build()
410 .unwrap();
411
412 assert_eq!(config.config_dir(), "/etc/postgresql/15/main");
413 assert_eq!(
414 config.postgresql_conf_path(),
415 "/etc/postgresql/15/main/postgresql.conf"
416 );
417 assert_eq!(
418 config.pg_hba_conf_path(),
419 "/etc/postgresql/15/main/pg_hba.conf"
420 );
421 }
422}