1use std::fmt;
2use std::str::FromStr;
3
4use thiserror::Error;
5use url::Url;
6
7use crate::auth::{auth_from_dsn_and_client, Auth};
8use crate::project_id::{ParseProjectIdError, ProjectId};
9
10#[derive(Debug, Error)]
12pub enum ParseDsnError {
13 #[error("no valid url provided")]
15 InvalidUrl,
16 #[error("no valid scheme")]
18 InvalidScheme,
19 #[error("username is empty")]
21 NoUsername,
22 #[error("empty path")]
24 NoProjectId,
25 #[error("invalid project id")]
27 InvalidProjectId(#[from] ParseProjectIdError),
28}
29
30#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
34pub enum Scheme {
35 Http,
37 Https,
39}
40
41impl Scheme {
42 pub fn default_port(self) -> u16 {
44 match self {
45 Scheme::Http => 80,
46 Scheme::Https => 443,
47 }
48 }
49}
50
51impl fmt::Display for Scheme {
52 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
53 write!(
54 f,
55 "{}",
56 match *self {
57 Scheme::Https => "https",
58 Scheme::Http => "http",
59 }
60 )
61 }
62}
63
64#[derive(Clone, Eq, PartialEq, Hash, Debug)]
66pub struct Dsn {
67 scheme: Scheme,
68 public_key: String,
69 secret_key: Option<String>,
70 host: String,
71 port: Option<u16>,
72 path: String,
73 project_id: ProjectId,
74}
75
76impl Dsn {
77 pub fn to_auth(&self, client_agent: Option<&str>) -> Auth {
82 auth_from_dsn_and_client(self, client_agent)
83 }
84
85 fn api_url(&self, endpoint: &str) -> Url {
86 use std::fmt::Write;
87 let mut buf = format!("{}://{}", self.scheme(), self.host());
88 if self.port() != self.scheme.default_port() {
89 write!(&mut buf, ":{}", self.port()).unwrap();
90 }
91 write!(
92 &mut buf,
93 "{}api/{}/{}/",
94 self.path,
95 self.project_id(),
96 endpoint
97 )
98 .unwrap();
99 Url::parse(&buf).unwrap()
100 }
101
102 pub fn store_api_url(&self) -> Url {
104 self.api_url("store")
105 }
106
107 pub fn envelope_api_url(&self) -> Url {
109 self.api_url("envelope")
110 }
111
112 pub fn scheme(&self) -> Scheme {
114 self.scheme
115 }
116
117 pub fn public_key(&self) -> &str {
119 &self.public_key
120 }
121
122 pub fn secret_key(&self) -> Option<&str> {
124 self.secret_key.as_deref()
125 }
126
127 pub fn host(&self) -> &str {
129 &self.host
130 }
131
132 pub fn port(&self) -> u16 {
134 self.port.unwrap_or_else(|| self.scheme.default_port())
135 }
136
137 pub fn path(&self) -> &str {
139 &self.path
140 }
141
142 pub fn project_id(&self) -> &ProjectId {
144 &self.project_id
145 }
146}
147
148impl fmt::Display for Dsn {
149 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
150 write!(f, "{}://{}:", self.scheme, self.public_key)?;
151 if let Some(ref secret_key) = self.secret_key {
152 write!(f, "{secret_key}")?;
153 }
154 write!(f, "@{}", self.host)?;
155 if let Some(ref port) = self.port {
156 write!(f, ":{port}")?;
157 }
158 write!(f, "{}{}", self.path, self.project_id)?;
159 Ok(())
160 }
161}
162
163impl FromStr for Dsn {
164 type Err = ParseDsnError;
165
166 fn from_str(s: &str) -> Result<Dsn, ParseDsnError> {
167 let url = Url::parse(s).map_err(|_| ParseDsnError::InvalidUrl)?;
168
169 if url.path() == "/" {
170 return Err(ParseDsnError::NoProjectId);
171 }
172
173 let mut path_segments = url.path().trim_matches('/').rsplitn(2, '/');
174
175 let project_id = path_segments
176 .next()
177 .ok_or(ParseDsnError::NoProjectId)?
178 .parse()
179 .map_err(ParseDsnError::InvalidProjectId)?;
180 let path = match path_segments.next().unwrap_or("") {
181 "" | "/" => "/".into(),
182 other => format!("/{other}/"),
183 };
184
185 let public_key = match url.username() {
186 "" => return Err(ParseDsnError::NoUsername),
187 username => username.to_string(),
188 };
189
190 let scheme = match url.scheme() {
191 "http" => Scheme::Http,
192 "https" => Scheme::Https,
193 _ => return Err(ParseDsnError::InvalidScheme),
194 };
195
196 let secret_key = url.password().map(|s| s.into());
197 let port = url.port();
198 let host = match url.host_str() {
199 Some(host) => host.into(),
200 None => return Err(ParseDsnError::InvalidUrl),
201 };
202
203 Ok(Dsn {
204 scheme,
205 public_key,
206 secret_key,
207 host,
208 port,
209 path,
210 project_id,
211 })
212 }
213}
214
215impl_str_serde!(Dsn);
216
217#[cfg(test)]
218mod test {
219 use super::*;
220
221 #[test]
222 fn test_dsn_serialize_deserialize() {
223 let dsn = Dsn::from_str("https://username:@domain/42").unwrap();
224 let serialized = serde_json::to_string(&dsn).unwrap();
225 assert_eq!(serialized, "\"https://username:@domain/42\"");
226 let deserialized: Dsn = serde_json::from_str(&serialized).unwrap();
227 assert_eq!(deserialized.to_string(), "https://username:@domain/42");
228 }
229
230 #[test]
231 fn test_dsn_parsing() {
232 let url = "https://username:password@domain:8888/23%21";
233 let dsn = url.parse::<Dsn>().unwrap();
234 assert_eq!(dsn.scheme(), Scheme::Https);
235 assert_eq!(dsn.public_key(), "username");
236 assert_eq!(dsn.secret_key(), Some("password"));
237 assert_eq!(dsn.host(), "domain");
238 assert_eq!(dsn.port(), 8888);
239 assert_eq!(dsn.path(), "/");
240 assert_eq!(dsn.project_id(), &ProjectId::new("23%21"));
241 assert_eq!(url, dsn.to_string());
242 }
243
244 #[test]
245 fn test_dsn_no_port() {
246 let url = "https://username:@domain/42";
247 let dsn = Dsn::from_str(url).unwrap();
248 assert_eq!(dsn.port(), 443);
249 assert_eq!(url, dsn.to_string());
250 assert_eq!(
251 dsn.store_api_url().to_string(),
252 "https://domain/api/42/store/"
253 );
254 assert_eq!(
255 dsn.envelope_api_url().to_string(),
256 "https://domain/api/42/envelope/"
257 );
258 }
259
260 #[test]
261 fn test_insecure_dsn_no_port() {
262 let url = "http://username:@domain/42";
263 let dsn = Dsn::from_str(url).unwrap();
264 assert_eq!(dsn.port(), 80);
265 assert_eq!(url, dsn.to_string());
266 assert_eq!(
267 dsn.store_api_url().to_string(),
268 "http://domain/api/42/store/"
269 );
270 assert_eq!(
271 dsn.envelope_api_url().to_string(),
272 "http://domain/api/42/envelope/"
273 );
274 }
275
276 #[test]
277 fn test_dsn_no_password() {
278 let url = "https://username:@domain:8888/42";
279 let dsn = Dsn::from_str(url).unwrap();
280 assert_eq!(url, dsn.to_string());
281 assert_eq!(
282 dsn.store_api_url().to_string(),
283 "https://domain:8888/api/42/store/"
284 );
285 assert_eq!(
286 dsn.envelope_api_url().to_string(),
287 "https://domain:8888/api/42/envelope/"
288 );
289 }
290
291 #[test]
292 fn test_dsn_no_password_colon() {
293 let url = "https://username@domain:8888/42";
294 let dsn = Dsn::from_str(url).unwrap();
295 assert_eq!("https://username:@domain:8888/42", dsn.to_string());
296 }
297
298 #[test]
299 fn test_dsn_http_url() {
300 let url = "http://username:@domain:8888/42";
301 let dsn = Dsn::from_str(url).unwrap();
302 assert_eq!(url, dsn.to_string());
303 }
304
305 #[test]
306 fn test_dsn_non_integer_project_id() {
307 let url = "https://username:password@domain:8888/abc123youandme%21%21";
308 let dsn = url.parse::<Dsn>().unwrap();
309 assert_eq!(dsn.project_id(), &ProjectId::new("abc123youandme%21%21"));
310 }
311
312 #[test]
313 fn test_dsn_more_than_one_non_integer_path() {
314 let url = "http://username:@domain:8888/pathone/pathtwo/pid";
315 let dsn = url.parse::<Dsn>().unwrap();
316 assert_eq!(dsn.project_id(), &ProjectId::new("pid"));
317 assert_eq!(dsn.path(), "/pathone/pathtwo/");
318 }
319
320 #[test]
321 #[should_panic(expected = "NoUsername")]
322 fn test_dsn_no_username() {
323 Dsn::from_str("https://:password@domain:8888/23").unwrap();
324 }
325
326 #[test]
327 #[should_panic(expected = "InvalidUrl")]
328 fn test_dsn_invalid_url() {
329 Dsn::from_str("random string").unwrap();
330 }
331
332 #[test]
333 #[should_panic(expected = "InvalidUrl")]
334 fn test_dsn_no_host() {
335 Dsn::from_str("https://username:password@:8888/42").unwrap();
336 }
337
338 #[test]
339 #[should_panic(expected = "NoProjectId")]
340 fn test_dsn_no_project_id() {
341 Dsn::from_str("https://username:password@domain:8888/").unwrap();
342 }
343
344 #[test]
345 #[should_panic(expected = "InvalidScheme")]
346 fn test_dsn_invalid_scheme() {
347 Dsn::from_str("ftp://username:password@domain:8888/1").unwrap();
348 }
349}