1pub mod error;
20
21use error::ErrorKind;
22use hickory_resolver::config::*;
23use hickory_resolver::name_server::TokioConnectionProvider;
24use hickory_resolver::proto::xfer::Protocol;
25use hickory_resolver::system_conf::read_system_conf;
26use hickory_resolver::TokioResolver;
27use regex::Regex;
28use std::collections::HashMap;
29use std::fmt;
30use std::fmt::{Display, Formatter};
31use std::net::SocketAddr;
32use std::time::Duration;
33use tracing::debug;
34use url::form_urlencoded;
35
36pub const DEFAULT_LEGACY_HTTP_PORT: u16 = 8091;
37pub const DEFAULT_LEGACY_HTTPS_PORT: u16 = 18091;
38pub const DEFAULT_MEMD_PORT: u16 = 11210;
39pub const DEFAULT_SSL_MEMD_PORT: u16 = 11207;
40pub const DEFAULT_COUCHBASE2_PORT: u16 = 18098;
41
42#[derive(Debug, Clone, Default, PartialEq)]
43pub struct ConnSpec {
44 scheme: Option<String>,
45 hosts: Vec<ConnSpecAddress>,
46 options: HashMap<String, Vec<String>>,
47}
48
49#[derive(Debug, Clone, Default, PartialEq)]
50pub struct Address {
51 pub host: String,
52 pub port: u16,
53}
54
55#[derive(Debug, Clone, PartialEq)]
56pub struct DnsConfig {
57 pub namespace: SocketAddr,
58 pub timeout: Option<Duration>,
59}
60
61impl Display for Address {
62 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
63 if self.host.contains(':') && !self.host.starts_with('[') {
64 write!(f, "[{}]:{}", self.host, self.port)
65 } else {
66 write!(f, "{}:{}", self.host, self.port)
67 }
68 }
69}
70
71#[derive(Debug, Clone, Default, PartialEq)]
72pub struct ConnSpecAddress {
73 host: String,
74 port: Option<u16>,
75}
76
77#[derive(Debug, Clone, Default, PartialEq)]
78pub struct SrvRecord {
79 pub proto: String,
80 pub scheme: String,
81 pub host: String,
82}
83
84impl ConnSpec {
85 fn srv_record(&self) -> Option<SrvRecord> {
86 if let Some(scheme_type) = &self.scheme {
87 let scheme = scheme_type.as_str();
88 if (scheme != "couchbase" && scheme != "couchbases")
89 || self.hosts.len() != 1
90 || self.hosts[0].port.is_some()
91 {
92 return None;
93 }
94
95 let host = &self.hosts[0].host;
96 if host_is_ip_address(host) {
97 return None;
98 }
99
100 return Some(SrvRecord {
101 scheme: scheme_type.clone(),
102 proto: "tcp".to_string(),
103 host: host.clone(),
104 });
105 }
106
107 None
108 }
109}
110
111impl Display for ConnSpec {
112 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
113 let scheme = self
114 .scheme
115 .clone()
116 .map(|scheme| format!("{scheme}://"))
117 .unwrap_or_default();
118
119 let hosts = self
120 .hosts
121 .iter()
122 .map(|host| {
123 if let Some(port) = &host.port {
124 if host.host.contains(':') && !host.host.starts_with('[') {
125 format!("[{}]:{}", host.host, port)
126 } else {
127 format!("{}:{}", host.host, port)
128 }
129 } else {
130 host.host.clone()
131 }
132 })
133 .collect::<Vec<String>>()
134 .join(",");
135
136 let mut url_options = self.options.iter().fold(String::new(), |acc, (k, v)| {
137 let values = v
138 .iter()
139 .map(|value| format!("{k}={value}"))
140 .collect::<Vec<String>>()
141 .join("&");
142 if acc.is_empty() {
143 values
144 } else {
145 format!("{acc}&{values}")
146 }
147 });
148 if !url_options.is_empty() {
149 url_options = format!("?{url_options}");
150 }
151
152 let out = format!("{scheme}{hosts}{url_options}");
153
154 write!(f, "{out}")
155 }
156}
157
158pub fn parse(conn_str: impl AsRef<str>) -> error::Result<ConnSpec> {
159 let conn_str = conn_str.as_ref();
160
161 let parts_matcher =
162 Regex::new(r"((.*)://)?(([^/?:]*)(:([^/?:@]*))?@)?([^/?]*)(/([^?]*))?(\?(.*))?").unwrap();
163 let host_matcher = Regex::new(r"((\[[^]]+]+)|([^;,:]+))(:([0-9]*))?(;,)?").unwrap();
164
165 if let Some(parts) = parts_matcher.captures(conn_str) {
166 let scheme = parts.get(2).map(|m| m.as_str().to_string());
167
168 let hosts = if let Some(hosts) = parts.get(7) {
169 let mut addresses = vec![];
170 for host_info in host_matcher.captures_iter(hosts.as_str()) {
171 let mut address = ConnSpecAddress {
172 host: host_info[1].to_string(),
173 port: None,
174 };
175
176 if let Some(port) = host_info.get(5) {
177 address.port = Some(
178 port.as_str()
179 .parse()
180 .map_err(|e| ErrorKind::Parse(format!("failed to parse port: {e}")))?,
181 );
182 }
183
184 addresses.push(address);
185 }
186 addresses
187 } else {
188 vec![]
189 };
190
191 let options = if let Some(options) = parts.get(11) {
192 form_urlencoded::parse(options.as_str().as_bytes())
193 .into_owned()
194 .fold(
195 HashMap::new(),
196 |mut acc: HashMap<String, Vec<String>>, (k, v)| {
197 acc.entry(k).or_default().push(v);
198 acc
199 },
200 )
201 } else {
202 HashMap::default()
203 };
204
205 return Ok(ConnSpec {
206 scheme,
207 hosts,
208 options,
209 });
210 }
211
212 Ok(ConnSpec::default())
213}
214
215#[derive(Debug, Clone, Default, PartialEq)]
216pub struct ResolvedConnSpec {
217 pub use_ssl: bool,
218 pub memd_hosts: Vec<Address>,
219 pub http_hosts: Vec<Address>,
220 pub couchbase2_host: Option<Address>,
221 pub srv_record: Option<SrvRecord>,
222 pub options: HashMap<String, Vec<String>>,
223}
224
225pub async fn resolve(
226 conn_spec: ConnSpec,
227 dns_config: impl Into<Option<DnsConfig>>,
228) -> error::Result<ResolvedConnSpec> {
229 let (default_port, has_explicit_scheme, use_ssl) = if let Some(scheme) = &conn_spec.scheme {
230 match scheme.as_str() {
231 "couchbase" => (DEFAULT_MEMD_PORT, true, false),
232 "couchbases" => (DEFAULT_SSL_MEMD_PORT, true, true),
233 "couchbase2" => {
234 return handle_couchbase2_scheme(conn_spec);
235 }
236 "" => (DEFAULT_MEMD_PORT, false, false),
237 _ => {
238 return Err(ErrorKind::InvalidArgument {
239 msg: "unrecognized scheme".to_string(),
240 arg: "scheme".to_string(),
241 }
242 .into());
243 }
244 }
245 } else {
246 (DEFAULT_MEMD_PORT, false, false)
247 };
248
249 if let Some(srv_record) = conn_spec.srv_record() {
250 match lookup_srv(
251 &srv_record.scheme,
252 &srv_record.proto,
253 &srv_record.host,
254 dns_config.into(),
255 )
256 .await
257 {
258 Ok(srv_records) => {
259 return Ok(ResolvedConnSpec {
260 use_ssl,
261 memd_hosts: srv_records,
262 http_hosts: vec![],
263 couchbase2_host: None,
264 srv_record: Some(SrvRecord {
265 proto: srv_record.proto,
266 scheme: srv_record.scheme,
267 host: srv_record.host,
268 }),
269 options: conn_spec.options,
270 });
271 }
272 Err(e) => {
273 debug!("Srv lookup failed {e}");
274 }
275 };
276 };
277
278 if conn_spec.hosts.is_empty() {
279 let (memd_port, http_port) = if use_ssl {
280 (DEFAULT_SSL_MEMD_PORT, DEFAULT_LEGACY_HTTPS_PORT)
281 } else {
282 (DEFAULT_MEMD_PORT, DEFAULT_LEGACY_HTTP_PORT)
283 };
284
285 return Ok(ResolvedConnSpec {
286 use_ssl,
287 memd_hosts: vec![Address {
288 host: "127.0.0.1".to_string(),
289 port: memd_port,
290 }],
291 http_hosts: vec![Address {
292 host: "127.0.0.1".to_string(),
293 port: http_port,
294 }],
295 couchbase2_host: None,
296 srv_record: None,
297 options: conn_spec.options,
298 });
299 }
300
301 let mut memd_hosts = vec![];
302 let mut http_hosts = vec![];
303 for address in conn_spec.hosts {
304 if let Some(port) = &address.port {
305 if *port == DEFAULT_LEGACY_HTTP_PORT {
306 return Err(ErrorKind::InvalidArgument{msg: "couchbase://host:8091 not supported for couchbase:// scheme. Use couchbase://host".to_string(), arg: "port".to_string()}.into());
307 }
308
309 if !has_explicit_scheme && address.port != Some(default_port) {
310 return Err(ErrorKind::InvalidArgument {
311 msg: "ambiguous port without scheme".to_string(),
312 arg: "port".to_string(),
313 }
314 .into());
315 }
316
317 memd_hosts.push(Address {
318 host: address.host,
319 port: *port,
320 });
321 } else {
322 let (memd_port, http_port) = if use_ssl {
323 (DEFAULT_SSL_MEMD_PORT, DEFAULT_LEGACY_HTTPS_PORT)
324 } else {
325 (DEFAULT_MEMD_PORT, DEFAULT_LEGACY_HTTP_PORT)
326 };
327
328 memd_hosts.push(Address {
329 host: address.host.clone(),
330 port: memd_port,
331 });
332
333 http_hosts.push(Address {
334 host: address.host,
335 port: http_port,
336 });
337 }
338 }
339
340 Ok(ResolvedConnSpec {
341 use_ssl,
342 memd_hosts,
343 http_hosts,
344 couchbase2_host: None,
345 srv_record: None,
346 options: conn_spec.options,
347 })
348}
349
350fn handle_couchbase2_scheme(conn_spec: ConnSpec) -> error::Result<ResolvedConnSpec> {
351 if conn_spec.hosts.len() > 1 {
352 return Err(ErrorKind::InvalidArgument {
353 msg: "couchbase2 scheme can only be used with a single host".to_string(),
354 arg: "scheme".to_string(),
355 }
356 .into());
357 }
358
359 let host = if conn_spec.hosts.is_empty() {
360 Address {
361 host: "127.0.0.1".to_string(),
362 port: DEFAULT_COUCHBASE2_PORT,
363 }
364 } else {
365 let address = conn_spec.hosts[0].clone();
366 if let Some(port) = &address.port {
367 Address {
368 host: address.host,
369 port: *port,
370 }
371 } else {
372 Address {
373 host: address.host,
374 port: DEFAULT_COUCHBASE2_PORT,
375 }
376 }
377 };
378
379 Ok(ResolvedConnSpec {
380 use_ssl: true,
381 memd_hosts: vec![],
382 http_hosts: vec![],
383 couchbase2_host: Some(host),
384 srv_record: None,
385 options: conn_spec.options,
386 })
387}
388
389async fn lookup_srv(
390 scheme: &str,
391 proto: &str,
392 host: &str,
393 dns_config: Option<DnsConfig>,
394) -> error::Result<Vec<Address>> {
395 let (resolver_config, resolver_opts) = match dns_config {
396 Some(dns) => {
397 let mut group = NameServerConfigGroup::with_capacity(2);
398 let udp = NameServerConfig::new(dns.namespace, Protocol::Udp);
399 let tcp = NameServerConfig::new(dns.namespace, Protocol::Tcp);
400 group.push(udp);
401 group.push(tcp);
402
403 let config = ResolverConfig::from_parts(None, vec![], group);
404
405 let mut opts = ResolverOpts::default();
406 if let Some(timeout) = dns.timeout {
407 opts.timeout = timeout;
408 }
409
410 (config, opts)
411 }
412 None => read_system_conf().map_err(ErrorKind::Resolve)?,
413 };
414
415 let resolver =
416 TokioResolver::builder_with_config(resolver_config, TokioConnectionProvider::default())
417 .with_options(resolver_opts)
418 .build();
419
420 let name = format!("_{scheme}._{proto}.{host}");
421 let response = resolver.srv_lookup(name).await?;
422
423 let mut addresses = vec![];
424 for addr in response.iter() {
425 addresses.push(Address {
426 host: addr.target().to_string(),
427 port: addr.port(),
428 });
429 }
430
431 Ok(addresses)
432}
433
434fn host_is_ip_address(host: &str) -> bool {
435 host.starts_with('[') || host.parse::<std::net::IpAddr>().is_ok()
436}
437
438#[cfg(test)]
439mod test {
440 use crate::{
441 parse, resolve, Address, ConnSpec, ConnSpecAddress, ResolvedConnSpec,
442 DEFAULT_COUCHBASE2_PORT, DEFAULT_MEMD_PORT, DEFAULT_SSL_MEMD_PORT,
443 };
444 use std::collections::HashMap;
445
446 fn parse_or_die(conn_str: &str) -> ConnSpec {
447 parse(conn_str).unwrap_or_else(|e| panic!("Failed to parse {conn_str}: {e:?}"))
448 }
449
450 async fn resolve_or_die(conn_spec: ConnSpec) -> ResolvedConnSpec {
451 resolve(conn_spec.clone(), None)
452 .await
453 .unwrap_or_else(|e| panic!("Failed to resolve {conn_spec:?}: {e:?}"))
454 }
455
456 fn check_address_parsing(
457 conn_str: &str,
458 cs: &ConnSpec,
459 expected_spec: &ConnSpec,
460 check_str: bool,
461 ) {
462 if check_str && cs.to_string() != conn_str {
463 panic!("ConnStr round-trip should match. {cs} != {conn_str}");
464 }
465
466 assert_eq!(cs.scheme, expected_spec.scheme, "Parsed incorrect scheme");
467 assert_eq!(
468 cs.hosts.len(),
469 expected_spec.hosts.len(),
470 "Some addresses were not parsed"
471 );
472
473 for (cs_addr, expected_addr) in cs.hosts.iter().zip(expected_spec.hosts.iter()) {
474 assert_eq!(cs_addr.host, expected_addr.host, "Parsed incorrect host");
475 assert_eq!(cs_addr.port, expected_addr.port, "Parsed incorrect port");
476 }
477 }
478
479 fn check_option_parsing(cs: &ConnSpec, expected_spec: &ConnSpec) {
480 assert_eq!(
481 cs.options.len(),
482 expected_spec.options.len(),
483 "Some options were not parsed"
484 );
485
486 for (key, opts) in &cs.options {
487 let expected_opts = expected_spec
488 .options
489 .get(key)
490 .expect("Missing expected option");
491 assert_eq!(
492 opts.len(),
493 expected_opts.len(),
494 "Some option values were not parsed"
495 );
496
497 for (opt, expected_opt) in opts.iter().zip(expected_opts.iter()) {
498 assert_eq!(opt, expected_opt, "Parsed incorrect option value");
499 }
500 }
501 }
502
503 async fn check_default_spec(
504 conn_str: &str,
505 expected_spec: ConnSpec,
506 expect_memd_hosts: Vec<Address>,
507 use_ssl: bool,
508 check_hosts: bool,
509 check_str: bool,
510 ) {
511 let cs = parse_or_die(conn_str);
512
513 check_address_parsing(conn_str, &cs, &expected_spec, check_str);
514 check_option_parsing(&cs, &expected_spec);
515
516 let rcs = resolve_or_die(cs).await;
517
518 assert_eq!(rcs.use_ssl, use_ssl, "Did not correctly mark SSL");
519
520 if check_hosts {
521 assert_eq!(
522 rcs.memd_hosts.len(),
523 expect_memd_hosts.len(),
524 "Some memd hosts were missing"
525 );
526 for (host, expect_host) in rcs.memd_hosts.iter().zip(expect_memd_hosts.iter()) {
527 assert_eq!(host.host, expect_host.host, "Resolved incorrect memd host");
528 assert_eq!(host.port, expect_host.port, "Resolved incorrect memd port");
529 }
530 }
531 }
532
533 async fn check_couchbase2_server_spec(
534 conn_str: &str,
535 expected_spec: ConnSpec,
536 expect_address: Address,
537 ) {
538 let cs = parse_or_die(conn_str);
539
540 check_address_parsing(conn_str, &cs, &expected_spec, true);
541 check_option_parsing(&cs, &expected_spec);
542
543 let rcs = resolve_or_die(cs).await;
544
545 assert!(rcs.couchbase2_host.is_some(), "Couchbase2 host was missing");
546 let couchbase2_host = rcs.couchbase2_host.unwrap();
547 assert_eq!(
548 couchbase2_host.host, expect_address.host,
549 "Resolved incorrect couchbase2 host"
550 );
551 assert_eq!(
552 couchbase2_host.port, expect_address.port,
553 "Resolved incorrect couchbase2 port"
554 );
555 }
556
557 #[tokio::test]
558 async fn test_parse_basic() {
559 check_default_spec(
560 "couchbase://1.2.3.4",
561 ConnSpec {
562 scheme: Some("couchbase".to_string()),
563 hosts: vec![ConnSpecAddress {
564 host: "1.2.3.4".to_string(),
565 port: None,
566 }],
567 ..Default::default()
568 },
569 vec![Address {
570 host: "1.2.3.4".to_string(),
571 port: DEFAULT_MEMD_PORT,
572 }],
573 false,
574 true,
575 true,
576 )
577 .await;
578
579 check_default_spec(
580 "couchbase://[2001:4860:4860::8888]",
581 ConnSpec {
582 scheme: Some("couchbase".to_string()),
583 hosts: vec![ConnSpecAddress {
584 host: "[2001:4860:4860::8888]".to_string(),
585 port: None,
586 }],
587 ..Default::default()
588 },
589 vec![Address {
590 host: "[2001:4860:4860::8888]".to_string(),
591 port: DEFAULT_MEMD_PORT,
592 }],
593 false,
594 true,
595 true,
596 )
597 .await;
598
599 check_default_spec(
600 "couchbase://",
601 ConnSpec {
602 scheme: Some("couchbase".to_string()),
603 ..Default::default()
604 },
605 vec![Address {
606 host: "127.0.0.1".to_string(),
607 port: DEFAULT_MEMD_PORT,
608 }],
609 false,
610 true,
611 true,
612 )
613 .await;
614
615 check_default_spec(
616 "couchbase://?",
617 ConnSpec {
618 scheme: Some("couchbase".to_string()),
619 ..Default::default()
620 },
621 vec![Address {
622 host: "127.0.0.1".to_string(),
623 port: DEFAULT_MEMD_PORT,
624 }],
625 false,
626 true,
627 false,
628 )
629 .await;
630
631 check_default_spec(
632 "1.2.3.4",
633 ConnSpec {
634 hosts: vec![ConnSpecAddress {
635 host: "1.2.3.4".to_string(),
636 port: None,
637 }],
638 ..Default::default()
639 },
640 vec![Address {
641 host: "1.2.3.4".to_string(),
642 port: DEFAULT_MEMD_PORT,
643 }],
644 false,
645 true,
646 true,
647 )
648 .await;
649
650 check_default_spec(
651 "[2001:4860:4860::8888]",
652 ConnSpec {
653 hosts: vec![ConnSpecAddress {
654 host: "[2001:4860:4860::8888]".to_string(),
655 port: None,
656 }],
657 ..Default::default()
658 },
659 vec![Address {
660 host: "[2001:4860:4860::8888]".to_string(),
661 port: DEFAULT_MEMD_PORT,
662 }],
663 false,
664 true,
665 true,
666 )
667 .await;
668
669 let cs = parse_or_die("1.2.3.4:8091");
670 assert!(
671 resolve(cs, None).await.is_err(),
672 "Expected error with http port"
673 );
674
675 let cs = parse_or_die("1.2.3.4:999");
676 assert!(
677 resolve(cs, None).await.is_err(),
678 "Expected error with non-default port without scheme"
679 );
680 }
681
682 #[tokio::test]
683 async fn test_parse_hosts() {
684 check_default_spec(
685 "couchbase://foo.com,bar.com,baz.com",
686 ConnSpec {
687 scheme: Some("couchbase".to_string()),
688 hosts: vec![
689 ConnSpecAddress {
690 host: "foo.com".to_string(),
691 port: None,
692 },
693 ConnSpecAddress {
694 host: "bar.com".to_string(),
695 port: None,
696 },
697 ConnSpecAddress {
698 host: "baz.com".to_string(),
699 port: None,
700 },
701 ],
702 ..Default::default()
703 },
704 vec![
705 Address {
706 host: "foo.com".to_string(),
707 port: DEFAULT_MEMD_PORT,
708 },
709 Address {
710 host: "bar.com".to_string(),
711 port: DEFAULT_MEMD_PORT,
712 },
713 Address {
714 host: "baz.com".to_string(),
715 port: DEFAULT_MEMD_PORT,
716 },
717 ],
718 false,
719 true,
720 true,
721 )
722 .await;
723
724 check_default_spec(
725 "couchbase://[2001:4860:4860::8822],[2001:4860:4860::8833]:888",
726 ConnSpec {
727 scheme: Some("couchbase".to_string()),
728 hosts: vec![
729 ConnSpecAddress {
730 host: "[2001:4860:4860::8822]".to_string(),
731 port: None,
732 },
733 ConnSpecAddress {
734 host: "[2001:4860:4860::8833]".to_string(),
735 port: Some(888),
736 },
737 ],
738 ..Default::default()
739 },
740 vec![
741 Address {
742 host: "[2001:4860:4860::8822]".to_string(),
743 port: DEFAULT_MEMD_PORT,
744 },
745 Address {
746 host: "[2001:4860:4860::8833]".to_string(),
747 port: 888,
748 },
749 ],
750 false,
751 true,
752 true,
753 )
754 .await;
755
756 let cs = parse_or_die("couchbase://foo.com:8091");
757 assert!(
758 resolve(cs, None).await.is_err(),
759 "Expected error for couchbase://XXX:8091"
760 );
761
762 check_default_spec(
763 "couchbase://foo.com:4444",
764 ConnSpec {
765 scheme: Some("couchbase".to_string()),
766 hosts: vec![ConnSpecAddress {
767 host: "foo.com".to_string(),
768 port: Some(4444),
769 }],
770 ..Default::default()
771 },
772 vec![Address {
773 host: "foo.com".to_string(),
774 port: 4444,
775 }],
776 false,
777 true,
778 true,
779 )
780 .await;
781
782 check_default_spec(
783 "couchbases://foo.com:4444",
784 ConnSpec {
785 scheme: Some("couchbases".to_string()),
786 hosts: vec![ConnSpecAddress {
787 host: "foo.com".to_string(),
788 port: Some(4444),
789 }],
790 ..Default::default()
791 },
792 vec![Address {
793 host: "foo.com".to_string(),
794 port: 4444,
795 }],
796 true,
797 true,
798 true,
799 )
800 .await;
801
802 check_default_spec(
803 "couchbases://",
804 ConnSpec {
805 scheme: Some("couchbases".to_string()),
806 ..Default::default()
807 },
808 vec![Address {
809 host: "127.0.0.1".to_string(),
810 port: DEFAULT_SSL_MEMD_PORT,
811 }],
812 true,
813 true,
814 true,
815 )
816 .await;
817
818 check_default_spec(
819 "couchbase://foo.com,bar.com:4444",
820 ConnSpec {
821 scheme: Some("couchbase".to_string()),
822 hosts: vec![
823 ConnSpecAddress {
824 host: "foo.com".to_string(),
825 port: None,
826 },
827 ConnSpecAddress {
828 host: "bar.com".to_string(),
829 port: Some(4444),
830 },
831 ],
832 ..Default::default()
833 },
834 vec![
835 Address {
836 host: "foo.com".to_string(),
837 port: DEFAULT_MEMD_PORT,
838 },
839 Address {
840 host: "bar.com".to_string(),
841 port: 4444,
842 },
843 ],
844 false,
845 true,
846 true,
847 )
848 .await;
849
850 check_default_spec(
851 "couchbase://foo.com;bar.com;baz.com",
852 ConnSpec {
853 scheme: Some("couchbase".to_string()),
854 hosts: vec![
855 ConnSpecAddress {
856 host: "foo.com".to_string(),
857 port: None,
858 },
859 ConnSpecAddress {
860 host: "bar.com".to_string(),
861 port: None,
862 },
863 ConnSpecAddress {
864 host: "baz.com".to_string(),
865 port: None,
866 },
867 ],
868 ..Default::default()
869 },
870 vec![
871 Address {
872 host: "foo.com".to_string(),
873 port: DEFAULT_MEMD_PORT,
874 },
875 Address {
876 host: "bar.com".to_string(),
877 port: DEFAULT_MEMD_PORT,
878 },
879 Address {
880 host: "baz.com".to_string(),
881 port: DEFAULT_MEMD_PORT,
882 },
883 ],
884 false,
885 true,
886 false,
887 )
888 .await;
889 }
890
891 #[tokio::test]
892 async fn test_options_passthrough() {
893 check_default_spec(
894 "couchbase:///?foo=bar",
895 ConnSpec {
896 scheme: Some("couchbase".to_string()),
897 options: {
898 let mut map = HashMap::new();
899 map.insert("foo".to_string(), vec!["bar".to_string()]);
900 map
901 },
902 ..Default::default()
903 },
904 vec![],
905 false,
906 false,
907 false,
908 )
909 .await;
910
911 check_default_spec(
912 "couchbase://?foo=bar",
913 ConnSpec {
914 scheme: Some("couchbase".to_string()),
915 options: {
916 let mut map = HashMap::new();
917 map.insert("foo".to_string(), vec!["bar".to_string()]);
918 map
919 },
920 ..Default::default()
921 },
922 vec![],
923 false,
924 false,
925 true,
926 )
927 .await;
928
929 check_default_spec(
930 "couchbase://?foo=fooval&bar=barval",
931 ConnSpec {
932 scheme: Some("couchbase".to_string()),
933 options: {
934 let mut map = HashMap::new();
935 map.insert("foo".to_string(), vec!["fooval".to_string()]);
936 map.insert("bar".to_string(), vec!["barval".to_string()]);
937 map
938 },
939 ..Default::default()
940 },
941 vec![],
942 false,
943 false,
944 false,
945 )
946 .await;
947
948 check_default_spec(
949 "couchbase://?foo=fooval&bar=barval&",
950 ConnSpec {
951 scheme: Some("couchbase".to_string()),
952 options: {
953 let mut map = HashMap::new();
954 map.insert("foo".to_string(), vec!["fooval".to_string()]);
955 map.insert("bar".to_string(), vec!["barval".to_string()]);
956 map
957 },
958 ..Default::default()
959 },
960 vec![],
961 false,
962 false,
963 false,
964 )
965 .await;
966
967 check_default_spec(
968 "couchbase://?foo=val1&foo=val2&",
969 ConnSpec {
970 scheme: Some("couchbase".to_string()),
971 options: {
972 let mut map = HashMap::new();
973 map.insert(
974 "foo".to_string(),
975 vec!["val1".to_string(), "val2".to_string()],
976 );
977 map
978 },
979 ..Default::default()
980 },
981 vec![],
982 false,
983 false,
984 false,
985 )
986 .await;
987 }
988
989 #[tokio::test]
990 async fn test_parse_couchbase2() {
991 check_couchbase2_server_spec(
992 "couchbase2://1.2.3.4",
993 ConnSpec {
994 scheme: Some("couchbase2".to_string()),
995 hosts: vec![ConnSpecAddress {
996 host: "1.2.3.4".to_string(),
997 port: None,
998 }],
999 ..Default::default()
1000 },
1001 Address {
1002 host: "1.2.3.4".to_string(),
1003 port: DEFAULT_COUCHBASE2_PORT,
1004 },
1005 )
1006 .await;
1007
1008 check_couchbase2_server_spec(
1009 "couchbase2://",
1010 ConnSpec {
1011 scheme: Some("couchbase2".to_string()),
1012 ..Default::default()
1013 },
1014 Address {
1015 host: "127.0.0.1".to_string(),
1016 port: DEFAULT_COUCHBASE2_PORT,
1017 },
1018 )
1019 .await;
1020
1021 check_couchbase2_server_spec(
1022 "couchbase2://1.2.3.4:1234",
1023 ConnSpec {
1024 scheme: Some("couchbase2".to_string()),
1025 hosts: vec![ConnSpecAddress {
1026 host: "1.2.3.4".to_string(),
1027 port: Some(1234),
1028 }],
1029 ..Default::default()
1030 },
1031 Address {
1032 host: "1.2.3.4".to_string(),
1033 port: 1234,
1034 },
1035 )
1036 .await;
1037
1038 check_couchbase2_server_spec(
1039 "couchbase2://1.2.3.4:18098",
1040 ConnSpec {
1041 scheme: Some("couchbase2".to_string()),
1042 hosts: vec![ConnSpecAddress {
1043 host: "1.2.3.4".to_string(),
1044 port: Some(18098),
1045 }],
1046 ..Default::default()
1047 },
1048 Address {
1049 host: "1.2.3.4".to_string(),
1050 port: DEFAULT_COUCHBASE2_PORT,
1051 },
1052 )
1053 .await;
1054 }
1055}