1use crate::DbkitError;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Backend {
6 Postgres,
7 MySql,
8 Sqlite,
9}
10
11impl Backend {
12 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 fn scheme(self) -> &'static str {
25 match self {
26 Backend::Postgres => "postgres",
27 Backend::MySql => "mysql",
28 Backend::Sqlite => "sqlite",
29 }
30 }
31
32 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#[derive(Debug, Clone)]
65pub struct DbkitConfig {
66 pub url: String,
69 pub pool_size: usize,
71 pub connect_timeout_secs: u64,
73 pub idle_timeout_secs: u64,
75 pub auto_create_db: bool,
77}
78
79impl DbkitConfig {
80 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 pub fn builder() -> ConfigBuilder {
93 ConfigBuilder::default()
94 }
95}
96
97pub 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#[derive(Debug, Clone, Copy, Default)]
120pub enum SslMode {
121 #[default]
123 Disable,
124 Prefer,
126 Require,
128}
129
130impl Default for ConfigBuilder {
131 fn default() -> Self {
132 Self {
133 backend: Backend::Postgres,
134 host: "localhost".into(),
135 port: None,
136 database: "postgres".into(),
137 user: None,
138 password: None,
139 pool_size: 16,
140 connect_timeout_secs: 30,
141 idle_timeout_secs: 300,
142 auto_create_db: true,
143 ssl_mode: SslMode::default(),
144 }
145 }
146}
147
148impl ConfigBuilder {
149 pub fn backend(mut self, backend: Backend) -> Self {
151 self.backend = backend;
152 self
153 }
154
155 pub fn host(mut self, host: &str) -> Self {
156 self.host = host.to_string();
157 self
158 }
159
160 pub fn port(mut self, port: u16) -> Self {
161 self.port = Some(port);
162 self
163 }
164
165 pub fn database(mut self, database: &str) -> Self {
166 self.database = database.to_string();
167 self
168 }
169
170 pub fn user(mut self, user: &str) -> Self {
171 self.user = Some(user.to_string());
172 self
173 }
174
175 pub fn password(mut self, password: &str) -> Self {
176 self.password = Some(password.to_string());
177 self
178 }
179
180 pub fn pool_size(mut self, size: usize) -> Self {
181 self.pool_size = size;
182 self
183 }
184
185 pub fn connect_timeout_secs(mut self, secs: u64) -> Self {
186 self.connect_timeout_secs = secs;
187 self
188 }
189
190 pub fn idle_timeout_secs(mut self, secs: u64) -> Self {
191 self.idle_timeout_secs = secs;
192 self
193 }
194
195 pub fn auto_create_db(mut self, enabled: bool) -> Self {
196 self.auto_create_db = enabled;
197 self
198 }
199
200 pub fn ssl_mode(mut self, mode: SslMode) -> Self {
201 self.ssl_mode = mode;
202 self
203 }
204
205 pub fn build(self) -> DbkitConfig {
211 let url = match self.backend {
212 Backend::Sqlite => format!("sqlite://{}", self.database),
213 backend => {
214 use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
218 let enc = |s: &str| utf8_percent_encode(s, NON_ALPHANUMERIC).to_string();
219 let auth = match (&self.user, &self.password) {
220 (Some(u), Some(p)) => format!("{}:{}@", enc(u), enc(p)),
221 (Some(u), None) => format!("{}@", enc(u)),
222 _ => String::new(),
223 };
224
225 let port = self.port.unwrap_or_else(|| backend.default_port());
226
227 let ssl_param = match (backend, self.ssl_mode) {
233 (Backend::MySql, SslMode::Disable) => "?ssl-mode=DISABLED",
234 (Backend::MySql, SslMode::Prefer) => "?ssl-mode=PREFERRED",
235 (Backend::MySql, SslMode::Require) => "?ssl-mode=REQUIRED",
236 (_, SslMode::Disable) => "?sslmode=disable",
237 (_, SslMode::Prefer) => "?sslmode=prefer",
238 (_, SslMode::Require) => "?sslmode=require",
239 };
240
241 format!(
242 "{}://{}{}:{}/{}{}",
243 backend.scheme(),
244 auth,
245 self.host,
246 port,
247 self.database,
248 ssl_param
249 )
250 }
251 };
252
253 DbkitConfig {
254 url,
255 pool_size: self.pool_size,
256 connect_timeout_secs: self.connect_timeout_secs,
257 idle_timeout_secs: self.idle_timeout_secs,
258 auto_create_db: self.auto_create_db,
259 }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_from_url() {
269 let config = DbkitConfig::from_url("postgres://localhost/mydb");
270 assert_eq!(config.url, "postgres://localhost/mydb");
271 assert_eq!(config.pool_size, 16);
272 assert!(config.auto_create_db);
273 }
274
275 #[test]
276 fn test_builder_full() {
277 let config = DbkitConfig::builder()
278 .host("db.example.com")
279 .port(5433)
280 .database("myapp")
281 .user("admin")
282 .password("secret")
283 .pool_size(32)
284 .connect_timeout_secs(10)
285 .ssl_mode(SslMode::Require)
286 .build();
287
288 assert_eq!(
289 config.url,
290 "postgres://admin:secret@db.example.com:5433/myapp?sslmode=require"
291 );
292 assert_eq!(config.pool_size, 32);
293 assert_eq!(config.connect_timeout_secs, 10);
294 }
295
296 #[test]
297 fn test_builder_minimal() {
298 let config = DbkitConfig::builder().database("test").build();
299 assert_eq!(config.url, "postgres://localhost:5432/test?sslmode=disable");
300 }
301
302 #[test]
303 fn test_builder_percent_encodes_credentials() {
304 let config = DbkitConfig::builder()
306 .host("localhost")
307 .port(5432)
308 .user("postgres")
309 .password("LexLuthern246!!??")
310 .database("sports_ai_baseball")
311 .build();
312 assert_eq!(
313 config.url,
314 "postgres://postgres:LexLuthern246%21%21%3F%3F@localhost:5432/sports_ai_baseball?sslmode=disable"
315 );
316 }
317
318 #[test]
319 fn test_builder_user_no_password() {
320 let config = DbkitConfig::builder()
321 .user("readonly")
322 .database("prod")
323 .build();
324 assert_eq!(config.url, "postgres://readonly@localhost:5432/prod?sslmode=disable");
325 }
326
327 #[test]
328 fn test_builder_mysql_default_port() {
329 let config = DbkitConfig::builder()
330 .backend(Backend::MySql)
331 .user("root")
332 .database("app")
333 .build();
334 assert_eq!(config.url, "mysql://root@localhost:3306/app?ssl-mode=DISABLED");
335 }
336
337 #[test]
338 fn test_builder_mysql_ssl_mode_spelling() {
339 let config = DbkitConfig::builder()
342 .backend(Backend::MySql)
343 .database("app")
344 .ssl_mode(SslMode::Require)
345 .build();
346 assert_eq!(config.url, "mysql://localhost:3306/app?ssl-mode=REQUIRED");
347 }
348
349 #[test]
350 fn test_builder_sqlite() {
351 let config = DbkitConfig::builder()
352 .backend(Backend::Sqlite)
353 .database("data/app.db")
354 .build();
355 assert_eq!(config.url, "sqlite://data/app.db");
356 }
357
358 #[test]
359 fn test_backend_from_url() {
360 assert_eq!(Backend::from_url("postgres://x/y").unwrap(), Backend::Postgres);
361 assert_eq!(Backend::from_url("postgresql://x/y").unwrap(), Backend::Postgres);
362 assert_eq!(Backend::from_url("mysql://x/y").unwrap(), Backend::MySql);
363 assert_eq!(Backend::from_url("sqlite://f.db").unwrap(), Backend::Sqlite);
364 assert!(Backend::from_url("oracle://x/y").is_err());
365 }
366}