lmrc_postgres/
config.rs

1//! PostgreSQL configuration with builder pattern
2//!
3//! This module provides a comprehensive configuration system for PostgreSQL
4//! with type-safe builder patterns.
5
6use crate::error::{Error, Result};
7use serde::{Deserialize, Serialize};
8
9/// PostgreSQL configuration
10///
11/// Use [`PostgresConfigBuilder`] to construct instances.
12///
13/// # Example
14///
15/// ```rust
16/// use lmrc_postgres::PostgresConfig;
17///
18/// let config = PostgresConfig::builder()
19///     .version("15")
20///     .database_name("myapp")
21///     .username("myuser")
22///     .password("secure_password")
23///     .build()
24///     .unwrap();
25/// ```
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
27pub struct PostgresConfig {
28    /// PostgreSQL version (e.g., "15", "14", "13")
29    pub version: String,
30
31    /// Database name to create
32    pub database_name: String,
33
34    /// Database username
35    pub username: String,
36
37    /// Database password
38    pub password: String,
39
40    /// Listen addresses (CIDR notation, e.g., "0.0.0.0/0" or "10.0.0.0/8")
41    pub listen_addresses: String,
42
43    /// PostgreSQL port (default: 5432)
44    pub port: u16,
45
46    /// Maximum number of concurrent connections
47    pub max_connections: Option<u32>,
48
49    /// Shared buffers size (e.g., "256MB", "1GB")
50    pub shared_buffers: Option<String>,
51
52    /// Effective cache size (e.g., "1GB", "4GB")
53    pub effective_cache_size: Option<String>,
54
55    /// Work memory (e.g., "4MB", "16MB")
56    pub work_mem: Option<String>,
57
58    /// Maintenance work memory (e.g., "64MB", "256MB")
59    pub maintenance_work_mem: Option<String>,
60
61    /// WAL buffers (e.g., "16MB")
62    pub wal_buffers: Option<String>,
63
64    /// Checkpoint completion target (0.0 to 1.0)
65    pub checkpoint_completion_target: Option<f32>,
66
67    /// Enable SSL
68    pub ssl: bool,
69
70    /// Additional configuration parameters
71    pub extra_config: std::collections::HashMap<String, String>,
72}
73
74impl PostgresConfig {
75    /// Create a new builder
76    pub fn builder() -> PostgresConfigBuilder {
77        PostgresConfigBuilder::default()
78    }
79
80    /// Validate the configuration (basic validation only)
81    ///
82    /// For comprehensive validation including memory sizes, CIDR notation,
83    /// conflicting settings, and resource limits, use [`validate_comprehensive`]
84    /// from the validation module.
85    pub fn validate(&self) -> Result<()> {
86        if self.version.is_empty() {
87            return Err(Error::MissingConfig("version".to_string()));
88        }
89
90        // Validate version format
91        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        // Validate memory sizes if specified
121        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        // Validate listen_addresses CIDR notation
142        crate::validation::validate_listen_addresses(&self.listen_addresses)?;
143
144        Ok(())
145    }
146
147    /// Get the PostgreSQL configuration directory path
148    pub fn config_dir(&self) -> String {
149        format!("/etc/postgresql/{}/main", self.version)
150    }
151
152    /// Get the postgresql.conf file path
153    pub fn postgresql_conf_path(&self) -> String {
154        format!("{}/postgresql.conf", self.config_dir())
155    }
156
157    /// Get the pg_hba.conf file path
158    pub fn pg_hba_conf_path(&self) -> String {
159        format!("{}/pg_hba.conf", self.config_dir())
160    }
161}
162
163/// Builder for [`PostgresConfig`]
164///
165/// # Example
166///
167/// ```rust
168/// use lmrc_postgres::PostgresConfig;
169///
170/// let config = PostgresConfig::builder()
171///     .version("15")
172///     .database_name("production_db")
173///     .username("app_user")
174///     .password("strong_password")
175///     .listen_addresses("10.0.0.0/8")
176///     .port(5432)
177///     .max_connections(200)
178///     .shared_buffers("512MB")
179///     .effective_cache_size("2GB")
180///     .ssl(true)
181///     .build()
182///     .unwrap();
183/// ```
184#[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    /// Set PostgreSQL version
205    pub fn version(mut self, version: impl Into<String>) -> Self {
206        self.version = Some(version.into());
207        self
208    }
209
210    /// Set database name
211    pub fn database_name(mut self, name: impl Into<String>) -> Self {
212        self.database_name = Some(name.into());
213        self
214    }
215
216    /// Set username
217    pub fn username(mut self, username: impl Into<String>) -> Self {
218        self.username = Some(username.into());
219        self
220    }
221
222    /// Set password
223    pub fn password(mut self, password: impl Into<String>) -> Self {
224        self.password = Some(password.into());
225        self
226    }
227
228    /// Set listen addresses (CIDR notation)
229    pub fn listen_addresses(mut self, addresses: impl Into<String>) -> Self {
230        self.listen_addresses = Some(addresses.into());
231        self
232    }
233
234    /// Set port (default: 5432)
235    pub fn port(mut self, port: u16) -> Self {
236        self.port = Some(port);
237        self
238    }
239
240    /// Set maximum connections
241    pub fn max_connections(mut self, max: u32) -> Self {
242        self.max_connections = Some(max);
243        self
244    }
245
246    /// Set shared buffers
247    pub fn shared_buffers(mut self, size: impl Into<String>) -> Self {
248        self.shared_buffers = Some(size.into());
249        self
250    }
251
252    /// Set effective cache size
253    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    /// Set work memory
259    pub fn work_mem(mut self, size: impl Into<String>) -> Self {
260        self.work_mem = Some(size.into());
261        self
262    }
263
264    /// Set maintenance work memory
265    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    /// Set WAL buffers
271    pub fn wal_buffers(mut self, size: impl Into<String>) -> Self {
272        self.wal_buffers = Some(size.into());
273        self
274    }
275
276    /// Set checkpoint completion target (0.0 to 1.0)
277    pub fn checkpoint_completion_target(mut self, target: f32) -> Self {
278        self.checkpoint_completion_target = Some(target);
279        self
280    }
281
282    /// Enable or disable SSL
283    pub fn ssl(mut self, enabled: bool) -> Self {
284        self.ssl = Some(enabled);
285        self
286    }
287
288    /// Add custom configuration parameter
289    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    /// Build the configuration
295    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}