From 44c175bb19e31a617aa6d4cbf5d84c930ec569c9 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Wed, 30 Dec 2020 16:10:04 -0800 Subject: [PATCH] feat(mysql): fill out more in MySqlOptions and settle on accessors [#659] --- Cargo.lock | 10 +- sqlx-core/Cargo.toml | 1 - sqlx-core/src/blocking/connection.rs | 6 +- sqlx-core/src/blocking/options.rs | 8 +- sqlx-core/src/connection.rs | 7 +- sqlx-core/src/error.rs | 49 ++++++- sqlx-core/src/options.rs | 3 +- sqlx-mysql/Cargo.toml | 3 + sqlx-mysql/src/async.rs | 2 - sqlx-mysql/src/async/connection.rs | 27 ---- sqlx-mysql/src/async/options.rs | 23 --- sqlx-mysql/src/blocking/connection.rs | 4 +- sqlx-mysql/src/blocking/options.rs | 7 +- sqlx-mysql/src/connection.rs | 29 +++- sqlx-mysql/src/lib.rs | 3 - sqlx-mysql/src/options.rs | 143 +++++++++++++++---- sqlx-mysql/src/options/builder.rs | 82 +++++++++++ sqlx-mysql/src/options/default.rs | 37 +++++ sqlx-mysql/src/options/parse.rs | 198 ++++++++++++++++++++++++++ 19 files changed, 536 insertions(+), 106 deletions(-) delete mode 100644 sqlx-mysql/src/async.rs delete mode 100644 sqlx-mysql/src/async/connection.rs delete mode 100644 sqlx-mysql/src/async/options.rs create mode 100644 sqlx-mysql/src/options/builder.rs create mode 100644 sqlx-mysql/src/options/default.rs create mode 100644 sqlx-mysql/src/options/parse.rs diff --git a/Cargo.lock b/Cargo.lock index 91503e81..5addb241 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,6 +246,12 @@ dependencies = [ "syn", ] +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "event-listener" version = "2.5.1" @@ -771,15 +777,17 @@ dependencies = [ "futures-util", "tokio 0.2.24", "tokio 1.0.1", - "url", ] [[package]] name = "sqlx-mysql" version = "0.6.0-pre" dependencies = [ + "either", "futures-util", + "percent-encoding", "sqlx-core", + "url", ] [[package]] diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index ca83fbc6..8bad07d2 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -39,4 +39,3 @@ _async-std = { version = "1.8.0", optional = true, package = "async-std" } futures-util = { version = "0.3.8", optional = true } _tokio = { version = "1.0.1", optional = true, package = "tokio", features = ["net"] } tokio_02 = { version = "0.2.24", optional = true, package = "tokio", features = ["net"] } -url = "2.2.0" diff --git a/sqlx-core/src/blocking/connection.rs b/sqlx-core/src/blocking/connection.rs index adc3c6d8..3e6b4d07 100644 --- a/sqlx-core/src/blocking/connection.rs +++ b/sqlx-core/src/blocking/connection.rs @@ -9,9 +9,8 @@ use crate::DefaultRuntime; pub trait Connection: crate::Connection where Rt: Runtime, + Self::Options: ConnectOptions, { - type Options: ConnectOptions; - /// Establish a new database connection. /// /// For detailed information, refer to the asynchronous version of @@ -21,7 +20,8 @@ where where Self: Sized, { - url.parse::<>::Options>()?.connect() + url.parse::<>::Options>()? + .connect() } /// Explicitly close this database connection. diff --git a/sqlx-core/src/blocking/options.rs b/sqlx-core/src/blocking/options.rs index d6f0fa74..eed024bb 100644 --- a/sqlx-core/src/blocking/options.rs +++ b/sqlx-core/src/blocking/options.rs @@ -6,18 +6,18 @@ use crate::DefaultRuntime; /// For detailed information, refer to the asynchronous version of /// this: [`ConnectOptions`][crate::ConnectOptions]. /// +#[allow(clippy::module_name_repetitions)] pub trait ConnectOptions: crate::ConnectOptions where Rt: Runtime, + Self::Connection: crate::Connection + Connection, { - type Connection: Connection + ?Sized; - /// Establish a connection to the database. /// /// For detailed information, refer to the asynchronous version of /// this: [`connect()`][crate::ConnectOptions::connect]. /// - fn connect(&self) -> crate::Result<>::Connection> + fn connect(&self) -> crate::Result where - >::Connection: Sized; + Self::Connection: Sized; } diff --git a/sqlx-core/src/connection.rs b/sqlx-core/src/connection.rs index e1a769e7..09e530fe 100644 --- a/sqlx-core/src/connection.rs +++ b/sqlx-core/src/connection.rs @@ -26,9 +26,8 @@ where /// /// ```rust,ignore /// use sqlx::postgres::PgConnection; - /// use sqlx::ConnectOptions; /// - /// let mut conn = PgConnection::connect( + /// let mut conn = ::connect( /// "postgres://postgres:password@localhost/database", /// ).await?; /// ``` @@ -36,10 +35,10 @@ where /// You may alternatively build the connection options imperatively. /// /// ```rust,ignore - /// use sqlx::mysql::{MySqlConnection, MySqlConnectOptions}; + /// use sqlx::mysql::MySqlConnectOptions; /// use sqlx::ConnectOptions; /// - /// let mut conn: MySqlConnection = MySqlConnectOptions::builder() + /// let mut conn = ::new() /// .host("localhost") /// .username("root") /// .password("password") diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs index e33e65ba..e36f4212 100644 --- a/sqlx-core/src/error.rs +++ b/sqlx-core/src/error.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::error::Error as StdError; use std::fmt::{self, Display, Formatter}; @@ -6,15 +7,51 @@ pub type Result = std::result::Result; #[derive(Debug)] #[non_exhaustive] pub enum Error { - InvalidConnectionUrl(url::ParseError), + Configuration { + message: Cow<'static, str>, + source: Option>, + }, + Network(std::io::Error), } +impl Error { + #[doc(hidden)] + pub fn configuration( + message: impl Into>, + source: impl Into>, + ) -> Self { + Self::Configuration { + message: message.into(), + source: Some(source.into()), + } + } + + #[doc(hidden)] + pub fn configuration_msg(message: impl Into>) -> Self { + Self::Configuration { + message: message.into(), + source: None, + } + } +} + impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - Self::InvalidConnectionUrl(source) => write!(f, "invalid connection url: {}", source), Self::Network(source) => write!(f, "network: {}", source), + + Self::Configuration { + message, + source: None, + } => write!(f, "configuration: {}", message), + + Self::Configuration { + message, + source: Some(source), + } => { + write!(f, "configuration: {}: {}", message, source) + } } } } @@ -22,8 +59,14 @@ impl Display for Error { impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { - Self::InvalidConnectionUrl(source) => Some(source), + Self::Configuration { + source: Some(source), + .. + } => Some(&**source), + Self::Network(source) => Some(source), + + _ => None, } } } diff --git a/sqlx-core/src/options.rs b/sqlx-core/src/options.rs index 1a8cc4a2..cf9cfe8b 100644 --- a/sqlx-core/src/options.rs +++ b/sqlx-core/src/options.rs @@ -4,8 +4,9 @@ use std::str::FromStr; use crate::{Connection, DefaultRuntime, Runtime}; /// Options which can be used to configure how a SQL connection is opened. +#[allow(clippy::module_name_repetitions)] pub trait ConnectOptions: - 'static + Send + Sync + Default + Debug + Clone + FromStr + 'static + Sized + Send + Sync + Default + Debug + Clone + FromStr where Rt: Runtime, { diff --git a/sqlx-mysql/Cargo.toml b/sqlx-mysql/Cargo.toml index 32db93f6..e7b00adb 100644 --- a/sqlx-mysql/Cargo.toml +++ b/sqlx-mysql/Cargo.toml @@ -29,3 +29,6 @@ async = ["futures-util", "sqlx-core/async"] [dependencies] sqlx-core = { version = "0.6.0-pre", path = "../sqlx-core" } futures-util = { version = "0.3.8", optional = true } +either = "1.6.1" +url = "2.2.0" +percent-encoding = "2.1.0" diff --git a/sqlx-mysql/src/async.rs b/sqlx-mysql/src/async.rs deleted file mode 100644 index 217280ea..00000000 --- a/sqlx-mysql/src/async.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod connection; -mod options; diff --git a/sqlx-mysql/src/async/connection.rs b/sqlx-mysql/src/async/connection.rs deleted file mode 100644 index 5ca83607..00000000 --- a/sqlx-mysql/src/async/connection.rs +++ /dev/null @@ -1,27 +0,0 @@ -use futures_util::future::BoxFuture; -use sqlx_core::{Async, Connection, Result, Runtime}; - -use crate::{MySql, MySqlConnectOptions, MySqlConnection}; - -impl Connection for MySqlConnection -where - Rt: Runtime, -{ - type Database = MySql; - - type Options = MySqlConnectOptions; - - fn close(self) -> BoxFuture<'static, Result<()>> - where - Rt: Async, - { - unimplemented!() - } - - fn ping(&mut self) -> BoxFuture<'_, Result<()>> - where - Rt: Async, - { - unimplemented!() - } -} diff --git a/sqlx-mysql/src/async/options.rs b/sqlx-mysql/src/async/options.rs deleted file mode 100644 index bddded69..00000000 --- a/sqlx-mysql/src/async/options.rs +++ /dev/null @@ -1,23 +0,0 @@ -use futures_util::{future::BoxFuture, FutureExt}; -use sqlx_core::{Async, ConnectOptions, Result, Runtime}; - -use crate::{MySqlConnectOptions, MySqlConnection}; - -impl ConnectOptions for MySqlConnectOptions -where - Rt: Runtime, -{ - type Connection = MySqlConnection; - - fn connect(&self) -> BoxFuture<'_, Result> - where - Self::Connection: Sized, - Rt: Async, - { - FutureExt::boxed(async move { - let stream = Rt::connect_tcp(&self.host, self.port).await?; - - Ok(MySqlConnection { stream }) - }) - } -} diff --git a/sqlx-mysql/src/blocking/connection.rs b/sqlx-mysql/src/blocking/connection.rs index 2b486898..199f9343 100644 --- a/sqlx-mysql/src/blocking/connection.rs +++ b/sqlx-mysql/src/blocking/connection.rs @@ -1,14 +1,12 @@ use sqlx_core::blocking::{Connection, Runtime}; use sqlx_core::Result; -use crate::{MySqlConnectOptions, MySqlConnection}; +use crate::MySqlConnection; impl Connection for MySqlConnection where Rt: Runtime, { - type Options = MySqlConnectOptions; - fn close(self) -> Result<()> { unimplemented!() } diff --git a/sqlx-mysql/src/blocking/options.rs b/sqlx-mysql/src/blocking/options.rs index db19e587..124f71b2 100644 --- a/sqlx-mysql/src/blocking/options.rs +++ b/sqlx-mysql/src/blocking/options.rs @@ -1,4 +1,4 @@ -use sqlx_core::blocking::{ConnectOptions, Runtime}; +use sqlx_core::blocking::{ConnectOptions, Connection, Runtime}; use sqlx_core::Result; use crate::{MySqlConnectOptions, MySqlConnection}; @@ -6,11 +6,10 @@ use crate::{MySqlConnectOptions, MySqlConnection}; impl ConnectOptions for MySqlConnectOptions where Rt: Runtime, + Self::Connection: sqlx_core::Connection + Connection, { - type Connection = MySqlConnection; - fn connect(&self) -> Result> { - let stream = ::connect_tcp(&self.host, self.port)?; + let stream = ::connect_tcp(self.get_host(), self.get_port())?; Ok(MySqlConnection { stream }) } diff --git a/sqlx-mysql/src/connection.rs b/sqlx-mysql/src/connection.rs index e795766f..37af485b 100644 --- a/sqlx-mysql/src/connection.rs +++ b/sqlx-mysql/src/connection.rs @@ -1,6 +1,8 @@ use std::fmt::{self, Debug, Formatter}; -use sqlx_core::{DefaultRuntime, Runtime}; +use sqlx_core::{Connection, DefaultRuntime, Runtime}; + +use crate::{MySql, MySqlConnectOptions}; pub struct MySqlConnection where @@ -17,3 +19,28 @@ where f.debug_struct("MySqlConnection").finish() } } + +impl Connection for MySqlConnection +where + Rt: Runtime, +{ + type Database = MySql; + + type Options = MySqlConnectOptions; + + #[cfg(feature = "async")] + fn close(self) -> futures_util::future::BoxFuture<'static, sqlx_core::Result<()>> + where + Rt: sqlx_core::Async, + { + unimplemented!() + } + + #[cfg(feature = "async")] + fn ping(&mut self) -> futures_util::future::BoxFuture<'_, sqlx_core::Result<()>> + where + Rt: sqlx_core::Async, + { + unimplemented!() + } +} diff --git a/sqlx-mysql/src/lib.rs b/sqlx-mysql/src/lib.rs index 075a0d85..09e15b51 100644 --- a/sqlx-mysql/src/lib.rs +++ b/sqlx-mysql/src/lib.rs @@ -26,9 +26,6 @@ mod options; #[cfg(feature = "blocking")] mod blocking; -#[cfg(feature = "async")] -mod r#async; - pub use connection::MySqlConnection; pub use database::MySql; pub use options::MySqlConnectOptions; diff --git a/sqlx-mysql/src/options.rs b/sqlx-mysql/src/options.rs index c1db8f41..c20107e2 100644 --- a/sqlx-mysql/src/options.rs +++ b/sqlx-mysql/src/options.rs @@ -1,29 +1,40 @@ use std::fmt::{self, Debug, Formatter}; use std::marker::PhantomData; -use std::str::FromStr; +use std::path::{Path, PathBuf}; -use sqlx_core::{DefaultRuntime, Runtime}; +use either::Either; +use sqlx_core::{ConnectOptions, DefaultRuntime, Runtime}; +use crate::MySqlConnection; + +mod builder; +mod default; +mod parse; + +/// Options which can be used to configure how a MySQL connection is opened. +/// +/// A value of `MySqlConnectOptions` can be parsed from a connection URL, +/// as described by the [MySQL JDBC connector reference](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-jdbc-url-format.html). +/// +/// ```text +/// mysql://[host][/database][?properties] +/// ``` +/// +/// - The protocol must be `mysql`. +/// +/// - Only a single host is supported. +/// pub struct MySqlConnectOptions where Rt: Runtime, { runtime: PhantomData, - pub(crate) host: String, - pub(crate) port: u16, -} - -impl Default for MySqlConnectOptions -where - Rt: Runtime, -{ - fn default() -> Self { - Self { - host: "localhost".to_owned(), - runtime: PhantomData, - port: 3306, - } - } + address: Either<(String, u16), PathBuf>, + username: Option, + password: Option, + database: Option, + timezone: String, + charset: String, } impl Clone for MySqlConnectOptions @@ -31,7 +42,15 @@ where Rt: Runtime, { fn clone(&self) -> Self { - unimplemented!() + Self { + runtime: PhantomData, + address: self.address.clone(), + username: self.username.clone(), + password: self.password.clone(), + database: self.database.clone(), + timezone: self.timezone.clone(), + charset: self.charset.clone(), + } } } @@ -40,21 +59,93 @@ where Rt: Runtime, { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_struct("MySqlConnectOptions").finish() + f.debug_struct("MySqlConnectOptions") + .field( + "address", + &self + .address + .as_ref() + .map_left(|(host, port)| format!("{}:{}", host, port)) + .map_right(|socket| socket.display()), + ) + .field("username", &self.username) + .field("password", &self.password) + .field("database", &self.database) + .field("timezone", &self.timezone) + .field("charset", &self.charset) + .finish() } } -impl FromStr for MySqlConnectOptions +impl MySqlConnectOptions where Rt: Runtime, { - type Err = sqlx_core::Error; + /// Returns the hostname of the database server. + pub fn get_host(&self) -> &str { + self.address + .as_ref() + .left() + .map(|(host, _)| &**host) + .unwrap_or(default::HOST) + } - fn from_str(s: &str) -> Result { - Ok(Self { - host: "localhost".to_owned(), - runtime: PhantomData, - port: 3306, + /// Returns the TCP port number of the database server. + pub fn get_port(&self) -> u16 { + self.address + .as_ref() + .left() + .map(|(_, port)| *port) + .unwrap_or(default::PORT) + } + + /// Returns the path to the Unix domain socket, if one is configured. + pub fn get_socket(&self) -> Option<&Path> { + self.address.as_ref().right().map(|buf| buf.as_path()) + } + + /// Returns the default database name. + pub fn get_database(&self) -> Option<&str> { + self.database.as_deref() + } + + /// Returns the username to be used for authentication. + pub fn get_username(&self) -> Option<&str> { + self.username.as_deref() + } + + /// Returns the password to be used for authentication. + pub fn get_password(&self) -> Option<&str> { + self.password.as_deref() + } + + /// Returns the character set for the connection. + pub fn get_charset(&self) -> &str { + &self.charset + } + + /// Returns the timezone for the connection. + pub fn get_timezone(&self) -> &str { + &self.timezone + } +} + +impl ConnectOptions for MySqlConnectOptions +where + Rt: Runtime, +{ + type Connection = MySqlConnection; + + #[cfg(feature = "async")] + fn connect(&self) -> futures_util::future::BoxFuture<'_, sqlx_core::Result> + where + Self::Connection: Sized, + Rt: sqlx_core::Async, + { + futures_util::FutureExt::boxed(async move { + let stream = Rt::connect_tcp(self.get_host(), self.get_port()).await?; + + Ok(MySqlConnection { stream }) }) } } diff --git a/sqlx-mysql/src/options/builder.rs b/sqlx-mysql/src/options/builder.rs new file mode 100644 index 00000000..e2e78479 --- /dev/null +++ b/sqlx-mysql/src/options/builder.rs @@ -0,0 +1,82 @@ +use std::mem; +use std::path::{Path, PathBuf}; + +use either::Either; +use sqlx_core::Runtime; + +impl super::MySqlConnectOptions +where + Rt: Runtime, +{ + /// Sets the hostname of the database server. + /// + /// If the hostname begins with a slash (`/`), it is interpreted as the absolute path + /// to a Unix domain socket file instead of a hostname of a server. + /// + /// Defaults to `localhost`. + /// + pub fn host(&mut self, host: impl AsRef) -> &mut Self { + let host = host.as_ref(); + + self.address = if host.starts_with('/') { + Either::Right(PathBuf::from(&*host)) + } else { + Either::Left((host.into(), self.get_port())) + }; + + self + } + + /// Sets the path of the Unix domain socket to connect to. + /// + /// Overrides [`host()`](#method.host) and [`port()`](#method.port). + /// + pub fn socket(&mut self, socket: impl AsRef) -> &mut Self { + self.address = Either::Right(socket.as_ref().to_owned()); + self + } + + /// Sets the TCP port number of the database server. + /// + /// Defaults to `3306`. + /// + pub fn port(&mut self, port: u16) -> &mut Self { + self.address = match self.address { + Either::Right(_) => Either::Left(("localhost".to_owned(), port)), + Either::Left((ref mut host, _)) => Either::Left((mem::take(host), port)), + }; + + self + } + + /// Sets the username to be used for authentication. + // FIXME: Specify what happens when you do NOT set this + pub fn username(&mut self, username: impl AsRef) -> &mut Self { + self.username = Some(username.as_ref().to_owned()); + self + } + + /// Sets the password to be used for authentication. + pub fn password(&mut self, password: impl AsRef) -> &mut Self { + self.password = Some(password.as_ref().to_owned()); + self + } + + /// Sets the default database for the connection. + pub fn database(&mut self, database: impl AsRef) -> &mut Self { + self.database = Some(database.as_ref().to_owned()); + self + } + + /// Sets the character set for the connection. + pub fn charset(&mut self, charset: impl AsRef) -> &mut Self { + self.charset = charset.as_ref().to_owned(); + self + } + + /// Sets the timezone for the connection. + pub fn timezone(&mut self, timezone: impl AsRef) -> &mut Self { + self.timezone = timezone.as_ref().to_owned(); + self + } +} diff --git a/sqlx-mysql/src/options/default.rs b/sqlx-mysql/src/options/default.rs new file mode 100644 index 00000000..289a0b01 --- /dev/null +++ b/sqlx-mysql/src/options/default.rs @@ -0,0 +1,37 @@ +use std::marker::PhantomData; + +use either::Either; +use sqlx_core::Runtime; + +use crate::MySqlConnectOptions; + +pub(crate) const HOST: &str = "localhost"; +pub(crate) const PORT: u16 = 3306; + +impl Default for MySqlConnectOptions +where + Rt: Runtime, +{ + fn default() -> Self { + Self { + runtime: PhantomData, + address: Either::Left((HOST.to_owned(), PORT)), + username: None, + password: None, + database: None, + charset: "utf8mb4".to_owned(), + timezone: "utc".to_owned(), + // todo: connect_timeout + } + } +} + +impl super::MySqlConnectOptions +where + Rt: Runtime, +{ + /// Creates a default set of options ready for configuration. + pub fn new() -> Self { + Self::default() + } +} diff --git a/sqlx-mysql/src/options/parse.rs b/sqlx-mysql/src/options/parse.rs new file mode 100644 index 00000000..44cba3cf --- /dev/null +++ b/sqlx-mysql/src/options/parse.rs @@ -0,0 +1,198 @@ +use std::str::FromStr; + +use percent_encoding::percent_decode_str; +use sqlx_core::{Error, Runtime}; +use url::Url; + +use crate::MySqlConnectOptions; + +impl FromStr for MySqlConnectOptions +where + Rt: Runtime, +{ + type Err = Error; + + fn from_str(s: &str) -> Result { + let url: Url = s + .parse() + .map_err(|error| Error::configuration("database url", error))?; + + if !matches!(url.scheme(), "mysql") { + return Err(Error::configuration_msg(format!( + "unsupported URL scheme {:?} for MySQL", + url.scheme() + ))); + } + + let mut options = Self::new(); + + if let Some(host) = url.host_str() { + options.host(percent_decode_str_utf8(host, "host in database url")?); + } + + if let Some(port) = url.port() { + options.port(port); + } + + let username = url.username(); + if !username.is_empty() { + options.username(percent_decode_str_utf8( + username, + "username in database url", + )?); + } + + if let Some(password) = url.password() { + options.password(percent_decode_str_utf8( + password, + "password in database url", + )?); + } + + let mut path = url.path(); + + if path.starts_with('/') { + path = &path[1..]; + } + + if !path.is_empty() { + options.database(path); + } + + for (key, value) in url.query_pairs().into_iter() { + let value = + percent_decode_str_utf8(&*value, &format!("parameter {:?} in database url", key))?; + + match &*key { + "user" | "username" => { + options.password(value); + } + + "password" => { + options.password(value); + } + + // ssl-mode compatibly with SQLx <= 0.5 + // sslmode compatibly with PostgreSQL + // sslMode compatibly with JDBC MySQL + // tls compatibly with Go MySQL [preferred] + "ssl-mode" | "sslmode" | "sslMode" | "tls" => { + todo!() + } + + "charset" => { + options.charset(value); + } + + "timezone" => { + options.timezone(value); + } + + "socket" => { + options.socket(value); + } + + _ => { + // ignore unknown connection parameters + // fixme: should we error or warn here? + } + } + } + + Ok(options) + } +} + +// todo: this should probably go somewhere common +fn percent_decode_str_utf8(value: &str, context: &str) -> Result { + percent_decode_str(value) + .decode_utf8() + .map_err(|err| Error::configuration(context.to_owned(), err)) + .map(|s| (&*s).to_owned()) +} + +#[cfg(test)] +mod tests { + use super::MySqlConnectOptions; + use std::path::Path; + + #[test] + fn it_should_parse() { + let url = "mysql://user:password@hostname:5432/database?timezone=system&charset=utf8"; + let options: MySqlConnectOptions = url.parse().unwrap(); + + assert_eq!(options.get_username(), Some("user")); + assert_eq!(options.get_password(), Some("password")); + assert_eq!(options.get_host(), "hostname"); + assert_eq!(options.get_port(), 5432); + assert_eq!(options.get_database(), Some("database")); + assert_eq!(options.get_timezone(), "system"); + assert_eq!(options.get_charset(), "utf8"); + } + + #[test] + fn it_should_parse_with_defaults() { + let url = "mysql://"; + let options: MySqlConnectOptions = url.parse().unwrap(); + + assert_eq!(options.get_username(), None); + assert_eq!(options.get_password(), None); + assert_eq!(options.get_host(), "localhost"); + assert_eq!(options.get_port(), 3306); + assert_eq!(options.get_database(), None); + assert_eq!(options.get_timezone(), "utc"); + assert_eq!(options.get_charset(), "utf8mb4"); + } + + #[test] + fn it_should_parse_socket_from_query() { + let url = "mysql://user:password@localhost/database?socket=/var/run/mysqld/mysqld.sock"; + let options: MySqlConnectOptions = url.parse().unwrap(); + + assert_eq!(options.get_username(), Some("user")); + assert_eq!(options.get_password(), Some("password")); + assert_eq!(options.get_database(), Some("database")); + assert_eq!( + options.get_socket(), + Some(Path::new("/var/run/mysqld/mysqld.sock")) + ); + } + + #[test] + fn it_should_parse_socket_from_host() { + // socket path in host requires URL encoding – but does work + let url = "mysql://user:password@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/database"; + let options: MySqlConnectOptions = url.parse().unwrap(); + + assert_eq!(options.get_username(), Some("user")); + assert_eq!(options.get_password(), Some("password")); + assert_eq!(options.get_database(), Some("database")); + assert_eq!( + options.get_socket(), + Some(Path::new("/var/run/mysqld/mysqld.sock")) + ); + } + + #[test] + #[should_panic] + fn it_should_fail_to_parse_non_mysql() { + let url = "postgres://user:password@hostname:5432/database?timezone=system&charset=utf8"; + let _: MySqlConnectOptions = url.parse().unwrap(); + } + + #[test] + fn it_should_parse_username_with_at_sign() { + let url = "mysql://user@hostname:password@hostname:5432/database"; + let options: MySqlConnectOptions = url.parse().unwrap(); + + assert_eq!(options.get_username(), Some("user@hostname")); + } + + #[test] + fn it_should_parse_password_with_non_ascii_chars() { + let url = "mysql://username:p@ssw0rd@hostname:5432/database"; + let options: MySqlConnectOptions = url.parse().unwrap(); + + assert_eq!(options.get_password(), Some("p@ssw0rd")); + } +}