1use core::str::FromStr;
2
3use alloc::format;
4use alloc::string::String;
5use alloc::string::ToString;
6use alloc::vec::Vec;
7
8use url::Url;
9
10use crate::HttpUrl;
11use crate::error::{HttpUrlError, Result};
12use crate::scheme::Scheme;
13use crate::util::percent_decode;
14
15#[derive(Debug, Clone)]
40pub struct HttpUrlBuilder {
41 scheme: Option<Scheme>,
43 username: String,
45 password: String,
47 host: Option<String>,
49 port: Option<u16>,
51 path_segments: Vec<String>,
53 query_pairs: Vec<(String, String)>,
55 fragment: Option<String>,
57}
58
59impl HttpUrlBuilder {
60 pub fn new() -> Self {
62 Self {
63 scheme: None,
64 username: String::new(),
65 password: String::new(),
66 host: None,
67 port: None,
68 path_segments: Vec::new(),
69 query_pairs: Vec::new(),
70 fragment: None,
71 }
72 }
73
74 pub fn from_url(url: Url) -> Result<Self> {
94 let scheme = Scheme::from_str(url.scheme())?;
95 Ok(Self {
96 scheme: Some(scheme),
97 username: url.username().to_string(),
98 password: url.password().unwrap_or("").to_string(),
99 host: Some(url.host_str().unwrap_or("").to_string()),
100 port: url.port(),
101 path_segments: url
102 .path_segments()
103 .map(|segs| segs.map(percent_decode).collect::<Vec<_>>())
104 .unwrap_or_default(),
105 query_pairs: url
106 .query_pairs()
107 .map(|(k, v)| (k.into_owned(), v.into_owned()))
108 .collect(),
109 fragment: url.fragment().map(percent_decode),
110 })
111 }
112
113 pub fn parse(url: &str) -> Result<Self> {
118 Self::from_url(Url::parse(url)?)
119 }
120
121 pub fn scheme(mut self, scheme: Scheme) -> Self {
123 self.scheme = Some(scheme);
124 self
125 }
126
127 pub fn username(mut self, username: &str) -> Self {
129 self.username = username.to_string();
130 self
131 }
132
133 pub fn password(mut self, password: &str) -> Self {
135 self.password = password.to_string();
136 self
137 }
138
139 pub fn host(mut self, host: &str) -> Self {
142 self.host = Some(host.to_string());
143 self
144 }
145
146 pub fn port(mut self, port: u16) -> Self {
149 self.port = Some(port);
150 self
151 }
152
153 pub fn remove_port(mut self) -> Self {
155 self.port = None;
156 self
157 }
158
159 pub fn add_path_segment(mut self, segment: &str) -> Self {
162 self.path_segments.push(segment.to_string());
163 self
164 }
165
166 pub fn add_path_segments(mut self, segments: &[&str]) -> Self {
168 for seg in segments {
169 self.path_segments.push(seg.to_string());
170 }
171 self
172 }
173
174 pub fn set_path(mut self, path: &str) -> Self {
181 self.path_segments.clear();
182 for seg in path.trim_start_matches('/').split('/') {
183 if !seg.is_empty() {
184 self.path_segments.push(seg.to_string());
185 }
186 }
187 self
188 }
189
190 pub fn add_query_parameter(mut self, name: &str, value: &str) -> Self {
194 self.query_pairs.push((name.to_string(), value.to_string()));
195 self
196 }
197
198 pub fn remove_query_parameter(mut self, name: &str) -> Self {
200 self.query_pairs.retain(|(n, _)| n != name);
201 self
202 }
203
204 pub fn clear_query_parameters(mut self) -> Self {
206 self.query_pairs.clear();
207 self
208 }
209
210 pub fn fragment(mut self, fragment: &str) -> Self {
212 self.fragment = Some(fragment.to_string());
213 self
214 }
215
216 pub fn remove_fragment(mut self) -> Self {
218 self.fragment = None;
219 self
220 }
221
222 pub fn build_url(self) -> Result<Url> {
231 let scheme = self
232 .scheme
233 .ok_or_else(|| HttpUrlError::BuilderValidation("scheme is required".to_string()))?;
234 let host = self
235 .host
236 .ok_or_else(|| HttpUrlError::BuilderValidation("host is required".to_string()))?;
237
238 let mut url = Url::parse(&format!("{}://{}", scheme.as_str(), host))?;
241
242 if !self.username.is_empty() {
244 let _ = url.set_username(&self.username);
245 }
246 if !self.password.is_empty() {
247 let _ = url.set_password(Some(&self.password));
248 }
249 if let Some(port) = self.port {
250 let _ = url.set_port(Some(port));
251 }
252
253 if !self.path_segments.is_empty() {
257 let mut segments = url.path_segments_mut().map_err(|_| {
258 HttpUrlError::BuilderValidation("URL cannot have a path".to_string())
259 })?;
260 segments.clear();
261 for seg in &self.path_segments {
262 segments.push(seg);
263 }
264 }
265
266 if !self.query_pairs.is_empty() {
270 let mut query = url.query_pairs_mut();
271 for (name, value) in &self.query_pairs {
272 query.append_pair(name, value);
273 }
274 } else {
275 url.set_query(None);
276 }
277
278 url.set_fragment(self.fragment.as_deref());
279
280 Ok(url)
281 }
282
283 pub fn build(self) -> Result<HttpUrl> {
288 HttpUrl::from_url(self.build_url()?)
289 }
290}
291
292impl Default for HttpUrlBuilder {
293 fn default() -> Self {
294 Self::new()
295 }
296}
297
298impl FromStr for HttpUrlBuilder {
299 type Err = HttpUrlError;
300
301 fn from_str(s: &str) -> Result<Self> {
302 Self::parse(s)
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn builder_basic() {
312 let url = HttpUrl::builder()
313 .scheme(Scheme::Https)
314 .host("example.com")
315 .build()
316 .unwrap();
317 assert_eq!(url.to_string(), "https://example.com/");
318 }
319
320 #[test]
321 fn builder_full() {
322 let url = HttpUrl::builder()
323 .scheme(Scheme::Https)
324 .username("user")
325 .password("pass")
326 .host("example.com")
327 .port(8080)
328 .add_path_segment("api")
329 .add_path_segment("v1")
330 .add_query_parameter("key", "value")
331 .fragment("section")
332 .build()
333 .unwrap();
334 assert_eq!(
335 url.to_string(),
336 "https://user:pass@example.com:8080/api/v1?key=value#section"
337 );
338 }
339
340 #[test]
341 fn builder_path_segments() {
342 let url = HttpUrl::builder()
343 .scheme(Scheme::Http)
344 .host("example.com")
345 .add_path_segment("a")
346 .add_path_segment("b")
347 .add_path_segment("c")
348 .build()
349 .unwrap();
350 assert_eq!(url.as_url().path(), "/a/b/c");
351 }
352
353 #[test]
354 fn builder_path_encoding() {
355 let url = HttpUrl::builder()
356 .scheme(Scheme::Http)
357 .host("example.com")
358 .add_path_segment("hello world")
359 .build()
360 .unwrap();
361 assert_eq!(url.as_url().path(), "/hello%20world");
362 }
363
364 #[test]
365 fn builder_unicode_segment() {
366 let url = HttpUrl::builder()
367 .scheme(Scheme::Http)
368 .host("example.com")
369 .add_path_segment("中文")
370 .build()
371 .unwrap();
372 assert_eq!(url.as_url().path(), "/%E4%B8%AD%E6%96%87");
373 }
374
375 #[test]
376 fn builder_set_path() {
377 let url = HttpUrl::builder()
378 .scheme(Scheme::Http)
379 .host("example.com")
380 .set_path("/a/b/c")
381 .build()
382 .unwrap();
383 assert_eq!(url.as_url().path(), "/a/b/c");
384 }
385
386 #[test]
387 fn builder_add_path_segments() {
388 let url = HttpUrl::builder()
389 .scheme(Scheme::Http)
390 .host("example.com")
391 .add_path_segments(&["a", "b", "c"])
392 .build()
393 .unwrap();
394 assert_eq!(url.as_url().path(), "/a/b/c");
395 }
396
397 #[test]
398 fn builder_query_encoding() {
399 let url = HttpUrl::builder()
400 .scheme(Scheme::Http)
401 .host("example.com")
402 .add_query_parameter("name", "hello world")
403 .build()
404 .unwrap();
405 assert_eq!(url.as_url().query(), Some("name=hello+world"));
407 }
408
409 #[test]
410 fn builder_clear_query_parameters() {
411 let url = HttpUrl::builder()
412 .scheme(Scheme::Http)
413 .host("example.com")
414 .add_query_parameter("a", "1")
415 .clear_query_parameters()
416 .build()
417 .unwrap();
418 assert_eq!(url.as_url().query(), None);
419 }
420
421 #[test]
422 fn builder_remove_query_parameter() {
423 let url = HttpUrl::builder()
424 .scheme(Scheme::Https)
425 .host("example.com")
426 .add_query_parameter("a", "1")
427 .add_query_parameter("b", "2")
428 .remove_query_parameter("a")
429 .build()
430 .unwrap();
431 assert_eq!(url.as_url().query_pairs().find(|(k, _)| k == "a"), None);
432 assert_eq!(
433 url.as_url()
434 .query_pairs()
435 .find(|(k, _)| k == "b")
436 .map(|(_, v)| v.into_owned()),
437 Some("2".to_string())
438 );
439 }
440
441 #[test]
442 fn builder_remove_fragment() {
443 let url = HttpUrl::builder()
444 .scheme(Scheme::Https)
445 .host("example.com")
446 .fragment("sec")
447 .remove_fragment()
448 .build()
449 .unwrap();
450 assert_eq!(url.as_url().fragment(), None);
451 }
452
453 #[test]
454 fn builder_remove_port() {
455 let url = HttpUrl::builder()
456 .scheme(Scheme::Http)
457 .host("example.com")
458 .port(8080)
459 .remove_port()
460 .build()
461 .unwrap();
462 assert_eq!(url.as_url().port(), None); }
464
465 #[test]
466 fn builder_missing_scheme() {
467 assert!(HttpUrl::builder().host("example.com").build().is_err());
468 }
469
470 #[test]
471 fn builder_missing_host() {
472 assert!(HttpUrl::builder().scheme(Scheme::Http).build().is_err());
473 }
474
475 #[test]
476 fn builder_ipv6_host() {
477 let url = HttpUrl::builder()
478 .scheme(Scheme::Http)
479 .host("[::1]")
480 .port(8080)
481 .build()
482 .unwrap();
483 assert_eq!(url.as_url().host_str(), Some("[::1]"));
484 assert_eq!(url.as_url().port(), Some(8080));
485 }
486
487 #[test]
488 fn builder_idn_normalization() {
489 let url = HttpUrl::builder()
491 .scheme(Scheme::Http)
492 .host("Bücher.de")
493 .build()
494 .unwrap();
495 assert_eq!(url.as_url().host_str(), Some("xn--bcher-kva.de"));
496 }
497
498 #[test]
499 fn builder_new_builder_roundtrip() {
500 let url = HttpUrl::parse("https://example.com/a/b?x=1&y=2#frag").unwrap();
501 let url2 = url.new_builder().build().unwrap();
502 assert_eq!(url, url2);
503 }
504
505 #[test]
506 fn builder_new_builder_modify() {
507 let url = HttpUrl::parse("https://example.com/a/b?x=1").unwrap();
508 let url2 = url
509 .new_builder()
510 .add_path_segment("c")
511 .add_query_parameter("y", "2")
512 .build()
513 .unwrap();
514 assert_eq!(url2.as_url().path(), "/a/b/c");
515 assert_eq!(
516 url2.as_url()
517 .query_pairs()
518 .find(|(k, _)| k == "x")
519 .map(|(_, v)| v.into_owned()),
520 Some("1".to_string())
521 );
522 assert_eq!(
523 url2.as_url()
524 .query_pairs()
525 .find(|(k, _)| k == "y")
526 .map(|(_, v)| v.into_owned()),
527 Some("2".to_string())
528 );
529 }
530
531 #[test]
532 fn builder_from_url() {
533 let u = url::Url::parse("https://example.com/a/b?q=1#frag").unwrap();
534 let url = HttpUrlBuilder::from_url(u.clone())
535 .unwrap()
536 .add_path_segment("c")
537 .build()
538 .unwrap();
539 assert_eq!(url.as_url().path(), "/a/b/c");
540 assert_eq!(
541 url.as_url()
542 .query_pairs()
543 .find(|(k, _)| k == "q")
544 .map(|(_, v)| v.into_owned()),
545 Some("1".to_string())
546 );
547 assert_eq!(url.as_url().fragment(), Some("frag"));
548 }
549
550 #[test]
551 fn builder_from_url_rejects_non_http() {
552 let u = url::Url::parse("ftp://example.com").unwrap();
553 assert!(HttpUrlBuilder::from_url(u).is_err());
554 }
555
556 #[test]
557 fn builder_parse() {
558 let b: HttpUrlBuilder = "https://example.com/a/b".parse().unwrap();
559 let url = b.add_path_segment("c").build().unwrap();
560 assert_eq!(url.as_url().path(), "/a/b/c");
561 }
562
563 #[test]
564 fn builder_build_url() {
565 let url = HttpUrl::builder()
566 .scheme(Scheme::Https)
567 .host("example.com")
568 .add_path_segment("api")
569 .add_query_parameter("k", "v")
570 .fragment("sec")
571 .build_url()
572 .unwrap();
573 assert_eq!(url.as_str(), "https://example.com/api?k=v#sec");
574 }
575
576 #[test]
577 fn builder_build_url_missing_scheme() {
578 assert!(HttpUrl::builder().host("example.com").build_url().is_err());
579 }
580
581 #[test]
582 fn builder_parse_rejects_non_http() {
583 assert!(HttpUrlBuilder::parse("ftp://example.com").is_err());
584 }
585}