query_string_builder/
slim.rs

1use std::fmt;
2use std::fmt::{Debug, Display, Formatter, Write};
3
4use crate::QUERY;
5use percent_encoding::utf8_percent_encode;
6
7/// A type alias for the [`WrappedQueryString`] root.
8pub type QueryStringSimple = WrappedQueryString<RootMarker, EmptyValue>;
9
10/// A query string builder for percent encoding key-value pairs.
11/// This variant reduces string allocations as much as possible, defers them to the
12/// time of actual rendering, and is capable of storing references.
13///
14/// ## Example
15///
16/// ```
17/// use query_string_builder::QueryString;
18///
19/// let weight: &f32 = &99.9;
20///
21/// let qs = QueryString::simple()
22///             .with_value("q", "apple")
23///             .with_value("category", "fruits and vegetables")
24///             .with_opt_value("weight", Some(weight));
25///
26/// assert_eq!(
27///     format!("https://example.com/{qs}"),
28///     "https://example.com/?q=apple&category=fruits%20and%20vegetables&weight=99.9"
29/// );
30/// ```
31pub struct WrappedQueryString<B, T>
32where
33    B: ConditionalDisplay + Identifiable,
34    T: Display,
35{
36    base: BaseOption<B>,
37    value: KvpOption<T>,
38}
39
40impl Default for QueryStringSimple {
41    fn default() -> Self {
42        QueryStringSimple::new()
43    }
44}
45
46/// A helper type to track the values of [`WrappedQueryString`].
47pub struct Kvp<K, V>
48where
49    K: Display,
50    V: Display,
51{
52    key: K,
53    value: V,
54}
55
56enum BaseOption<B> {
57    Some(B),
58    None,
59}
60
61enum KvpOption<T> {
62    Some(T),
63    None,
64}
65
66/// This type serves as a root marker for the builder. It has no public constructor,
67/// thus can only be created within this crate.
68pub struct RootMarker(());
69
70/// This type serves as an empty value marker for the builder. It has no public constructor,
71/// thus can only be created within this crate.
72pub struct EmptyValue(());
73
74impl<B, T> WrappedQueryString<B, T>
75where
76    B: ConditionalDisplay + Identifiable,
77    T: Display,
78{
79    /// Creates a new, empty query string builder.
80    pub(crate) fn new() -> WrappedQueryString<RootMarker, EmptyValue> {
81        WrappedQueryString {
82            base: BaseOption::None,
83            value: KvpOption::None,
84        }
85    }
86
87    /// Appends a key-value pair to the query string.
88    ///
89    /// ## Example
90    ///
91    /// ```
92    /// use query_string_builder::QueryString;
93    ///
94    /// let qs = QueryString::dynamic()
95    ///             .with_value("q", "🍎 apple")
96    ///             .with_value("category", "fruits and vegetables")
97    ///             .with_value("answer", 42);
98    ///
99    /// assert_eq!(
100    ///     format!("https://example.com/{qs}"),
101    ///     "https://example.com/?q=%F0%9F%8D%8E%20apple&category=fruits%20and%20vegetables&answer=42"
102    /// );
103    /// ```
104    pub fn with_value<K: Display, V: Display>(
105        self,
106        key: K,
107        value: V,
108    ) -> WrappedQueryString<Self, Kvp<K, V>> {
109        WrappedQueryString {
110            base: BaseOption::Some(self),
111            value: KvpOption::Some(Kvp { key, value }),
112        }
113    }
114
115    /// Appends a key-value pair to the query string if the value exists.
116    ///
117    /// ## Example
118    ///
119    /// ```
120    /// use query_string_builder::QueryString;
121    ///
122    /// let qs = QueryString::dynamic()
123    ///             .with_opt_value("q", Some("🍎 apple"))
124    ///             .with_opt_value("f", None::<String>)
125    ///             .with_opt_value("category", Some("fruits and vegetables"))
126    ///             .with_opt_value("works", Some(true));
127    ///
128    /// assert_eq!(
129    ///     format!("https://example.com/{qs}"),
130    ///     "https://example.com/?q=%F0%9F%8D%8E%20apple&category=fruits%20and%20vegetables&works=true"
131    /// );
132    /// ```
133    pub fn with_opt_value<K: Display, V: Display>(
134        self,
135        key: K,
136        value: Option<V>,
137    ) -> WrappedQueryString<Self, Kvp<K, V>> {
138        if let Some(value) = value {
139            WrappedQueryString {
140                base: BaseOption::Some(self),
141                value: KvpOption::Some(Kvp { key, value }),
142            }
143        } else {
144            WrappedQueryString {
145                base: BaseOption::Some(self),
146                value: KvpOption::None,
147            }
148        }
149    }
150
151    /// Determines the number of key-value pairs currently in the builder.
152    pub fn len(&self) -> usize {
153        if self.is_empty() {
154            return 0;
155        }
156
157        1 + self.base.len()
158    }
159
160    /// Determines if the builder is currently empty.
161    pub fn is_empty(&self) -> bool {
162        // If this is the root node, and we don't have a value, we're empty.
163        if self.is_root() && self.value.is_empty() {
164            return true;
165        }
166
167        // If we're not the root node we need to check if all values are empty.
168        if !self.value.is_empty() {
169            return false;
170        }
171
172        self.base.is_empty()
173    }
174}
175
176pub trait Identifiable {
177    fn is_root(&self) -> bool;
178    fn is_empty(&self) -> bool;
179    fn len(&self) -> usize;
180}
181
182pub trait ConditionalDisplay {
183    fn cond_fmt(&self, should_display: bool, f: &mut Formatter<'_>) -> Result<usize, fmt::Error>;
184}
185
186impl Identifiable for RootMarker {
187    fn is_root(&self) -> bool {
188        unreachable!()
189    }
190
191    fn is_empty(&self) -> bool {
192        unreachable!()
193    }
194
195    fn len(&self) -> usize {
196        unreachable!()
197    }
198}
199
200impl ConditionalDisplay for RootMarker {
201    fn cond_fmt(&self, _should_display: bool, _f: &mut Formatter<'_>) -> Result<usize, fmt::Error> {
202        unreachable!()
203    }
204}
205
206impl Display for RootMarker {
207    fn fmt(&self, _f: &mut Formatter<'_>) -> fmt::Result {
208        unreachable!()
209    }
210}
211
212impl<B> ConditionalDisplay for BaseOption<B>
213where
214    B: ConditionalDisplay,
215{
216    fn cond_fmt(&self, should_display: bool, f: &mut Formatter<'_>) -> Result<usize, fmt::Error> {
217        match self {
218            BaseOption::Some(base) => Ok(base.cond_fmt(should_display, f)?),
219            BaseOption::None => {
220                // Reached the root marker.
221                if should_display {
222                    f.write_char('?')?;
223                }
224                Ok(0)
225            }
226        }
227    }
228}
229
230impl<B, T> ConditionalDisplay for WrappedQueryString<B, T>
231where
232    B: ConditionalDisplay + Identifiable,
233    T: Display,
234{
235    fn cond_fmt(&self, should_display: bool, f: &mut Formatter<'_>) -> Result<usize, fmt::Error> {
236        let depth = if !should_display {
237            // Our caller had nothing to display. If we have nothing to display either,
238            // we move on to our parent.
239            if self.value.is_empty() {
240                return self.base.cond_fmt(false, f);
241            }
242
243            // We do have things to display - render the parent!
244            self.base.cond_fmt(true, f)?
245        } else {
246            // The caller has things to display - go ahead regardless.
247            self.base.cond_fmt(true, f)?
248        };
249
250        // If we have nothing to render, return the known depth.
251        if self.value.is_empty() {
252            return Ok(depth);
253        }
254
255        // Display and increase the depth.
256        self.value.fmt(f)?;
257
258        // If our parent indicated content was displayable, add the combinator.
259        if should_display {
260            f.write_char('&')?;
261        }
262
263        Ok(depth + 1)
264    }
265}
266
267impl<B> BaseOption<B>
268where
269    B: Identifiable + ConditionalDisplay,
270{
271    fn is_empty(&self) -> bool {
272        match self {
273            BaseOption::Some(value) => value.is_empty(),
274            BaseOption::None => true,
275        }
276    }
277
278    fn len(&self) -> usize {
279        match self {
280            BaseOption::Some(value) => value.len(),
281            BaseOption::None => 0,
282        }
283    }
284}
285
286impl<B, T> Identifiable for WrappedQueryString<B, T>
287where
288    B: ConditionalDisplay + Identifiable,
289    T: Display,
290{
291    fn is_root(&self) -> bool {
292        match self.base {
293            BaseOption::Some(_) => false,
294            BaseOption::None => true,
295        }
296    }
297
298    fn is_empty(&self) -> bool {
299        match self.value {
300            KvpOption::Some(_) => false,
301            KvpOption::None => self.base.is_empty(),
302        }
303    }
304
305    fn len(&self) -> usize {
306        match self.value {
307            KvpOption::Some(_) => 1 + self.base.len(),
308            KvpOption::None => self.base.len(),
309        }
310    }
311}
312
313impl<T> KvpOption<T> {
314    fn is_empty(&self) -> bool {
315        match self {
316            KvpOption::Some(_) => false,
317            KvpOption::None => true,
318        }
319    }
320}
321
322impl Display for EmptyValue {
323    fn fmt(&self, _f: &mut Formatter<'_>) -> fmt::Result {
324        Ok(())
325    }
326}
327
328impl<T> Display for BaseOption<T>
329where
330    T: Display,
331{
332    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
333        match self {
334            BaseOption::Some(d) => Display::fmt(d, f),
335            BaseOption::None => Ok(()),
336        }
337    }
338}
339
340impl<T> Display for KvpOption<T>
341where
342    T: Display,
343{
344    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
345        match self {
346            KvpOption::Some(d) => Display::fmt(d, f),
347            KvpOption::None => Ok(()),
348        }
349    }
350}
351
352impl<K, V> Display for Kvp<K, V>
353where
354    K: Display,
355    V: Display,
356{
357    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
358        Display::fmt(&utf8_percent_encode(&self.key.to_string(), QUERY), f)?;
359        f.write_char('=')?;
360        Display::fmt(&utf8_percent_encode(&self.value.to_string(), QUERY), f)
361    }
362}
363
364impl<B, T> Display for WrappedQueryString<B, T>
365where
366    B: ConditionalDisplay + Identifiable,
367    T: Display,
368{
369    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
370        let should_display = !self.value.is_empty();
371
372        self.base.cond_fmt(should_display, f)?;
373        if should_display {
374            Display::fmt(&self.value, f)?;
375        }
376
377        Ok(())
378    }
379}
380
381impl<B, T> Debug for WrappedQueryString<B, T>
382where
383    B: ConditionalDisplay + Identifiable,
384    T: Display,
385{
386    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
387        Display::fmt(self, f)
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use crate::slim::{BaseOption, EmptyValue, KvpOption};
394    use crate::QueryString;
395
396    #[test]
397    fn test_empty() {
398        let qs = QueryString::simple();
399
400        assert!(qs.is_empty());
401        assert_eq!(qs.len(), 0);
402
403        assert_eq!(qs.to_string(), "");
404    }
405
406    #[test]
407    fn test_empty_complex() {
408        let qs = QueryString::simple().with_opt_value("key", None::<&str>);
409
410        assert!(qs.is_empty());
411        assert_eq!(qs.len(), 0);
412
413        assert_eq!(qs.to_string(), "");
414    }
415
416    #[test]
417    fn test_simple() {
418        let apple = "apple???";
419
420        let qs = QueryString::simple()
421            .with_value("q", &apple)
422            .with_value("category", "fruits and vegetables")
423            .with_value("tasty", true)
424            .with_value("weight", 99.9);
425
426        assert!(!qs.is_empty());
427        assert_eq!(qs.len(), 4);
428
429        assert_eq!(
430            format!("{qs}"),
431            "?q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9"
432        );
433    }
434
435    #[test]
436    fn test_encoding() {
437        let qs = QueryString::simple()
438            .with_value("q", "Grünkohl")
439            .with_value("category", "Gemüse");
440
441        assert!(!qs.is_empty());
442        assert_eq!(qs.len(), 2);
443
444        assert_eq!(qs.to_string(), "?q=Gr%C3%BCnkohl&category=Gem%C3%BCse");
445    }
446
447    #[test]
448    fn test_emoji() {
449        let qs = QueryString::simple()
450            .with_value("q", "🥦")
451            .with_value("🍽️", "🍔🍕");
452
453        assert!(!qs.is_empty());
454        assert_eq!(qs.len(), 2);
455
456        assert_eq!(
457            format!("{qs:?}"),
458            "?q=%F0%9F%A5%A6&%F0%9F%8D%BD%EF%B8%8F=%F0%9F%8D%94%F0%9F%8D%95"
459        );
460    }
461
462    #[test]
463    fn test_optional() {
464        let qs = QueryString::simple()
465            .with_value("q", "celery")
466            .with_opt_value("taste", None::<String>)
467            .with_opt_value("category", Some("fruits and vegetables"))
468            .with_opt_value("tasty", Some(true))
469            .with_opt_value("weight", Some(99.9));
470
471        assert!(!qs.is_empty());
472        assert_eq!(qs.len(), 4);
473
474        assert_eq!(
475            qs.to_string(),
476            "?q=celery&category=fruits%20and%20vegetables&tasty=true&weight=99.9"
477        );
478        assert_eq!(qs.len(), 4); // not five!
479    }
480
481    #[test]
482    fn test_display() {
483        assert_eq!(format!("{}", KvpOption::<i32>::None), "");
484        assert_eq!(format!("{}", BaseOption::<i32>::None), "");
485        assert_eq!(format!("{}", EmptyValue(())), "");
486    }
487}