Skip to main content

dbkit/
config.rs

1use crate::DbkitError;
2
3/// A supported transactional database backend, detected from the URL scheme.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Backend {
6    Postgres,
7    MySql,
8    Sqlite,
9}
10
11impl Backend {
12    /// Detect the backend from a connection URL's scheme
13    /// (`postgres://`, `mysql://`, `sqlite://`).
14    pub fn from_url(url: &str) -> Result<Self, DbkitError> {
15        match url.split(':').next().unwrap_or("") {
16            "postgres" | "postgresql" => Ok(Backend::Postgres),
17            "mysql" => Ok(Backend::MySql),
18            "sqlite" => Ok(Backend::Sqlite),
19            other => Err(DbkitError::UnsupportedBackend(other.to_string())),
20        }
21    }
22
23    /// The URL scheme used when building a URL from parts.
24    fn scheme(self) -> &'static str {
25        match self {
26            Backend::Postgres => "postgres",
27            Backend::MySql => "mysql",
28            Backend::Sqlite => "sqlite",
29        }
30    }
31
32    /// The default TCP port for server backends (0 for SQLite, which is fileless).
33    fn default_port(self) -> u16 {
34        match self {
35            Backend::Postgres => 5432,
36            Backend::MySql => 3306,
37            Backend::Sqlite => 0,
38        }
39    }
40}
41
42/// Configuration for a dbkit database connection.
43///
44/// Can be built from a URL string or constructed with the builder.
45///
46/// # Example
47/// ```
48/// use dbkit::DbkitConfig;
49///
50/// // From URL
51/// let config = DbkitConfig::from_url("postgres://localhost/mydb");
52///
53/// // From builder
54/// let config = DbkitConfig::builder()
55///     .host("db.example.com")
56///     .port(5432)
57///     .database("myapp")
58///     .user("admin")
59///     .password("secret")
60///     .pool_size(16)
61///     .connect_timeout_secs(10)
62///     .build();
63/// ```
64#[derive(Debug, Clone)]
65pub struct DbkitConfig {
66    /// Connection URL. The scheme selects the backend
67    /// (`postgres://`, `mysql://`, `sqlite://`).
68    pub url: String,
69    /// Maximum pool size. Default: 16.
70    pub pool_size: usize,
71    /// Connection timeout in seconds. Default: 30.
72    pub connect_timeout_secs: u64,
73    /// Idle timeout in seconds. Connections idle longer are reaped. Default: 300.
74    pub idle_timeout_secs: u64,
75    /// Auto-create the database if it doesn't exist. Default: true.
76    pub auto_create_db: bool,
77}
78
79impl DbkitConfig {
80    /// Create config from a connection URL with default settings.
81    pub fn from_url(url: &str) -> Self {
82        Self {
83            url: url.to_string(),
84            pool_size: 16,
85            connect_timeout_secs: 30,
86            idle_timeout_secs: 300,
87            auto_create_db: true,
88        }
89    }
90
91    /// Start building a config from individual connection parameters.
92    pub fn builder() -> ConfigBuilder {
93        ConfigBuilder::default()
94    }
95}
96
97/// Builder for constructing a [`DbkitConfig`] from individual parameters.
98pub struct ConfigBuilder {
99    backend: Backend,
100    host: String,
101    port: Option<u16>,
102    database: String,
103    user: Option<String>,
104    password: Option<String>,
105    pool_size: usize,
106    connect_timeout_secs: u64,
107    idle_timeout_secs: u64,
108    auto_create_db: bool,
109    ssl_mode: SslMode,
110}
111
112/// SSL mode for Postgres connections.
113#[derive(Debug, Clone, Copy, Default)]
114pub enum SslMode {
115    /// No SSL (default — matches current NoTls behavior).
116    #[default]
117    Disable,
118    /// Prefer SSL but allow fallback.
119    Prefer,
120    /// Require SSL.
121    Require,
122}
123
124impl Default for ConfigBuilder {
125    fn default() -> Self {
126        Self {
127            backend: Backend::Postgres,
128            host: "localhost".into(),
129            port: None,
130            database: "postgres".into(),
131            user: None,
132            password: None,
133            pool_size: 16,
134            connect_timeout_secs: 30,
135            idle_timeout_secs: 300,
136            auto_create_db: true,
137            ssl_mode: SslMode::default(),
138        }
139    }
140}
141
142impl ConfigBuilder {
143    /// Select the target backend. Defaults to [`Backend::Postgres`].
144    pub fn backend(mut self, backend: Backend) -> Self {
145        self.backend = backend;
146        self
147    }
148
149    pub fn host(mut self, host: &str) -> Self {
150        self.host = host.to_string();
151        self
152    }
153
154    pub fn port(mut self, port: u16) -> Self {
155        self.port = Some(port);
156        self
157    }
158
159    pub fn database(mut self, database: &str) -> Self {
160        self.database = database.to_string();
161        self
162    }
163
164    pub fn user(mut self, user: &str) -> Self {
165        self.user = Some(user.to_string());
166        self
167    }
168
169    pub fn password(mut self, password: &str) -> Self {
170        self.password = Some(password.to_string());
171        self
172    }
173
174    pub fn pool_size(mut self, size: usize) -> Self {
175        self.pool_size = size;
176        self
177    }
178
179    pub fn connect_timeout_secs(mut self, secs: u64) -> Self {
180        self.connect_timeout_secs = secs;
181        self
182    }
183
184    pub fn idle_timeout_secs(mut self, secs: u64) -> Self {
185        self.idle_timeout_secs = secs;
186        self
187    }
188
189    pub fn auto_create_db(mut self, enabled: bool) -> Self {
190        self.auto_create_db = enabled;
191        self
192    }
193
194    pub fn ssl_mode(mut self, mode: SslMode) -> Self {
195        self.ssl_mode = mode;
196        self
197    }
198
199    /// Build the config, constructing the connection URL from parts.
200    ///
201    /// For [`Backend::Sqlite`] the URL is `sqlite://{database}`, treating
202    /// `database` as a file path; host/port/auth/SSL are ignored. SQLite users
203    /// are usually better served by [`DbkitConfig::from_url`].
204    pub fn build(self) -> DbkitConfig {
205        let url = match self.backend {
206            Backend::Sqlite => format!("sqlite://{}", self.database),
207            backend => {
208                let auth = match (&self.user, &self.password) {
209                    (Some(u), Some(p)) => format!("{}:{}@", u, p),
210                    (Some(u), None) => format!("{}@", u),
211                    _ => String::new(),
212                };
213
214                let port = self.port.unwrap_or_else(|| backend.default_port());
215
216                let ssl_param = match self.ssl_mode {
217                    SslMode::Disable => "",
218                    SslMode::Prefer => "?sslmode=prefer",
219                    SslMode::Require => "?sslmode=require",
220                };
221
222                format!(
223                    "{}://{}{}:{}/{}{}",
224                    backend.scheme(),
225                    auth,
226                    self.host,
227                    port,
228                    self.database,
229                    ssl_param
230                )
231            }
232        };
233
234        DbkitConfig {
235            url,
236            pool_size: self.pool_size,
237            connect_timeout_secs: self.connect_timeout_secs,
238            idle_timeout_secs: self.idle_timeout_secs,
239            auto_create_db: self.auto_create_db,
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_from_url() {
250        let config = DbkitConfig::from_url("postgres://localhost/mydb");
251        assert_eq!(config.url, "postgres://localhost/mydb");
252        assert_eq!(config.pool_size, 16);
253        assert!(config.auto_create_db);
254    }
255
256    #[test]
257    fn test_builder_full() {
258        let config = DbkitConfig::builder()
259            .host("db.example.com")
260            .port(5433)
261            .database("myapp")
262            .user("admin")
263            .password("secret")
264            .pool_size(32)
265            .connect_timeout_secs(10)
266            .ssl_mode(SslMode::Require)
267            .build();
268
269        assert_eq!(
270            config.url,
271            "postgres://admin:secret@db.example.com:5433/myapp?sslmode=require"
272        );
273        assert_eq!(config.pool_size, 32);
274        assert_eq!(config.connect_timeout_secs, 10);
275    }
276
277    #[test]
278    fn test_builder_minimal() {
279        let config = DbkitConfig::builder().database("test").build();
280        assert_eq!(config.url, "postgres://localhost:5432/test");
281    }
282
283    #[test]
284    fn test_builder_user_no_password() {
285        let config = DbkitConfig::builder()
286            .user("readonly")
287            .database("prod")
288            .build();
289        assert_eq!(config.url, "postgres://readonly@localhost:5432/prod");
290    }
291
292    #[test]
293    fn test_builder_mysql_default_port() {
294        let config = DbkitConfig::builder()
295            .backend(Backend::MySql)
296            .user("root")
297            .database("app")
298            .build();
299        assert_eq!(config.url, "mysql://root@localhost:3306/app");
300    }
301
302    #[test]
303    fn test_builder_sqlite() {
304        let config = DbkitConfig::builder()
305            .backend(Backend::Sqlite)
306            .database("data/app.db")
307            .build();
308        assert_eq!(config.url, "sqlite://data/app.db");
309    }
310
311    #[test]
312    fn test_backend_from_url() {
313        assert_eq!(Backend::from_url("postgres://x/y").unwrap(), Backend::Postgres);
314        assert_eq!(Backend::from_url("postgresql://x/y").unwrap(), Backend::Postgres);
315        assert_eq!(Backend::from_url("mysql://x/y").unwrap(), Backend::MySql);
316        assert_eq!(Backend::from_url("sqlite://f.db").unwrap(), Backend::Sqlite);
317        assert!(Backend::from_url("oracle://x/y").is_err());
318    }
319}