Skip to main content

quex/
options.rs

1use std::path::PathBuf;
2use std::time::Duration;
3
4use crate::{Error, Result};
5
6/// The database driver selected for a connection or pool.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Driver {
9    /// mysql or mariadb through libmariadb.
10    Mysql,
11    /// postgres through libpq.
12    Pgsql,
13    /// sqlite through sqlite3.
14    Sqlite,
15}
16
17impl Driver {
18    fn parse(value: &str) -> Result<Self> {
19        match value {
20            "mariadb" | "mysql" => Ok(Self::Mysql),
21            "pgsql" | "postgres" | "postgresql" => Ok(Self::Pgsql),
22            "sqlite" => Ok(Self::Sqlite),
23            _ => Err(Error::invalid_url(format!("unknown driver `{value}`"))),
24        }
25    }
26}
27
28/// Connection settings parsed from a URL or built manually.
29///
30/// Use this when the driver is selected dynamically. For direct construction,
31/// [`MysqlConnectOptions`], [`PostgresConnectOptions`], and
32/// [`SqliteConnectOptions`] keep call sites a little clearer.
33///
34/// ```
35/// let options = quex::ConnectOptions::from_url("sqlite::memory:")?;
36/// # Ok::<(), quex::Error>(())
37/// ```
38#[derive(Debug, Clone, Default)]
39pub struct ConnectOptions {
40    pub(crate) driver: Option<Driver>,
41    pub(crate) host: Option<String>,
42    pub(crate) port: Option<u16>,
43    pub(crate) username: Option<String>,
44    pub(crate) password: Option<String>,
45    pub(crate) database: Option<String>,
46    pub(crate) unix_socket: Option<String>,
47    pub(crate) path: Option<PathBuf>,
48    pub(crate) in_memory: bool,
49    pub(crate) read_only: bool,
50    pub(crate) create_if_missing: bool,
51    pub(crate) busy_timeout: Option<Duration>,
52}
53
54impl ConnectOptions {
55    /// Starts a new option builder for the selected driver.
56    pub fn new(driver: Driver) -> Self {
57        Self {
58            driver: Some(driver),
59            create_if_missing: true,
60            ..Self::default()
61        }
62    }
63
64    /// Sets the selected driver.
65    pub fn driver(mut self, value: Driver) -> Self {
66        self.driver = Some(value);
67        self
68    }
69
70    /// Sets the network host.
71    ///
72    /// This is used by mysql, mariadb, and postgres.
73    pub fn host(mut self, value: impl Into<String>) -> Self {
74        self.host = Some(value.into());
75        self
76    }
77
78    /// Sets the network port.
79    pub fn port(mut self, value: u16) -> Self {
80        self.port = Some(value);
81        self
82    }
83
84    /// Sets the database username.
85    pub fn username(mut self, value: impl Into<String>) -> Self {
86        self.username = Some(value.into());
87        self
88    }
89
90    /// Sets the database password.
91    pub fn password(mut self, value: impl Into<String>) -> Self {
92        self.password = Some(value.into());
93        self
94    }
95
96    /// Sets the database name.
97    pub fn database(mut self, value: impl Into<String>) -> Self {
98        self.database = Some(value.into());
99        self
100    }
101
102    /// Sets a unix socket path for mysql or mariadb.
103    pub fn unix_socket(mut self, value: impl Into<String>) -> Self {
104        self.unix_socket = Some(value.into());
105        self
106    }
107
108    /// Sets the sqlite database path.
109    pub fn path(mut self, value: impl Into<PathBuf>) -> Self {
110        self.path = Some(value.into());
111        self
112    }
113
114    /// Uses an in-memory sqlite database.
115    ///
116    /// This clears any path already set on the options.
117    pub fn in_memory(mut self) -> Self {
118        self.in_memory = true;
119        self.path = None;
120        self
121    }
122
123    /// Sets whether sqlite opens the database as read-only.
124    pub fn read_only(mut self, value: bool) -> Self {
125        self.read_only = value;
126        self
127    }
128
129    /// Sets whether sqlite creates the database file when missing.
130    pub fn create_if_missing(mut self, value: bool) -> Self {
131        self.create_if_missing = value;
132        self
133    }
134
135    /// Sets sqlite's busy timeout.
136    pub fn busy_timeout(mut self, value: Duration) -> Self {
137        self.busy_timeout = Some(value);
138        self
139    }
140
141    /// Parses a database URL.
142    ///
143    /// Supported schemes are `mysql`, `mariadb`, `postgres`, `postgresql`,
144    /// `pgsql`, and `sqlite`. sqlite also accepts `sqlite::memory:`.
145    pub fn from_url(input: &str) -> Result<Self> {
146        if input == "sqlite::memory:" {
147            return Ok(Self::new(Driver::Sqlite).in_memory());
148        }
149
150        if let Some(path) = input.strip_prefix("sqlite:") {
151            return parse_sqlite_url(path);
152        }
153
154        let url = ParsedUrl::parse(input)?;
155        let driver = Driver::parse(&url.scheme)?;
156
157        match driver {
158            Driver::Mysql => {
159                let mut options = Self::new(Driver::Mysql);
160                if let Some(host) = url.host.as_deref() {
161                    options = options.host(host);
162                }
163                if let Some(port) = url.port {
164                    options = options.port(port);
165                }
166                if let Some(username) = url.username.as_deref() {
167                    if !username.is_empty() {
168                        options = options.username(username);
169                    }
170                }
171                if let Some(password) = url.password.as_deref() {
172                    options = options.password(password);
173                }
174                let database = url.path.trim_start_matches('/');
175                if !database.is_empty() {
176                    options = options.database(database);
177                }
178                Ok(options)
179            }
180            Driver::Pgsql => {
181                let mut options = Self::new(Driver::Pgsql);
182                if let Some(host) = url.host.as_deref() {
183                    options = options.host(host);
184                }
185                if let Some(port) = url.port {
186                    options = options.port(port);
187                }
188                if let Some(username) = url.username.as_deref() {
189                    if !username.is_empty() {
190                        options = options.username(username);
191                    }
192                }
193                if let Some(password) = url.password.as_deref() {
194                    options = options.password(password);
195                }
196                let database = url.path.trim_start_matches('/');
197                if !database.is_empty() {
198                    options = options.database(database);
199                }
200                Ok(options)
201            }
202            Driver::Sqlite => unreachable!("sqlite URLs are handled before generic parsing"),
203        }
204    }
205}
206
207/// mysql or mariadb connection settings.
208#[derive(Debug, Clone, Default)]
209pub struct MysqlConnectOptions {
210    pub(crate) host: Option<String>,
211    pub(crate) port: Option<u32>,
212    pub(crate) username: Option<String>,
213    pub(crate) password: Option<String>,
214    pub(crate) database: Option<String>,
215    pub(crate) unix_socket: Option<String>,
216}
217
218impl MysqlConnectOptions {
219    /// Starts with driver defaults.
220    pub fn new() -> Self {
221        Self::default()
222    }
223
224    /// Sets the network host.
225    pub fn host(mut self, value: impl Into<String>) -> Self {
226        self.host = Some(value.into());
227        self
228    }
229
230    /// Sets the network port.
231    pub fn port(mut self, value: u32) -> Self {
232        self.port = Some(value);
233        self
234    }
235
236    /// Sets the database username.
237    pub fn username(mut self, value: impl Into<String>) -> Self {
238        self.username = Some(value.into());
239        self
240    }
241
242    /// Sets the database password.
243    pub fn password(mut self, value: impl Into<String>) -> Self {
244        self.password = Some(value.into());
245        self
246    }
247
248    /// Sets the database name.
249    pub fn database(mut self, value: impl Into<String>) -> Self {
250        self.database = Some(value.into());
251        self
252    }
253
254    /// Sets a unix socket path.
255    pub fn unix_socket(mut self, value: impl Into<String>) -> Self {
256        self.unix_socket = Some(value.into());
257        self
258    }
259}
260
261#[cfg(feature = "mariadb")]
262impl From<MysqlConnectOptions> for quex_driver::mysql::ConnectOptions {
263    fn from(value: MysqlConnectOptions) -> Self {
264        let mut options = quex_driver::mysql::ConnectOptions::new();
265        if let Some(host) = value.host {
266            options = options.host(host);
267        }
268        if let Some(port) = value.port {
269            options = options.port(port);
270        }
271        if let Some(username) = value.username {
272            options = options.user(username);
273        }
274        if let Some(password) = value.password {
275            options = options.password(password);
276        }
277        if let Some(database) = value.database {
278            options = options.database(database);
279        }
280        if let Some(unix_socket) = value.unix_socket {
281            options = options.unix_socket(unix_socket);
282        }
283        options
284    }
285}
286
287impl From<MysqlConnectOptions> for ConnectOptions {
288    fn from(value: MysqlConnectOptions) -> Self {
289        let mut options = Self::new(Driver::Mysql);
290        if let Some(host) = value.host {
291            options = options.host(host);
292        }
293        if let Some(port) = value.port {
294            options = options.port(port as u16);
295        }
296        if let Some(username) = value.username {
297            options = options.username(username);
298        }
299        if let Some(password) = value.password {
300            options = options.password(password);
301        }
302        if let Some(database) = value.database {
303            options = options.database(database);
304        }
305        if let Some(unix_socket) = value.unix_socket {
306            options = options.unix_socket(unix_socket);
307        }
308        options
309    }
310}
311
312/// postgres connection settings.
313#[derive(Debug, Clone, Default)]
314pub struct PostgresConnectOptions {
315    pub(crate) host: Option<String>,
316    pub(crate) port: Option<u16>,
317    pub(crate) username: Option<String>,
318    pub(crate) password: Option<String>,
319    pub(crate) database: Option<String>,
320}
321
322impl PostgresConnectOptions {
323    /// Starts with driver defaults.
324    pub fn new() -> Self {
325        Self::default()
326    }
327
328    /// Sets the network host.
329    pub fn host(mut self, value: impl Into<String>) -> Self {
330        self.host = Some(value.into());
331        self
332    }
333
334    /// Sets the network port.
335    pub fn port(mut self, value: u16) -> Self {
336        self.port = Some(value);
337        self
338    }
339
340    /// Sets the database username.
341    pub fn username(mut self, value: impl Into<String>) -> Self {
342        self.username = Some(value.into());
343        self
344    }
345
346    /// Sets the database password.
347    pub fn password(mut self, value: impl Into<String>) -> Self {
348        self.password = Some(value.into());
349        self
350    }
351
352    /// Sets the database name.
353    pub fn database(mut self, value: impl Into<String>) -> Self {
354        self.database = Some(value.into());
355        self
356    }
357}
358
359#[cfg(feature = "postgres")]
360impl From<PostgresConnectOptions> for quex_driver::postgres::ConnectOptions {
361    fn from(value: PostgresConnectOptions) -> Self {
362        let mut options = quex_driver::postgres::ConnectOptions::new();
363        if let Some(host) = value.host {
364            options = options.host(host);
365        }
366        if let Some(port) = value.port {
367            options = options.port(port);
368        }
369        if let Some(username) = value.username {
370            options = options.user(username);
371        }
372        if let Some(password) = value.password {
373            options = options.password(password);
374        }
375        if let Some(database) = value.database {
376            options = options.database(database);
377        }
378        options
379    }
380}
381
382impl From<PostgresConnectOptions> for ConnectOptions {
383    fn from(value: PostgresConnectOptions) -> Self {
384        let mut options = Self::new(Driver::Pgsql);
385        if let Some(host) = value.host {
386            options = options.host(host);
387        }
388        if let Some(port) = value.port {
389            options = options.port(port);
390        }
391        if let Some(username) = value.username {
392            options = options.username(username);
393        }
394        if let Some(password) = value.password {
395            options = options.password(password);
396        }
397        if let Some(database) = value.database {
398            options = options.database(database);
399        }
400        options
401    }
402}
403
404/// sqlite connection settings.
405#[derive(Debug, Clone, Default)]
406pub struct SqliteConnectOptions {
407    pub(crate) path: Option<PathBuf>,
408    pub(crate) in_memory: bool,
409    pub(crate) read_only: bool,
410    pub(crate) create_if_missing: bool,
411    pub(crate) busy_timeout: Option<Duration>,
412}
413
414impl SqliteConnectOptions {
415    /// Starts with an on-disk sqlite database created when missing.
416    pub fn new() -> Self {
417        Self {
418            create_if_missing: true,
419            ..Self::default()
420        }
421    }
422
423    /// Sets the database path.
424    pub fn path(mut self, value: impl Into<PathBuf>) -> Self {
425        self.path = Some(value.into());
426        self
427    }
428
429    /// Uses an in-memory database.
430    ///
431    /// This clears any path already set on the options.
432    pub fn in_memory(mut self) -> Self {
433        self.in_memory = true;
434        self.path = None;
435        self
436    }
437
438    /// Sets whether sqlite opens the database as read-only.
439    pub fn read_only(mut self, value: bool) -> Self {
440        self.read_only = value;
441        self
442    }
443
444    /// Sets whether sqlite creates the database file when missing.
445    pub fn create_if_missing(mut self, value: bool) -> Self {
446        self.create_if_missing = value;
447        self
448    }
449
450    /// Sets sqlite's busy timeout.
451    pub fn busy_timeout(mut self, value: Duration) -> Self {
452        self.busy_timeout = Some(value);
453        self
454    }
455}
456
457#[cfg(feature = "sqlite")]
458impl From<SqliteConnectOptions> for quex_driver::sqlite::ConnectOptions {
459    fn from(value: SqliteConnectOptions) -> Self {
460        let mut options = quex_driver::sqlite::ConnectOptions::new()
461            .read_only(value.read_only)
462            .create_if_missing(value.create_if_missing);
463        if let Some(timeout) = value.busy_timeout {
464            options = options.busy_timeout(timeout);
465        }
466        if value.in_memory {
467            options = options.in_memory();
468        } else if let Some(path) = value.path {
469            options = options.path(path);
470        }
471        options
472    }
473}
474
475impl From<SqliteConnectOptions> for ConnectOptions {
476    fn from(value: SqliteConnectOptions) -> Self {
477        let mut options = Self::new(Driver::Sqlite)
478            .read_only(value.read_only)
479            .create_if_missing(value.create_if_missing);
480        if let Some(timeout) = value.busy_timeout {
481            options = options.busy_timeout(timeout);
482        }
483        if value.in_memory {
484            options = options.in_memory();
485        } else if let Some(path) = value.path {
486            options = options.path(path);
487        }
488        options
489    }
490}
491
492fn parse_sqlite_url(rest: &str) -> Result<ConnectOptions> {
493    if rest == ":memory:" {
494        return Ok(ConnectOptions::new(Driver::Sqlite).in_memory());
495    }
496    if rest.is_empty() {
497        return Err(Error::invalid_url("sqlite URL is missing a database path"));
498    }
499    if let Some(path) = rest.strip_prefix("///") {
500        return Ok(ConnectOptions::new(Driver::Sqlite).path(format!("/{}", path)));
501    }
502    if let Some(path) = rest.strip_prefix("//") {
503        if path.is_empty() {
504            return Err(Error::invalid_url("sqlite URL is missing a database path"));
505        }
506        return Ok(ConnectOptions::new(Driver::Sqlite).path(path));
507    }
508    Ok(ConnectOptions::new(Driver::Sqlite).path(rest))
509}
510
511#[derive(Debug)]
512struct ParsedUrl {
513    scheme: String,
514    username: Option<String>,
515    password: Option<String>,
516    host: Option<String>,
517    port: Option<u16>,
518    path: String,
519}
520
521impl ParsedUrl {
522    fn parse(input: &str) -> Result<Self> {
523        let (scheme, remainder) = input
524            .split_once("://")
525            .ok_or_else(|| Error::invalid_url("URL is missing a scheme separator"))?;
526        let scheme = scheme.to_ascii_lowercase();
527
528        let (authority, path_and_more) = remainder.split_once('/').unwrap_or((remainder, ""));
529        let path = if path_and_more.is_empty() {
530            String::new()
531        } else {
532            let path = format!("/{path_and_more}");
533            strip_suffixes(&path, &['?', '#']).to_owned()
534        };
535
536        let (username, password, host, port) = parse_authority(authority)?;
537
538        Ok(Self {
539            scheme,
540            username,
541            password,
542            host,
543            port,
544            path,
545        })
546    }
547}
548
549fn parse_authority(
550    authority: &str,
551) -> Result<(Option<String>, Option<String>, Option<String>, Option<u16>)> {
552    let (userinfo, host_port) = if let Some((userinfo, host_port)) = authority.rsplit_once('@') {
553        (Some(userinfo), host_port)
554    } else {
555        (None, authority)
556    };
557
558    let (username, password) = if let Some(userinfo) = userinfo {
559        if let Some((username, password)) = userinfo.split_once(':') {
560            (Some(username.to_owned()), Some(password.to_owned()))
561        } else {
562            (Some(userinfo.to_owned()), None)
563        }
564    } else {
565        (None, None)
566    };
567
568    let (host, port) = parse_host_port(host_port)?;
569    Ok((username, password, host, port))
570}
571
572fn parse_host_port(input: &str) -> Result<(Option<String>, Option<u16>)> {
573    if input.is_empty() {
574        return Ok((None, None));
575    }
576
577    if let Some(host) = input.strip_prefix('[') {
578        let (host, remainder) = host
579            .split_once(']')
580            .ok_or_else(|| Error::invalid_url("invalid IPv6 host"))?;
581        let port = if remainder.is_empty() {
582            None
583        } else if let Some(port) = remainder.strip_prefix(':') {
584            Some(parse_port(port)?)
585        } else {
586            return Err(Error::invalid_url("invalid host and port"));
587        };
588
589        return Ok((Some(host.to_owned()), port));
590    }
591
592    if let Some((host, port)) = input.rsplit_once(':') {
593        if host.contains(':') {
594            return Ok((Some(input.to_owned()), None));
595        }
596
597        return Ok((Some(host.to_owned()), Some(parse_port(port)?)));
598    }
599
600    Ok((Some(input.to_owned()), None))
601}
602
603fn parse_port(input: &str) -> Result<u16> {
604    input
605        .parse()
606        .map_err(|_| Error::invalid_url(format!("invalid port `{input}`")))
607}
608
609fn strip_suffixes<'a>(input: &'a str, separators: &[char]) -> &'a str {
610    input
611        .find(|c| separators.contains(&c))
612        .map(|index| &input[..index])
613        .unwrap_or(input)
614}