use std::{fmt::Display, str::FromStr};
use serde::{Deserialize, Serialize};
use crate::ConnectionSecurity;
#[cfg(doc)]
use crate::NetHsm;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("The format of URL {url} is invalid because {context}")]
UrlInvalidFormat {
url: url::Url,
context: &'static str,
},
#[error("URL parser error:\n{0}")]
UrlParse(#[from] url::ParseError),
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct Connection {
pub(crate) url: Url,
pub(crate) tls_security: ConnectionSecurity,
}
impl Connection {
pub fn new(url: Url, tls_security: ConnectionSecurity) -> Self {
Self { url, tls_security }
}
pub fn url(&self) -> &Url {
&self.url
}
pub fn tls_security(&self) -> &ConnectionSecurity {
&self.tls_security
}
}
impl Display for Connection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} (TLS security: {})", self.url, self.tls_security)
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(try_from = "String")]
pub struct Url(url::Url);
impl Url {
pub fn new(url: &str) -> Result<Self, crate::Error> {
let url = url::Url::parse(url).map_err(Error::UrlParse)?;
if !url.scheme().eq("https") {
Err(Error::UrlInvalidFormat {
url,
context: "a URL must use TLS",
}
.into())
} else if !url.has_host() {
Err(Error::UrlInvalidFormat {
url,
context: "a URL must have a host component",
}
.into())
} else if url.password().is_some() {
Err(Error::UrlInvalidFormat {
url,
context: "a URL must not have a password component",
}
.into())
} else if !url.username().is_empty() {
Err(Error::UrlInvalidFormat {
url,
context: "a URL must not have a user component",
}
.into())
} else if url.query().is_some() {
Err(Error::UrlInvalidFormat {
url,
context: "a URL must not have a query component",
}
.into())
} else {
Ok(Self(url))
}
}
}
impl Display for Url {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl TryFrom<&str> for Url {
type Error = crate::Error;
fn try_from(value: &str) -> Result<Self, crate::Error> {
Self::new(value)
}
}
impl TryFrom<String> for Url {
type Error = crate::Error;
fn try_from(value: String) -> Result<Self, crate::Error> {
Self::new(&value)
}
}
impl FromStr for Url {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use testresult::TestResult;
use super::*;
#[rstest]
#[case(ConnectionSecurity::Unsafe, "unsafe")]
#[case(ConnectionSecurity::Native, "native")]
fn connection_display(
#[case] connection_security: ConnectionSecurity,
#[case] expected_str: &str,
) -> TestResult {
let url = "https://example.org/";
let connection = Connection::new(url.parse()?, connection_security);
assert_eq!(
format!("{connection}"),
format!("{url} (TLS security: {expected_str})")
);
Ok(())
}
}