use crate::QUERY;
use percent_encoding::utf8_percent_encode;
use std::fmt::{self, Display, Formatter, Write};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct QueryStringOwned {
pairs: Vec<(String, String)>,
}
impl QueryStringOwned {
pub fn new() -> Self {
Self { pairs: Vec::new() }
}
pub(crate) fn from_pairs(pairs: Vec<(String, String)>) -> Self {
Self { pairs }
}
#[doc(alias = "with_value")]
pub fn with<K: ToString, V: ToString>(mut self, key: K, value: V) -> Self {
self.pairs.push((key.to_string(), value.to_string()));
self
}
#[doc(alias = "with_opt_value")]
pub fn with_opt<K: ToString, V: ToString>(self, key: K, value: Option<V>) -> Self {
if let Some(value) = value {
self.with(key, value)
} else {
self
}
}
pub fn push<K: ToString, V: ToString>(&mut self, key: K, value: V) -> &mut Self {
self.pairs.push((key.to_string(), value.to_string()));
self
}
pub fn push_opt<K: ToString, V: ToString>(&mut self, key: K, value: Option<V>) -> &mut Self {
if let Some(value) = value {
self.push(key, value)
} else {
self
}
}
pub fn len(&self) -> usize {
self.pairs.len()
}
pub fn is_empty(&self) -> bool {
self.pairs.is_empty()
}
pub fn append(&mut self, mut other: QueryStringOwned) {
self.pairs.append(&mut other.pairs)
}
pub fn append_into(mut self, mut other: QueryStringOwned) -> Self {
self.pairs.append(&mut other.pairs);
self
}
}
impl Display for QueryStringOwned {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.pairs.is_empty() {
return Ok(());
}
f.write_char('?')?;
for (i, (key, value)) in self.pairs.iter().enumerate() {
if i > 0 {
f.write_char('&')?;
}
Display::fmt(&utf8_percent_encode(key, QUERY), f)?;
f.write_char('=')?;
Display::fmt(&utf8_percent_encode(value, QUERY), f)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
let qs = QueryStringOwned::new();
assert_eq!(qs.to_string(), "");
assert_eq!(qs.len(), 0);
assert!(qs.is_empty());
}
#[test]
fn test_simple() {
let qs = QueryStringOwned::new()
.with("q", "apple???")
.with("category", "fruits and vegetables")
.with("tasty", true)
.with("weight", 99.9);
assert_eq!(
qs.to_string(),
"?q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9"
);
assert_eq!(qs.len(), 4);
assert!(!qs.is_empty());
}
#[test]
fn test_encoding() {
let qs = QueryStringOwned::new()
.with("q", "Grünkohl")
.with("category", "Gemüse");
assert_eq!(qs.to_string(), "?q=Gr%C3%BCnkohl&category=Gem%C3%BCse");
}
#[test]
fn test_emoji() {
let qs = QueryStringOwned::new().with("q", "🥦").with("🍽️", "🍔🍕");
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 = QueryStringOwned::new()
.with("q", "celery")
.with_opt("taste", None::<String>)
.with_opt("category", Some("fruits and vegetables"))
.with_opt("tasty", Some(true))
.with_opt("weight", Some(99.9));
assert_eq!(
qs.to_string(),
"?q=celery&category=fruits%20and%20vegetables&tasty=true&weight=99.9"
);
assert_eq!(qs.len(), 4); }
#[test]
fn test_push_optional() {
let mut qs = QueryStringOwned::new();
qs.push("a", "apple");
qs.push_opt("b", None::<String>);
qs.push_opt("c", Some("🍎 apple"));
assert_eq!(
format!("https://example.com/{qs}"),
"https://example.com/?a=apple&c=%F0%9F%8D%8E%20apple"
);
}
#[test]
fn test_append() {
let qs = QueryStringOwned::new().with("q", "apple");
let more = QueryStringOwned::new().with("q", "pear");
let mut qs = qs.append_into(more);
qs.append(QueryStringOwned::new().with("answer", "42"));
assert_eq!(
format!("https://example.com/{qs}"),
"https://example.com/?q=apple&q=pear&answer=42"
);
}
#[test]
fn test_characters() {
let tests = vec![
("space", " ", "%20"),
("double_quote", "\"", "%22"),
("hash", "#", "%23"),
("less_than", "<", "%3C"),
("equals", "=", "%3D"),
("greater_than", ">", "%3E"),
("percent", "%", "%25"),
("ampersand", "&", "%26"),
("plus", "+", "%2B"),
("dollar", "$", "$"),
("single_quote", "'", "'"),
("comma", ",", ","),
("forward_slash", "/", "/"),
("colon", ":", ":"),
("semicolon", ";", ";"),
("question_mark", "?", "?"),
("at", "@", "@"),
("left_bracket", "[", "["),
("backslash", "\\", "\\"),
("right_bracket", "]", "]"),
("caret", "^", "^"),
("underscore", "_", "_"),
("grave", "^", "^"),
("left_curly", "{", "{"),
("pipe", "|", "|"),
("right_curly", "}", "}"),
];
let mut qs = QueryStringOwned::new();
for (key, value, _) in &tests {
qs.push(key, value);
}
let mut expected = String::new();
for (i, (key, _, value)) in tests.iter().enumerate() {
if i > 0 {
expected.push('&');
}
expected.push_str(&format!("{key}={value}"));
}
assert_eq!(
format!("https://example.com/{qs}"),
format!("https://example.com/?{expected}")
);
}
}