From 0a982d849d79069ad349af358c1c8fbcdb9a8b35 Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Sat, 8 Jun 2024 00:13:11 +0200 Subject: [PATCH 1/3] Add QueryStringSimple type --- CHANGELOG.md | 12 ++ Cargo.toml | 4 + benches/bench.rs | 8 +- benches/bench_slim.rs | 31 +++ src/lib.rs | 75 ++++--- src/slim.rs | 479 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 581 insertions(+), 28 deletions(-) create mode 100644 benches/bench_slim.rs create mode 100644 src/slim.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e50fe5..b9bb868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- The `QueryString::simple` function was added to construct the new `QueryStringSimple` type. + This type reduces string allocations, defers rendering and can keep references + but at the cost of a complex type signature slightly more rigid handling. + +### Changed + +- The `QueryString::new` function was renamed to `QueryString::dynamic`. + ## [0.5.1] - 2024-05-24 [0.5.1]: https://github.com/sunsided/query-string-builder/releases/tag/v0.5.1 diff --git a/Cargo.toml b/Cargo.toml index 42fc64a..452d12b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,7 @@ criterion = "0.5.1" [[bench]] name = "bench" harness = false + +[[bench]] +name = "bench_slim" +harness = false diff --git a/benches/bench.rs b/benches/bench.rs index efee663..3a82b6a 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -6,7 +6,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { // `with_value` method benchmark c.bench_function("with_value", |b| { b.iter(|| { - let qs = QueryString::new() + let qs = QueryString::dynamic() .with_value("q", "apple???") .with_value("category", "fruits and vegetables"); format!("{qs}") @@ -16,7 +16,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { // `with_opt_value` method benchmark c.bench_function("with_opt_value", |b| { b.iter(|| { - let qs = QueryString::new() + let qs = QueryString::dynamic() .with_value("q", "celery") .with_opt_value("taste", None::) .with_opt_value("category", Some("fruits and vegetables")) @@ -29,12 +29,12 @@ pub fn criterion_benchmark(c: &mut Criterion) { // Full test including creating, pushing and appending c.bench_function("push_opt_and_append", |b| { b.iter(|| { - let mut qs = QueryString::new(); + let mut qs = QueryString::dynamic(); qs.push("a", "apple"); qs.push_opt("b", None::); qs.push_opt("c", Some("🍎 apple")); - let more = QueryString::new().with_value("q", "pear"); + let more = QueryString::dynamic().with_value("q", "pear"); let qs = qs.append_into(more); format!("{qs}") diff --git a/benches/bench_slim.rs b/benches/bench_slim.rs new file mode 100644 index 0000000..bb6d7e0 --- /dev/null +++ b/benches/bench_slim.rs @@ -0,0 +1,31 @@ +use criterion::{criterion_group, criterion_main, Criterion}; + +use query_string_builder::QueryString; + +pub fn criterion_benchmark(c: &mut Criterion) { + // `with_value` method benchmark + c.bench_function("with_value (slim)", |b| { + b.iter(|| { + let qs = QueryString::simple() + .with_value("q", "apple???") + .with_value("category", "fruits and vegetables"); + format!("{qs}") + }) + }); + + // `with_opt_value` method benchmark + c.bench_function("with_opt_value (slim)", |b| { + b.iter(|| { + let qs = QueryString::simple() + .with_value("q", "celery") + .with_opt_value("taste", None::) + .with_opt_value("category", Some("fruits and vegetables")) + .with_opt_value("tasty", Some(true)) + .with_opt_value("weight", Some(99.9)); + format!("{qs}") + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/src/lib.rs b/src/lib.rs index a206fb4..6d4a870 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ //! ``` //! use query_string_builder::QueryString; //! -//! let qs = QueryString::new() +//! let qs = QueryString::dynamic() //! .with_value("q", "🍎 apple") //! .with_value("tasty", true) //! .with_opt_value("color", None::) @@ -22,12 +22,15 @@ #![deny(unsafe_code)] -use std::fmt::{Debug, Display, Formatter, Write}; +mod slim; use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; +use std::fmt::{Debug, Display, Formatter, Write}; + +pub use slim::{QueryStringSimple, WrappedQueryString}; /// https://url.spec.whatwg.org/#query-percent-encode-set -const QUERY: &AsciiSet = &CONTROLS +pub(crate) const QUERY: &AsciiSet = &CONTROLS .add(b' ') .add(b'"') .add(b'#') @@ -50,7 +53,7 @@ const QUERY: &AsciiSet = &CONTROLS /// ``` /// use query_string_builder::QueryString; /// -/// let qs = QueryString::new() +/// let qs = QueryString::dynamic() /// .with_value("q", "apple") /// .with_value("category", "fruits and vegetables"); /// @@ -59,14 +62,38 @@ const QUERY: &AsciiSet = &CONTROLS /// "https://example.com/?q=apple&category=fruits%20and%20vegetables" /// ); /// ``` -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct QueryString { pairs: Vec, } impl QueryString { /// Creates a new, empty query string builder. - pub fn new() -> Self { + /// + /// ## Example + /// + /// ``` + /// use query_string_builder::QueryString; + /// + /// let weight: &f32 = &99.9; + /// + /// let qs = QueryString::simple() + /// .with_value("q", "apple") + /// .with_value("category", "fruits and vegetables") + /// .with_opt_value("weight", Some(weight)); + /// + /// assert_eq!( + /// format!("https://example.com/{qs}"), + /// "https://example.com/?q=apple&category=fruits%20and%20vegetables&weight=99.9" + /// ); + /// ``` + #[allow(clippy::new_ret_no_self)] + pub fn simple() -> QueryStringSimple { + QueryStringSimple::new() + } + + /// Creates a new, empty query string builder. + pub fn dynamic() -> Self { Self { pairs: Vec::default(), } @@ -79,7 +106,7 @@ impl QueryString { /// ``` /// use query_string_builder::QueryString; /// - /// let qs = QueryString::new() + /// let qs = QueryString::dynamic() /// .with_value("q", "🍎 apple") /// .with_value("category", "fruits and vegetables") /// .with_value("answer", 42); @@ -104,7 +131,7 @@ impl QueryString { /// ``` /// use query_string_builder::QueryString; /// - /// let qs = QueryString::new() + /// let qs = QueryString::dynamic() /// .with_opt_value("q", Some("🍎 apple")) /// .with_opt_value("f", None::) /// .with_opt_value("category", Some("fruits and vegetables")) @@ -130,7 +157,7 @@ impl QueryString { /// ``` /// use query_string_builder::QueryString; /// - /// let mut qs = QueryString::new(); + /// let mut qs = QueryString::dynamic(); /// qs.push("q", "apple"); /// qs.push("category", "fruits and vegetables"); /// @@ -154,7 +181,7 @@ impl QueryString { /// ``` /// use query_string_builder::QueryString; /// - /// let mut qs = QueryString::new(); + /// let mut qs = QueryString::dynamic(); /// qs.push_opt("q", None::); /// qs.push_opt("q", Some("🍎 apple")); /// @@ -188,8 +215,8 @@ impl QueryString { /// ``` /// use query_string_builder::QueryString; /// - /// let mut qs = QueryString::new().with_value("q", "apple"); - /// let more = QueryString::new().with_value("q", "pear"); + /// let mut qs = QueryString::dynamic().with_value("q", "apple"); + /// let more = QueryString::dynamic().with_value("q", "pear"); /// /// qs.append(more); /// @@ -209,8 +236,8 @@ impl QueryString { /// ``` /// use query_string_builder::QueryString; /// - /// let qs = QueryString::new().with_value("q", "apple"); - /// let more = QueryString::new().with_value("q", "pear"); + /// let qs = QueryString::dynamic().with_value("q", "apple"); + /// let more = QueryString::dynamic().with_value("q", "pear"); /// /// let qs = qs.append_into(more); /// @@ -257,7 +284,7 @@ mod tests { #[test] fn test_empty() { - let qs = QueryString::new(); + let qs = QueryString::dynamic(); assert_eq!(qs.to_string(), ""); assert_eq!(qs.len(), 0); assert!(qs.is_empty()); @@ -265,7 +292,7 @@ mod tests { #[test] fn test_simple() { - let qs = QueryString::new() + let qs = QueryString::dynamic() .with_value("q", "apple???") .with_value("category", "fruits and vegetables") .with_value("tasty", true) @@ -280,7 +307,7 @@ mod tests { #[test] fn test_encoding() { - let qs = QueryString::new() + let qs = QueryString::dynamic() .with_value("q", "Grünkohl") .with_value("category", "Gemüse"); assert_eq!(qs.to_string(), "?q=Gr%C3%BCnkohl&category=Gem%C3%BCse"); @@ -288,7 +315,7 @@ mod tests { #[test] fn test_emoji() { - let qs = QueryString::new() + let qs = QueryString::dynamic() .with_value("q", "🥦") .with_value("🍽️", "🍔🍕"); assert_eq!( @@ -299,7 +326,7 @@ mod tests { #[test] fn test_optional() { - let qs = QueryString::new() + let qs = QueryString::dynamic() .with_value("q", "celery") .with_opt_value("taste", None::) .with_opt_value("category", Some("fruits and vegetables")) @@ -314,7 +341,7 @@ mod tests { #[test] fn test_push_optional() { - let mut qs = QueryString::new(); + let mut qs = QueryString::dynamic(); qs.push("a", "apple"); qs.push_opt("b", None::); qs.push_opt("c", Some("🍎 apple")); @@ -327,11 +354,11 @@ mod tests { #[test] fn test_append() { - let qs = QueryString::new().with_value("q", "apple"); - let more = QueryString::new().with_value("q", "pear"); + let qs = QueryString::dynamic().with_value("q", "apple"); + let more = QueryString::dynamic().with_value("q", "pear"); let mut qs = qs.append_into(more); - qs.append(QueryString::new().with_value("answer", "42")); + qs.append(QueryString::dynamic().with_value("answer", "42")); assert_eq!( format!("https://example.com/{qs}"), @@ -371,7 +398,7 @@ mod tests { ("right_curly", "}", "}"), ]; - let mut qs = QueryString::new(); + let mut qs = QueryString::dynamic(); for (key, value, _) in &tests { qs.push(key.to_string(), value.to_string()); } diff --git a/src/slim.rs b/src/slim.rs new file mode 100644 index 0000000..78ac4a3 --- /dev/null +++ b/src/slim.rs @@ -0,0 +1,479 @@ +use std::fmt; +use std::fmt::{Debug, Display, Formatter, Write}; + +use crate::{QueryString, QUERY}; +use percent_encoding::utf8_percent_encode; + +/// A type alias for the [`WrappedQueryString`] root. +pub type QueryStringSimple = WrappedQueryString; + +/// A query string builder for percent encoding key-value pairs. +/// This variant reduces string allocations as much as possible, defers them to the +/// time of actual rendering, and is capable of storing references. +/// +/// ## Example +/// +/// ``` +/// use query_string_builder::QueryString; +/// +/// let weight: &f32 = &99.9; +/// +/// let qs = QueryString::simple() +/// .with_value("q", "apple") +/// .with_value("category", "fruits and vegetables") +/// .with_opt_value("weight", Some(weight)); +/// +/// assert_eq!( +/// format!("https://example.com/{qs}"), +/// "https://example.com/?q=apple&category=fruits%20and%20vegetables&weight=99.9" +/// ); +/// ``` +pub struct WrappedQueryString +where + B: ConditionalDisplay + Identifyable, + T: Display, +{ + base: BaseOption, + value: KvpOption, +} + +impl Default for QueryStringSimple { + fn default() -> Self { + QueryString::simple() + } +} + +/// A helper type to track the values of [`WrappedQueryString`]. +pub struct Kvp +where + K: Display, + V: Display, +{ + key: K, + value: V, +} + +enum BaseOption { + Some(B), + None, +} + +enum KvpOption { + Some(T), + None, +} + +/// This type serves as a root marker for the builder. It has no public constructor, +/// thus can only be created within this crate. +pub struct RootMarker(()); + +/// This type serves as an empty value marker for the builder. It has no public constructor, +/// thus can only be created within this crate. +pub struct EmptyValue(()); + +impl WrappedQueryString +where + B: ConditionalDisplay + Identifyable, + T: Display, +{ + /// Creates a new, empty query string builder. + pub(crate) fn new() -> WrappedQueryString { + WrappedQueryString { + base: BaseOption::None, + value: KvpOption::None, + } + } + + /// Appends a key-value pair to the query string. + /// + /// ## Example + /// + /// ``` + /// use query_string_builder::QueryString; + /// + /// let qs = QueryString::dynamic() + /// .with_value("q", "🍎 apple") + /// .with_value("category", "fruits and vegetables") + /// .with_value("answer", 42); + /// + /// assert_eq!( + /// format!("https://example.com/{qs}"), + /// "https://example.com/?q=%F0%9F%8D%8E%20apple&category=fruits%20and%20vegetables&answer=42" + /// ); + /// ``` + pub fn with_value( + self, + key: K, + value: V, + ) -> WrappedQueryString> { + WrappedQueryString { + base: BaseOption::Some(self), + value: KvpOption::Some(Kvp { key, value }), + } + } + + /// Appends a key-value pair to the query string if the value exists. + /// + /// ## Example + /// + /// ``` + /// use query_string_builder::QueryString; + /// + /// let qs = QueryString::dynamic() + /// .with_opt_value("q", Some("🍎 apple")) + /// .with_opt_value("f", None::) + /// .with_opt_value("category", Some("fruits and vegetables")) + /// .with_opt_value("works", Some(true)); + /// + /// assert_eq!( + /// format!("https://example.com/{qs}"), + /// "https://example.com/?q=%F0%9F%8D%8E%20apple&category=fruits%20and%20vegetables&works=true" + /// ); + /// ``` + pub fn with_opt_value( + self, + key: K, + value: Option, + ) -> WrappedQueryString> { + if let Some(value) = value { + WrappedQueryString { + base: BaseOption::Some(self), + value: KvpOption::Some(Kvp { key, value }), + } + } else { + WrappedQueryString { + base: BaseOption::Some(self), + value: KvpOption::None, + } + } + } + + /// Determines the number of key-value pairs currently in the builder. + pub fn len(&self) -> usize { + if self.is_empty() { + return 0; + } + + 1 + self.base.len() + } + + /// Determines if the builder is currently empty. + pub fn is_empty(&self) -> bool { + // If this is the root node, and we don't have a value, we're empty. + if self.is_root() && self.value.is_empty() { + return true; + } + + // If we're not the root node we need to check if all values are empty. + if !self.value.is_empty() { + return false; + } + + self.base.is_empty() + } +} + +pub trait Identifyable { + fn is_root(&self) -> bool; + fn is_empty(&self) -> bool; + fn len(&self) -> usize; +} + +impl Identifyable for RootMarker { + fn is_root(&self) -> bool { + true + } + + fn is_empty(&self) -> bool { + true + } + + fn len(&self) -> usize { + 0 + } +} + +pub trait ConditionalDisplay { + fn cond_fmt(&self, should_display: bool, f: &mut Formatter<'_>) -> Result; +} + +impl ConditionalDisplay for RootMarker { + fn cond_fmt(&self, _should_display: bool, _f: &mut Formatter<'_>) -> Result { + unreachable!() + } +} + +impl ConditionalDisplay for BaseOption +where + B: ConditionalDisplay, +{ + fn cond_fmt(&self, should_display: bool, f: &mut Formatter<'_>) -> Result { + match self { + BaseOption::Some(base) => Ok(base.cond_fmt(should_display, f)?), + BaseOption::None => { + // Reached the root marker. + if should_display { + f.write_char('?')?; + } + Ok(0) + } + } + } +} + +impl ConditionalDisplay for WrappedQueryString +where + B: ConditionalDisplay + Identifyable, + T: Display, +{ + fn cond_fmt(&self, should_display: bool, f: &mut Formatter<'_>) -> Result { + let depth = if !should_display { + // Our caller had nothing to display. If we have nothing to display either, + // we move on to our parent. + if self.value.is_empty() { + return self.base.cond_fmt(false, f); + } + + // We do have things to display - render the parent! + self.base.cond_fmt(true, f)? + } else { + // The caller has things to display - go ahead regardless. + self.base.cond_fmt(true, f)? + }; + + // If we have nothing to render, return the known depth. + if self.value.is_empty() { + return Ok(depth); + } + + // Display and increase the depth. + self.value.fmt(f)?; + + // If our parent indicated content was displayable, add the combinator. + if should_display { + f.write_char('&')?; + } + + Ok(depth + 1) + } +} + +impl BaseOption +where + B: Identifyable + ConditionalDisplay, +{ + fn is_empty(&self) -> bool { + match self { + BaseOption::Some(value) => value.is_empty(), + BaseOption::None => true, + } + } + + fn len(&self) -> usize { + match self { + BaseOption::Some(value) => value.len(), + BaseOption::None => 0, + } + } +} + +impl Identifyable for WrappedQueryString +where + B: ConditionalDisplay + Identifyable, + T: Display, +{ + fn is_root(&self) -> bool { + match self.base { + BaseOption::Some(_) => false, + BaseOption::None => true, + } + } + + fn is_empty(&self) -> bool { + match self.value { + KvpOption::Some(_) => false, + KvpOption::None => self.base.is_empty(), + } + } + + fn len(&self) -> usize { + match self.value { + KvpOption::Some(_) => 1 + self.base.len(), + KvpOption::None => self.base.len(), + } + } +} + +impl KvpOption { + fn is_empty(&self) -> bool { + match self { + KvpOption::Some(_) => false, + KvpOption::None => true, + } + } +} + +impl Display for RootMarker { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_char('?') + } +} + +impl Display for EmptyValue { + fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} + +impl Display for BaseOption +where + T: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + BaseOption::Some(d) => Display::fmt(d, f), + BaseOption::None => Ok(()), + } + } +} + +impl Display for KvpOption +where + T: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + KvpOption::Some(d) => Display::fmt(d, f), + KvpOption::None => Ok(()), + } + } +} + +impl Display for Kvp +where + K: Display, + V: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&utf8_percent_encode(&self.key.to_string(), QUERY), f)?; + f.write_char('=')?; + Display::fmt(&utf8_percent_encode(&self.value.to_string(), QUERY), f) + } +} + +impl Display for WrappedQueryString +where + B: ConditionalDisplay + Identifyable, + T: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let should_display = !self.value.is_empty(); + + self.base.cond_fmt(should_display, f)?; + if should_display { + Display::fmt(&self.value, f)?; + } + + Ok(()) + } +} + +impl Debug for WrappedQueryString +where + B: ConditionalDisplay + Identifyable, + T: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +#[cfg(test)] +mod tests { + use crate::QueryString; + + #[test] + fn test_empty() { + let qs = QueryString::simple(); + + assert!(qs.is_empty()); + assert_eq!(qs.len(), 0); + + assert_eq!(qs.to_string(), ""); + } + + #[test] + fn test_empty_complex() { + let qs = QueryString::simple().with_opt_value("key", None::<&str>); + + assert!(qs.is_empty()); + assert_eq!(qs.len(), 0); + + assert_eq!(qs.to_string(), ""); + } + + #[test] + fn test_simple() { + let apple = "apple???"; + + let qs = QueryString::simple() + .with_value("q", &apple) + .with_value("category", "fruits and vegetables") + .with_value("tasty", true) + .with_value("weight", 99.9); + + assert!(!qs.is_empty()); + assert_eq!(qs.len(), 4); + + assert_eq!( + qs.to_string(), + "?q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9" + ); + } + + #[test] + fn test_encoding() { + let qs = QueryString::simple() + .with_value("q", "Grünkohl") + .with_value("category", "Gemüse"); + + assert!(!qs.is_empty()); + assert_eq!(qs.len(), 2); + + assert_eq!(qs.to_string(), "?q=Gr%C3%BCnkohl&category=Gem%C3%BCse"); + } + + #[test] + fn test_emoji() { + let qs = QueryString::simple() + .with_value("q", "🥦") + .with_value("🍽️", "🍔🍕"); + + assert!(!qs.is_empty()); + assert_eq!(qs.len(), 2); + + assert_eq!( + qs.to_string(), + "?q=%F0%9F%A5%A6&%F0%9F%8D%BD%EF%B8%8F=%F0%9F%8D%94%F0%9F%8D%95" + ); + } + + #[test] + fn test_optional() { + let qs = QueryString::simple() + .with_value("q", "celery") + .with_opt_value("taste", None::) + .with_opt_value("category", Some("fruits and vegetables")) + .with_opt_value("tasty", Some(true)) + .with_opt_value("weight", Some(99.9)); + + assert!(!qs.is_empty()); + assert_eq!(qs.len(), 4); + + assert_eq!( + qs.to_string(), + "?q=celery&category=fruits%20and%20vegetables&tasty=true&weight=99.9" + ); + assert_eq!(qs.len(), 4); // not five! + } +} From 557308b159f23a25c55c6c755a5bdaa3d9acd3a8 Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Sat, 8 Jun 2024 00:17:16 +0200 Subject: [PATCH 2/3] Add codespell workflow --- .codespellrc | 3 +++ .github/workflows/codespell.yml | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .codespellrc create mode 100644 .github/workflows/codespell.yml diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..66eacc0 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,3 @@ +[codespell] +ignore-words-list = crate +skip = .git,*.lock diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 0000000..ca60d4f --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,19 @@ +--- +name: Codespell + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + codespell: + name: Check for spelling errors + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Codespell + uses: codespell-project/actions-codespell@v2 From 872277075b8ac1d5ef9cf472153f2d713116165e Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Sat, 8 Jun 2024 00:20:56 +0200 Subject: [PATCH 3/3] Improve code coverage --- src/lib.rs | 4 ++-- src/slim.rs | 64 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6d4a870..a06eba8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,7 +89,7 @@ impl QueryString { /// ``` #[allow(clippy::new_ret_no_self)] pub fn simple() -> QueryStringSimple { - QueryStringSimple::new() + QueryStringSimple::default() } /// Creates a new, empty query string builder. @@ -284,7 +284,7 @@ mod tests { #[test] fn test_empty() { - let qs = QueryString::dynamic(); + let qs = QueryStringSimple::default(); assert_eq!(qs.to_string(), ""); assert_eq!(qs.len(), 0); assert!(qs.is_empty()); diff --git a/src/slim.rs b/src/slim.rs index 78ac4a3..154ddb5 100644 --- a/src/slim.rs +++ b/src/slim.rs @@ -1,7 +1,7 @@ use std::fmt; use std::fmt::{Debug, Display, Formatter, Write}; -use crate::{QueryString, QUERY}; +use crate::QUERY; use percent_encoding::utf8_percent_encode; /// A type alias for the [`WrappedQueryString`] root. @@ -30,7 +30,7 @@ pub type QueryStringSimple = WrappedQueryString; /// ``` pub struct WrappedQueryString where - B: ConditionalDisplay + Identifyable, + B: ConditionalDisplay + Identifiable, T: Display, { base: BaseOption, @@ -39,7 +39,7 @@ where impl Default for QueryStringSimple { fn default() -> Self { - QueryString::simple() + QueryStringSimple::new() } } @@ -73,7 +73,7 @@ pub struct EmptyValue(()); impl WrappedQueryString where - B: ConditionalDisplay + Identifyable, + B: ConditionalDisplay + Identifiable, T: Display, { /// Creates a new, empty query string builder. @@ -173,36 +173,42 @@ where } } -pub trait Identifyable { +pub trait Identifiable { fn is_root(&self) -> bool; fn is_empty(&self) -> bool; fn len(&self) -> usize; } -impl Identifyable for RootMarker { +pub trait ConditionalDisplay { + fn cond_fmt(&self, should_display: bool, f: &mut Formatter<'_>) -> Result; +} + +impl Identifiable for RootMarker { fn is_root(&self) -> bool { - true + unreachable!() } fn is_empty(&self) -> bool { - true + unreachable!() } fn len(&self) -> usize { - 0 + unreachable!() } } -pub trait ConditionalDisplay { - fn cond_fmt(&self, should_display: bool, f: &mut Formatter<'_>) -> Result; -} - impl ConditionalDisplay for RootMarker { fn cond_fmt(&self, _should_display: bool, _f: &mut Formatter<'_>) -> Result { unreachable!() } } +impl Display for RootMarker { + fn fmt(&self, _f: &mut Formatter<'_>) -> fmt::Result { + unreachable!() + } +} + impl ConditionalDisplay for BaseOption where B: ConditionalDisplay, @@ -223,7 +229,7 @@ where impl ConditionalDisplay for WrappedQueryString where - B: ConditionalDisplay + Identifyable, + B: ConditionalDisplay + Identifiable, T: Display, { fn cond_fmt(&self, should_display: bool, f: &mut Formatter<'_>) -> Result { @@ -260,7 +266,7 @@ where impl BaseOption where - B: Identifyable + ConditionalDisplay, + B: Identifiable + ConditionalDisplay, { fn is_empty(&self) -> bool { match self { @@ -277,9 +283,9 @@ where } } -impl Identifyable for WrappedQueryString +impl Identifiable for WrappedQueryString where - B: ConditionalDisplay + Identifyable, + B: ConditionalDisplay + Identifiable, T: Display, { fn is_root(&self) -> bool { @@ -313,14 +319,8 @@ impl KvpOption { } } -impl Display for RootMarker { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_char('?') - } -} - impl Display for EmptyValue { - fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, _f: &mut Formatter<'_>) -> fmt::Result { Ok(()) } } @@ -363,7 +363,7 @@ where impl Display for WrappedQueryString where - B: ConditionalDisplay + Identifyable, + B: ConditionalDisplay + Identifiable, T: Display, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { @@ -380,7 +380,7 @@ where impl Debug for WrappedQueryString where - B: ConditionalDisplay + Identifyable, + B: ConditionalDisplay + Identifiable, T: Display, { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { @@ -390,6 +390,7 @@ where #[cfg(test)] mod tests { + use crate::slim::{BaseOption, EmptyValue, KvpOption}; use crate::QueryString; #[test] @@ -426,7 +427,7 @@ mod tests { assert_eq!(qs.len(), 4); assert_eq!( - qs.to_string(), + format!("{qs}"), "?q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9" ); } @@ -453,7 +454,7 @@ mod tests { assert_eq!(qs.len(), 2); assert_eq!( - qs.to_string(), + format!("{qs:?}"), "?q=%F0%9F%A5%A6&%F0%9F%8D%BD%EF%B8%8F=%F0%9F%8D%94%F0%9F%8D%95" ); } @@ -476,4 +477,11 @@ mod tests { ); assert_eq!(qs.len(), 4); // not five! } + + #[test] + fn test_display() { + assert_eq!(format!("{}", KvpOption::::None), ""); + assert_eq!(format!("{}", BaseOption::::None), ""); + assert_eq!(format!("{}", EmptyValue(())), ""); + } }