use std::time::Duration;
#[derive(Debug, Clone)]
#[must_use = "Config uses a consuming builder pattern - each method takes ownership and returns a new instance. You must use the returned value or your configuration changes will be lost"]
pub struct Config {
host: String,
port: u16,
database: Option<String>,
user: Option<String>,
password: Option<String>,
connect_timeout: Option<Duration>,
application_name: Option<String>,
options: Vec<(String, String)>,
}
impl Config {
pub fn new() -> Self {
Config {
host: "localhost".to_string(),
port: 7483,
database: None,
user: None,
password: None,
connect_timeout: Some(Duration::from_secs(30)),
application_name: None,
options: vec![("result_format_code".to_string(), "HyperBinary".to_string())],
}
}
pub fn with_host(mut self, host: impl Into<String>) -> Self {
self.host = host.into();
self
}
pub fn with_port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn with_database(mut self, database: impl Into<String>) -> Self {
self.database = Some(database.into());
self
}
pub fn with_user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
pub fn with_password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = Some(timeout);
self
}
pub fn with_application_name(mut self, name: impl Into<String>) -> Self {
self.application_name = Some(name.into());
self
}
pub fn with_option(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.options.push((name.into(), value.into()));
self
}
#[must_use]
pub fn host(&self) -> &str {
&self.host
}
#[must_use]
pub fn port(&self) -> u16 {
self.port
}
#[must_use]
pub fn database(&self) -> Option<&str> {
self.database.as_deref()
}
#[must_use]
pub fn user(&self) -> Option<&str> {
self.user.as_deref()
}
#[must_use]
pub fn password(&self) -> Option<&str> {
self.password.as_deref()
}
#[must_use]
pub fn connect_timeout(&self) -> Option<Duration> {
self.connect_timeout
}
#[must_use]
pub fn application_name(&self) -> Option<&str> {
self.application_name.as_deref()
}
#[must_use]
pub fn options(&self) -> &[(String, String)] {
&self.options
}
#[must_use]
pub fn startup_params(&self) -> Vec<(&str, &str)> {
let mut params = Vec::new();
if let Some(ref user) = self.user {
params.push(("user", user.as_str()));
}
if let Some(ref database) = self.database {
params.push(("database", database.as_str()));
}
if let Some(ref app_name) = self.application_name {
params.push(("application_name", app_name.as_str()));
}
for (name, value) in &self.options {
params.push((name.as_str(), value.as_str()));
}
params
}
}
impl std::str::FromStr for Config {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut config = Config::new();
let (addr, rest) = if let Some(idx) = s.find('/') {
(&s[..idx], &s[idx + 1..])
} else {
(s, "")
};
if let Some(idx) = addr.rfind(':') {
config.host = addr[..idx].to_string();
config.port = addr[idx + 1..].parse().map_err(|_| "invalid port number")?;
} else {
config.host = addr.to_string();
}
let (database, query) = if let Some(idx) = rest.find('?') {
(&rest[..idx], &rest[idx + 1..])
} else {
(rest, "")
};
if !database.is_empty() {
config.database = Some(database.to_string());
}
for param in query.split('&') {
if param.is_empty() {
continue;
}
if let Some(idx) = param.find('=') {
let name = ¶m[..idx];
let value = ¶m[idx + 1..];
match name {
"user" => config.user = Some(value.to_string()),
"password" => config.password = Some(value.to_string()),
"application_name" => config.application_name = Some(value.to_string()),
_ => config.options.push((name.to_string(), value.to_string())),
}
}
}
Ok(config)
}
}
impl Default for Config {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_from_str() {
let config: Config = "localhost:7483/mydb?user=test".parse().unwrap();
assert_eq!(config.host, "localhost");
assert_eq!(config.port, 7483);
assert_eq!(config.database, Some("mydb".to_string()));
assert_eq!(config.user, Some("test".to_string()));
}
#[test]
fn test_config_builder() {
let config = Config::new()
.with_host("localhost")
.with_port(7483)
.with_database("test.hyper")
.with_user("myuser")
.with_password("mypass")
.with_connect_timeout(Duration::from_secs(30));
assert_eq!(config.host, "localhost");
assert_eq!(config.port, 7483);
assert_eq!(config.database.as_deref(), Some("test.hyper"));
assert_eq!(config.user.as_deref(), Some("myuser"));
assert_eq!(config.password.as_deref(), Some("mypass"));
assert_eq!(config.connect_timeout, Some(Duration::from_secs(30)));
}
}