query_string_builder/borrowed.rs
1//! The borrowing, zero-allocation query string builder.
2
3use crate::encode::write_encoded;
4use crate::{QUERY, QueryStringOwned};
5use percent_encoding::utf8_percent_encode;
6use std::fmt::{self, Debug, Display, Formatter, Write};
7
8/// A borrowed key or value of a query string pair.
9///
10/// You usually don't interact with this type directly; it is produced by the
11/// [`IntoPart`] conversions accepted by [`QueryString`]'s methods.
12#[derive(Clone, Copy)]
13pub enum Part<'a> {
14 /// A plain string slice.
15 Str(&'a str),
16 /// Any other borrowed [`Display`] value, rendered on demand.
17 Display(&'a dyn Display),
18}
19
20impl Display for Part<'_> {
21 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
22 match self {
23 Part::Str(s) => Display::fmt(s, f),
24 Part::Display(d) => Display::fmt(d, f),
25 }
26 }
27}
28
29impl Part<'_> {
30 /// Writes this part into the formatter, percent-encoded, without
31 /// allocating intermediate strings.
32 fn write_encoded_to(&self, f: &mut Formatter<'_>) -> fmt::Result {
33 match self {
34 Part::Str(s) => {
35 for piece in utf8_percent_encode(s, QUERY) {
36 f.write_str(piece)?;
37 }
38 Ok(())
39 }
40 Part::Display(d) => write_encoded(f, *d),
41 }
42 }
43}
44
45/// Conversion into a borrowed query string [`Part`].
46///
47/// Implemented for `&str` and for `&T` of any [`Display`] type, so string
48/// literals, `&String`, `&i32`, `&bool` etc. can all be passed to
49/// [`QueryString`]'s methods directly.
50pub trait IntoPart<'a> {
51 /// Performs the conversion.
52 fn into_part(self) -> Part<'a>;
53}
54
55impl<'a> IntoPart<'a> for &'a str {
56 fn into_part(self) -> Part<'a> {
57 Part::Str(self)
58 }
59}
60
61impl<'a, T: Display> IntoPart<'a> for &'a T {
62 fn into_part(self) -> Part<'a> {
63 Part::Display(self)
64 }
65}
66
67/// A zero-allocation query string builder for percent encoding key-value pairs.
68///
69/// This builder borrows all keys and values. Building performs a single [`Vec`]
70/// allocation for the pair list; rendering percent-encodes each value on the
71/// fly without allocating intermediate strings.
72///
73/// If you need a builder without a lifetime parameter — e.g. to store it in a
74/// struct or return it from a function that owns the values — use
75/// [`QueryStringOwned`] or convert via [`into_owned`](Self::into_owned).
76///
77/// ## Example
78///
79/// ```
80/// use query_string_builder::QueryString;
81///
82/// let tasty = true;
83/// let qs = QueryString::new()
84/// .with("q", "apple")
85/// .with("tasty", &tasty)
86/// .with_opt("category", Some("fruits and vegetables"));
87///
88/// assert_eq!(
89/// format!("https://example.com/{qs}"),
90/// "https://example.com/?q=apple&tasty=true&category=fruits%20and%20vegetables"
91/// );
92/// ```
93///
94/// ## Borrowing footgun
95///
96/// Because the builder borrows its values, temporaries created inline do not
97/// live long enough — bind them to a variable first:
98///
99/// ```compile_fail
100/// use query_string_builder::QueryString;
101///
102/// let qs = QueryString::new().with("answer", &42.to_string()); // temporary dropped here
103/// println!("{qs}");
104/// ```
105///
106/// ```
107/// use query_string_builder::QueryString;
108///
109/// let answer = 42.to_string();
110/// let qs = QueryString::new().with("answer", &answer);
111/// assert_eq!(qs.to_string(), "?answer=42");
112/// ```
113#[derive(Clone, Default)]
114pub struct QueryString<'a> {
115 pairs: Vec<(Part<'a>, Part<'a>)>,
116}
117
118impl<'a> QueryString<'a> {
119 /// Creates a new, empty query string builder.
120 pub fn new() -> Self {
121 Self { pairs: Vec::new() }
122 }
123
124 /// Appends a key-value pair to the query string.
125 ///
126 /// ## Example
127 ///
128 /// ```
129 /// use query_string_builder::QueryString;
130 ///
131 /// let answer = 42;
132 /// let qs = QueryString::new()
133 /// .with("q", "🍎 apple")
134 /// .with("category", "fruits and vegetables")
135 /// .with("answer", &answer);
136 ///
137 /// assert_eq!(
138 /// format!("https://example.com/{qs}"),
139 /// "https://example.com/?q=%F0%9F%8D%8E%20apple&category=fruits%20and%20vegetables&answer=42"
140 /// );
141 /// ```
142 #[doc(alias = "with_value")]
143 pub fn with<K, V>(mut self, key: K, value: V) -> Self
144 where
145 K: IntoPart<'a>,
146 V: IntoPart<'a>,
147 {
148 self.pairs.push((key.into_part(), value.into_part()));
149 self
150 }
151
152 /// Appends a key-value pair to the query string if the value exists.
153 ///
154 /// ## Example
155 ///
156 /// ```
157 /// use query_string_builder::QueryString;
158 ///
159 /// let works = true;
160 /// let qs = QueryString::new()
161 /// .with_opt("q", Some("🍎 apple"))
162 /// .with_opt("f", None::<&str>)
163 /// .with_opt("category", Some("fruits and vegetables"))
164 /// .with_opt("works", Some(&works));
165 ///
166 /// assert_eq!(
167 /// format!("https://example.com/{qs}"),
168 /// "https://example.com/?q=%F0%9F%8D%8E%20apple&category=fruits%20and%20vegetables&works=true"
169 /// );
170 /// ```
171 #[doc(alias = "with_opt_value")]
172 pub fn with_opt<K, V>(self, key: K, value: Option<V>) -> Self
173 where
174 K: IntoPart<'a>,
175 V: IntoPart<'a>,
176 {
177 if let Some(value) = value {
178 self.with(key, value)
179 } else {
180 self
181 }
182 }
183
184 /// Appends a key-value pair to the query string.
185 ///
186 /// ## Example
187 ///
188 /// ```
189 /// use query_string_builder::QueryString;
190 ///
191 /// let mut qs = QueryString::new();
192 /// qs.push("q", "apple");
193 /// qs.push("category", "fruits and vegetables");
194 ///
195 /// assert_eq!(
196 /// format!("https://example.com/{qs}"),
197 /// "https://example.com/?q=apple&category=fruits%20and%20vegetables"
198 /// );
199 /// ```
200 pub fn push<K, V>(&mut self, key: K, value: V) -> &mut Self
201 where
202 K: IntoPart<'a>,
203 V: IntoPart<'a>,
204 {
205 self.pairs.push((key.into_part(), value.into_part()));
206 self
207 }
208
209 /// Appends a key-value pair to the query string if the value exists.
210 ///
211 /// ## Example
212 ///
213 /// ```
214 /// use query_string_builder::QueryString;
215 ///
216 /// let mut qs = QueryString::new();
217 /// qs.push_opt("q", None::<&str>);
218 /// qs.push_opt("q", Some("🍎 apple"));
219 ///
220 /// assert_eq!(
221 /// format!("https://example.com/{qs}"),
222 /// "https://example.com/?q=%F0%9F%8D%8E%20apple"
223 /// );
224 /// ```
225 pub fn push_opt<K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
226 where
227 K: IntoPart<'a>,
228 V: IntoPart<'a>,
229 {
230 if let Some(value) = value {
231 self.push(key, value)
232 } else {
233 self
234 }
235 }
236
237 /// Determines the number of key-value pairs currently in the builder.
238 pub fn len(&self) -> usize {
239 self.pairs.len()
240 }
241
242 /// Determines if the builder is currently empty.
243 pub fn is_empty(&self) -> bool {
244 self.pairs.is_empty()
245 }
246
247 /// Appends another query string builder's values.
248 ///
249 /// ## Example
250 ///
251 /// ```
252 /// use query_string_builder::QueryString;
253 ///
254 /// let mut qs = QueryString::new().with("q", "apple");
255 /// let more = QueryString::new().with("q", "pear");
256 ///
257 /// qs.append(more);
258 ///
259 /// assert_eq!(
260 /// format!("https://example.com/{qs}"),
261 /// "https://example.com/?q=apple&q=pear"
262 /// );
263 /// ```
264 pub fn append(&mut self, mut other: QueryString<'a>) {
265 self.pairs.append(&mut other.pairs)
266 }
267
268 /// Appends another query string builder's values, consuming both types.
269 ///
270 /// ## Example
271 ///
272 /// ```
273 /// use query_string_builder::QueryString;
274 ///
275 /// let qs = QueryString::new().with("q", "apple");
276 /// let more = QueryString::new().with("q", "pear");
277 ///
278 /// let qs = qs.append_into(more);
279 ///
280 /// assert_eq!(
281 /// format!("https://example.com/{qs}"),
282 /// "https://example.com/?q=apple&q=pear"
283 /// );
284 /// ```
285 pub fn append_into(mut self, mut other: QueryString<'a>) -> Self {
286 self.pairs.append(&mut other.pairs);
287 self
288 }
289
290 /// Converts this borrowing builder into a [`QueryStringOwned`] by rendering
291 /// each key and value to an owned [`String`].
292 ///
293 /// Useful for building cheaply with borrows and then storing or returning
294 /// the result past the borrows' lifetimes.
295 ///
296 /// ## Example
297 ///
298 /// ```
299 /// use query_string_builder::{QueryString, QueryStringOwned};
300 ///
301 /// let qs: QueryStringOwned = {
302 /// let q = String::from("apple");
303 /// QueryString::new().with("q", &q).into_owned()
304 /// };
305 ///
306 /// assert_eq!(qs.to_string(), "?q=apple");
307 /// ```
308 pub fn into_owned(self) -> QueryStringOwned {
309 QueryStringOwned::from_pairs(
310 self.pairs
311 .into_iter()
312 .map(|(key, value)| (key.to_string(), value.to_string()))
313 .collect(),
314 )
315 }
316}
317
318impl Display for QueryString<'_> {
319 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
320 if self.pairs.is_empty() {
321 return Ok(());
322 }
323 f.write_char('?')?;
324 for (i, (key, value)) in self.pairs.iter().enumerate() {
325 if i > 0 {
326 f.write_char('&')?;
327 }
328 key.write_encoded_to(f)?;
329 f.write_char('=')?;
330 value.write_encoded_to(f)?;
331 }
332 Ok(())
333 }
334}
335
336impl Debug for QueryString<'_> {
337 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
338 f.debug_map()
339 .entries(
340 self.pairs
341 .iter()
342 .map(|(key, value)| (key.to_string(), value.to_string())),
343 )
344 .finish()
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn test_empty() {
354 let qs = QueryString::new();
355 assert_eq!(qs.to_string(), "");
356 assert_eq!(qs.len(), 0);
357 assert!(qs.is_empty());
358 }
359
360 #[test]
361 fn test_simple() {
362 let tasty = true;
363 let weight = 99.9;
364 let qs = QueryString::new()
365 .with("q", "apple???")
366 .with("category", "fruits and vegetables")
367 .with("tasty", &tasty)
368 .with("weight", &weight);
369 assert_eq!(
370 qs.to_string(),
371 "?q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9"
372 );
373 assert_eq!(qs.len(), 4);
374 assert!(!qs.is_empty());
375 }
376
377 #[test]
378 fn test_encoding() {
379 let qs = QueryString::new()
380 .with("q", "Grünkohl")
381 .with("category", "Gemüse");
382 assert_eq!(qs.to_string(), "?q=Gr%C3%BCnkohl&category=Gem%C3%BCse");
383 }
384
385 #[test]
386 fn test_emoji() {
387 let qs = QueryString::new().with("q", "🥦").with("🍽️", "🍔🍕");
388 assert_eq!(
389 qs.to_string(),
390 "?q=%F0%9F%A5%A6&%F0%9F%8D%BD%EF%B8%8F=%F0%9F%8D%94%F0%9F%8D%95"
391 );
392 }
393
394 #[test]
395 fn test_optional() {
396 let tasty = true;
397 let weight = 99.9;
398 let qs = QueryString::new()
399 .with("q", "celery")
400 .with_opt("taste", None::<&str>)
401 .with_opt("category", Some("fruits and vegetables"))
402 .with_opt("tasty", Some(&tasty))
403 .with_opt("weight", Some(&weight));
404 assert_eq!(
405 qs.to_string(),
406 "?q=celery&category=fruits%20and%20vegetables&tasty=true&weight=99.9"
407 );
408 assert_eq!(qs.len(), 4); // not five!
409 }
410
411 #[test]
412 fn test_push_optional() {
413 let mut qs = QueryString::new();
414 qs.push("a", "apple");
415 qs.push_opt("b", None::<&str>);
416 qs.push_opt("c", Some("🍎 apple"));
417
418 assert_eq!(
419 format!("https://example.com/{qs}"),
420 "https://example.com/?a=apple&c=%F0%9F%8D%8E%20apple"
421 );
422 }
423
424 #[test]
425 fn test_append() {
426 let qs = QueryString::new().with("q", "apple");
427 let more = QueryString::new().with("q", "pear");
428
429 let mut qs = qs.append_into(more);
430 qs.append(QueryString::new().with("answer", "42"));
431
432 assert_eq!(
433 format!("https://example.com/{qs}"),
434 "https://example.com/?q=apple&q=pear&answer=42"
435 );
436 }
437
438 #[test]
439 fn test_characters() {
440 let tests = vec![
441 ("space", " ", "%20"),
442 ("double_quote", "\"", "%22"),
443 ("hash", "#", "%23"),
444 ("less_than", "<", "%3C"),
445 ("equals", "=", "%3D"),
446 ("greater_than", ">", "%3E"),
447 ("percent", "%", "%25"),
448 ("ampersand", "&", "%26"),
449 ("plus", "+", "%2B"),
450 //
451 ("dollar", "$", "$"),
452 ("single_quote", "'", "'"),
453 ("comma", ",", ","),
454 ("forward_slash", "/", "/"),
455 ("colon", ":", ":"),
456 ("semicolon", ";", ";"),
457 ("question_mark", "?", "?"),
458 ("at", "@", "@"),
459 ("left_bracket", "[", "["),
460 ("backslash", "\\", "\\"),
461 ("right_bracket", "]", "]"),
462 ("caret", "^", "^"),
463 ("underscore", "_", "_"),
464 ("grave", "^", "^"),
465 ("left_curly", "{", "{"),
466 ("pipe", "|", "|"),
467 ("right_curly", "}", "}"),
468 ];
469
470 let mut qs = QueryString::new();
471 for (key, value, _) in &tests {
472 qs.push(*key, *value);
473 }
474
475 let mut expected = String::new();
476 for (i, (key, _, value)) in tests.iter().enumerate() {
477 if i > 0 {
478 expected.push('&');
479 }
480 expected.push_str(&format!("{key}={value}"));
481 }
482
483 assert_eq!(
484 format!("https://example.com/{qs}"),
485 format!("https://example.com/?{expected}")
486 );
487 }
488
489 #[test]
490 fn test_non_string_refs() {
491 let count = 12i32;
492 let tasty = true;
493 let weight = 99.9f64;
494 let owned_string = String::from("kale");
495 let qs = QueryString::new()
496 .with("count", &count)
497 .with("tasty", &tasty)
498 .with("weight", &weight)
499 .with("q", &owned_string);
500 assert_eq!(qs.to_string(), "?count=12&tasty=true&weight=99.9&q=kale");
501 }
502
503 #[test]
504 fn test_into_owned() {
505 let owned = {
506 let q = String::from("Grünkohl");
507 QueryString::new().with("q", &q).into_owned()
508 };
509 assert_eq!(owned.to_string(), "?q=Gr%C3%BCnkohl");
510 assert_eq!(owned.len(), 1);
511 }
512
513 #[test]
514 fn test_debug() {
515 let qs = QueryString::new().with("q", "apple");
516 assert_eq!(format!("{qs:?}"), r#"{"q": "apple"}"#);
517 }
518
519 #[test]
520 fn test_clone_default() {
521 let qs = QueryString::default().with("q", "apple");
522 let clone = qs.clone();
523 assert_eq!(clone.to_string(), qs.to_string());
524 }
525}