Skip to content

Commit 4543db1

Browse files
committed
Add draft for lazily evaluated values
1 parent 684da0e commit 4543db1

File tree

1 file changed

+138
-8
lines changed

1 file changed

+138
-8
lines changed

src/lib.rs

Lines changed: 138 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
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

27-
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
29+
use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
2830

2931
/// https://url.spec.whatwg.org/#fragment-percent-encode-set
3032
const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
@@ -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)]
4951
pub 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,96 @@ 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)]
237266
struct 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 where T: ToString + 'static {
323+
fn from(value: Rc<T>) -> Self {
324+
Value::lazy_rc(value)
325+
}
326+
}
327+
328+
329+
impl<T> From<Arc<T>> for Value where T: ToString + 'static {
330+
fn from(value: Arc<T>) -> Self {
331+
Value::lazy_arc(value)
332+
}
333+
}
334+
335+
336+
impl<F, T> From<F> for Value where
337+
F: Fn() -> T + 'static,
338+
T: ToString + 'static, {
339+
fn from(value: F) -> Self {
340+
Value::lazy_fn(value)
341+
}
240342
}
241343

242344
#[cfg(test)]
@@ -266,6 +368,34 @@ mod tests {
266368
assert!(!qs.is_empty());
267369
}
268370

371+
#[test]
372+
fn test_lazy() {
373+
let qs = QueryString::new()
374+
.with("x", Value::eager("y"))
375+
.with("q", Value::lazy("apple???"))
376+
.with("category", Value::lazy_fn(|| "fruits and vegetables"))
377+
.with("tasty", Value::lazy_box(Box::new(true)))
378+
.with("weight", Value::lazy_arc(Arc::new(99.9)));
379+
assert_eq!(
380+
qs.to_string(),
381+
"?x=y&q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9"
382+
);
383+
}
384+
385+
386+
#[test]
387+
fn test_lazy_implicit() {
388+
let qs = QueryString::new()
389+
.with("q", Value::lazy("apple???"))
390+
.with("category", || "fruits and vegetables")
391+
.with("tasty", Rc::new(true))
392+
.with("weight", Arc::new(99.9));
393+
assert_eq!(
394+
qs.to_string(),
395+
"?q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9"
396+
);
397+
}
398+
269399
#[test]
270400
fn test_encoding() {
271401
let qs = QueryString::new()

0 commit comments

Comments
 (0)