2222
2323#![ deny( unsafe_code) ]
2424
25- use std:: fmt:: { Debug , Display , Formatter } ;
25+ use std:: fmt:: { Display , Formatter } ;
26+ use std:: rc:: Rc ;
27+ use std:: sync:: Arc ;
2628
2729use percent_encoding:: { utf8_percent_encode, AsciiSet , CONTROLS } ;
2830
@@ -45,7 +47,7 @@ const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').ad
4547/// "https://example.com/?q=apple&category=fruits%20and%20vegetables"
4648/// );
4749/// ```
48- #[ derive( Debug , Default , Clone ) ]
50+ #[ derive( Default ) ]
4951pub struct QueryString {
5052 pairs : Vec < Kvp > ,
5153}
@@ -78,7 +80,33 @@ impl QueryString {
7880 pub fn with_value < K : ToString , V : ToString > ( mut self , key : K , value : V ) -> Self {
7981 self . pairs . push ( Kvp {
8082 key : key. to_string ( ) ,
81- value : value. to_string ( ) ,
83+ value : Value :: eager ( value) ,
84+ } ) ;
85+ self
86+ }
87+
88+ /// Appends a key-value pair to the query string. Supports lazy evaluation.
89+ ///
90+ /// ## Example
91+ ///
92+ /// ```
93+ /// use std::sync::Arc;
94+ /// use query_string_builder::{QueryString, Value};
95+ ///
96+ /// let qs = QueryString::new()
97+ /// .with("q", Value::eager("🍎 apple"))
98+ /// .with("category", || "fruits and vegetables")
99+ /// .with("answer", Arc::new(42));
100+ ///
101+ /// assert_eq!(
102+ /// format!("https://example.com/{qs}"),
103+ /// "https://example.com/?q=%F0%9F%8D%8E%20apple&category=fruits%20and%20vegetables&answer=42"
104+ /// );
105+ /// ```
106+ pub fn with < K : ToString , V : Into < Value > > ( mut self , key : K , value : V ) -> Self {
107+ self . pairs . push ( Kvp {
108+ key : key. to_string ( ) ,
109+ value : value. into ( ) ,
82110 } ) ;
83111 self
84112 }
@@ -128,7 +156,7 @@ impl QueryString {
128156 pub fn push < K : ToString , V : ToString > ( & mut self , key : K , value : V ) -> & Self {
129157 self . pairs . push ( Kvp {
130158 key : key. to_string ( ) ,
131- value : value . to_string ( ) ,
159+ value : Value :: eager ( value ) ,
132160 } ) ;
133161 self
134162 }
@@ -221,22 +249,102 @@ impl Display for QueryString {
221249 if i > 0 {
222250 write ! ( f, "&" ) ?;
223251 }
252+
253+ let value = pair. value . render ( ) ;
224254 write ! (
225255 f,
226256 "{key}={value}" ,
227257 key = utf8_percent_encode( & pair. key, FRAGMENT ) ,
228- value = utf8_percent_encode( & pair . value, FRAGMENT )
258+ value = utf8_percent_encode( & value, FRAGMENT )
229259 ) ?;
230260 }
231261 Ok ( ( ) )
232262 }
233263 }
234264}
235265
236- #[ derive( Debug , Clone ) ]
237266struct Kvp {
238267 key : String ,
239- value : String ,
268+ value : Value ,
269+ }
270+
271+ pub struct Value {
272+ value : Box < dyn Fn ( ) -> String > ,
273+ }
274+
275+ impl Value {
276+ pub fn eager < T : ToString > ( value : T ) -> Self {
277+ let value = value. to_string ( ) ;
278+ Value {
279+ value : Box :: new ( move || value. to_string ( ) ) ,
280+ }
281+ }
282+
283+ pub fn lazy < T : ToString + ' static > ( value : T ) -> Self {
284+ Value {
285+ value : Box :: new ( move || value. to_string ( ) ) ,
286+ }
287+ }
288+
289+ pub fn lazy_box < T : ToString + ' static > ( value : Box < T > ) -> Self {
290+ Value {
291+ value : Box :: new ( move || value. to_string ( ) ) ,
292+ }
293+ }
294+
295+ pub fn lazy_rc < T : ToString + ' static > ( value : Rc < T > ) -> Self {
296+ Value {
297+ value : Box :: new ( move || value. to_string ( ) ) ,
298+ }
299+ }
300+
301+ pub fn lazy_arc < T : ToString + ' static > ( value : Arc < T > ) -> Self {
302+ Value {
303+ value : Box :: new ( move || value. to_string ( ) ) ,
304+ }
305+ }
306+
307+ pub fn lazy_fn < F , T > ( func : F ) -> Self
308+ where
309+ F : Fn ( ) -> T + ' static ,
310+ T : ToString + ' static ,
311+ {
312+ Value {
313+ value : Box :: new ( move || func ( ) . to_string ( ) ) ,
314+ }
315+ }
316+
317+ fn render ( & self ) -> String {
318+ ( self . value ) ( )
319+ }
320+ }
321+
322+ impl < T > From < Rc < T > > for Value
323+ where
324+ T : ToString + ' static ,
325+ {
326+ fn from ( value : Rc < T > ) -> Self {
327+ Value :: lazy_rc ( value)
328+ }
329+ }
330+
331+ impl < T > From < Arc < T > > for Value
332+ where
333+ T : ToString + ' static ,
334+ {
335+ fn from ( value : Arc < T > ) -> Self {
336+ Value :: lazy_arc ( value)
337+ }
338+ }
339+
340+ impl < F , T > From < F > for Value
341+ where
342+ F : Fn ( ) -> T + ' static ,
343+ T : ToString + ' static ,
344+ {
345+ fn from ( value : F ) -> Self {
346+ Value :: lazy_fn ( value)
347+ }
240348}
241349
242350#[ cfg( test) ]
@@ -266,6 +374,33 @@ mod tests {
266374 assert ! ( !qs. is_empty( ) ) ;
267375 }
268376
377+ #[ test]
378+ fn test_lazy ( ) {
379+ let qs = QueryString :: new ( )
380+ . with ( "x" , Value :: eager ( "y" ) )
381+ . with ( "q" , Value :: lazy ( "apple???" ) )
382+ . with ( "category" , Value :: lazy_fn ( || "fruits and vegetables" ) )
383+ . with ( "tasty" , Value :: lazy_box ( Box :: new ( true ) ) )
384+ . with ( "weight" , Value :: lazy_arc ( Arc :: new ( 99.9 ) ) ) ;
385+ assert_eq ! (
386+ qs. to_string( ) ,
387+ "?x=y&q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9"
388+ ) ;
389+ }
390+
391+ #[ test]
392+ fn test_lazy_implicit ( ) {
393+ let qs = QueryString :: new ( )
394+ . with ( "q" , Value :: lazy ( "apple???" ) )
395+ . with ( "category" , || "fruits and vegetables" )
396+ . with ( "tasty" , Rc :: new ( true ) )
397+ . with ( "weight" , Arc :: new ( 99.9 ) ) ;
398+ assert_eq ! (
399+ qs. to_string( ) ,
400+ "?q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9"
401+ ) ;
402+ }
403+
269404 #[ test]
270405 fn test_encoding ( ) {
271406 let qs = QueryString :: new ( )
0 commit comments