1use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
23
24type SubstitutePairs<'a> = Vec<(&'a str, &'a str)>;
25type QueryParams<'a> = Vec<(&'a str, &'a str)>;
26
27fn strip_double_slash<'a>(base_url: &str, route_template: &'a str) -> &'a str {
28 if base_url.ends_with("/") && route_template.starts_with("/") {
29 &route_template[1..]
30 } else {
31 route_template
32 }
33}
34
35fn format_path(route_template: &str, substitutes: &SubstitutePairs) -> String {
36 substitutes
37 .iter()
38 .fold(route_template.to_owned(), |route, (key, value)| {
39 route.replace(
40 &format!(":{key}"),
41 &utf8_percent_encode(&value, NON_ALPHANUMERIC).to_string(),
42 )
43 })
44}
45
46fn naive_encode_query_string<'a>(query_params: &QueryParams<'a>) -> String {
47 let query_string = query_params
48 .iter()
49 .map(|(key, value)| {
50 format!(
51 "{}={}",
52 utf8_percent_encode(key, NON_ALPHANUMERIC),
53 utf8_percent_encode(value, NON_ALPHANUMERIC)
54 )
55 })
56 .collect::<Vec<String>>()
57 .join("&");
58
59 "?".to_string() + (&query_string)
60}
61
62pub struct FormatUrl<'a> {
64 base: &'a str,
65 disable_encoding: bool,
66 path_template: Option<&'a str>,
67 query_params: Option<QueryParams<'a>>,
68 substitutes: Option<SubstitutePairs<'a>>,
69}
70
71impl<'a> FormatUrl<'a> {
72 pub fn disable_encoding(mut self) -> Self {
74 self.disable_encoding = true;
75 self
76 }
77
78 pub fn format_url(self) -> String {
80 let formatted_path = match (self.path_template, &self.substitutes) {
81 (Some(path_template), Some(substitutes)) => format_path(path_template, &substitutes),
82 (Some(path_template), _) => path_template.to_string(),
83 _ => String::from(""),
84 };
85
86 let formatted_querystring = &self.query_params.map_or_else(
87 || String::new(),
88 |query_params| match self.disable_encoding {
89 false => naive_encode_query_string(&query_params),
90 true => {
91 let query_string = query_params
92 .iter()
93 .map(|(key, value)| format!("{key}={value}"))
94 .collect::<Vec<String>>()
95 .join("&");
96 "?".to_string() + &query_string
97 }
98 },
99 );
100
101 let safe_formatted_route = strip_double_slash(self.base, &formatted_path);
102
103 format!(
104 "{}{}{}",
105 self.base, safe_formatted_route, formatted_querystring
106 )
107 }
108
109 pub fn new(base: &'a str) -> Self {
111 Self {
112 base,
113 disable_encoding: false,
114 path_template: None,
115 query_params: None,
116 substitutes: None,
117 }
118 }
119
120 pub fn with_path_template(mut self, path_template: &'a str) -> Self {
122 self.path_template = Some(path_template);
123 self
124 }
125
126 pub fn with_query_params(mut self, params: QueryParams<'a>) -> Self {
128 self.query_params = Some(params);
129 self
130 }
131
132 pub fn with_substitutes(mut self, substitutes: SubstitutePairs<'a>) -> Self {
134 self.substitutes = Some(substitutes);
135 self
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use crate::FormatUrl;
142
143 #[test]
144 fn no_formatting_test() {
145 assert_eq!(
146 FormatUrl::new("https://api.example.com").format_url(),
147 "https://api.example.com".to_string()
148 );
149 }
150
151 #[test]
152 fn path_test() {
153 assert_eq!(
154 FormatUrl::new("https://api.example.com",)
155 .with_path_template("/user")
156 .format_url(),
157 "https://api.example.com/user"
158 );
159 }
160
161 #[test]
162 fn strip_double_slash_test() {
163 assert_eq!(
164 FormatUrl::new("https://api.example.com/")
165 .with_path_template("/user")
166 .format_url(),
167 "https://api.example.com/user"
168 );
169 }
170
171 #[test]
172 fn path_substitutes_test() {
173 assert_eq!(
174 FormatUrl::new("https://api.example.com/",)
175 .with_path_template("/user/:id",)
176 .with_substitutes(vec![("id", "alextes")])
177 .format_url(),
178 "https://api.example.com/user/alextes"
179 );
180 }
181
182 #[test]
183 fn querystring_test() {
184 assert_eq!(
185 FormatUrl::new("https://api.example.com/user",)
186 .with_query_params(vec![("id", "alextes")],)
187 .format_url(),
188 "https://api.example.com/user?id=alextes"
189 );
190 }
191
192 #[test]
193 fn percent_encode_substitutes_test() {
194 assert_eq!(
195 FormatUrl::new("https://api.example.com/",)
196 .with_path_template("/user/:id",)
197 .with_substitutes(vec![("id", "alex tes")])
198 .format_url(),
199 "https://api.example.com/user/alex%20tes"
200 )
201 }
202
203 #[test]
204 fn percent_encode_query_params_test() {
205 assert_eq!(
206 FormatUrl::new("https://api.example.com/user",)
207 .with_query_params(vec![("id", "alex+tes")],)
208 .format_url(),
209 "https://api.example.com/user?id=alex%2Btes"
210 )
211 }
212
213 #[test]
214 fn disable_encoding_test() {
215 assert_eq!(
216 FormatUrl::new("https://api.example.com/user",)
217 .with_query_params(vec![("id", "alex+tes")],)
218 .disable_encoding()
219 .format_url(),
220 "https://api.example.com/user?id=alex+tes"
221 )
222 }
223}