1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct QueryParam {
7 pub key: String,
8 pub value: Option<String>,
9}
10
11#[must_use]
13pub fn parse_query(input: &str) -> Vec<QueryParam> {
14 let query = normalize_query_input(input);
15 if query.is_empty() {
16 return Vec::new();
17 }
18
19 query
20 .split('&')
21 .filter(|segment| !segment.is_empty())
22 .map(|segment| match segment.split_once('=') {
23 Some((key, value)) => QueryParam {
24 key: key.to_string(),
25 value: Some(value.to_string()),
26 },
27 None => QueryParam {
28 key: segment.to_string(),
29 value: None,
30 },
31 })
32 .collect()
33}
34
35#[must_use]
37pub fn build_query(params: &[QueryParam]) -> String {
38 params
39 .iter()
40 .map(|param| match ¶m.value {
41 Some(value) => format!("{}={value}", param.key),
42 None => param.key.clone(),
43 })
44 .collect::<Vec<_>>()
45 .join("&")
46}
47
48#[must_use]
50pub fn get_query_param(input: &str, key: &str) -> Option<String> {
51 parse_query(input)
52 .into_iter()
53 .find(|param| param.key == key)
54 .and_then(|param| param.value)
55}
56
57#[must_use]
59pub fn has_query_param(input: &str, key: &str) -> bool {
60 parse_query(input).into_iter().any(|param| param.key == key)
61}
62
63#[must_use]
65pub fn remove_query_param(input: &str, key: &str) -> String {
66 let filtered = parse_query(input)
67 .into_iter()
68 .filter(|param| param.key != key)
69 .collect::<Vec<_>>();
70
71 build_query(&filtered)
72}
73
74#[must_use]
76pub fn append_query_param(input: &str, key: &str, value: &str) -> String {
77 let mut params = parse_query(input);
78 params.push(QueryParam {
79 key: key.to_string(),
80 value: Some(value.to_string()),
81 });
82 build_query(¶ms)
83}
84
85fn normalize_query_input(input: &str) -> &str {
86 let trimmed = input.trim();
87 let before_fragment = trimmed
88 .split_once('#')
89 .map_or(trimmed, |(before, _)| before);
90
91 if let Some((_, query)) = before_fragment.split_once('?') {
92 query
93 } else {
94 before_fragment.strip_prefix('?').unwrap_or(before_fragment)
95 }
96}