use super::{DeserializeSnafu, Error};
use serde::de::DeserializeOwned;
use snafu::ResultExt;
use url::Url;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ParsedUri<'a, Q> {
pub origin_uri: &'a str,
pub schema: &'a str,
pub username: Option<&'a str>,
pub password: Option<&'a str>,
pub hosts: Vec<Host<'a>>,
pub path: Option<&'a str>,
pub raw_query: Option<&'a str>,
pub query: Q,
}
impl<'a, Q> ParsedUri<'a, Q> {
pub fn host_strings(&self) -> Vec<String> {
self.hosts.iter().map(Host::to_authority).collect()
}
pub fn endpoint(&self) -> String {
match self.hosts.first() {
Some(host) => format!("{}://{}", self.schema, host.to_authority()),
None => String::new(),
}
}
pub fn url(&self) -> Result<Url> {
let host = self.hosts.first().ok_or(Error::Invalid {
message: "uri has no host".to_string(),
})?;
let mut buf = String::with_capacity(self.origin_uri.len());
buf.push_str(self.schema);
buf.push_str("://");
if let Some(user) = self.username {
buf.push_str(user);
if let Some(pass) = self.password {
buf.push(':');
buf.push_str(pass);
}
buf.push('@');
}
buf.push_str(&host.to_authority());
if let Some(path) = self.path {
buf.push('/');
buf.push_str(path);
}
if let Some(query) = self.raw_query {
buf.push('?');
buf.push_str(query);
}
Url::parse(&buf).map_err(|e| Error::Invalid {
message: e.to_string(),
})
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Host<'a> {
pub name: &'a str,
pub port: Option<u16>,
}
impl Host<'_> {
pub fn to_authority(&self) -> String {
let is_ipv6 = self.name.contains(':');
match (is_ipv6, self.port) {
(true, Some(port)) => format!("[{}]:{}", self.name, port),
(true, None) => format!("[{}]", self.name),
(false, Some(port)) => format!("{}:{}", self.name, port),
(false, None) => self.name.to_string(),
}
}
}
fn parse_port(value: &str) -> Result<u16> {
value.parse::<u16>().map_err(|e| Error::Invalid {
message: format!("invalid port {value:?}: {e}"),
})
}
fn parse_host(part: &str) -> Result<Host<'_>> {
if let Some(rest) = part.strip_prefix('[') {
let (addr, tail) = rest.split_once(']').ok_or(Error::Invalid {
message: format!("unclosed IPv6 bracket in host {part:?}"),
})?;
let port = match tail {
"" => None,
_ => {
let digits = tail.strip_prefix(':').ok_or(Error::Invalid {
message: format!("unexpected text after IPv6 bracket in host {part:?}"),
})?;
Some(parse_port(digits)?)
}
};
return Ok(Host { name: addr, port });
}
if part.matches(':').count() > 1 {
return Err(Error::Invalid {
message: format!("IPv6 address must be bracketed: [{part}]"),
});
}
match part.rsplit_once(':') {
Some((name, port_str)) => Ok(Host {
name,
port: Some(parse_port(port_str)?),
}),
None => Ok(Host {
name: part,
port: None,
}),
}
}
pub fn parse_uri<'a, Q>(uri: &'a str) -> Result<ParsedUri<'a, Q>>
where
Q: DeserializeOwned,
{
let (schema, rest) = uri.split_once("://").ok_or(Error::Invalid {
message: "Missing scheme".to_string(),
})?;
let (main, query_str) = rest.split_once('?').unwrap_or((rest, ""));
let (authority, path) = main.split_once('/').unwrap_or((main, ""));
let path = if path.is_empty() { None } else { Some(path) };
let (user_info, hosts_str) = authority.rsplit_once('@').unwrap_or(("", authority));
let (username, password) = if user_info.is_empty() {
(None, None)
} else {
let (user, pass) = user_info.split_once(':').unwrap_or((user_info, ""));
(Some(user), if pass.is_empty() { None } else { Some(pass) })
};
if hosts_str.is_empty() {
return Err(Error::Invalid {
message: "Missing hosts".to_string(),
});
}
let hosts: Result<Vec<Host>> = hosts_str
.split(',')
.filter(|s| !s.is_empty())
.map(parse_host)
.collect();
let query: Q = serde_urlencoded::from_str(query_str).context(DeserializeSnafu)?;
let raw_query = if query_str.is_empty() {
None
} else {
Some(query_str)
};
let hosts = hosts?;
if hosts.is_empty() {
return Err(Error::Invalid {
message: "Missing hosts".to_string(),
});
}
Ok(ParsedUri {
origin_uri: uri,
schema,
username,
password,
raw_query,
hosts,
path,
query,
})
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize, Debug, PartialEq)]
struct TestQuery {
#[serde(rename = "replicaSet")]
replica_set: Option<String>,
#[serde(default)]
timeout: u32,
}
#[test]
fn test_deserialize_to_struct() {
let uri = "mongodb://user@node1:27017/db?replicaSet=rs0&timeout=5";
let parsed = parse_uri::<TestQuery>(uri).unwrap();
assert_eq!(parsed.schema, "mongodb");
assert_eq!(parsed.username, Some("user"));
assert_eq!(
parsed.hosts,
vec![Host {
name: "node1",
port: Some(27017)
}]
);
assert_eq!(parsed.path, Some("db"));
assert_eq!(parsed.query.replica_set, Some("rs0".to_string()));
assert_eq!(parsed.query.timeout, 5);
}
#[test]
fn test_deserialize_to_hashmap() {
let uri = "kafka://broker:9092?client.id=app-1&retries=3";
let parsed = parse_uri::<HashMap<String, String>>(uri).unwrap();
assert_eq!(parsed.query.get("client.id"), Some(&"app-1".to_string()));
assert_eq!(parsed.query.get("retries"), Some(&"3".to_string()));
}
#[test]
fn test_deserialization_error() {
let uri = "schema://host?timeout=five";
let err = parse_uri::<TestQuery>(uri).unwrap_err();
assert_eq!(err.to_string(), "invalid digit found in string");
}
#[test]
fn ipv6_literal_with_and_without_port() {
let parsed = parse_uri::<HashMap<String, String>>("redis://[::1]:6379").unwrap();
assert_eq!(
parsed.hosts,
vec![Host {
name: "::1", port: Some(6379),
}]
);
assert_eq!(parsed.host_strings(), vec!["[::1]:6379".to_string()]);
assert_eq!(parsed.endpoint(), "redis://[::1]:6379");
let parsed = parse_uri::<HashMap<String, String>>("redis://[fe80::1ff:fe23:4567:890a]")
.expect("无端口的 IPv6 也必须能解析");
assert_eq!(
parsed.hosts,
vec![Host {
name: "fe80::1ff:fe23:4567:890a",
port: None,
}]
);
assert_eq!(
parsed.host_strings(),
vec!["[fe80::1ff:fe23:4567:890a]".to_string()]
);
}
#[test]
fn ipv6_with_userinfo_path_and_query() {
let parsed = parse_uri::<HashMap<String, String>>(
"postgres://user:pw@[::1]:5432/mydb?sslmode=require",
)
.unwrap();
assert_eq!(parsed.username, Some("user"));
assert_eq!(parsed.password, Some("pw"));
assert_eq!(
parsed.hosts,
vec![Host {
name: "::1",
port: Some(5432)
}]
);
assert_eq!(parsed.path, Some("mydb"));
assert_eq!(
parsed.url().unwrap().as_str(),
"postgres://user:pw@[::1]:5432/mydb?sslmode=require"
);
}
#[test]
fn mixed_ipv4_and_ipv6_hosts() {
let parsed =
parse_uri::<HashMap<String, String>>("redis://10.0.0.1:6379,[::1]:6380,node3").unwrap();
assert_eq!(
parsed.host_strings(),
vec![
"10.0.0.1:6379".to_string(),
"[::1]:6380".to_string(),
"node3".to_string(),
]
);
}
#[test]
fn unbracketed_ipv6_is_rejected_not_silently_misparsed() {
let err = parse_uri::<HashMap<String, String>>("redis://::1:6379").unwrap_err();
assert!(
err.to_string().contains("must be bracketed"),
"错误信息应提示补方括号,实际: {err}"
);
assert!(parse_uri::<HashMap<String, String>>("redis://[::1:6379").is_err());
}
#[test]
fn url_rebuild_does_not_corrupt_query_containing_host_text() {
let uri = "postgres://h1:5432,h2:5432/mydb?fallback=h1:5432,h2:5432";
let parsed = parse_uri::<HashMap<String, String>>(uri).unwrap();
let url = parsed.url().unwrap();
assert_eq!(url.host_str(), Some("h1"));
assert_eq!(url.port(), Some(5432));
assert_eq!(url.path(), "/mydb");
assert_eq!(url.query(), Some("fallback=h1:5432,h2:5432"));
}
#[test]
fn url_single_host_round_trips() {
let parsed =
parse_uri::<HashMap<String, String>>("postgres://u:p@db.internal:5432/app").unwrap();
assert_eq!(
parsed.url().unwrap().as_str(),
"postgres://u:p@db.internal:5432/app"
);
}
#[test]
fn unescaped_at_in_password_splits_on_last_separator() {
let parsed = parse_uri::<HashMap<String, String>>("redis://user:p@ss@host:6379").unwrap();
assert_eq!(parsed.username, Some("user"));
assert_eq!(parsed.password, Some("p@ss"));
assert_eq!(parsed.host_strings(), vec!["host:6379".to_string()]);
assert_eq!(parsed.endpoint(), "redis://host:6379");
let parsed = parse_uri::<HashMap<String, String>>("redis://:p@ss@host:6379").unwrap();
assert_eq!(parsed.username, Some(""));
assert_eq!(parsed.password, Some("p@ss"));
assert_eq!(parsed.host_strings(), vec!["host:6379".to_string()]);
let parsed =
parse_uri::<HashMap<String, String>>("postgres://u:pa@ss@db.internal:5432/app")
.unwrap();
assert_eq!(
parsed.hosts,
vec![Host {
name: "db.internal",
port: Some(5432)
}]
);
let url = parsed.url().unwrap();
assert_eq!(url.host_str(), Some("db.internal"));
assert_eq!(url.port(), Some(5432));
}
#[test]
fn no_userinfo_is_unaffected() {
let parsed = parse_uri::<HashMap<String, String>>("redis://host:6379").unwrap();
assert_eq!(parsed.username, None);
assert_eq!(parsed.password, None);
assert_eq!(parsed.host_strings(), vec!["host:6379".to_string()]);
}
#[test]
fn authority_with_only_separators_is_rejected() {
for uri in ["redis://,", "redis://,,", "redis://,,,/db"] {
let err = parse_uri::<HashMap<String, String>>(uri)
.expect_err("{uri} 应当被拒绝,而不是解析出零个 host");
assert!(
err.to_string().contains("Missing hosts"),
"错误信息应指出缺少 host,实际: {err}"
);
}
}
}