use std::fmt::Display;
#[derive(Debug, Clone, Default)]
pub(crate) struct QueryBuilder {
pairs: Vec<(String, String)>,
}
impl QueryBuilder {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn set(mut self, key: &str, value: impl Display) -> Self {
self.pairs.push((key.to_string(), value.to_string()));
self
}
pub(crate) fn opt(self, key: &str, value: Option<impl Display>) -> Self {
match value {
Some(value) => self.set(key, value),
None => self,
}
}
pub(crate) fn opt_str(self, key: &str, value: Option<impl AsRef<str>>) -> Self {
match value {
Some(value) if !value.as_ref().is_empty() => self.set(key, value.as_ref()),
_ => self,
}
}
pub(crate) fn str_val(self, key: &str, value: impl AsRef<str>) -> Self {
self.opt_str(key, Some(value))
}
pub(crate) fn csv(self, key: &str, values: &[impl AsRef<str>]) -> Self {
if values.is_empty() {
return self;
}
let joined = values
.iter()
.map(AsRef::as_ref)
.collect::<Vec<_>>()
.join(",");
self.set(key, joined)
}
pub(crate) fn into_pairs(self) -> Vec<(String, String)> {
self.pairs
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn omits_none_and_empty_strings() {
let pairs = QueryBuilder::new()
.opt_str("a", None::<&str>)
.opt_str("b", Some(""))
.opt_str("c", Some("kept"))
.str_val("d", "")
.into_pairs();
assert_eq!(pairs, [("c".to_string(), "kept".to_string())]);
}
#[test]
fn keeps_zero_and_false_but_omits_none() {
let pairs = QueryBuilder::new()
.opt("page", Some(0))
.opt("enabled", Some(false))
.opt("skipped", None::<i32>)
.into_pairs();
assert_eq!(
pairs,
[
("page".to_string(), "0".to_string()),
("enabled".to_string(), "false".to_string()),
]
);
}
#[test]
fn comma_joins_lists_and_omits_empty_ones() {
let pairs = QueryBuilder::new()
.csv("kinds", &["audio", "video"])
.csv("empty", &[] as &[&str])
.into_pairs();
assert_eq!(pairs, [("kinds".to_string(), "audio,video".to_string())]);
}
}