From 323ba48eefb7216248770dd1a4567b8a82c58ead Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Thu, 23 May 2024 15:16:24 +0200 Subject: [PATCH 01/15] Change function argements to take T: ToString See #2 --- CHANGELOG.md | 8 ++++++++ src/lib.rs | 31 +++++++++++++++++-------------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0457a29..8d5536b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ 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 + +### Changed + +- [#3](https://github.com/sunsided/query-string-builder/pull/3): + The functions now change inputs that implement `ToString` rather than requiring `Into`. + This allows for any `Display` types to be used directly. + ## [0.4.0] - 2023-07-08 ### Added diff --git a/src/lib.rs b/src/lib.rs index e6482a8..847dec7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,17 +65,18 @@ impl QueryString { /// /// let qs = QueryString::new() /// .with_value("q", "🍎 apple") - /// .with_value("category", "fruits and vegetables"); + /// .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" + /// "https://example.com/?q=%F0%9F%8D%8E%20apple&category=fruits%20and%20vegetables&answer=42" /// ); /// ``` - pub fn with_value, V: Into>(mut self, key: K, value: V) -> Self { + pub fn with_value(mut self, key: K, value: V) -> Self { self.pairs.push(Kvp { - key: key.into(), - value: value.into(), + key: key.to_string(), + value: value.to_string(), }); self } @@ -90,14 +91,15 @@ impl QueryString { /// let qs = QueryString::new() /// .with_opt_value("q", Some("🍎 apple")) /// .with_opt_value("f", None::) - /// .with_opt_value("category", Some("fruits and vegetables")); + /// .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" + /// "https://example.com/?q=%F0%9F%8D%8E%20apple&category=fruits%20and%20vegetables&works=true" /// ); /// ``` - pub fn with_opt_value, V: Into>( + pub fn with_opt_value( self, key: K, value: Option, @@ -125,10 +127,10 @@ impl QueryString { /// "https://example.com/?q=apple&category=fruits%20and%20vegetables" /// ); /// ``` - pub fn push, V: Into>(&mut self, key: K, value: V) -> &Self { + pub fn push(&mut self, key: K, value: V) -> &Self { self.pairs.push(Kvp { - key: key.into(), - value: value.into(), + key: key.to_string(), + value: value.to_string(), }); self } @@ -149,7 +151,7 @@ impl QueryString { /// "https://example.com/?q=%F0%9F%8D%8E%20apple" /// ); /// ``` - pub fn push_opt, V: Into>( + pub fn push_opt( &mut self, key: K, value: Option, @@ -262,8 +264,9 @@ mod tests { fn test_encoding() { let qs = QueryString::new() .with_value("q", "Grünkohl") - .with_value("category", "Gemüse"); - assert_eq!(qs.to_string(), "?q=Gr%C3%BCnkohl&category=Gem%C3%BCse"); + .with_value("category", "Gemüse") + .with_value("answer", 42); + assert_eq!(qs.to_string(), "?q=Gr%C3%BCnkohl&category=Gem%C3%BCse&answer=42"); } #[test] From 7f75d1c24d9fcf3a381f6d98da45b62a9172eb99 Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Thu, 23 May 2024 15:44:41 +0200 Subject: [PATCH 02/15] Add more display examples to README and documentation --- README.md | 3 ++- src/lib.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fff0e1e..16105d2 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,13 @@ use query_string_builder::QueryString; fn main() { let qs = QueryString::new() .with_value("q", "apple") + .with_value("tasty", true) .with_opt_value("color", None::) .with_opt_value("category", Some("fruits and vegetables?")); assert_eq!( format!("https://example.com/{qs}"), - "https://example.com/?q=apple&category=fruits%20and%20vegetables?" + "https://example.com/?q=apple&tasty=true&category=fruits%20and%20vegetables?&tasty=true" ); } ``` diff --git a/src/lib.rs b/src/lib.rs index 847dec7..2135783 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,12 +10,13 @@ //! //! let qs = QueryString::new() //! .with_value("q", "🍎 apple") +//! .with_value("tasty", true) //! .with_opt_value("color", None::) //! .with_opt_value("category", Some("fruits and vegetables?")); //! //! assert_eq!( //! format!("example.com/{qs}"), -//! "example.com/?q=%F0%9F%8D%8E%20apple&category=fruits%20and%20vegetables?" +//! "example.com/?q=%F0%9F%8D%8E%20apple&tasty=true&category=fruits%20and%20vegetables?" //! ); //! ``` From 684da0e3c873c187463f04def4a5ac7a4c9da59c Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Thu, 23 May 2024 16:09:58 +0200 Subject: [PATCH 03/15] Add more ToString test cases --- src/lib.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3b94202..9dc572c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -255,12 +255,14 @@ mod tests { fn test_simple() { let qs = QueryString::new() .with_value("q", "apple???") - .with_value("category", "fruits and vegetables"); + .with_value("category", "fruits and vegetables") + .with_value("tasty", true) + .with_value("weight", 99.9); assert_eq!( qs.to_string(), - "?q=apple???&category=fruits%20and%20vegetables" + "?q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9" ); - assert_eq!(qs.len(), 2); + assert_eq!(qs.len(), 4); assert!(!qs.is_empty()); } @@ -268,12 +270,8 @@ mod tests { fn test_encoding() { let qs = QueryString::new() .with_value("q", "Grünkohl") - .with_value("category", "Gemüse") - .with_value("answer", 42); - assert_eq!( - qs.to_string(), - "?q=Gr%C3%BCnkohl&category=Gem%C3%BCse&answer=42" - ); + .with_value("category", "Gemüse"); + assert_eq!(qs.to_string(), "?q=Gr%C3%BCnkohl&category=Gem%C3%BCse"); } #[test] @@ -292,12 +290,14 @@ mod tests { let qs = QueryString::new() .with_value("q", "celery") .with_opt_value("taste", None::) - .with_opt_value("category", Some("fruits and vegetables")); + .with_opt_value("category", Some("fruits and vegetables")) + .with_opt_value("tasty", Some(true)) + .with_opt_value("weight", Some(99.9)); assert_eq!( qs.to_string(), - "?q=celery&category=fruits%20and%20vegetables" + "?q=celery&category=fruits%20and%20vegetables&tasty=true&weight=99.9" ); - assert_eq!(qs.len(), 2); // not three! + assert_eq!(qs.len(), 4); // not five! } #[test] From 8cf110622202c4b09b78a113e9dfcabe369a474e Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Thu, 23 May 2024 19:11:52 +0200 Subject: [PATCH 04/15] Add draft for lazily evaluated values --- src/lib.rs | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 142 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9dc572c..4dfd9af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,9 @@ #![deny(unsafe_code)] -use std::fmt::{Debug, Display, Formatter}; +use std::fmt::{Display, Formatter}; +use std::rc::Rc; +use std::sync::Arc; use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; @@ -45,7 +47,7 @@ const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').ad /// "https://example.com/?q=apple&category=fruits%20and%20vegetables" /// ); /// ``` -#[derive(Debug, Default, Clone)] +#[derive(Default)] pub struct QueryString { pairs: Vec, } @@ -78,7 +80,33 @@ impl QueryString { pub fn with_value(mut self, key: K, value: V) -> Self { self.pairs.push(Kvp { key: key.to_string(), - value: value.to_string(), + value: Value::eager(value), + }); + self + } + + /// Appends a key-value pair to the query string. Supports lazy evaluation. + /// + /// ## Example + /// + /// ``` + /// use std::sync::Arc; + /// use query_string_builder::{QueryString, Value}; + /// + /// let qs = QueryString::new() + /// .with("q", Value::eager("🍎 apple")) + /// .with("category", || "fruits and vegetables") + /// .with("answer", Arc::new(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>(mut self, key: K, value: V) -> Self { + self.pairs.push(Kvp { + key: key.to_string(), + value: value.into(), }); self } @@ -128,7 +156,7 @@ impl QueryString { pub fn push(&mut self, key: K, value: V) -> &Self { self.pairs.push(Kvp { key: key.to_string(), - value: value.to_string(), + value: Value::eager(value), }); self } @@ -221,11 +249,13 @@ impl Display for QueryString { if i > 0 { write!(f, "&")?; } + + let value = pair.value.render(); write!( f, "{key}={value}", key = utf8_percent_encode(&pair.key, FRAGMENT), - value = utf8_percent_encode(&pair.value, FRAGMENT) + value = utf8_percent_encode(&value, FRAGMENT) )?; } Ok(()) @@ -233,10 +263,88 @@ impl Display for QueryString { } } -#[derive(Debug, Clone)] struct Kvp { key: String, - value: String, + value: Value, +} + +pub struct Value { + value: Box String>, +} + +impl Value { + pub fn eager(value: T) -> Self { + let value = value.to_string(); + Value { + value: Box::new(move || value.to_string()), + } + } + + pub fn lazy(value: T) -> Self { + Value { + value: Box::new(move || value.to_string()), + } + } + + pub fn lazy_box(value: Box) -> Self { + Value { + value: Box::new(move || value.to_string()), + } + } + + pub fn lazy_rc(value: Rc) -> Self { + Value { + value: Box::new(move || value.to_string()), + } + } + + pub fn lazy_arc(value: Arc) -> Self { + Value { + value: Box::new(move || value.to_string()), + } + } + + pub fn lazy_fn(func: F) -> Self + where + F: Fn() -> T + 'static, + T: ToString + 'static, + { + Value { + value: Box::new(move || func().to_string()), + } + } + + fn render(&self) -> String { + (self.value)() + } +} + +impl From> for Value +where + T: ToString + 'static, +{ + fn from(value: Rc) -> Self { + Value::lazy_rc(value) + } +} + +impl From> for Value +where + T: ToString + 'static, +{ + fn from(value: Arc) -> Self { + Value::lazy_arc(value) + } +} + +impl From for Value +where + F: Fn() -> T + 'static, + T: ToString + 'static, +{ + fn from(value: F) -> Self { + Value::lazy_fn(value) + } } #[cfg(test)] @@ -266,6 +374,33 @@ mod tests { assert!(!qs.is_empty()); } + #[test] + fn test_lazy() { + let qs = QueryString::new() + .with("x", Value::eager("y")) + .with("q", Value::lazy("apple???")) + .with("category", Value::lazy_fn(|| "fruits and vegetables")) + .with("tasty", Value::lazy_box(Box::new(true))) + .with("weight", Value::lazy_arc(Arc::new(99.9))); + assert_eq!( + qs.to_string(), + "?x=y&q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9" + ); + } + + #[test] + fn test_lazy_implicit() { + let qs = QueryString::new() + .with("q", Value::lazy("apple???")) + .with("category", || "fruits and vegetables") + .with("tasty", Rc::new(true)) + .with("weight", Arc::new(99.9)); + assert_eq!( + qs.to_string(), + "?q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9" + ); + } + #[test] fn test_encoding() { let qs = QueryString::new() From 2dd9df5fdebcf3d2ef0586c7f582b1cae9194ab3 Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Thu, 23 May 2024 19:16:16 +0200 Subject: [PATCH 05/15] Add note about string generation and percent encoding --- src/lib.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4dfd9af..8c5f572 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,7 @@ use std::fmt::{Display, Formatter}; use std::rc::Rc; use std::sync::Arc; -use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; +use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode}; /// https://url.spec.whatwg.org/#fragment-percent-encode-set const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); @@ -250,7 +250,11 @@ impl Display for QueryString { write!(f, "&")?; } + // We convert the value to a String here. Ideally we'd print it directly (skipping + // the intermediate string generation) but due to the required percent encoding + // this isn't immediately possible. let value = pair.value.render(); + write!( f, "{key}={value}", @@ -305,9 +309,9 @@ impl Value { } pub fn lazy_fn(func: F) -> Self - where - F: Fn() -> T + 'static, - T: ToString + 'static, + where + F: Fn() -> T + 'static, + T: ToString + 'static, { Value { value: Box::new(move || func().to_string()), @@ -320,8 +324,8 @@ impl Value { } impl From> for Value -where - T: ToString + 'static, + where + T: ToString + 'static, { fn from(value: Rc) -> Self { Value::lazy_rc(value) @@ -329,8 +333,8 @@ where } impl From> for Value -where - T: ToString + 'static, + where + T: ToString + 'static, { fn from(value: Arc) -> Self { Value::lazy_arc(value) @@ -338,9 +342,9 @@ where } impl From for Value -where - F: Fn() -> T + 'static, - T: ToString + 'static, + where + F: Fn() -> T + 'static, + T: ToString + 'static, { fn from(value: F) -> Self { Value::lazy_fn(value) From 8ada3ca981043ce365aa85ee79194e92bc88aced Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Thu, 23 May 2024 20:11:11 +0200 Subject: [PATCH 06/15] Fix code formatting --- src/lib.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8c5f572..5986a55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,7 @@ use std::fmt::{Display, Formatter}; use std::rc::Rc; use std::sync::Arc; -use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode}; +use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; /// https://url.spec.whatwg.org/#fragment-percent-encode-set const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); @@ -309,9 +309,9 @@ impl Value { } pub fn lazy_fn(func: F) -> Self - where - F: Fn() -> T + 'static, - T: ToString + 'static, + where + F: Fn() -> T + 'static, + T: ToString + 'static, { Value { value: Box::new(move || func().to_string()), @@ -324,8 +324,8 @@ impl Value { } impl From> for Value - where - T: ToString + 'static, +where + T: ToString + 'static, { fn from(value: Rc) -> Self { Value::lazy_rc(value) @@ -333,8 +333,8 @@ impl From> for Value } impl From> for Value - where - T: ToString + 'static, +where + T: ToString + 'static, { fn from(value: Arc) -> Self { Value::lazy_arc(value) @@ -342,9 +342,9 @@ impl From> for Value } impl From for Value - where - F: Fn() -> T + 'static, - T: ToString + 'static, +where + F: Fn() -> T + 'static, + T: ToString + 'static, { fn from(value: F) -> Self { Value::lazy_fn(value) From 28eef6e707ac1d6abab5836c5ac94bbb1b3b20b3 Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Fri, 24 May 2024 17:19:42 +0200 Subject: [PATCH 07/15] Re-implement deferred evaluation --- src/lib.rs | 202 ++++++++++++----------------------------------------- 1 file changed, 43 insertions(+), 159 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5986a55..aeb3e0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,14 +22,12 @@ #![deny(unsafe_code)] -use std::fmt::{Display, Formatter}; -use std::rc::Rc; -use std::sync::Arc; +use std::fmt::{Display, Formatter, Write}; -use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; +use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode}; /// https://url.spec.whatwg.org/#fragment-percent-encode-set -const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); +const QUERY: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); /// A query string builder for percent encoding key-value pairs. /// @@ -48,11 +46,11 @@ const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').ad /// ); /// ``` #[derive(Default)] -pub struct QueryString { - pairs: Vec, +pub struct QueryString<'a> { + pairs: Vec>, } -impl QueryString { +impl<'a> QueryString<'a> { /// Creates a new, empty query string builder. pub fn new() -> Self { Self { @@ -77,37 +75,8 @@ impl QueryString { /// "https://example.com/?q=%F0%9F%8D%8E%20apple&category=fruits%20and%20vegetables&answer=42" /// ); /// ``` - pub fn with_value(mut self, key: K, value: V) -> Self { - self.pairs.push(Kvp { - key: key.to_string(), - value: Value::eager(value), - }); - self - } - - /// Appends a key-value pair to the query string. Supports lazy evaluation. - /// - /// ## Example - /// - /// ``` - /// use std::sync::Arc; - /// use query_string_builder::{QueryString, Value}; - /// - /// let qs = QueryString::new() - /// .with("q", Value::eager("🍎 apple")) - /// .with("category", || "fruits and vegetables") - /// .with("answer", Arc::new(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>(mut self, key: K, value: V) -> Self { - self.pairs.push(Kvp { - key: key.to_string(), - value: value.into(), - }); + pub fn with_value(mut self, key: K, value: V) -> Self { + self.pairs.push(Kvp::new(QueryPart::from_tostring(key), QueryPart::from_tostring(value))); self } @@ -129,7 +98,7 @@ impl QueryString { /// "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) -> Self { + pub fn with_opt_value(self, key: K, value: Option) -> Self { if let Some(value) = value { self.with_value(key, value) } else { @@ -153,11 +122,8 @@ impl QueryString { /// "https://example.com/?q=apple&category=fruits%20and%20vegetables" /// ); /// ``` - pub fn push(&mut self, key: K, value: V) -> &Self { - self.pairs.push(Kvp { - key: key.to_string(), - value: Value::eager(value), - }); + pub fn push(&mut self, key: K, value: V) -> &Self { + self.pairs.push(Kvp::new(QueryPart::from_tostring(key), QueryPart::from_tostring(value))); self } @@ -177,7 +143,7 @@ impl QueryString { /// "https://example.com/?q=%F0%9F%8D%8E%20apple" /// ); /// ``` - pub fn push_opt(&mut self, key: K, value: Option) -> &Self { + pub fn push_opt(&mut self, key: K, value: Option) -> &Self { if let Some(value) = value { self.push(key, value) } else { @@ -212,7 +178,7 @@ impl QueryString { /// "https://example.com/?q=apple&q=pear" /// ); /// ``` - pub fn append(&mut self, mut other: QueryString) { + pub fn append(&mut self, mut other: QueryString<'a>) { self.pairs.append(&mut other.pairs) } @@ -233,121 +199,66 @@ impl QueryString { /// "https://example.com/?q=apple&q=pear" /// ); /// ``` - pub fn append_into(mut self, mut other: QueryString) -> Self { + pub fn append_into(mut self, mut other: QueryString<'a>) -> Self { self.pairs.append(&mut other.pairs); self } } -impl Display for QueryString { +impl<'a> Display for QueryString<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if self.pairs.is_empty() { Ok(()) } else { - write!(f, "?")?; + f.write_char('?')?; for (i, pair) in self.pairs.iter().enumerate() { if i > 0 { - write!(f, "&")?; + f.write_char('&')?; } - // We convert the value to a String here. Ideally we'd print it directly (skipping - // the intermediate string generation) but due to the required percent encoding - // this isn't immediately possible. - let value = pair.value.render(); - - write!( - f, - "{key}={value}", - key = utf8_percent_encode(&pair.key, FRAGMENT), - value = utf8_percent_encode(&value, FRAGMENT) - )?; + Display::fmt(&utf8_percent_encode(&pair.key.to_string(), QUERY), f)?; + f.write_char('=')?; + Display::fmt(&utf8_percent_encode(&pair.value.to_string(), QUERY), f)?; } Ok(()) } } } -struct Kvp { - key: String, - value: Value, -} +pub struct Key<'a>(QueryPart<'a>); -pub struct Value { - value: Box String>, -} +pub struct Value<'a>(QueryPart<'a>); -impl Value { - pub fn eager(value: T) -> Self { - let value = value.to_string(); - Value { - value: Box::new(move || value.to_string()), - } - } - - pub fn lazy(value: T) -> Self { - Value { - value: Box::new(move || value.to_string()), - } - } - - pub fn lazy_box(value: Box) -> Self { - Value { - value: Box::new(move || value.to_string()), - } - } - - pub fn lazy_rc(value: Rc) -> Self { - Value { - value: Box::new(move || value.to_string()), - } - } - - pub fn lazy_arc(value: Arc) -> Self { - Value { - value: Box::new(move || value.to_string()), - } - } - - pub fn lazy_fn(func: F) -> Self - where - F: Fn() -> T + 'static, - T: ToString + 'static, - { - Value { - value: Box::new(move || func().to_string()), - } - } +struct Kvp<'a> { + key: QueryPart<'a>, + value: QueryPart<'a>, +} - fn render(&self) -> String { - (self.value)() +impl<'a> Kvp<'a> { + pub fn new>, V: Into>>(key: K, value: V) -> Self { + Self { key: key.into(), value: value.into() } } } -impl From> for Value -where - T: ToString + 'static, -{ - fn from(value: Rc) -> Self { - Value::lazy_rc(value) - } +enum QueryPart<'a> { + Owned(Box), + Reference(&'a dyn ToString), + Boxed(Box std::fmt::Result>), } -impl From> for Value -where - T: ToString + 'static, -{ - fn from(value: Arc) -> Self { - Value::lazy_arc(value) +impl<'a> QueryPart<'a> { + pub fn from_tostring(text: T) -> Self { + Self::Owned(Box::new(text)) } } -impl From for Value -where - F: Fn() -> T + 'static, - T: ToString + 'static, -{ - fn from(value: F) -> Self { - Value::lazy_fn(value) +impl<'a> Display for QueryPart<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + QueryPart::Owned(b) => f.write_str(&b.to_string()), + QueryPart::Reference(b) => f.write_str(&b.to_string()), + QueryPart::Boxed(b) => b(f), + } } } @@ -378,33 +289,6 @@ mod tests { assert!(!qs.is_empty()); } - #[test] - fn test_lazy() { - let qs = QueryString::new() - .with("x", Value::eager("y")) - .with("q", Value::lazy("apple???")) - .with("category", Value::lazy_fn(|| "fruits and vegetables")) - .with("tasty", Value::lazy_box(Box::new(true))) - .with("weight", Value::lazy_arc(Arc::new(99.9))); - assert_eq!( - qs.to_string(), - "?x=y&q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9" - ); - } - - #[test] - fn test_lazy_implicit() { - let qs = QueryString::new() - .with("q", Value::lazy("apple???")) - .with("category", || "fruits and vegetables") - .with("tasty", Rc::new(true)) - .with("weight", Arc::new(99.9)); - assert_eq!( - qs.to_string(), - "?q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9" - ); - } - #[test] fn test_encoding() { let qs = QueryString::new() From d025d1fc91b839b9379fe620e216ee0bfe4ae8f6 Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Fri, 24 May 2024 18:15:20 +0200 Subject: [PATCH 08/15] Add deferred rendering for str and owned ToString --- src/lib.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index aeb3e0f..7507bda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,7 +76,12 @@ impl<'a> QueryString<'a> { /// ); /// ``` pub fn with_value(mut self, key: K, value: V) -> Self { - self.pairs.push(Kvp::new(QueryPart::from_tostring(key), QueryPart::from_tostring(value))); + self.pairs.push(Kvp::new(QueryPart::from_tostring_value(key), QueryPart::from_tostring_value(value))); + self + } + + pub fn with>, V: Into>>(mut self, key: K, value: V) -> Self { + self.pairs.push(Kvp::new(key.into().0, value.into().0)); self } @@ -123,7 +128,7 @@ impl<'a> QueryString<'a> { /// ); /// ``` pub fn push(&mut self, key: K, value: V) -> &Self { - self.pairs.push(Kvp::new(QueryPart::from_tostring(key), QueryPart::from_tostring(value))); + self.pairs.push(Kvp::new(QueryPart::from_tostring_value(key), QueryPart::from_tostring_value(value))); self } @@ -216,9 +221,9 @@ impl<'a> Display for QueryString<'a> { f.write_char('&')?; } - Display::fmt(&utf8_percent_encode(&pair.key.to_string(), QUERY), f)?; + Display::fmt(&pair.key, f)?; f.write_char('=')?; - Display::fmt(&utf8_percent_encode(&pair.value.to_string(), QUERY), f)?; + Display::fmt(&pair.value, f)?; } Ok(()) } @@ -229,6 +234,34 @@ pub struct Key<'a>(QueryPart<'a>); pub struct Value<'a>(QueryPart<'a>); +impl<'a> Key<'a> { + pub fn from_str(key: &'a str) -> Key<'a> { + Self(QueryPart::RefStr(key)) + } +} + +impl<'a> Value<'a> { + pub fn from(value: T) -> Self { + Self(QueryPart::Owned(Box::new(value))) + } + + pub fn from_str(value: &'a str) -> Self { + Self(QueryPart::RefStr(&value)) + } +} + +impl<'a> From<&'static str> for Key<'a> { + fn from(value: &'static str) -> Self { + Self::from_str(value) + } +} + +impl<'a> From<&'static str> for Value<'a> { + fn from(value: &'static str) -> Self { + Self::from_str(value) + } +} + struct Kvp<'a> { key: QueryPart<'a>, value: QueryPart<'a>, @@ -241,23 +274,32 @@ impl<'a> Kvp<'a> { } enum QueryPart<'a> { + /// Captures a string reference. + RefStr(&'a str), Owned(Box), Reference(&'a dyn ToString), - Boxed(Box std::fmt::Result>), + DisplayFn(Box std::fmt::Result>), } impl<'a> QueryPart<'a> { - pub fn from_tostring(text: T) -> Self { + pub fn from_tostring_value(text: T) -> Self { Self::Owned(Box::new(text)) } + + pub fn from_tostring_ref(text: &'a T) -> Self { + Self::Reference(text) + } } impl<'a> Display for QueryPart<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let encode = |x| utf8_percent_encode(x, QUERY); + let mut write = |x| Display::fmt(&encode(x), f); match self { - QueryPart::Owned(b) => f.write_str(&b.to_string()), - QueryPart::Reference(b) => f.write_str(&b.to_string()), - QueryPart::Boxed(b) => b(f), + QueryPart::Owned(b) => write(&b.to_string()), + QueryPart::Reference(b) => write(&b.to_string()), + QueryPart::DisplayFn(b) => b(f), + QueryPart::RefStr(s) => write(s) } } } @@ -289,6 +331,28 @@ mod tests { assert!(!qs.is_empty()); } + #[test] + fn test_deferred() { + let query = String::from("apple???"); + + struct Complex; + impl Display for Complex { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "complex") + } + } + + let complex = Complex; + + let qs = QueryString::new() + .with("q", Value::from_str(&query)) + .with("owned", Value::from(complex)); + assert_eq!( + qs.to_string(), + "?q=apple???&owned=complex" + ); + } + #[test] fn test_encoding() { let qs = QueryString::new() From bd968bd9b92b85c7ec4ab4ad54aa439dda698c08 Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Fri, 24 May 2024 18:17:54 +0200 Subject: [PATCH 09/15] Remove string lifetime restrictions --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7507bda..3bb8a0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -250,14 +250,14 @@ impl<'a> Value<'a> { } } -impl<'a> From<&'static str> for Key<'a> { - fn from(value: &'static str) -> Self { +impl<'a> From<&'a str> for Key<'a> { + fn from(value: &'a str) -> Self { Self::from_str(value) } } -impl<'a> From<&'static str> for Value<'a> { - fn from(value: &'static str) -> Self { +impl<'a> From<&'a str> for Value<'a> { + fn from(value: &'a str) -> Self { Self::from_str(value) } } From d07483d32c43a544bda405a20af442551214f5b1 Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Fri, 24 May 2024 18:30:04 +0200 Subject: [PATCH 10/15] Add working versions for borrowed ToString --- src/lib.rs | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3bb8a0e..5fe2135 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,8 +80,9 @@ impl<'a> QueryString<'a> { self } + /// TODO: Provide documentation pub fn with>, V: Into>>(mut self, key: K, value: V) -> Self { - self.pairs.push(Kvp::new(key.into().0, value.into().0)); + self.pairs.push(Kvp::new(key.into(), value.into())); self } @@ -128,7 +129,7 @@ impl<'a> QueryString<'a> { /// ); /// ``` pub fn push(&mut self, key: K, value: V) -> &Self { - self.pairs.push(Kvp::new(QueryPart::from_tostring_value(key), QueryPart::from_tostring_value(value))); + self.pairs.push(Kvp::new(Key::from(key), Value::from(value))); self } @@ -235,6 +236,14 @@ pub struct Key<'a>(QueryPart<'a>); pub struct Value<'a>(QueryPart<'a>); impl<'a> Key<'a> { + pub fn from(value: T) -> Self { + Self(QueryPart::Owned(Box::new(value))) + } + + pub fn from_ref(value: &'a T) -> Self { + Self(QueryPart::Reference(value)) + } + pub fn from_str(key: &'a str) -> Key<'a> { Self(QueryPart::RefStr(key)) } @@ -245,6 +254,10 @@ impl<'a> Value<'a> { Self(QueryPart::Owned(Box::new(value))) } + pub fn from_ref(value: &'a T) -> Self { + Self(QueryPart::Reference(value)) + } + pub fn from_str(value: &'a str) -> Self { Self(QueryPart::RefStr(&value)) } @@ -277,6 +290,7 @@ enum QueryPart<'a> { /// Captures a string reference. RefStr(&'a str), Owned(Box), + Borrowed(Box<&'a dyn ToString>), Reference(&'a dyn ToString), DisplayFn(Box std::fmt::Result>), } @@ -291,12 +305,25 @@ impl<'a> QueryPart<'a> { } } +impl<'a> From> for QueryPart<'a> { + fn from(value: Key<'a>) -> Self { + value.0 + } +} + +impl<'a> From> for QueryPart<'a> { + fn from(value: Value<'a>) -> Self { + value.0 + } +} + impl<'a> Display for QueryPart<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let encode = |x| utf8_percent_encode(x, QUERY); let mut write = |x| Display::fmt(&encode(x), f); match self { QueryPart::Owned(b) => write(&b.to_string()), + QueryPart::Borrowed(b) => write(&b.to_string()), QueryPart::Reference(b) => write(&b.to_string()), QueryPart::DisplayFn(b) => b(f), QueryPart::RefStr(s) => write(s) @@ -343,13 +370,15 @@ mod tests { } let complex = Complex; + let complex_ref = Complex; let qs = QueryString::new() .with("q", Value::from_str(&query)) - .with("owned", Value::from(complex)); + .with("owned", Value::from(complex)) + .with("borrowed", Value::from_ref(&complex_ref)); assert_eq!( qs.to_string(), - "?q=apple???&owned=complex" + "?q=apple???&owned=complex&borrowed=complex" ); } From c0f4b0aa8ec4f31f0e9204ef2cb568cbb8ea2619 Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Fri, 24 May 2024 18:31:17 +0200 Subject: [PATCH 11/15] Remove obsolete constructor functions --- src/lib.rs | 58 +++++++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5fe2135..5c58e31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ use std::fmt::{Display, Formatter, Write}; -use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode}; +use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; /// https://url.spec.whatwg.org/#fragment-percent-encode-set const QUERY: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); @@ -75,8 +75,13 @@ impl<'a> QueryString<'a> { /// "https://example.com/?q=%F0%9F%8D%8E%20apple&category=fruits%20and%20vegetables&answer=42" /// ); /// ``` - pub fn with_value(mut self, key: K, value: V) -> Self { - self.pairs.push(Kvp::new(QueryPart::from_tostring_value(key), QueryPart::from_tostring_value(value))); + pub fn with_value( + mut self, + key: K, + value: V, + ) -> Self { + self.pairs + .push(Kvp::new(Key::from(key), Value::from(value))); self } @@ -104,7 +109,11 @@ impl<'a> QueryString<'a> { /// "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) -> Self { + pub fn with_opt_value( + self, + key: K, + value: Option, + ) -> Self { if let Some(value) = value { self.with_value(key, value) } else { @@ -128,8 +137,13 @@ impl<'a> QueryString<'a> { /// "https://example.com/?q=apple&category=fruits%20and%20vegetables" /// ); /// ``` - pub fn push(&mut self, key: K, value: V) -> &Self { - self.pairs.push(Kvp::new(Key::from(key), Value::from(value))); + pub fn push( + &mut self, + key: K, + value: V, + ) -> &Self { + self.pairs + .push(Kvp::new(Key::from(key), Value::from(value))); self } @@ -149,7 +163,11 @@ impl<'a> QueryString<'a> { /// "https://example.com/?q=%F0%9F%8D%8E%20apple" /// ); /// ``` - pub fn push_opt(&mut self, key: K, value: Option) -> &Self { + pub fn push_opt( + &mut self, + key: K, + value: Option, + ) -> &Self { if let Some(value) = value { self.push(key, value) } else { @@ -282,7 +300,10 @@ struct Kvp<'a> { impl<'a> Kvp<'a> { pub fn new>, V: Into>>(key: K, value: V) -> Self { - Self { key: key.into(), value: value.into() } + Self { + key: key.into(), + value: value.into(), + } } } @@ -290,19 +311,7 @@ enum QueryPart<'a> { /// Captures a string reference. RefStr(&'a str), Owned(Box), - Borrowed(Box<&'a dyn ToString>), Reference(&'a dyn ToString), - DisplayFn(Box std::fmt::Result>), -} - -impl<'a> QueryPart<'a> { - pub fn from_tostring_value(text: T) -> Self { - Self::Owned(Box::new(text)) - } - - pub fn from_tostring_ref(text: &'a T) -> Self { - Self::Reference(text) - } } impl<'a> From> for QueryPart<'a> { @@ -323,10 +332,8 @@ impl<'a> Display for QueryPart<'a> { let mut write = |x| Display::fmt(&encode(x), f); match self { QueryPart::Owned(b) => write(&b.to_string()), - QueryPart::Borrowed(b) => write(&b.to_string()), QueryPart::Reference(b) => write(&b.to_string()), - QueryPart::DisplayFn(b) => b(f), - QueryPart::RefStr(s) => write(s) + QueryPart::RefStr(s) => write(s), } } } @@ -376,10 +383,7 @@ mod tests { .with("q", Value::from_str(&query)) .with("owned", Value::from(complex)) .with("borrowed", Value::from_ref(&complex_ref)); - assert_eq!( - qs.to_string(), - "?q=apple???&owned=complex&borrowed=complex" - ); + assert_eq!(qs.to_string(), "?q=apple???&owned=complex&borrowed=complex"); } #[test] From 676c5cf033c52a7921c3a189b1eb351591369273 Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Fri, 24 May 2024 10:19:38 +0200 Subject: [PATCH 12/15] Add benchmarks --- Cargo.toml | 7 +++++++ benches/bench.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 benches/bench.rs diff --git a/Cargo.toml b/Cargo.toml index e9856f6..6ac6ccf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,10 @@ license = "EUPL-1.2" [dependencies] percent-encoding = { version = "2.3.0", default-features = false, features = ["std"] } + +[dev-dependencies] +criterion = "0.5.1" + +[[bench]] +name = "bench" +harness = false diff --git a/benches/bench.rs b/benches/bench.rs new file mode 100644 index 0000000..efee663 --- /dev/null +++ b/benches/bench.rs @@ -0,0 +1,46 @@ +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", |b| { + b.iter(|| { + let qs = QueryString::new() + .with_value("q", "apple???") + .with_value("category", "fruits and vegetables"); + format!("{qs}") + }) + }); + + // `with_opt_value` method benchmark + c.bench_function("with_opt_value", |b| { + b.iter(|| { + let qs = QueryString::new() + .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}") + }) + }); + + // Full test including creating, pushing and appending + c.bench_function("push_opt_and_append", |b| { + b.iter(|| { + let mut qs = QueryString::new(); + qs.push("a", "apple"); + qs.push_opt("b", None::); + qs.push_opt("c", Some("🍎 apple")); + + let more = QueryString::new().with_value("q", "pear"); + let qs = qs.append_into(more); + + format!("{qs}") + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); From 636ed0ee23daf6ad41223ade6c14eb5fbae93367 Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Fri, 24 May 2024 18:38:39 +0200 Subject: [PATCH 13/15] Add inlining markers to convenience functions --- src/lib.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 5c58e31..3d7f5ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ use std::fmt::{Display, Formatter, Write}; -use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; +use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode}; /// https://url.spec.whatwg.org/#fragment-percent-encode-set const QUERY: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); @@ -254,40 +254,48 @@ pub struct Key<'a>(QueryPart<'a>); pub struct Value<'a>(QueryPart<'a>); impl<'a> Key<'a> { + #[inline(always)] pub fn from(value: T) -> Self { Self(QueryPart::Owned(Box::new(value))) } + #[inline(always)] pub fn from_ref(value: &'a T) -> Self { Self(QueryPart::Reference(value)) } + #[inline(always)] pub fn from_str(key: &'a str) -> Key<'a> { Self(QueryPart::RefStr(key)) } } impl<'a> Value<'a> { + #[inline(always)] pub fn from(value: T) -> Self { Self(QueryPart::Owned(Box::new(value))) } + #[inline(always)] pub fn from_ref(value: &'a T) -> Self { Self(QueryPart::Reference(value)) } + #[inline(always)] pub fn from_str(value: &'a str) -> Self { Self(QueryPart::RefStr(&value)) } } impl<'a> From<&'a str> for Key<'a> { + #[inline(always)] fn from(value: &'a str) -> Self { Self::from_str(value) } } impl<'a> From<&'a str> for Value<'a> { + #[inline(always)] fn from(value: &'a str) -> Self { Self::from_str(value) } @@ -299,6 +307,7 @@ struct Kvp<'a> { } impl<'a> Kvp<'a> { + #[inline(always)] pub fn new>, V: Into>>(key: K, value: V) -> Self { Self { key: key.into(), @@ -315,12 +324,14 @@ enum QueryPart<'a> { } impl<'a> From> for QueryPart<'a> { + #[inline(always)] fn from(value: Key<'a>) -> Self { value.0 } } impl<'a> From> for QueryPart<'a> { + #[inline(always)] fn from(value: Value<'a>) -> Self { value.0 } From 6fed5b8f65e60c3199b528a5452c40f5dcd5aa0e Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Fri, 24 May 2024 18:43:16 +0200 Subject: [PATCH 14/15] Add another benchmark with more data --- benches/bench.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/benches/bench.rs b/benches/bench.rs index efee663..62504f8 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -1,4 +1,4 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, criterion_group, criterion_main}; use query_string_builder::QueryString; @@ -13,6 +13,17 @@ pub fn criterion_benchmark(c: &mut Criterion) { }) }); + c.bench_function("with_value_more", |b| { + b.iter(|| { + let qs = QueryString::new() + .with_value("q", "apple???") + .with_value("category", "fruits and vegetables") + .with_value("tasty", true) + .with_value("weight", 99.9); + format!("{qs}") + }) + }); + // `with_opt_value` method benchmark c.bench_function("with_opt_value", |b| { b.iter(|| { From 70532fe5ff84785a05bd9c0905809f64084c084a Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Fri, 24 May 2024 18:50:42 +0200 Subject: [PATCH 15/15] Simplify deferred string rendering function --- src/lib.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3d7f5ab..1d26282 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -339,16 +339,19 @@ impl<'a> From> for QueryPart<'a> { impl<'a> Display for QueryPart<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let encode = |x| utf8_percent_encode(x, QUERY); - let mut write = |x| Display::fmt(&encode(x), f); match self { - QueryPart::Owned(b) => write(&b.to_string()), - QueryPart::Reference(b) => write(&b.to_string()), - QueryPart::RefStr(s) => write(s), + QueryPart::Owned(b) => write(&b.to_string(), f), + QueryPart::Reference(b) => write(&b.to_string(), f), + QueryPart::RefStr(s) => write(s, f), } } } +#[inline(always)] +fn write(x: &str, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&utf8_percent_encode(x, QUERY), f) +} + #[cfg(test)] mod tests { use super::*;