use alloc::string::String;
use alloc::string::ToString;
use alloc::vec::Vec;
use crate::error::Result;
use crate::util::{host, percent};
use crate::{HttpUrl, HttpUrlError, Scheme};
#[derive(Debug, Clone)]
pub struct HttpUrlBuilder {
pub(crate) scheme: Option<Scheme>,
pub(crate) username: String,
pub(crate) password: String,
pub(crate) host: Option<String>,
pub(crate) port: Option<u16>,
pub(crate) path_segments: Vec<String>,
pub(crate) query_names_and_values: Vec<String>,
pub(crate) 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_names_and_values: Vec::new(),
fragment: None,
}
}
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 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();
let path = path.trim_start_matches('/');
for seg in path.split('/') {
if !seg.is_empty() {
self.path_segments
.push(percent::decode(seg).unwrap_or_else(|_| seg.to_string()));
}
}
self
}
pub fn set_encoded_path(mut self, path: &str) -> Self {
self.path_segments.clear();
let path = path.trim_start_matches('/');
for seg in path.split('/') {
if !seg.is_empty() {
let decoded = percent::decode(seg).unwrap_or_else(|_| seg.to_string());
self.path_segments.push(decoded);
}
}
self
}
pub fn add_query_parameter(mut self, name: &str, value: &str) -> Self {
self.query_names_and_values.push(name.to_string());
self.query_names_and_values.push(value.to_string());
self
}
pub fn remove_query_parameter(mut self, name: &str) -> Self {
let mut i = 0;
while i < self.query_names_and_values.len() {
if self.query_names_and_values[i] == name {
self.query_names_and_values.remove(i);
if i < self.query_names_and_values.len() {
self.query_names_and_values.remove(i);
}
} else {
i += 2;
}
}
self
}
pub fn clear_query_parameters(mut self) -> Self {
self.query_names_and_values.clear();
self
}
pub fn add_encoded_query_parameter(mut self, name: &str, value: &str) -> Self {
let decoded_name = percent::decode(name).unwrap_or_else(|_| name.to_string());
let decoded_value = percent::decode(value).unwrap_or_else(|_| value.to_string());
self.query_names_and_values.push(decoded_name);
self.query_names_and_values.push(decoded_value);
self
}
pub fn fragment(mut self, fragment: &str) -> Self {
self.fragment = Some(fragment.to_string());
self
}
pub fn encoded_fragment(mut self, fragment: &str) -> Self {
self.fragment = percent::decode(fragment).ok();
self
}
pub fn remove_fragment(mut self) -> Self {
self.fragment = None;
self
}
pub fn build(self) -> Result<HttpUrl> {
let scheme = self
.scheme
.ok_or_else(|| HttpUrlError::BuilderValidation("scheme is required".to_string()))?;
let host_str = self
.host
.ok_or_else(|| HttpUrlError::BuilderValidation("host is required".to_string()))?;
let host = host::canonicalize_host(&host_str)?;
let port = self.port.unwrap_or_else(|| scheme.default_port());
Ok(HttpUrl {
scheme,
username: self.username,
password: self.password,
host,
port,
path_segments: self.path_segments,
query_names_and_values: self.query_names_and_values,
fragment: self.fragment,
})
}
}
impl Default for HttpUrlBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_basic() {
let url = HttpUrl::builder()
.scheme(Scheme::Https)
.host("example.com")
.build()
.unwrap();
assert_eq!(url.to_string(), "https://example.com/");
}
#[test]
fn test_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 test_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.path(), "/a/b/c");
assert_eq!(url.encoded_path(), "/a/b/c");
}
#[test]
fn test_builder_path_encoding() {
let url = HttpUrl::builder()
.scheme(Scheme::Http)
.host("example.com")
.add_path_segment("hello world")
.build()
.unwrap();
assert_eq!(url.encoded_path(), "/hello%20world");
assert_eq!(url.path(), "/hello world");
}
#[test]
fn test_builder_missing_scheme() {
let result = HttpUrl::builder().host("example.com").build();
assert!(result.is_err());
}
#[test]
fn test_builder_missing_host() {
let result = HttpUrl::builder().scheme(Scheme::Http).build();
assert!(result.is_err());
}
#[test]
fn test_unicode_in_builder() {
let url = HttpUrl::builder()
.scheme(Scheme::Http)
.host("example.com")
.add_path_segment("\u{4e2d}\u{6587}")
.build()
.unwrap();
assert_eq!(url.path(), "/\u{4e2d}\u{6587}");
}
#[test]
fn test_builder_set_path() {
let url = HttpUrl::builder()
.scheme(Scheme::Http)
.host("example.com")
.set_path("/a/b/c")
.build()
.unwrap();
assert_eq!(url.path(), "/a/b/c");
}
#[test]
fn test_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.path(), "/a/b/c");
}
#[test]
fn test_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.query(), None);
}
#[test]
fn test_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.query_parameter("a"), None);
assert_eq!(url.query_parameter("b"), Some("2"));
}
}