1use std::{fmt::Display, str::FromStr};
4
5use serde::{Deserialize, Serialize};
6
7use crate::ConnectionSecurity;
8#[cfg(doc)]
9use crate::NetHsm;
10
11#[derive(Debug, thiserror::Error)]
13pub enum Error {
14 #[error("The format of URL {url} is invalid because {context}")]
19 UrlInvalidFormat {
20 url: url::Url,
22
23 context: &'static str,
27 },
28
29 #[error("URL parser error:\n{0}")]
31 UrlParse(#[from] url::ParseError),
32}
33
34#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
38pub struct Connection {
39 pub(crate) url: Url,
40 pub(crate) tls_security: ConnectionSecurity,
41}
42
43impl Connection {
44 pub fn new(url: Url, tls_security: ConnectionSecurity) -> Self {
46 Self { url, tls_security }
47 }
48
49 pub fn url(&self) -> &Url {
51 &self.url
52 }
53
54 pub fn tls_security(&self) -> &ConnectionSecurity {
56 &self.tls_security
57 }
58}
59
60impl Display for Connection {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 write!(f, "{} (TLS security: {})", self.url, self.tls_security)
63 }
64}
65
66#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
75#[serde(try_from = "String")]
76pub struct Url(url::Url);
77
78impl Url {
79 pub fn new(url: &str) -> Result<Self, crate::Error> {
106 let url = url::Url::parse(url).map_err(Error::UrlParse)?;
107 if !url.scheme().eq("https") {
108 Err(Error::UrlInvalidFormat {
109 url,
110 context: "a URL must use TLS",
111 }
112 .into())
113 } else if !url.has_host() {
114 Err(Error::UrlInvalidFormat {
115 url,
116 context: "a URL must have a host component",
117 }
118 .into())
119 } else if url.password().is_some() {
120 Err(Error::UrlInvalidFormat {
121 url,
122 context: "a URL must not have a password component",
123 }
124 .into())
125 } else if !url.username().is_empty() {
126 Err(Error::UrlInvalidFormat {
127 url,
128 context: "a URL must not have a user component",
129 }
130 .into())
131 } else if url.query().is_some() {
132 Err(Error::UrlInvalidFormat {
133 url,
134 context: "a URL must not have a query component",
135 }
136 .into())
137 } else {
138 Ok(Self(url))
139 }
140 }
141}
142
143impl Display for Url {
144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 write!(f, "{}", self.0)
146 }
147}
148
149impl TryFrom<&str> for Url {
150 type Error = crate::Error;
151
152 fn try_from(value: &str) -> Result<Self, crate::Error> {
153 Self::new(value)
154 }
155}
156
157impl TryFrom<String> for Url {
158 type Error = crate::Error;
159
160 fn try_from(value: String) -> Result<Self, crate::Error> {
161 Self::new(&value)
162 }
163}
164
165impl FromStr for Url {
166 type Err = crate::Error;
167
168 fn from_str(s: &str) -> Result<Self, Self::Err> {
169 Self::new(s)
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use rstest::rstest;
176 use testresult::TestResult;
177
178 use super::*;
179
180 #[rstest]
181 #[case(ConnectionSecurity::Unsafe, "unsafe")]
182 #[case(ConnectionSecurity::Native, "native")]
183 fn connection_display(
184 #[case] connection_security: ConnectionSecurity,
185 #[case] expected_str: &str,
186 ) -> TestResult {
187 let url = "https://example.org/";
188 let connection = Connection::new(url.parse()?, connection_security);
189 assert_eq!(
190 format!("{connection}"),
191 format!("{url} (TLS security: {expected_str})")
192 );
193
194 Ok(())
195 }
196}