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 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}