use std::path::PathBuf;
use std::time::Duration;
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Driver {
Mysql,
Pgsql,
Sqlite,
}
impl Driver {
fn parse(value: &str) -> Result<Self> {
match value {
"mariadb" | "mysql" => Ok(Self::Mysql),
"pgsql" | "postgres" | "postgresql" => Ok(Self::Pgsql),
"sqlite" => Ok(Self::Sqlite),
_ => Err(Error::invalid_url(format!("unknown driver `{value}`"))),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ConnectOptions {
pub(crate) driver: Option<Driver>,
pub(crate) host: Option<String>,
pub(crate) port: Option<u16>,
pub(crate) username: Option<String>,
pub(crate) password: Option<String>,
pub(crate) database: Option<String>,
pub(crate) unix_socket: Option<String>,
pub(crate) path: Option<PathBuf>,
pub(crate) in_memory: bool,
pub(crate) read_only: bool,
pub(crate) create_if_missing: bool,
pub(crate) busy_timeout: Option<Duration>,
}
impl ConnectOptions {
pub fn new(driver: Driver) -> Self {
Self {
driver: Some(driver),
create_if_missing: true,
..Self::default()
}
}
pub fn driver(mut self, value: Driver) -> Self {
self.driver = Some(value);
self
}
pub fn host(mut self, value: impl Into<String>) -> Self {
self.host = Some(value.into());
self
}
pub fn port(mut self, value: u16) -> Self {
self.port = Some(value);
self
}
pub fn username(mut self, value: impl Into<String>) -> Self {
self.username = Some(value.into());
self
}
pub fn password(mut self, value: impl Into<String>) -> Self {
self.password = Some(value.into());
self
}
pub fn database(mut self, value: impl Into<String>) -> Self {
self.database = Some(value.into());
self
}
pub fn unix_socket(mut self, value: impl Into<String>) -> Self {
self.unix_socket = Some(value.into());
self
}
pub fn path(mut self, value: impl Into<PathBuf>) -> Self {
self.path = Some(value.into());
self
}
pub fn in_memory(mut self) -> Self {
self.in_memory = true;
self.path = None;
self
}
pub fn read_only(mut self, value: bool) -> Self {
self.read_only = value;
self
}
pub fn create_if_missing(mut self, value: bool) -> Self {
self.create_if_missing = value;
self
}
pub fn busy_timeout(mut self, value: Duration) -> Self {
self.busy_timeout = Some(value);
self
}
pub fn from_url(input: &str) -> Result<Self> {
if input == "sqlite::memory:" {
return Ok(Self::new(Driver::Sqlite).in_memory());
}
if let Some(path) = input.strip_prefix("sqlite:") {
return parse_sqlite_url(path);
}
let url = ParsedUrl::parse(input)?;
let driver = Driver::parse(&url.scheme)?;
match driver {
Driver::Mysql => {
let mut options = Self::new(Driver::Mysql);
if let Some(host) = url.host.as_deref() {
options = options.host(host);
}
if let Some(port) = url.port {
options = options.port(port);
}
if let Some(username) = url.username.as_deref() {
if !username.is_empty() {
options = options.username(username);
}
}
if let Some(password) = url.password.as_deref() {
options = options.password(password);
}
let database = url.path.trim_start_matches('/');
if !database.is_empty() {
options = options.database(database);
}
Ok(options)
}
Driver::Pgsql => {
let mut options = Self::new(Driver::Pgsql);
if let Some(host) = url.host.as_deref() {
options = options.host(host);
}
if let Some(port) = url.port {
options = options.port(port);
}
if let Some(username) = url.username.as_deref() {
if !username.is_empty() {
options = options.username(username);
}
}
if let Some(password) = url.password.as_deref() {
options = options.password(password);
}
let database = url.path.trim_start_matches('/');
if !database.is_empty() {
options = options.database(database);
}
Ok(options)
}
Driver::Sqlite => unreachable!("sqlite URLs are handled before generic parsing"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct MysqlConnectOptions {
pub(crate) host: Option<String>,
pub(crate) port: Option<u32>,
pub(crate) username: Option<String>,
pub(crate) password: Option<String>,
pub(crate) database: Option<String>,
pub(crate) unix_socket: Option<String>,
}
impl MysqlConnectOptions {
pub fn new() -> Self {
Self::default()
}
pub fn host(mut self, value: impl Into<String>) -> Self {
self.host = Some(value.into());
self
}
pub fn port(mut self, value: u32) -> Self {
self.port = Some(value);
self
}
pub fn username(mut self, value: impl Into<String>) -> Self {
self.username = Some(value.into());
self
}
pub fn password(mut self, value: impl Into<String>) -> Self {
self.password = Some(value.into());
self
}
pub fn database(mut self, value: impl Into<String>) -> Self {
self.database = Some(value.into());
self
}
pub fn unix_socket(mut self, value: impl Into<String>) -> Self {
self.unix_socket = Some(value.into());
self
}
}
#[cfg(feature = "mariadb")]
impl From<MysqlConnectOptions> for quex_driver::mysql::ConnectOptions {
fn from(value: MysqlConnectOptions) -> Self {
let mut options = quex_driver::mysql::ConnectOptions::new();
if let Some(host) = value.host {
options = options.host(host);
}
if let Some(port) = value.port {
options = options.port(port);
}
if let Some(username) = value.username {
options = options.user(username);
}
if let Some(password) = value.password {
options = options.password(password);
}
if let Some(database) = value.database {
options = options.database(database);
}
if let Some(unix_socket) = value.unix_socket {
options = options.unix_socket(unix_socket);
}
options
}
}
impl From<MysqlConnectOptions> for ConnectOptions {
fn from(value: MysqlConnectOptions) -> Self {
let mut options = Self::new(Driver::Mysql);
if let Some(host) = value.host {
options = options.host(host);
}
if let Some(port) = value.port {
options = options.port(port as u16);
}
if let Some(username) = value.username {
options = options.username(username);
}
if let Some(password) = value.password {
options = options.password(password);
}
if let Some(database) = value.database {
options = options.database(database);
}
if let Some(unix_socket) = value.unix_socket {
options = options.unix_socket(unix_socket);
}
options
}
}
#[derive(Debug, Clone, Default)]
pub struct PostgresConnectOptions {
pub(crate) host: Option<String>,
pub(crate) port: Option<u16>,
pub(crate) username: Option<String>,
pub(crate) password: Option<String>,
pub(crate) database: Option<String>,
}
impl PostgresConnectOptions {
pub fn new() -> Self {
Self::default()
}
pub fn host(mut self, value: impl Into<String>) -> Self {
self.host = Some(value.into());
self
}
pub fn port(mut self, value: u16) -> Self {
self.port = Some(value);
self
}
pub fn username(mut self, value: impl Into<String>) -> Self {
self.username = Some(value.into());
self
}
pub fn password(mut self, value: impl Into<String>) -> Self {
self.password = Some(value.into());
self
}
pub fn database(mut self, value: impl Into<String>) -> Self {
self.database = Some(value.into());
self
}
}
#[cfg(feature = "postgres")]
impl From<PostgresConnectOptions> for quex_driver::postgres::ConnectOptions {
fn from(value: PostgresConnectOptions) -> Self {
let mut options = quex_driver::postgres::ConnectOptions::new();
if let Some(host) = value.host {
options = options.host(host);
}
if let Some(port) = value.port {
options = options.port(port);
}
if let Some(username) = value.username {
options = options.user(username);
}
if let Some(password) = value.password {
options = options.password(password);
}
if let Some(database) = value.database {
options = options.database(database);
}
options
}
}
impl From<PostgresConnectOptions> for ConnectOptions {
fn from(value: PostgresConnectOptions) -> Self {
let mut options = Self::new(Driver::Pgsql);
if let Some(host) = value.host {
options = options.host(host);
}
if let Some(port) = value.port {
options = options.port(port);
}
if let Some(username) = value.username {
options = options.username(username);
}
if let Some(password) = value.password {
options = options.password(password);
}
if let Some(database) = value.database {
options = options.database(database);
}
options
}
}
#[derive(Debug, Clone, Default)]
pub struct SqliteConnectOptions {
pub(crate) path: Option<PathBuf>,
pub(crate) in_memory: bool,
pub(crate) read_only: bool,
pub(crate) create_if_missing: bool,
pub(crate) busy_timeout: Option<Duration>,
}
impl SqliteConnectOptions {
pub fn new() -> Self {
Self {
create_if_missing: true,
..Self::default()
}
}
pub fn path(mut self, value: impl Into<PathBuf>) -> Self {
self.path = Some(value.into());
self
}
pub fn in_memory(mut self) -> Self {
self.in_memory = true;
self.path = None;
self
}
pub fn read_only(mut self, value: bool) -> Self {
self.read_only = value;
self
}
pub fn create_if_missing(mut self, value: bool) -> Self {
self.create_if_missing = value;
self
}
pub fn busy_timeout(mut self, value: Duration) -> Self {
self.busy_timeout = Some(value);
self
}
}
#[cfg(feature = "sqlite")]
impl From<SqliteConnectOptions> for quex_driver::sqlite::ConnectOptions {
fn from(value: SqliteConnectOptions) -> Self {
let mut options = quex_driver::sqlite::ConnectOptions::new()
.read_only(value.read_only)
.create_if_missing(value.create_if_missing);
if let Some(timeout) = value.busy_timeout {
options = options.busy_timeout(timeout);
}
if value.in_memory {
options = options.in_memory();
} else if let Some(path) = value.path {
options = options.path(path);
}
options
}
}
impl From<SqliteConnectOptions> for ConnectOptions {
fn from(value: SqliteConnectOptions) -> Self {
let mut options = Self::new(Driver::Sqlite)
.read_only(value.read_only)
.create_if_missing(value.create_if_missing);
if let Some(timeout) = value.busy_timeout {
options = options.busy_timeout(timeout);
}
if value.in_memory {
options = options.in_memory();
} else if let Some(path) = value.path {
options = options.path(path);
}
options
}
}
fn parse_sqlite_url(rest: &str) -> Result<ConnectOptions> {
if rest == ":memory:" {
return Ok(ConnectOptions::new(Driver::Sqlite).in_memory());
}
if rest.is_empty() {
return Err(Error::invalid_url("sqlite URL is missing a database path"));
}
if let Some(path) = rest.strip_prefix("///") {
return Ok(ConnectOptions::new(Driver::Sqlite).path(format!("/{}", path)));
}
if let Some(path) = rest.strip_prefix("//") {
if path.is_empty() {
return Err(Error::invalid_url("sqlite URL is missing a database path"));
}
return Ok(ConnectOptions::new(Driver::Sqlite).path(path));
}
Ok(ConnectOptions::new(Driver::Sqlite).path(rest))
}
#[derive(Debug)]
struct ParsedUrl {
scheme: String,
username: Option<String>,
password: Option<String>,
host: Option<String>,
port: Option<u16>,
path: String,
}
impl ParsedUrl {
fn parse(input: &str) -> Result<Self> {
let (scheme, remainder) = input
.split_once("://")
.ok_or_else(|| Error::invalid_url("URL is missing a scheme separator"))?;
let scheme = scheme.to_ascii_lowercase();
let (authority, path_and_more) = remainder.split_once('/').unwrap_or((remainder, ""));
let path = if path_and_more.is_empty() {
String::new()
} else {
let path = format!("/{path_and_more}");
strip_suffixes(&path, &['?', '#']).to_owned()
};
let (username, password, host, port) = parse_authority(authority)?;
Ok(Self {
scheme,
username,
password,
host,
port,
path,
})
}
}
fn parse_authority(
authority: &str,
) -> Result<(Option<String>, Option<String>, Option<String>, Option<u16>)> {
let (userinfo, host_port) = if let Some((userinfo, host_port)) = authority.rsplit_once('@') {
(Some(userinfo), host_port)
} else {
(None, authority)
};
let (username, password) = if let Some(userinfo) = userinfo {
if let Some((username, password)) = userinfo.split_once(':') {
(Some(username.to_owned()), Some(password.to_owned()))
} else {
(Some(userinfo.to_owned()), None)
}
} else {
(None, None)
};
let (host, port) = parse_host_port(host_port)?;
Ok((username, password, host, port))
}
fn parse_host_port(input: &str) -> Result<(Option<String>, Option<u16>)> {
if input.is_empty() {
return Ok((None, None));
}
if let Some(host) = input.strip_prefix('[') {
let (host, remainder) = host
.split_once(']')
.ok_or_else(|| Error::invalid_url("invalid IPv6 host"))?;
let port = if remainder.is_empty() {
None
} else if let Some(port) = remainder.strip_prefix(':') {
Some(parse_port(port)?)
} else {
return Err(Error::invalid_url("invalid host and port"));
};
return Ok((Some(host.to_owned()), port));
}
if let Some((host, port)) = input.rsplit_once(':') {
if host.contains(':') {
return Ok((Some(input.to_owned()), None));
}
return Ok((Some(host.to_owned()), Some(parse_port(port)?)));
}
Ok((Some(input.to_owned()), None))
}
fn parse_port(input: &str) -> Result<u16> {
input
.parse()
.map_err(|_| Error::invalid_url(format!("invalid port `{input}`")))
}
fn strip_suffixes<'a>(input: &'a str, separators: &[char]) -> &'a str {
input
.find(|c| separators.contains(&c))
.map(|index| &input[..index])
.unwrap_or(input)
}