use std::fmt;
use std::fmt::{Debug, Display, Formatter, Write};
use crate::QUERY;
use percent_encoding::utf8_percent_encode;
pub type QueryStringSimple = WrappedQueryString<RootMarker, EmptyValue>;
pub struct WrappedQueryString<B, T>
where
B: ConditionalDisplay + Identifiable,
T: Display,
{
base: BaseOption<B>,
value: KvpOption<T>,
}
impl Default for QueryStringSimple {
fn default() -> Self {
QueryStringSimple::new()
}
}
pub struct Kvp<K, V>
where
K: Display,
V: Display,
{
key: K,
value: V,
}
enum BaseOption<B> {
Some(B),
None,
}
enum KvpOption<T> {
Some(T),
None,
}
pub struct RootMarker(());
pub struct EmptyValue(());
impl<B, T> WrappedQueryString<B, T>
where
B: ConditionalDisplay + Identifiable,
T: Display,
{
pub(crate) fn new() -> WrappedQueryString<RootMarker, EmptyValue> {
WrappedQueryString {
base: BaseOption::None,
value: KvpOption::None,
}
}
pub fn with_value<K: Display, V: Display>(
self,
key: K,
value: V,
) -> WrappedQueryString<Self, Kvp<K, V>> {
WrappedQueryString {
base: BaseOption::Some(self),
value: KvpOption::Some(Kvp { key, value }),
}
}
pub fn with_opt_value<K: Display, V: Display>(
self,
key: K,
value: Option<V>,
) -> WrappedQueryString<Self, Kvp<K, V>> {
if let Some(value) = value {
WrappedQueryString {
base: BaseOption::Some(self),
value: KvpOption::Some(Kvp { key, value }),
}
} else {
WrappedQueryString {
base: BaseOption::Some(self),
value: KvpOption::None,
}
}
}
pub fn len(&self) -> usize {
if self.is_empty() {
return 0;
}
1 + self.base.len()
}
pub fn is_empty(&self) -> bool {
if self.is_root() && self.value.is_empty() {
return true;
}
if !self.value.is_empty() {
return false;
}
self.base.is_empty()
}
}
pub trait Identifiable {
fn is_root(&self) -> bool;
fn is_empty(&self) -> bool;
fn len(&self) -> usize;
}
pub trait ConditionalDisplay {
fn cond_fmt(&self, should_display: bool, f: &mut Formatter<'_>) -> Result<usize, fmt::Error>;
}
impl Identifiable for RootMarker {
fn is_root(&self) -> bool {
unreachable!()
}
fn is_empty(&self) -> bool {
unreachable!()
}
fn len(&self) -> usize {
unreachable!()
}
}
impl ConditionalDisplay for RootMarker {
fn cond_fmt(&self, _should_display: bool, _f: &mut Formatter<'_>) -> Result<usize, fmt::Error> {
unreachable!()
}
}
impl Display for RootMarker {
fn fmt(&self, _f: &mut Formatter<'_>) -> fmt::Result {
unreachable!()
}
}
impl<B> ConditionalDisplay for BaseOption<B>
where
B: ConditionalDisplay,
{
fn cond_fmt(&self, should_display: bool, f: &mut Formatter<'_>) -> Result<usize, fmt::Error> {
match self {
BaseOption::Some(base) => Ok(base.cond_fmt(should_display, f)?),
BaseOption::None => {
if should_display {
f.write_char('?')?;
}
Ok(0)
}
}
}
}
impl<B, T> ConditionalDisplay for WrappedQueryString<B, T>
where
B: ConditionalDisplay + Identifiable,
T: Display,
{
fn cond_fmt(&self, should_display: bool, f: &mut Formatter<'_>) -> Result<usize, fmt::Error> {
let depth = if !should_display {
if self.value.is_empty() {
return self.base.cond_fmt(false, f);
}
self.base.cond_fmt(true, f)?
} else {
self.base.cond_fmt(true, f)?
};
if self.value.is_empty() {
return Ok(depth);
}
self.value.fmt(f)?;
if should_display {
f.write_char('&')?;
}
Ok(depth + 1)
}
}
impl<B> BaseOption<B>
where
B: Identifiable + ConditionalDisplay,
{
fn is_empty(&self) -> bool {
match self {
BaseOption::Some(value) => value.is_empty(),
BaseOption::None => true,
}
}
fn len(&self) -> usize {
match self {
BaseOption::Some(value) => value.len(),
BaseOption::None => 0,
}
}
}
impl<B, T> Identifiable for WrappedQueryString<B, T>
where
B: ConditionalDisplay + Identifiable,
T: Display,
{
fn is_root(&self) -> bool {
match self.base {
BaseOption::Some(_) => false,
BaseOption::None => true,
}
}
fn is_empty(&self) -> bool {
match self.value {
KvpOption::Some(_) => false,
KvpOption::None => self.base.is_empty(),
}
}
fn len(&self) -> usize {
match self.value {
KvpOption::Some(_) => 1 + self.base.len(),
KvpOption::None => self.base.len(),
}
}
}
impl<T> KvpOption<T> {
fn is_empty(&self) -> bool {
match self {
KvpOption::Some(_) => false,
KvpOption::None => true,
}
}
}
impl Display for EmptyValue {
fn fmt(&self, _f: &mut Formatter<'_>) -> fmt::Result {
Ok(())
}
}
impl<T> Display for BaseOption<T>
where
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
BaseOption::Some(d) => Display::fmt(d, f),
BaseOption::None => Ok(()),
}
}
}
impl<T> Display for KvpOption<T>
where
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KvpOption::Some(d) => Display::fmt(d, f),
KvpOption::None => Ok(()),
}
}
}
impl<K, V> Display for Kvp<K, V>
where
K: Display,
V: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&utf8_percent_encode(&self.key.to_string(), QUERY), f)?;
f.write_char('=')?;
Display::fmt(&utf8_percent_encode(&self.value.to_string(), QUERY), f)
}
}
impl<B, T> Display for WrappedQueryString<B, T>
where
B: ConditionalDisplay + Identifiable,
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let should_display = !self.value.is_empty();
self.base.cond_fmt(should_display, f)?;
if should_display {
Display::fmt(&self.value, f)?;
}
Ok(())
}
}
impl<B, T> Debug for WrappedQueryString<B, T>
where
B: ConditionalDisplay + Identifiable,
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
#[cfg(test)]
mod tests {
use crate::slim::{BaseOption, EmptyValue, KvpOption};
use crate::QueryString;
#[test]
fn test_empty() {
let qs = QueryString::simple();
assert!(qs.is_empty());
assert_eq!(qs.len(), 0);
assert_eq!(qs.to_string(), "");
}
#[test]
fn test_empty_complex() {
let qs = QueryString::simple().with_opt_value("key", None::<&str>);
assert!(qs.is_empty());
assert_eq!(qs.len(), 0);
assert_eq!(qs.to_string(), "");
}
#[test]
fn test_simple() {
let apple = "apple???";
let qs = QueryString::simple()
.with_value("q", &apple)
.with_value("category", "fruits and vegetables")
.with_value("tasty", true)
.with_value("weight", 99.9);
assert!(!qs.is_empty());
assert_eq!(qs.len(), 4);
assert_eq!(
format!("{qs}"),
"?q=apple???&category=fruits%20and%20vegetables&tasty=true&weight=99.9"
);
}
#[test]
fn test_encoding() {
let qs = QueryString::simple()
.with_value("q", "Grünkohl")
.with_value("category", "Gemüse");
assert!(!qs.is_empty());
assert_eq!(qs.len(), 2);
assert_eq!(qs.to_string(), "?q=Gr%C3%BCnkohl&category=Gem%C3%BCse");
}
#[test]
fn test_emoji() {
let qs = QueryString::simple()
.with_value("q", "🥦")
.with_value("🍽️", "🍔🍕");
assert!(!qs.is_empty());
assert_eq!(qs.len(), 2);
assert_eq!(
format!("{qs:?}"),
"?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 = QueryString::simple()
.with_value("q", "celery")
.with_opt_value("taste", None::<String>)
.with_opt_value("category", Some("fruits and vegetables"))
.with_opt_value("tasty", Some(true))
.with_opt_value("weight", Some(99.9));
assert!(!qs.is_empty());
assert_eq!(qs.len(), 4);
assert_eq!(
qs.to_string(),
"?q=celery&category=fruits%20and%20vegetables&tasty=true&weight=99.9"
);
assert_eq!(qs.len(), 4); }
#[test]
fn test_display() {
assert_eq!(format!("{}", KvpOption::<i32>::None), "");
assert_eq!(format!("{}", BaseOption::<i32>::None), "");
assert_eq!(format!("{}", EmptyValue(())), "");
}
}