use std::fmt::{Display, Formatter, Result as FmtResult};
use super::codec::{urlencoding_decode, urlencoding_encode};
#[derive(Clone, Debug, Default)]
pub struct QueryParams {
params: Vec<(String, Vec<String>)>
}
impl PartialEq for QueryParams {
fn eq(&self, other: &Self) -> bool {
self.params.len() == other.params.len()
&& self
.params
.iter()
.all(|(key, values)| other.get_all(key) == Some(values))
}
}
impl Eq for QueryParams {}
impl QueryParams {
fn entry_mut(&mut self, key: &str) -> Option<&mut Vec<String>> {
self.params
.iter_mut()
.find_map(|(k, values)| (k == key).then_some(values))
}
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn parse(query: &str) -> Self {
let query = query.trim_start_matches('?');
let mut result = Self::default();
for pair in query.split('&') {
let (key, value) = pair
.split_once('=')
.map_or((pair, None), |(k, v)| (k, Some(v)));
let decoded_key = urlencoding_decode(key).unwrap_or_else(|| key.to_string());
if !decoded_key.is_empty() {
let value = value
.map(|v| urlencoding_decode(v).unwrap_or_else(|| v.to_string()))
.unwrap_or_default();
match result.entry_mut(&decoded_key) {
Some(values) => values.push(value),
None => result.params.push((decoded_key, vec![value]))
}
}
}
result
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&str> {
self.get_all(key)
.and_then(|values| values.first())
.map(String::as_str)
}
#[must_use]
pub fn get_one(&self, key: &str) -> Option<&str> {
self.get(key)
}
#[must_use]
pub fn get_all(&self, key: &str) -> Option<&Vec<String>> {
self.params
.iter()
.find_map(|(k, values)| (k == key).then_some(values))
}
#[must_use]
pub fn has(&self, key: &str) -> bool {
self.params.iter().any(|(k, _)| k == key)
}
#[must_use]
pub fn contains_key(&self, key: &str) -> bool {
self.has(key)
}
pub fn set(&mut self, key: &str, value: &str) {
match self.entry_mut(key) {
Some(values) => values.push(value.to_string()),
None => self.params.push((key.to_string(), vec![value.to_string()]))
}
}
pub fn set_value(&mut self, key: &str, value: &str) {
match self.entry_mut(key) {
Some(values) => *values = vec![value.to_string()],
None => self.params.push((key.to_string(), vec![value.to_string()]))
}
}
pub fn remove(&mut self, key: &str) {
self.params.retain(|(k, _)| k != key);
}
#[must_use]
pub fn to_query_string(&self) -> String {
if self.params.is_empty() {
return String::new();
}
let mut pairs: Vec<String> = Vec::with_capacity(self.params.len());
for (key, values) in &self.params {
for value in values {
pairs.push(format!(
"{}={}",
urlencoding_encode(key),
urlencoding_encode(value)
));
}
}
format!("?{}", pairs.join("&"))
}
#[must_use]
pub const fn len(&self) -> usize {
self.params.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.params.is_empty()
}
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.params.iter().map(|(key, _)| key.as_str())
}
pub fn values(&self) -> impl Iterator<Item = &str> {
self.params
.iter()
.map(|(_, values)| values.first().map_or("", String::as_str))
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.params
.iter()
.map(|(key, values)| (key.as_str(), values.first().map_or("", String::as_str)))
}
pub fn iter_all(&self) -> impl Iterator<Item = (&str, &str)> {
self.params.iter().flat_map(|(key, values)| {
values
.iter()
.map(move |value| (key.as_str(), value.as_str()))
})
}
}
impl Display for QueryParams {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}", self.to_query_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_params_parse_empty() {
let params = QueryParams::parse("");
assert!(params.is_empty());
}
#[test]
fn query_params_parse_single() {
let params = QueryParams::parse("page=1");
assert_eq!(params.get("page"), Some("1"));
}
#[test]
fn query_params_parse_multiple() {
let params = QueryParams::parse("page=1&limit=10&sort=name");
assert_eq!(params.get("page"), Some("1"));
assert_eq!(params.get("limit"), Some("10"));
assert_eq!(params.get("sort"), Some("name"));
}
#[test]
fn query_params_parse_with_plus() {
let params = QueryParams::parse("search=hello+world");
assert_eq!(params.get("search"), Some("hello world"));
}
#[test]
fn query_params_parse_multiple_values_same_key() {
let params = QueryParams::parse("tag=rust&tag=web&tag=wasm");
assert_eq!(params.get("tag"), Some("rust"));
assert_eq!(params.get_one("tag"), Some("rust"));
let all = params.get_all("tag").unwrap();
assert_eq!(all.len(), 3);
assert_eq!(all[0], "rust");
assert_eq!(all[1], "web");
assert_eq!(all[2], "wasm");
}
#[test]
fn query_params_to_string() {
let mut params = QueryParams::new();
params.set("page", "1");
params.set("sort", "name");
let result = params.to_string();
assert!(result.contains("page=1"));
assert!(result.contains("sort=name"));
}
#[test]
fn query_params_set_get() {
let mut params = QueryParams::new();
params.set("key", "value");
assert_eq!(params.get("key"), Some("value"));
}
#[test]
fn query_params_set_appends() {
let mut params = QueryParams::new();
params.set("key", "first");
params.set("key", "second");
let all = params.get_all("key").unwrap();
assert_eq!(all, &["first".to_string(), "second".to_string()]);
}
#[test]
fn query_params_set_value_replaces() {
let mut params = QueryParams::new();
params.set("key", "first");
params.set("key", "second");
params.set_value("key", "replaced");
let all = params.get_all("key").unwrap();
assert_eq!(all, &["replaced".to_string()]);
}
#[test]
fn query_params_remove() {
let mut params = QueryParams::new();
params.set("key", "value");
params.remove("key");
assert!(!params.has("key"));
}
#[test]
fn query_params_keys() {
let params = QueryParams::parse("a=1&b=2");
assert_eq!(params.keys().count(), 2);
}
#[test]
fn query_params_values() {
let params = QueryParams::parse("a=1&b=2");
assert_eq!(params.values().count(), 2);
}
#[test]
fn query_params_default() {
let params = QueryParams::default();
assert!(params.is_empty());
}
#[test]
fn query_params_len() {
let params = QueryParams::parse("a=1&b=2");
assert_eq!(params.len(), 2);
}
#[test]
fn query_params_parse_with_leading_question_mark() {
let params = QueryParams::parse("?page=1&limit=10");
assert_eq!(params.get("page"), Some("1"));
assert_eq!(params.get("limit"), Some("10"));
}
#[test]
fn query_params_parse_key_without_value() {
let params = QueryParams::parse("key&other=value");
assert_eq!(params.get("key"), Some(""));
assert_eq!(params.get("other"), Some("value"));
}
#[test]
fn query_params_parse_special_chars() {
let params = QueryParams::parse("search=hello%20world");
assert_eq!(params.get("search"), Some("hello world"));
}
#[test]
fn query_params_to_string_multiple_values() {
let mut params = QueryParams::new();
params.set("tag", "rust");
params.set("tag", "web");
let qs = params.to_query_string();
assert!(qs.contains("tag=rust"));
assert!(qs.contains("tag=web"));
}
#[test]
fn query_params_to_string_empty() {
let params = QueryParams::new();
assert_eq!(params.to_query_string(), "");
}
#[test]
fn query_params_has() {
let params = QueryParams::parse("key=value");
assert!(params.has("key"));
assert!(!params.has("nonexistent"));
}
#[test]
fn query_params_contains_key_alias() {
let params = QueryParams::parse("key=value");
assert!(params.contains_key("key"));
assert!(!params.contains_key("missing"));
}
#[test]
fn query_params_iter() {
let params = QueryParams::parse("a=1&b=2");
assert_eq!(params.iter().count(), 2);
}
#[test]
fn query_params_iter_all() {
let params = QueryParams::parse("tag=rust&tag=web&page=1");
let count = params.iter_all().count();
assert_eq!(count, 3);
}
#[test]
fn query_params_display() {
let mut params = QueryParams::new();
params.set("key", "value");
let display_str = params.to_string();
assert!(display_str.starts_with('?'));
assert!(display_str.contains("key=value"));
}
#[test]
fn query_params_clone() {
let mut params = QueryParams::parse("a=1");
params.set("b", "2");
let cloned = params.clone();
assert_eq!(cloned.get("a"), Some("1"));
assert_eq!(cloned.get("b"), Some("2"));
}
#[test]
fn query_params_eq() {
let p1 = QueryParams::parse("a=1");
let p2 = QueryParams::parse("a=1");
let p3 = QueryParams::parse("a=2");
assert_eq!(p1, p2);
assert_ne!(p1, p3);
}
#[test]
fn query_params_parse_empty_value() {
let params = QueryParams::parse("key=");
assert!(params.has("key"));
assert_eq!(params.get("key"), Some(""));
}
#[test]
fn query_params_parse_multiple_empty() {
let params = QueryParams::parse("=&=&=");
assert!(params.is_empty());
}
#[test]
fn query_params_get_all_none() {
let params = QueryParams::parse("a=1");
assert_eq!(params.get_all("missing"), None);
}
#[test]
fn query_params_to_string_preserves_insertion_order() {
let params = QueryParams::parse("z=1&a=2&m=3&z=4");
assert_eq!(params.to_query_string(), "?z=1&z=4&a=2&m=3");
let mut built = QueryParams::new();
built.set("beta", "1");
built.set("alpha", "2");
assert_eq!(built.to_query_string(), "?beta=1&alpha=2");
}
#[test]
fn query_params_eq_ignores_key_order() {
let p1 = QueryParams::parse("a=1&b=2");
let p2 = QueryParams::parse("b=2&a=1");
assert_eq!(p1, p2);
}
#[test]
fn query_params_to_query_string_reversible() {
let mut params = QueryParams::new();
params.set("name", "hello");
params.set("name", "world");
params.set("page", "1");
let qs = params.to_query_string();
let parsed = QueryParams::parse(&qs[1..]);
assert_eq!(parsed.get("page"), Some("1"));
assert_eq!(parsed.get("name"), Some("hello"));
let all = parsed.get_all("name").unwrap();
assert_eq!(all, &["hello".to_string(), "world".to_string()]);
}
}