Skip to main content

couchbase_connstr/
lib.rs

1/*
2 *
3 *  * Copyright (c) 2025 Couchbase, Inc.
4 *  *
5 *  * Licensed under the Apache License, Version 2.0 (the "License");
6 *  * you may not use this file except in compliance with the License.
7 *  * You may obtain a copy of the License at
8 *  *
9 *  *    http://www.apache.org/licenses/LICENSE-2.0
10 *  *
11 *  * Unless required by applicable law or agreed to in writing, software
12 *  * distributed under the License is distributed on an "AS IS" BASIS,
13 *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  * See the License for the specific language governing permissions and
15 *  * limitations under the License.
16 *
17 */
18
19pub 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}