Skip to main content

http_url/
builder.rs

1use alloc::string::String;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4
5use crate::error::Result;
6use crate::util::{host, percent};
7use crate::{HttpUrl, HttpUrlError, Scheme};
8
9/// Builder for constructing [`HttpUrl`] values programmatically.
10///
11/// # Example
12///
13/// ```
14/// use http_url::{HttpUrl, Scheme};
15///
16/// let url = HttpUrl::builder()
17///     .scheme(Scheme::Https)
18///     .host("example.com")
19///     .add_path_segment("api")
20///     .add_path_segment("v1")
21///     .add_query_parameter("key", "value")
22///     .fragment("section")
23///     .build()
24///     .unwrap();
25///
26/// assert_eq!(url.to_string(), "https://example.com/api/v1?key=value#section");
27/// ```
28#[derive(Debug, Clone)]
29pub struct HttpUrlBuilder {
30    /// The URL scheme, or `None` if not yet set.
31    pub(crate) scheme: Option<Scheme>,
32    /// Username for basic authentication.
33    pub(crate) username: String,
34    /// Password for basic authentication.
35    pub(crate) password: String,
36    /// The host, or `None` if not yet set.
37    pub(crate) host: Option<String>,
38    /// The port, or `None` to use the scheme's default.
39    pub(crate) port: Option<u16>,
40    /// Decoded path segments.
41    pub(crate) path_segments: Vec<String>,
42    /// Flattened query names and values (name0, value0, name1, value1, …).
43    pub(crate) query_names_and_values: Vec<String>,
44    /// The fragment (decoded), or `None`.
45    pub(crate) fragment: Option<String>,
46}
47
48impl HttpUrlBuilder {
49    /// Create a new empty builder.
50    pub fn new() -> Self {
51        Self {
52            scheme: None,
53            username: String::new(),
54            password: String::new(),
55            host: None,
56            port: None,
57            path_segments: Vec::new(),
58            query_names_and_values: Vec::new(),
59            fragment: None,
60        }
61    }
62
63    /// Set the scheme.
64    pub fn scheme(mut self, scheme: Scheme) -> Self {
65        self.scheme = Some(scheme);
66        self
67    }
68
69    /// Set the username for basic auth.
70    pub fn username(mut self, username: &str) -> Self {
71        self.username = username.to_string();
72        self
73    }
74
75    /// Set the password for basic auth.
76    pub fn password(mut self, password: &str) -> Self {
77        self.password = password.to_string();
78        self
79    }
80
81    /// Set the host (domain name or IP address).
82    pub fn host(mut self, host: &str) -> Self {
83        self.host = Some(host.to_string());
84        self
85    }
86
87    /// Set the port. Use `None` to use the scheme's default.
88    pub fn port(mut self, port: u16) -> Self {
89        self.port = Some(port);
90        self
91    }
92
93    /// Add a path segment (decoded form, will be percent-encoded on build).
94    pub fn add_path_segment(mut self, segment: &str) -> Self {
95        self.path_segments.push(segment.to_string());
96        self
97    }
98
99    /// Add multiple path segments.
100    pub fn add_path_segments(mut self, segments: &[&str]) -> Self {
101        for seg in segments {
102            self.path_segments.push(seg.to_string());
103        }
104        self
105    }
106
107    /// Set the entire path from a decoded path string (e.g., `/a/b/c`).
108    /// The path is split on `/` and each segment is decoded.
109    pub fn set_path(mut self, path: &str) -> Self {
110        self.path_segments.clear();
111        let path = path.trim_start_matches('/');
112        for seg in path.split('/') {
113            if !seg.is_empty() {
114                self.path_segments
115                    .push(percent::decode(seg).unwrap_or_else(|_| seg.to_string()));
116            }
117        }
118        self
119    }
120
121    /// Set the entire path from an already-encoded path string.
122    /// Each segment is percent-decoded before storage.
123    pub fn set_encoded_path(mut self, path: &str) -> Self {
124        self.path_segments.clear();
125        let path = path.trim_start_matches('/');
126        for seg in path.split('/') {
127            if !seg.is_empty() {
128                let decoded = percent::decode(seg).unwrap_or_else(|_| seg.to_string());
129                self.path_segments.push(decoded);
130            }
131        }
132        self
133    }
134
135    /// Add a query parameter (name and value in decoded form).
136    pub fn add_query_parameter(mut self, name: &str, value: &str) -> Self {
137        self.query_names_and_values.push(name.to_string());
138        self.query_names_and_values.push(value.to_string());
139        self
140    }
141
142    /// Remove all query parameters with the given name.
143    pub fn remove_query_parameter(mut self, name: &str) -> Self {
144        let mut i = 0;
145        while i < self.query_names_and_values.len() {
146            if self.query_names_and_values[i] == name {
147                self.query_names_and_values.remove(i);
148                if i < self.query_names_and_values.len() {
149                    self.query_names_and_values.remove(i);
150                }
151            } else {
152                i += 2;
153            }
154        }
155        self
156    }
157
158    /// Clear all query parameters.
159    pub fn clear_query_parameters(mut self) -> Self {
160        self.query_names_and_values.clear();
161        self
162    }
163
164    /// Add a query parameter from already-encoded name and value.
165    pub fn add_encoded_query_parameter(mut self, name: &str, value: &str) -> Self {
166        let decoded_name = percent::decode(name).unwrap_or_else(|_| name.to_string());
167        let decoded_value = percent::decode(value).unwrap_or_else(|_| value.to_string());
168        self.query_names_and_values.push(decoded_name);
169        self.query_names_and_values.push(decoded_value);
170        self
171    }
172
173    /// Set the fragment (decoded form).
174    pub fn fragment(mut self, fragment: &str) -> Self {
175        self.fragment = Some(fragment.to_string());
176        self
177    }
178
179    /// Set the fragment from an encoded form.
180    pub fn encoded_fragment(mut self, fragment: &str) -> Self {
181        self.fragment = percent::decode(fragment).ok();
182        self
183    }
184
185    /// Remove the fragment.
186    pub fn remove_fragment(mut self) -> Self {
187        self.fragment = None;
188        self
189    }
190
191    /// Consume the builder and produce an `HttpUrl`.
192    ///
193    /// Returns an error if required components (scheme, host) are missing.
194    pub fn build(self) -> Result<HttpUrl> {
195        let scheme = self
196            .scheme
197            .ok_or_else(|| HttpUrlError::BuilderValidation("scheme is required".to_string()))?;
198
199        let host_str = self
200            .host
201            .ok_or_else(|| HttpUrlError::BuilderValidation("host is required".to_string()))?;
202
203        let host = host::canonicalize_host(&host_str)?;
204
205        let port = self.port.unwrap_or_else(|| scheme.default_port());
206
207        Ok(HttpUrl {
208            scheme,
209            username: self.username,
210            password: self.password,
211            host,
212            port,
213            path_segments: self.path_segments,
214            query_names_and_values: self.query_names_and_values,
215            fragment: self.fragment,
216        })
217    }
218}
219
220impl Default for HttpUrlBuilder {
221    fn default() -> Self {
222        Self::new()
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_builder_basic() {
232        let url = HttpUrl::builder()
233            .scheme(Scheme::Https)
234            .host("example.com")
235            .build()
236            .unwrap();
237        assert_eq!(url.to_string(), "https://example.com/");
238    }
239
240    #[test]
241    fn test_builder_full() {
242        let url = HttpUrl::builder()
243            .scheme(Scheme::Https)
244            .username("user")
245            .password("pass")
246            .host("example.com")
247            .port(8080)
248            .add_path_segment("api")
249            .add_path_segment("v1")
250            .add_query_parameter("key", "value")
251            .fragment("section")
252            .build()
253            .unwrap();
254        assert_eq!(
255            url.to_string(),
256            "https://user:pass@example.com:8080/api/v1?key=value#section"
257        );
258    }
259
260    #[test]
261    fn test_builder_path_segments() {
262        let url = HttpUrl::builder()
263            .scheme(Scheme::Http)
264            .host("example.com")
265            .add_path_segment("a")
266            .add_path_segment("b")
267            .add_path_segment("c")
268            .build()
269            .unwrap();
270        assert_eq!(url.path(), "/a/b/c");
271        assert_eq!(url.encoded_path(), "/a/b/c");
272    }
273
274    #[test]
275    fn test_builder_path_encoding() {
276        let url = HttpUrl::builder()
277            .scheme(Scheme::Http)
278            .host("example.com")
279            .add_path_segment("hello world")
280            .build()
281            .unwrap();
282        assert_eq!(url.encoded_path(), "/hello%20world");
283        assert_eq!(url.path(), "/hello world");
284    }
285
286    #[test]
287    fn test_builder_missing_scheme() {
288        let result = HttpUrl::builder().host("example.com").build();
289        assert!(result.is_err());
290    }
291
292    #[test]
293    fn test_builder_missing_host() {
294        let result = HttpUrl::builder().scheme(Scheme::Http).build();
295        assert!(result.is_err());
296    }
297
298    #[test]
299    fn test_unicode_in_builder() {
300        let url = HttpUrl::builder()
301            .scheme(Scheme::Http)
302            .host("example.com")
303            .add_path_segment("\u{4e2d}\u{6587}")
304            .build()
305            .unwrap();
306        assert_eq!(url.path(), "/\u{4e2d}\u{6587}");
307    }
308
309    #[test]
310    fn test_builder_set_path() {
311        let url = HttpUrl::builder()
312            .scheme(Scheme::Http)
313            .host("example.com")
314            .set_path("/a/b/c")
315            .build()
316            .unwrap();
317        assert_eq!(url.path(), "/a/b/c");
318    }
319
320    #[test]
321    fn test_builder_add_path_segments() {
322        let url = HttpUrl::builder()
323            .scheme(Scheme::Http)
324            .host("example.com")
325            .add_path_segments(&["a", "b", "c"])
326            .build()
327            .unwrap();
328        assert_eq!(url.path(), "/a/b/c");
329    }
330
331    #[test]
332    fn test_builder_clear_query_parameters() {
333        let url = HttpUrl::builder()
334            .scheme(Scheme::Http)
335            .host("example.com")
336            .add_query_parameter("a", "1")
337            .clear_query_parameters()
338            .build()
339            .unwrap();
340        assert_eq!(url.query(), None);
341    }
342
343    #[test]
344    fn test_builder_remove_query_parameter() {
345        let url = HttpUrl::builder()
346            .scheme(Scheme::Https)
347            .host("example.com")
348            .add_query_parameter("a", "1")
349            .add_query_parameter("b", "2")
350            .remove_query_parameter("a")
351            .build()
352            .unwrap();
353        assert_eq!(url.query_parameter("a"), None);
354        assert_eq!(url.query_parameter("b"), Some("2"));
355    }
356}