use core::str::FromStr;
use alloc::format;
use alloc::string::String;
use alloc::string::ToString;
use alloc::vec::Vec;
use url::Url;
use crate::HttpUrl;
use crate::error::{HttpUrlError, Result};
use crate::scheme::Scheme;
use crate::util::percent_decode;
#[derive(Debug, Clone)]
pub struct HttpUrlBuilder {
scheme: Option<Scheme>,
username: String,
password: String,
host: Option<String>,
port: Option<u16>,
path_segments: Vec<String>,
query_pairs: Vec<(String, String)>,
fragment: Option<String>,
}
impl HttpUrlBuilder {
pub fn new() -> Self {
Self {
scheme: None,
username: String::new(),
password: String::new(),
host: None,
port: None,
path_segments: Vec::new(),
query_pairs: Vec::new(),
fragment: None,
}
}
pub fn from_url(url: Url) -> Result<Self> {
let scheme = Scheme::from_str(url.scheme())?;
Ok(Self {
scheme: Some(scheme),
username: url.username().to_string(),
password: url.password().unwrap_or("").to_string(),
host: Some(url.host_str().unwrap_or("").to_string()),
port: url.port(),
path_segments: url
.path_segments()
.map(|segs| segs.map(percent_decode).collect::<Vec<_>>())
.unwrap_or_default(),
query_pairs: url
.query_pairs()
.map(|(k, v)| (k.into_owned(), v.into_owned()))
.collect(),
fragment: url.fragment().map(percent_decode),
})
}
pub fn parse(url: &str) -> Result<Self> {
Self::from_url(Url::parse(url)?)
}
pub fn scheme(mut self, scheme: Scheme) -> Self {
self.scheme = Some(scheme);
self
}
pub fn username(mut self, username: &str) -> Self {
self.username = username.to_string();
self
}
pub fn password(mut self, password: &str) -> Self {
self.password = password.to_string();
self
}
pub fn host(mut self, host: &str) -> Self {
self.host = Some(host.to_string());
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}
pub fn remove_port(mut self) -> Self {
self.port = None;
self
}
pub fn add_path_segment(mut self, segment: &str) -> Self {
self.path_segments.push(segment.to_string());
self
}
pub fn add_path_segments(mut self, segments: &[&str]) -> Self {
for seg in segments {
self.path_segments.push(seg.to_string());
}
self
}
pub fn set_path(mut self, path: &str) -> Self {
self.path_segments.clear();
for seg in path.trim_start_matches('/').split('/') {
if !seg.is_empty() {
self.path_segments.push(seg.to_string());
}
}
self
}
pub fn add_query_parameter(mut self, name: &str, value: &str) -> Self {
self.query_pairs.push((name.to_string(), value.to_string()));
self
}
pub fn remove_query_parameter(mut self, name: &str) -> Self {
self.query_pairs.retain(|(n, _)| n != name);
self
}
pub fn clear_query_parameters(mut self) -> Self {
self.query_pairs.clear();
self
}
pub fn fragment(mut self, fragment: &str) -> Self {
self.fragment = Some(fragment.to_string());
self
}
pub fn remove_fragment(mut self) -> Self {
self.fragment = None;
self
}
pub fn build_url(self) -> Result<Url> {
let scheme = self
.scheme
.ok_or_else(|| HttpUrlError::BuilderValidation("scheme is required".to_string()))?;
let host = self
.host
.ok_or_else(|| HttpUrlError::BuilderValidation("host is required".to_string()))?;
let mut url = Url::parse(&format!("{}://{}", scheme.as_str(), host))?;
if !self.username.is_empty() {
let _ = url.set_username(&self.username);
}
if !self.password.is_empty() {
let _ = url.set_password(Some(&self.password));
}
if let Some(port) = self.port {
let _ = url.set_port(Some(port));
}
if !self.path_segments.is_empty() {
let mut segments = url.path_segments_mut().map_err(|_| {
HttpUrlError::BuilderValidation("URL cannot have a path".to_string())
})?;
segments.clear();
for seg in &self.path_segments {
segments.push(seg);
}
}
if !self.query_pairs.is_empty() {
let mut query = url.query_pairs_mut();
for (name, value) in &self.query_pairs {
query.append_pair(name, value);
}
} else {
url.set_query(None);
}
url.set_fragment(self.fragment.as_deref());
Ok(url)
}
pub fn build(self) -> Result<HttpUrl> {
HttpUrl::from_url(self.build_url()?)
}
}
impl Default for HttpUrlBuilder {
fn default() -> Self {
Self::new()
}
}
impl FromStr for HttpUrlBuilder {
type Err = HttpUrlError;
fn from_str(s: &str) -> Result<Self> {
Self::parse(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_basic() {
let url = HttpUrl::builder()
.scheme(Scheme::Https)
.host("example.com")
.build()
.unwrap();
assert_eq!(url.to_string(), "https://example.com/");
}
#[test]
fn builder_full() {
let url = HttpUrl::builder()
.scheme(Scheme::Https)
.username("user")
.password("pass")
.host("example.com")
.port(8080)
.add_path_segment("api")
.add_path_segment("v1")
.add_query_parameter("key", "value")
.fragment("section")
.build()
.unwrap();
assert_eq!(
url.to_string(),
"https://user:pass@example.com:8080/api/v1?key=value#section"
);
}
#[test]
fn builder_path_segments() {
let url = HttpUrl::builder()
.scheme(Scheme::Http)
.host("example.com")
.add_path_segment("a")
.add_path_segment("b")
.add_path_segment("c")
.build()
.unwrap();
assert_eq!(url.as_url().path(), "/a/b/c");
}
#[test]
fn builder_path_encoding() {
let url = HttpUrl::builder()
.scheme(Scheme::Http)
.host("example.com")
.add_path_segment("hello world")
.build()
.unwrap();
assert_eq!(url.as_url().path(), "/hello%20world");
}
#[test]
fn builder_unicode_segment() {
let url = HttpUrl::builder()
.scheme(Scheme::Http)
.host("example.com")
.add_path_segment("䏿–‡")
.build()
.unwrap();
assert_eq!(url.as_url().path(), "/%E4%B8%AD%E6%96%87");
}
#[test]
fn builder_set_path() {
let url = HttpUrl::builder()
.scheme(Scheme::Http)
.host("example.com")
.set_path("/a/b/c")
.build()
.unwrap();
assert_eq!(url.as_url().path(), "/a/b/c");
}
#[test]
fn builder_add_path_segments() {
let url = HttpUrl::builder()
.scheme(Scheme::Http)
.host("example.com")
.add_path_segments(&["a", "b", "c"])
.build()
.unwrap();
assert_eq!(url.as_url().path(), "/a/b/c");
}
#[test]
fn builder_query_encoding() {
let url = HttpUrl::builder()
.scheme(Scheme::Http)
.host("example.com")
.add_query_parameter("name", "hello world")
.build()
.unwrap();
assert_eq!(url.as_url().query(), Some("name=hello+world"));
}
#[test]
fn builder_clear_query_parameters() {
let url = HttpUrl::builder()
.scheme(Scheme::Http)
.host("example.com")
.add_query_parameter("a", "1")
.clear_query_parameters()
.build()
.unwrap();
assert_eq!(url.as_url().query(), None);
}
#[test]
fn builder_remove_query_parameter() {
let url = HttpUrl::builder()
.scheme(Scheme::Https)
.host("example.com")
.add_query_parameter("a", "1")
.add_query_parameter("b", "2")
.remove_query_parameter("a")
.build()
.unwrap();
assert_eq!(url.as_url().query_pairs().find(|(k, _)| k == "a"), None);
assert_eq!(
url.as_url()
.query_pairs()
.find(|(k, _)| k == "b")
.map(|(_, v)| v.into_owned()),
Some("2".to_string())
);
}
#[test]
fn builder_remove_fragment() {
let url = HttpUrl::builder()
.scheme(Scheme::Https)
.host("example.com")
.fragment("sec")
.remove_fragment()
.build()
.unwrap();
assert_eq!(url.as_url().fragment(), None);
}
#[test]
fn builder_remove_port() {
let url = HttpUrl::builder()
.scheme(Scheme::Http)
.host("example.com")
.port(8080)
.remove_port()
.build()
.unwrap();
assert_eq!(url.as_url().port(), None); }
#[test]
fn builder_missing_scheme() {
assert!(HttpUrl::builder().host("example.com").build().is_err());
}
#[test]
fn builder_missing_host() {
assert!(HttpUrl::builder().scheme(Scheme::Http).build().is_err());
}
#[test]
fn builder_ipv6_host() {
let url = HttpUrl::builder()
.scheme(Scheme::Http)
.host("[::1]")
.port(8080)
.build()
.unwrap();
assert_eq!(url.as_url().host_str(), Some("[::1]"));
assert_eq!(url.as_url().port(), Some(8080));
}
#[test]
fn builder_idn_normalization() {
let url = HttpUrl::builder()
.scheme(Scheme::Http)
.host("Bücher.de")
.build()
.unwrap();
assert_eq!(url.as_url().host_str(), Some("xn--bcher-kva.de"));
}
#[test]
fn builder_new_builder_roundtrip() {
let url = HttpUrl::parse("https://example.com/a/b?x=1&y=2#frag").unwrap();
let url2 = url.new_builder().build().unwrap();
assert_eq!(url, url2);
}
#[test]
fn builder_new_builder_modify() {
let url = HttpUrl::parse("https://example.com/a/b?x=1").unwrap();
let url2 = url
.new_builder()
.add_path_segment("c")
.add_query_parameter("y", "2")
.build()
.unwrap();
assert_eq!(url2.as_url().path(), "/a/b/c");
assert_eq!(
url2.as_url()
.query_pairs()
.find(|(k, _)| k == "x")
.map(|(_, v)| v.into_owned()),
Some("1".to_string())
);
assert_eq!(
url2.as_url()
.query_pairs()
.find(|(k, _)| k == "y")
.map(|(_, v)| v.into_owned()),
Some("2".to_string())
);
}
#[test]
fn builder_from_url() {
let u = url::Url::parse("https://example.com/a/b?q=1#frag").unwrap();
let url = HttpUrlBuilder::from_url(u.clone())
.unwrap()
.add_path_segment("c")
.build()
.unwrap();
assert_eq!(url.as_url().path(), "/a/b/c");
assert_eq!(
url.as_url()
.query_pairs()
.find(|(k, _)| k == "q")
.map(|(_, v)| v.into_owned()),
Some("1".to_string())
);
assert_eq!(url.as_url().fragment(), Some("frag"));
}
#[test]
fn builder_from_url_rejects_non_http() {
let u = url::Url::parse("ftp://example.com").unwrap();
assert!(HttpUrlBuilder::from_url(u).is_err());
}
#[test]
fn builder_parse() {
let b: HttpUrlBuilder = "https://example.com/a/b".parse().unwrap();
let url = b.add_path_segment("c").build().unwrap();
assert_eq!(url.as_url().path(), "/a/b/c");
}
#[test]
fn builder_build_url() {
let url = HttpUrl::builder()
.scheme(Scheme::Https)
.host("example.com")
.add_path_segment("api")
.add_query_parameter("k", "v")
.fragment("sec")
.build_url()
.unwrap();
assert_eq!(url.as_str(), "https://example.com/api?k=v#sec");
}
#[test]
fn builder_build_url_missing_scheme() {
assert!(HttpUrl::builder().host("example.com").build_url().is_err());
}
#[test]
fn builder_parse_rejects_non_http() {
assert!(HttpUrlBuilder::parse("ftp://example.com").is_err());
}
}