url_constructor/
lib.rs

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>, //ordered keys for determinism
12    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    /// Subdomains will appear left-to-right in the calling order in the final
52    /// URL e.g. calling `.host("google.com").subdomain("api").subdomain("v2")`
53    /// will be built as `api.v2.google.com`.
54    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            + &params_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}