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)]
114pub enum SslMode {
115 #[default]
117 Disable,
118 Prefer,
120 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 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 pub fn build(self) -> DbkitConfig {
205 let url = match self.backend {
206 Backend::Sqlite => format!("sqlite://{}", self.database),
207 backend => {
208 use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
212 let enc = |s: &str| utf8_percent_encode(s, NON_ALPHANUMERIC).to_string();
213 let auth = match (&self.user, &self.password) {
214 (Some(u), Some(p)) => format!("{}:{}@", enc(u), enc(p)),
215 (Some(u), None) => format!("{}@", enc(u)),
216 _ => String::new(),
217 };
218
219 let port = self.port.unwrap_or_else(|| backend.default_port());
220
221 let ssl_param = match self.ssl_mode {
222 SslMode::Disable => "",
223 SslMode::Prefer => "?sslmode=prefer",
224 SslMode::Require => "?sslmode=require",
225 };
226
227 format!(
228 "{}://{}{}:{}/{}{}",
229 backend.scheme(),
230 auth,
231 self.host,
232 port,
233 self.database,
234 ssl_param
235 )
236 }
237 };
238
239 DbkitConfig {
240 url,
241 pool_size: self.pool_size,
242 connect_timeout_secs: self.connect_timeout_secs,
243 idle_timeout_secs: self.idle_timeout_secs,
244 auto_create_db: self.auto_create_db,
245 }
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn test_from_url() {
255 let config = DbkitConfig::from_url("postgres://localhost/mydb");
256 assert_eq!(config.url, "postgres://localhost/mydb");
257 assert_eq!(config.pool_size, 16);
258 assert!(config.auto_create_db);
259 }
260
261 #[test]
262 fn test_builder_full() {
263 let config = DbkitConfig::builder()
264 .host("db.example.com")
265 .port(5433)
266 .database("myapp")
267 .user("admin")
268 .password("secret")
269 .pool_size(32)
270 .connect_timeout_secs(10)
271 .ssl_mode(SslMode::Require)
272 .build();
273
274 assert_eq!(
275 config.url,
276 "postgres://admin:secret@db.example.com:5433/myapp?sslmode=require"
277 );
278 assert_eq!(config.pool_size, 32);
279 assert_eq!(config.connect_timeout_secs, 10);
280 }
281
282 #[test]
283 fn test_builder_minimal() {
284 let config = DbkitConfig::builder().database("test").build();
285 assert_eq!(config.url, "postgres://localhost:5432/test");
286 }
287
288 #[test]
289 fn test_builder_percent_encodes_credentials() {
290 let config = DbkitConfig::builder()
292 .host("localhost")
293 .port(5432)
294 .user("postgres")
295 .password("LexLuthern246!!??")
296 .database("sports_ai_baseball")
297 .build();
298 assert_eq!(
299 config.url,
300 "postgres://postgres:LexLuthern246%21%21%3F%3F@localhost:5432/sports_ai_baseball"
301 );
302 }
303
304 #[test]
305 fn test_builder_user_no_password() {
306 let config = DbkitConfig::builder()
307 .user("readonly")
308 .database("prod")
309 .build();
310 assert_eq!(config.url, "postgres://readonly@localhost:5432/prod");
311 }
312
313 #[test]
314 fn test_builder_mysql_default_port() {
315 let config = DbkitConfig::builder()
316 .backend(Backend::MySql)
317 .user("root")
318 .database("app")
319 .build();
320 assert_eq!(config.url, "mysql://root@localhost:3306/app");
321 }
322
323 #[test]
324 fn test_builder_sqlite() {
325 let config = DbkitConfig::builder()
326 .backend(Backend::Sqlite)
327 .database("data/app.db")
328 .build();
329 assert_eq!(config.url, "sqlite://data/app.db");
330 }
331
332 #[test]
333 fn test_backend_from_url() {
334 assert_eq!(Backend::from_url("postgres://x/y").unwrap(), Backend::Postgres);
335 assert_eq!(Backend::from_url("postgresql://x/y").unwrap(), Backend::Postgres);
336 assert_eq!(Backend::from_url("mysql://x/y").unwrap(), Backend::MySql);
337 assert_eq!(Backend::from_url("sqlite://f.db").unwrap(), Backend::Sqlite);
338 assert!(Backend::from_url("oracle://x/y").is_err());
339 }
340}