use crate::encode::write_encoded;
use crate::{QUERY, QueryStringOwned};
use percent_encoding::utf8_percent_encode;
use std::fmt::{self, Debug, Display, Formatter, Write};
#[derive(Clone, Copy)]
pub enum Part<'a> {
Str(&'a str),
Display(&'a dyn Display),
}
impl Display for Part<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Part::Str(s) => Display::fmt(s, f),
Part::Display(d) => Display::fmt(d, f),
}
}
}
impl Part<'_> {
fn write_encoded_to(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Part::Str(s) => {
for piece in utf8_percent_encode(s, QUERY) {
f.write_str(piece)?;
}
Ok(())
}
Part::Display(d) => write_encoded(f, *d),
}
}
}
pub trait IntoPart<'a> {
fn into_part(self) -> Part<'a>;
}
impl<'a> IntoPart<'a> for &'a str {
fn into_part(self) -> Part<'a> {
Part::Str(self)
}
}
impl<'a, T: Display> IntoPart<'a> for &'a T {
fn into_part(self) -> Part<'a> {
Part::Display(self)
}
}
#[derive(Clone, Default)]
pub struct QueryString<'a> {
pairs: Vec<(Part<'a>, Part<'a>)>,
}
impl<'a> QueryString<'a> {
pub fn new() -> Self {
Self { pairs: Vec::new() }
}
#[doc(alias = "with_value")]
pub fn with<K, V>(mut self, key: K, value: V) -> Self
where
K: IntoPart<'a>,
V: IntoPart<'a>,
{
self.pairs.push((key.into_part(), value.into_part()));
self
}
#[doc(alias = "with_opt_value")]
pub fn with_opt<K, V>(self, key: K, value: Option<V>) -> Self
where
K: IntoPart<'a>,
V: IntoPart<'a>,
{
if let Some(value) = value {
self.with(key, value)
} else {
self
}
}
pub fn push<K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: IntoPart<'a>,
V: IntoPart<'a>,
{
self.pairs.push((key.into_part(), value.into_part()));
self
}
pub fn push_opt<K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
where
K: IntoPart<'a>,
V: IntoPart<'a>,
{
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: QueryString<'a>) {
self.pairs.append(&mut other.pairs)
}
pub fn append_into(mut self, mut other: QueryString<'a>) -> Self {
self.pairs.append(&mut other.pairs);
self
}
pub fn into_owned(self) -> QueryStringOwned {
QueryStringOwned::from_pairs(
self.pairs
.into_iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect(),
)
}
}
impl Display for QueryString<'_> {
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('&')?;
}
key.write_encoded_to(f)?;
f.write_char('=')?;
value.write_encoded_to(f)?;
}
Ok(())
}
}
impl Debug for QueryString<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_map()
.entries(
self.pairs
.iter()
.map(|(key, value)| (key.to_string(), value.to_string())),
)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
let qs = QueryString::new();
assert_eq!(qs.to_string(), "");
assert_eq!(qs.len(), 0);
assert!(qs.is_empty());
}
#[test]
fn test_simple() {
let tasty = true;
let weight = 99.9;
let qs = QueryString::new()
.with("q", "apple???")
.with("category", "fruits and vegetables")
.with("tasty", &tasty)
.with("weight", &weight);
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 = QueryString::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 = QueryString::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 tasty = true;
let weight = 99.9;
let qs = QueryString::new()
.with("q", "celery")
.with_opt("taste", None::<&str>)
.with_opt("category", Some("fruits and vegetables"))
.with_opt("tasty", Some(&tasty))
.with_opt("weight", Some(&weight));
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 = QueryString::new();
qs.push("a", "apple");
qs.push_opt("b", None::<&str>);
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 = QueryString::new().with("q", "apple");
let more = QueryString::new().with("q", "pear");
let mut qs = qs.append_into(more);
qs.append(QueryString::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 = QueryString::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}")
);
}
#[test]
fn test_non_string_refs() {
let count = 12i32;
let tasty = true;
let weight = 99.9f64;
let owned_string = String::from("kale");
let qs = QueryString::new()
.with("count", &count)
.with("tasty", &tasty)
.with("weight", &weight)
.with("q", &owned_string);
assert_eq!(qs.to_string(), "?count=12&tasty=true&weight=99.9&q=kale");
}
#[test]
fn test_into_owned() {
let owned = {
let q = String::from("Grünkohl");
QueryString::new().with("q", &q).into_owned()
};
assert_eq!(owned.to_string(), "?q=Gr%C3%BCnkohl");
assert_eq!(owned.len(), 1);
}
#[test]
fn test_debug() {
let qs = QueryString::new().with("q", "apple");
assert_eq!(format!("{qs:?}"), r#"{"q": "apple"}"#);
}
#[test]
fn test_clone_default() {
let qs = QueryString::default().with("q", "apple");
let clone = qs.clone();
assert_eq!(clone.to_string(), qs.to_string());
}
}