1use crate::error::{Error, Result};
30use std::collections::HashMap;
31
32pub const DEFAULT_PORT: u16 = 3141;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum Transport {
38 Quic,
40 Grpc,
42}
43
44impl std::fmt::Display for Transport {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Transport::Quic => write!(f, "quic"),
48 Transport::Grpc => write!(f, "grpc"),
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
57pub struct Dsn {
58 transport: Transport,
59 host: String,
60 port: u16,
61 username: Option<String>,
62 password: Option<String>,
63 tls_enabled: bool,
64 skip_verify: bool,
65 page_size: usize,
66 client_name: String,
67 client_version: String,
68 conformance: String,
69 ca_cert: Option<String>,
70 client_cert: Option<String>,
71 client_key: Option<String>,
72 server_name: Option<String>,
73 connect_timeout_secs: Option<u64>,
74 options: HashMap<String, String>,
75}
76
77impl Default for Dsn {
78 fn default() -> Self {
79 Self {
80 transport: Transport::Quic,
81 host: "localhost".to_string(),
82 port: DEFAULT_PORT,
83 username: None,
84 password: None,
85 tls_enabled: true,
86 skip_verify: false,
87 page_size: 1000,
88 client_name: "geode-rust".to_string(),
89 client_version: env!("CARGO_PKG_VERSION").to_string(),
90 conformance: "min".to_string(),
91 ca_cert: None,
92 client_cert: None,
93 client_key: None,
94 server_name: None,
95 connect_timeout_secs: None,
96 options: HashMap::new(),
97 }
98 }
99}
100
101impl Dsn {
102 pub fn parse(dsn: &str) -> Result<Self> {
144 let dsn = dsn.trim();
145 if dsn.is_empty() {
146 return Err(Error::invalid_dsn("DSN cannot be empty"));
147 }
148
149 if dsn.starts_with("quic://") {
151 Self::parse_url(dsn, Transport::Quic)
152 } else if dsn.starts_with("grpc://") {
153 Self::parse_url(dsn, Transport::Grpc)
154 } else if dsn.contains("://") {
155 let scheme = dsn.split("://").next().unwrap_or("");
157 Err(Error::invalid_dsn(format!(
158 "Unsupported scheme '{}'. Supported schemes: quic://, grpc://",
159 scheme
160 )))
161 } else {
162 Self::parse_legacy(dsn)
164 }
165 }
166
167 fn parse_url(dsn: &str, transport: Transport) -> Result<Self> {
169 let url = url::Url::parse(dsn)
171 .map_err(|e| Error::invalid_dsn(format!("Invalid URL format: {}", e)))?;
172
173 let host_raw = url
174 .host_str()
175 .ok_or_else(|| Error::invalid_dsn("Host is required"))?;
176
177 let host = if host_raw.starts_with('[') && host_raw.ends_with(']') {
179 host_raw[1..host_raw.len() - 1].to_string()
180 } else {
181 host_raw.to_string()
182 };
183
184 if host.is_empty() {
185 return Err(Error::invalid_dsn("Host is required"));
186 }
187
188 let port = url.port().unwrap_or(DEFAULT_PORT);
189
190 let username = if !url.username().is_empty() {
192 Some(
193 urlencoding::decode(url.username())
194 .map_err(|e| Error::invalid_dsn(format!("Invalid username encoding: {}", e)))?
195 .into_owned(),
196 )
197 } else {
198 None
199 };
200
201 let password = url.password().map(|p| {
202 urlencoding::decode(p)
203 .map(|s| s.into_owned())
204 .unwrap_or_else(|_| p.to_string())
205 });
206
207 let params: HashMap<String, String> = url.query_pairs().into_owned().collect();
209
210 let mut result = Self {
211 transport,
212 host,
213 port,
214 username,
215 password,
216 ..Default::default()
217 };
218
219 result.apply_params(¶ms)?;
220
221 Ok(result)
222 }
223
224 fn parse_legacy(dsn: &str) -> Result<Self> {
226 let (host_port, query_str) = if let Some(idx) = dsn.find('?') {
228 (&dsn[..idx], Some(&dsn[idx + 1..]))
229 } else {
230 (dsn, None)
231 };
232
233 let (host, port) = Self::parse_host_port(host_port)?;
235
236 if host.is_empty() {
237 return Err(Error::invalid_dsn("Host is required"));
238 }
239
240 let mut result = Self {
241 transport: Transport::Quic, host,
243 port,
244 ..Default::default()
245 };
246
247 if let Some(qs) = query_str {
249 let params: HashMap<String, String> = qs
250 .split('&')
251 .filter_map(|pair| {
252 let mut parts = pair.splitn(2, '=');
253 let key = parts.next()?;
254 let value = parts.next().unwrap_or("");
255 let decoded_value = urlencoding::decode(value)
257 .map(|s| s.into_owned())
258 .unwrap_or_else(|_| value.to_string());
259 Some((key.to_string(), decoded_value))
260 })
261 .collect();
262
263 result.apply_params(¶ms)?;
264 }
265
266 Ok(result)
267 }
268
269 fn parse_host_port(s: &str) -> Result<(String, u16)> {
271 if s.starts_with('[') {
273 if let Some(bracket_end) = s.find(']') {
275 let host = s[1..bracket_end].to_string();
276 let remainder = &s[bracket_end + 1..];
277 let port = if let Some(rest) = remainder.strip_prefix(':') {
278 rest.parse::<u16>()
279 .map_err(|_| Error::invalid_dsn(format!("Invalid port: {}", rest)))?
280 } else if remainder.is_empty() {
281 DEFAULT_PORT
282 } else {
283 return Err(Error::invalid_dsn("Invalid IPv6 address format"));
284 };
285 return Ok((host, port));
286 } else {
287 return Err(Error::invalid_dsn("Unclosed bracket in IPv6 address"));
288 }
289 }
290
291 if let Some(idx) = s.rfind(':') {
293 let host = s[..idx].to_string();
294 let port_str = &s[idx + 1..];
295 let port = port_str
296 .parse::<u16>()
297 .map_err(|_| Error::invalid_dsn(format!("Invalid port: {}", port_str)))?;
298 Ok((host, port))
299 } else {
300 Ok((s.to_string(), DEFAULT_PORT))
302 }
303 }
304
305 fn apply_params(&mut self, params: &HashMap<String, String>) -> Result<()> {
307 for (key, value) in params {
308 match key.as_str() {
309 "tls" => {
310 self.tls_enabled = parse_bool(value).unwrap_or(true);
311 }
312 "insecure_tls_skip_verify" | "insecure" | "skip_verify" => {
313 self.skip_verify = parse_bool(value).unwrap_or(false);
314 }
315 "page_size" => {
316 self.page_size = value
317 .parse()
318 .map_err(|_| Error::invalid_dsn(format!("Invalid page_size: {}", value)))?;
319 }
320 "client_name" | "hello_name" => {
321 self.client_name = value.clone();
322 }
323 "client_version" | "hello_ver" => {
324 self.client_version = value.clone();
325 }
326 "conformance" => {
327 self.conformance = value.clone();
328 }
329 "username" | "user" => {
330 self.username = Some(value.clone());
331 }
332 "password" | "pass" => {
333 self.password = Some(value.clone());
334 }
335 "ca" | "ca_cert" => {
336 self.ca_cert = Some(value.clone());
337 }
338 "cert" | "client_cert" => {
339 self.client_cert = Some(value.clone());
340 }
341 "key" | "client_key" => {
342 self.client_key = Some(value.clone());
343 }
344 "server_name" => {
345 self.server_name = Some(value.clone());
346 }
347 "connect_timeout" | "timeout" => {
348 self.connect_timeout_secs = value.parse().ok();
349 }
350 _ => {
351 self.options.insert(key.clone(), value.clone());
353 }
354 }
355 }
356 Ok(())
357 }
358
359 pub fn transport(&self) -> Transport {
361 self.transport
362 }
363
364 pub fn host(&self) -> &str {
366 &self.host
367 }
368
369 pub fn port(&self) -> u16 {
371 self.port
372 }
373
374 pub fn username(&self) -> Option<&str> {
376 self.username.as_deref()
377 }
378
379 pub fn password(&self) -> Option<&str> {
381 self.password.as_deref()
382 }
383
384 pub fn tls_enabled(&self) -> bool {
386 self.tls_enabled
387 }
388
389 pub fn skip_verify(&self) -> bool {
391 self.skip_verify
392 }
393
394 pub fn page_size(&self) -> usize {
396 self.page_size
397 }
398
399 pub fn client_name(&self) -> &str {
401 &self.client_name
402 }
403
404 pub fn client_version(&self) -> &str {
406 &self.client_version
407 }
408
409 pub fn conformance(&self) -> &str {
411 &self.conformance
412 }
413
414 pub fn options(&self) -> &HashMap<String, String> {
416 &self.options
417 }
418
419 pub fn ca_cert(&self) -> Option<&str> {
421 self.ca_cert.as_deref()
422 }
423
424 pub fn client_cert(&self) -> Option<&str> {
426 self.client_cert.as_deref()
427 }
428
429 pub fn client_key(&self) -> Option<&str> {
431 self.client_key.as_deref()
432 }
433
434 pub fn server_name(&self) -> Option<&str> {
436 self.server_name.as_deref()
437 }
438
439 pub fn connect_timeout_secs(&self) -> Option<u64> {
441 self.connect_timeout_secs
442 }
443
444 pub fn address(&self) -> String {
446 if self.host.contains(':') {
447 format!("[{}]:{}", self.host, self.port)
449 } else {
450 format!("{}:{}", self.host, self.port)
451 }
452 }
453}
454
455fn parse_bool(s: &str) -> Option<bool> {
457 match s.to_lowercase().as_str() {
458 "true" | "1" | "yes" | "on" => Some(true),
459 "false" | "0" | "no" | "off" => Some(false),
460 _ => None,
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
473 fn test_dsn_parse_quic_basic() {
474 let dsn = Dsn::parse("quic://localhost:3141").unwrap();
475 assert_eq!(dsn.transport(), Transport::Quic);
476 assert_eq!(dsn.host(), "localhost");
477 assert_eq!(dsn.port(), 3141);
478 assert!(dsn.tls_enabled());
479 assert!(!dsn.skip_verify());
480 }
481
482 #[test]
483 fn test_dsn_parse_quic_with_ip() {
484 let dsn = Dsn::parse("quic://127.0.0.1:3141").unwrap();
485 assert_eq!(dsn.transport(), Transport::Quic);
486 assert_eq!(dsn.host(), "127.0.0.1");
487 assert_eq!(dsn.port(), 3141);
488 }
489
490 #[test]
491 fn test_dsn_parse_quic_default_port() {
492 let dsn = Dsn::parse("quic://localhost").unwrap();
493 assert_eq!(dsn.port(), DEFAULT_PORT);
494 }
495
496 #[test]
497 fn test_dsn_parse_quic_with_options() {
498 let dsn = Dsn::parse("quic://localhost:3141?page_size=500&insecure=true").unwrap();
499 assert_eq!(dsn.page_size(), 500);
500 assert!(dsn.skip_verify());
501 }
502
503 #[test]
508 fn test_dsn_parse_grpc_basic() {
509 let dsn = Dsn::parse("grpc://localhost:50051").unwrap();
510 assert_eq!(dsn.transport(), Transport::Grpc);
511 assert_eq!(dsn.host(), "localhost");
512 assert_eq!(dsn.port(), 50051);
513 }
514
515 #[test]
516 fn test_dsn_parse_grpc_with_tls_disabled() {
517 let dsn = Dsn::parse("grpc://127.0.0.1:50051?tls=0").unwrap();
518 assert_eq!(dsn.transport(), Transport::Grpc);
519 assert_eq!(dsn.host(), "127.0.0.1");
520 assert_eq!(dsn.port(), 50051);
521 assert!(!dsn.tls_enabled());
522 }
523
524 #[test]
525 fn test_dsn_parse_grpc_with_tls_false() {
526 let dsn = Dsn::parse("grpc://localhost:50051?tls=false").unwrap();
527 assert!(!dsn.tls_enabled());
528 }
529
530 #[test]
531 fn test_dsn_parse_unsupported_schemes() {
532 let err = Dsn::parse("grcp://localhost:50051").unwrap_err();
534 assert!(err.to_string().contains("Unsupported scheme"));
535
536 let err = Dsn::parse("geode://localhost:3141").unwrap_err();
538 assert!(err.to_string().contains("Unsupported scheme"));
539 }
540
541 #[test]
546 fn test_dsn_parse_ipv6_grpc() {
547 let dsn = Dsn::parse("grpc://[::1]:50051").unwrap();
548 assert_eq!(dsn.transport(), Transport::Grpc);
549 assert_eq!(dsn.host(), "::1");
550 assert_eq!(dsn.port(), 50051);
551 }
552
553 #[test]
554 fn test_dsn_parse_ipv6_quic() {
555 let dsn = Dsn::parse("quic://[::1]:3141").unwrap();
556 assert_eq!(dsn.transport(), Transport::Quic);
557 assert_eq!(dsn.host(), "::1");
558 assert_eq!(dsn.port(), 3141);
559 }
560
561 #[test]
562 fn test_dsn_parse_ipv6_full_address() {
563 let dsn = Dsn::parse("grpc://[2001:db8::1]:50051").unwrap();
564 assert_eq!(dsn.host(), "2001:db8::1");
565 assert_eq!(dsn.port(), 50051);
566 }
567
568 #[test]
569 fn test_dsn_parse_ipv6_default_port() {
570 let dsn = Dsn::parse("quic://[::1]").unwrap();
571 assert_eq!(dsn.host(), "::1");
572 assert_eq!(dsn.port(), DEFAULT_PORT);
573 }
574
575 #[test]
576 fn test_dsn_address_ipv6() {
577 let dsn = Dsn::parse("grpc://[::1]:50051").unwrap();
578 assert_eq!(dsn.address(), "[::1]:50051");
579 }
580
581 #[test]
586 fn test_dsn_parse_legacy_host_port() {
587 let dsn = Dsn::parse("localhost:3141").unwrap();
588 assert_eq!(dsn.transport(), Transport::Quic); assert_eq!(dsn.host(), "localhost");
590 assert_eq!(dsn.port(), 3141);
591 }
592
593 #[test]
594 fn test_dsn_parse_legacy_with_options() {
595 let dsn = Dsn::parse("localhost:3141?insecure=true&page_size=500").unwrap();
596 assert_eq!(dsn.transport(), Transport::Quic);
597 assert!(dsn.skip_verify());
598 assert_eq!(dsn.page_size(), 500);
599 }
600
601 #[test]
602 fn test_dsn_parse_legacy_ipv6() {
603 let dsn = Dsn::parse("[::1]:3141").unwrap();
604 assert_eq!(dsn.host(), "::1");
605 assert_eq!(dsn.port(), 3141);
606 }
607
608 #[test]
613 fn test_dsn_parse_with_auth() {
614 let dsn = Dsn::parse("quic://admin:secret@localhost:3141").unwrap();
615 assert_eq!(dsn.username(), Some("admin"));
616 assert_eq!(dsn.password(), Some("secret"));
617 }
618
619 #[test]
620 fn test_dsn_parse_auth_via_query_params() {
621 let dsn = Dsn::parse("grpc://localhost:50051?username=admin&password=secret").unwrap();
622 assert_eq!(dsn.username(), Some("admin"));
623 assert_eq!(dsn.password(), Some("secret"));
624 }
625
626 #[test]
627 fn test_dsn_parse_auth_percent_encoded() {
628 let dsn = Dsn::parse("quic://user%40domain:p%40ss%3Dword@localhost:3141").unwrap();
629 assert_eq!(dsn.username(), Some("user@domain"));
630 assert_eq!(dsn.password(), Some("p@ss=word"));
631 }
632
633 #[test]
638 fn test_dsn_parse_empty() {
639 let err = Dsn::parse("").unwrap_err();
640 assert!(err.to_string().contains("empty"));
641 }
642
643 #[test]
644 fn test_dsn_parse_unsupported_scheme() {
645 let err = Dsn::parse("http://localhost:3141").unwrap_err();
646 assert!(err.to_string().contains("Unsupported scheme"));
647 assert!(err.to_string().contains("http"));
648 }
649
650 #[test]
651 fn test_dsn_parse_invalid_port() {
652 let err = Dsn::parse("quic://localhost:invalid").unwrap_err();
653 assert!(err.to_string().contains("port") || err.to_string().contains("Invalid"));
654 }
655
656 #[test]
657 fn test_dsn_parse_port_too_large() {
658 let err = Dsn::parse("quic://localhost:99999").unwrap_err();
659 assert!(err.to_string().contains("port") || err.to_string().contains("Invalid"));
660 }
661
662 #[test]
663 fn test_dsn_parse_missing_host() {
664 let result = Dsn::parse("quic://:3141");
667 if let Ok(dsn) = result {
669 assert!(dsn.host().is_empty() || dsn.host() == "");
670 }
671 }
672
673 #[test]
678 fn test_dsn_parse_all_options() {
679 let dsn = Dsn::parse(
680 "grpc://localhost:50051?tls=1&insecure=false&page_size=2000&client_name=test-app&conformance=full"
681 ).unwrap();
682
683 assert!(dsn.tls_enabled());
684 assert!(!dsn.skip_verify());
685 assert_eq!(dsn.page_size(), 2000);
686 assert_eq!(dsn.client_name(), "test-app");
687 assert_eq!(dsn.conformance(), "full");
688 }
689
690 #[test]
691 fn test_dsn_parse_unknown_options_preserved() {
692 let dsn = Dsn::parse("quic://localhost:3141?custom_option=value").unwrap();
693 assert_eq!(
694 dsn.options().get("custom_option"),
695 Some(&"value".to_string())
696 );
697 }
698
699 #[test]
700 fn test_dsn_parse_option_aliases() {
701 let dsn1 = Dsn::parse("grpc://localhost:50051?user=admin").unwrap();
703 let dsn2 = Dsn::parse("grpc://localhost:50051?username=admin").unwrap();
704 assert_eq!(dsn1.username(), dsn2.username());
705
706 let dsn3 = Dsn::parse("grpc://localhost:50051?pass=secret").unwrap();
708 let dsn4 = Dsn::parse("grpc://localhost:50051?password=secret").unwrap();
709 assert_eq!(dsn3.password(), dsn4.password());
710
711 let dsn5 = Dsn::parse("grpc://localhost:50051?skip_verify=true").unwrap();
713 let dsn6 = Dsn::parse("grpc://localhost:50051?insecure=true").unwrap();
714 assert_eq!(dsn5.skip_verify(), dsn6.skip_verify());
715 }
716
717 #[test]
718 fn test_dsn_parse_percent_encoded_values() {
719 let dsn = Dsn::parse("quic://localhost:3141?client_name=My%20App").unwrap();
720 assert_eq!(dsn.client_name(), "My App");
721 }
722
723 #[test]
724 fn test_dsn_parse_mtls_options() {
725 let dsn = Dsn::parse(
726 "grpc://localhost:50051?ca=/path/ca.crt&cert=/path/client.crt&key=/path/client.key",
727 )
728 .unwrap();
729 assert_eq!(dsn.ca_cert(), Some("/path/ca.crt"));
730 assert_eq!(dsn.client_cert(), Some("/path/client.crt"));
731 assert_eq!(dsn.client_key(), Some("/path/client.key"));
732 }
733
734 #[test]
735 fn test_dsn_parse_mtls_options_alt_names() {
736 let dsn = Dsn::parse("grpc://localhost:50051?ca_cert=/path/ca.crt&client_cert=/path/client.crt&client_key=/path/client.key").unwrap();
737 assert_eq!(dsn.ca_cert(), Some("/path/ca.crt"));
738 assert_eq!(dsn.client_cert(), Some("/path/client.crt"));
739 assert_eq!(dsn.client_key(), Some("/path/client.key"));
740 }
741
742 #[test]
743 fn test_dsn_parse_connect_timeout() {
744 let dsn = Dsn::parse("quic://localhost:3141?connect_timeout=60").unwrap();
745 assert_eq!(dsn.connect_timeout_secs(), Some(60));
746 }
747
748 #[test]
749 fn test_dsn_parse_server_name() {
750 let dsn = Dsn::parse("quic://localhost:3141?server_name=geode.example.com").unwrap();
751 assert_eq!(dsn.server_name(), Some("geode.example.com"));
752 }
753
754 #[test]
759 fn test_dsn_parse_insecure_tls_skip_verify_primary() {
760 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=true").unwrap();
762 assert!(dsn.skip_verify());
763
764 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=1").unwrap();
765 assert!(dsn.skip_verify());
766
767 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=yes").unwrap();
768 assert!(dsn.skip_verify());
769
770 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=on").unwrap();
771 assert!(dsn.skip_verify());
772
773 let dsn = Dsn::parse("quic://localhost:3141?insecure_tls_skip_verify=false").unwrap();
774 assert!(!dsn.skip_verify());
775 }
776
777 #[test]
778 fn test_dsn_parse_insecure_alias() {
779 let dsn = Dsn::parse("quic://localhost:3141?insecure=true").unwrap();
781 assert!(dsn.skip_verify());
782
783 let dsn = Dsn::parse("grpc://localhost:50051?insecure=1").unwrap();
784 assert!(dsn.skip_verify());
785
786 let dsn = Dsn::parse("localhost:3141?insecure=yes").unwrap();
787 assert!(dsn.skip_verify());
788 }
789
790 #[test]
791 fn test_dsn_parse_skip_verify_alias() {
792 let dsn = Dsn::parse("quic://localhost:3141?skip_verify=true").unwrap();
794 assert!(dsn.skip_verify());
795
796 let dsn = Dsn::parse("grpc://localhost:50051?skip_verify=1").unwrap();
797 assert!(dsn.skip_verify());
798
799 let dsn = Dsn::parse("quic://localhost:3141?skip_verify=on").unwrap();
800 assert!(dsn.skip_verify());
801 }
802
803 #[test]
804 fn test_dsn_skip_verify_default_false() {
805 let dsn = Dsn::parse("quic://localhost:3141").unwrap();
807 assert!(!dsn.skip_verify());
808
809 let dsn = Dsn::parse("grpc://localhost:50051").unwrap();
810 assert!(!dsn.skip_verify());
811
812 let dsn = Dsn::parse("localhost:3141").unwrap();
813 assert!(!dsn.skip_verify());
814 }
815
816 #[test]
821 fn test_transport_display() {
822 assert_eq!(Transport::Quic.to_string(), "quic");
823 assert_eq!(Transport::Grpc.to_string(), "grpc");
824 }
825
826 #[test]
831 fn test_dsn_address_ipv4() {
832 let dsn = Dsn::parse("quic://192.168.1.1:3141").unwrap();
833 assert_eq!(dsn.address(), "192.168.1.1:3141");
834 }
835
836 #[test]
837 fn test_dsn_address_hostname() {
838 let dsn = Dsn::parse("grpc://geode.example.com:50051").unwrap();
839 assert_eq!(dsn.address(), "geode.example.com:50051");
840 }
841
842 #[test]
847 fn test_acceptance_quic_localhost() {
848 let dsn = Dsn::parse("quic://localhost:1234").unwrap();
850 assert_eq!(dsn.transport(), Transport::Quic);
851 assert_eq!(dsn.host(), "localhost");
852 assert_eq!(dsn.port(), 1234);
853 }
854
855 #[test]
856 fn test_acceptance_grpc_with_tls_disabled() {
857 let dsn = Dsn::parse("grpc://127.0.0.1:50051?tls=0").unwrap();
859 assert_eq!(dsn.transport(), Transport::Grpc);
860 assert_eq!(dsn.host(), "127.0.0.1");
861 assert_eq!(dsn.port(), 50051);
862 assert!(!dsn.tls_enabled());
863 }
864
865 #[test]
866 fn test_acceptance_invalid_scheme() {
867 let err = Dsn::parse("http://localhost:1").unwrap_err();
869 let err_str = err.to_string();
870 assert!(
871 err_str.contains("Unsupported scheme") || err_str.contains("unsupported"),
872 "Expected 'unsupported scheme' error, got: {}",
873 err_str
874 );
875 }
876}