1use std::collections::BTreeMap;
2
3#[derive(Debug, Clone)]
4pub struct UrlConstructor {
5 scheme: String,
6 subdomains: Vec<String>,
7 userinfo: Option<String>,
8 host: String,
9 port: Option<u16>,
10 subdirs: Vec<String>,
11 params: BTreeMap<String, String>, fragment: Option<String>
13}
14
15impl UrlConstructor {
16 pub fn new() -> Self {
17 Self {
18 scheme: "https".to_owned(),
19 subdomains: Vec::new(),
20 userinfo: None,
21 host: String::new(),
22 port: None,
23 subdirs: Vec::new(),
24 params: BTreeMap::new(),
25 fragment: None,
26 }
27 }
28
29 pub fn scheme<S>(&mut self, scheme: S) -> &mut Self
30 where S: Into<String>
31 {
32 self.scheme = scheme.into();
33 self
34 }
35
36 pub fn userinfo<S>(&mut self, userinfo: S) -> &mut Self
37 where S: Into<String>
38 {
39 self.userinfo = Some(userinfo.into());
40 self
41 }
42
43 pub fn host<S>(&mut self, host: S) -> &mut Self
44 where S: Into<String>
45 {
46 let host_s: String = host.into();
47 self.host = host_s;
48 self
49 }
50
51 pub fn subdomain<S>(&mut self, subdomain: S) -> &mut Self
55 where S: Into<String>
56 {
57 self.subdomains.push(subdomain.into());
58 self
59 }
60
61 pub fn port(&mut self, port: u16) -> &mut Self
62 {
63 self.port = Some(port);
64 self
65 }
66
67 pub fn subdir<S>(&mut self, subdir: S) -> &mut Self
68 where S: Into<String>
69 {
70 self.subdirs.push(subdir.into());
71 self
72 }
73
74 pub fn param<S1, S2>(&mut self, key: S1, value: S2) -> &mut Self
75 where
76 S1: Into<String>,
77 S2: Into<String>
78 {
79 self.params.insert(key.into(), value.into());
80 self
81 }
82
83 pub fn fragment<S>(&mut self, fragment: S) -> &mut Self
84 where S: Into<String>
85 {
86 self.fragment = Some(fragment.into());
87 self
88 }
89
90 pub fn build(&self) -> String {
91 let scheme_s = if self.scheme.is_empty() {
92 "".to_owned()
93 } else {
94 self.scheme.clone() + "://"
95 };
96
97 let userinfo_s = match &self.userinfo {
98 Some(v) => v.clone() + "@",
99 None => String::new(),
100 };
101
102 let subdomains_s = self
103 .subdomains
104 .iter()
105 .cloned()
106 .reduce(|a, b| a + "." + &b)
107 .map(|s| if self.host.is_empty() { s } else { s + "." })
108 .or(Some(String::new()))
109 .unwrap();
110
111 let port_s = match self.port {
112 Some(num) => ":".to_owned() + &num.to_string(),
113 None => String::new(),
114 };
115
116 let subdirs_s = self
117 .subdirs
118 .iter()
119 .cloned()
120 .reduce(|a, b| a + "/" + &b)
121 .map(|s| "/".to_owned() + &s)
122 .or(Some(String::new()))
123 .unwrap();
124
125 let params_s = self
126 .params
127 .clone()
128 .into_iter()
129 .map(|(k, v)| k + "=" + &v)
130 .reduce(|p1, p2| p1 + "&" + &p2)
131 .map(|s| "?".to_owned() + &s)
132 .or(Some(String::new()))
133 .unwrap();
134
135 let fragment_s = match &self.fragment {
136 Some(v) => "#".to_owned() + v,
137 None => String::new(),
138 };
139
140 scheme_s
141 + &userinfo_s
142 + &subdomains_s
143 + &self.host
144 + &port_s
145 + &subdirs_s
146 + ¶ms_s
147 + &fragment_s
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 static HOST: &str = "google.com";
156
157 #[test]
158 fn builder_readme_example() {
159 let url = UrlConstructor::new()
160 .scheme("http")
161 .userinfo("alex:password1")
162 .subdomain("api")
163 .host("google.com")
164 .port(8080)
165 .subdir("v2")
166 .subdir("users")
167 .param("salary", ">10000")
168 .param("lastName", "Wallace")
169 .fragment("id")
170 .build();
171
172 assert_eq!(
173 url,
174 "http://alex:password1@api.google.com:8080/v2/users?lastName=Wallace&salary=>10000#id"
175 )
176 }
177
178 #[test]
179 fn builder_normal_usage() {
180 let actual = UrlConstructor::new()
181 .scheme("http")
182 .userinfo("user:password")
183 .subdomain("api")
184 .subdomain("v2")
185 .host(HOST)
186 .port(400)
187 .subdir("s1")
188 .subdir("s2")
189 .param("k1", "v1")
190 .param("k2", "v2")
191 .param("k3", "v4")
192 .fragment("foo")
193 .build();
194 let expected = format!("http://user:password@api.v2.{}:400/s1/s2?k1=v1&k2=v2&k3=v4#foo", HOST.to_owned());
195 assert_eq!(expected, actual);
196 }
197
198 #[test]
199 fn test_builder_empty() {
200 let actual = UrlConstructor::new().scheme("").build();
201 let expected = "";
202 assert_eq!(expected, actual);
203 }
204
205 #[test]
206 fn test_builder_scheme() {
207 let actual = UrlConstructor::new()
208 .scheme("ssh")
209 .build();
210 let expected = format!("ssh://");
211 assert_eq!(expected, actual);
212 }
213
214 #[test]
215 fn test_builder_no_scheme() {
216 let actual = UrlConstructor::new()
217 .scheme("")
218 .build();
219 assert_eq!("", actual);
220 }
221
222 #[test]
223 fn test_builder_userinfo() {
224 let actual = UrlConstructor::new()
225 .userinfo("user:pass")
226 .build();
227 assert_eq!("https://user:pass@", actual);
228 }
229
230 #[test]
231 fn test_builder_userinfo_host() {
232 let actual = UrlConstructor::new()
233 .userinfo("user:pass")
234 .host(HOST)
235 .build();
236 let expected = format!("https://user:pass@{}", HOST);
237 assert_eq!(expected, actual);
238 }
239
240 #[test]
241 fn test_builder_no_scheme_with_host() {
242 let actual = UrlConstructor::new()
243 .scheme("")
244 .host(HOST)
245 .build();
246 assert_eq!(HOST, actual);
247 }
248
249 #[test]
250 fn test_builder_host() {
251 let actual = UrlConstructor::new()
252 .host(HOST)
253 .build();
254 let expected = format!("https://{}", HOST);
255 assert_eq!(expected, actual);
256 }
257
258 #[test]
259 fn test_builder_subdomains() {
260 let actual = UrlConstructor::new()
261 .subdomain("api")
262 .subdomain("google")
263 .subdomain("com")
264 .build();
265 let expected = format!("https://api.google.com");
266 assert_eq!(expected, actual);
267 }
268
269 #[test]
270 fn test_builder_host_subdomains() {
271 let actual = UrlConstructor::new()
272 .host("google.com")
273 .subdomain("api")
274 .subdomain("v2")
275 .build();
276 let expected = format!("https://api.v2.google.com");
277 assert_eq!(expected, actual);
278 }
279
280 #[test]
281 fn test_builder_port() {
282 let actual = UrlConstructor::new()
283 .port(443)
284 .build();
285 let expected = format!("https://:443");
286 assert_eq!(expected, actual);
287 }
288
289 #[test]
290 fn test_builder_host_port() {
291 let actual = UrlConstructor::new()
292 .host(HOST)
293 .port(443)
294 .build();
295 let expected = format!("https://{}:443", HOST);
296 assert_eq!(expected, actual);
297 }
298
299 #[test]
300 fn test_builder_subdirs() {
301 let actual = UrlConstructor::new()
302 .subdir("s1")
303 .subdir("s2")
304 .build();
305 let expected = format!("https:///s1/s2");
306 assert_eq!(expected, actual);
307 }
308
309 #[test]
310 fn test_builder_host_subdirs() {
311 let actual = UrlConstructor::new()
312 .host(HOST)
313 .subdir("s1")
314 .subdir("s2")
315 .build();
316 let expected = format!("https://{}/s1/s2", HOST);
317 assert_eq!(expected, actual);
318 }
319
320 #[test]
321 fn test_builder_host_params() {
322 let actual = UrlConstructor::new()
323 .host(HOST)
324 .param("k1", "v1")
325 .param("k2", "v2")
326 .param("k3", "v4")
327 .build();
328 let expected = format!("https://{}?k1=v1&k2=v2&k3=v4", HOST.to_owned());
329 assert_eq!(expected, actual);
330 }
331
332 #[test]
333 fn test_builder_params() {
334 let actual = UrlConstructor::new()
335 .param("k1", "v1")
336 .param("k2", "v2")
337 .param("k3", "v4")
338 .build();
339 let expected = format!("https://?k1=v1&k2=v2&k3=v4");
340 assert_eq!(expected, actual);
341 }
342
343 #[test]
344 fn test_builder_fragment() {
345 let actual = UrlConstructor::new()
346 .fragment("foo")
347 .build();
348 let expected = format!("https://#foo");
349 assert_eq!(expected, actual);
350 }
351}