1use std::cmp::Ordering;
4use std::collections::BTreeMap;
5use std::fmt;
6use std::str::FromStr;
7
8use crate::error::QueryError;
9
10#[derive(Debug, Clone, PartialEq, Eq, Default)]
22pub struct QueryParams {
23 params: BTreeMap<String, String>,
24}
25
26impl QueryParams {
27 #[must_use]
29 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn parse(input: &str) -> Result<Self, QueryError> {
39 if input.is_empty() {
40 return Ok(Self::new());
41 }
42
43 let mut params = BTreeMap::new();
44
45 for pair in input.split('&') {
46 if pair.is_empty() {
47 continue;
48 }
49
50 let (name, value) = if let Some(eq_idx) = pair.find('=') {
51 let name = &pair[..eq_idx];
52 let value = &pair[eq_idx + 1..];
53 (name, value)
54 } else {
55 (pair, "")
56 };
57
58 Self::validate_param_name(name)?;
60
61 let decoded_value = Self::decode_value(name, value)?;
63
64 if params.contains_key(name) {
66 return Err(QueryError::DuplicateParam {
67 name: name.to_string(),
68 });
69 }
70
71 params.insert(name.to_string(), decoded_value);
72 }
73
74 Ok(Self { params })
75 }
76
77 #[must_use]
79 pub fn get(&self, name: &str) -> Option<&str> {
80 self.params.get(name).map(String::as_str)
81 }
82
83 #[must_use]
85 pub fn is_empty(&self) -> bool {
86 self.params.is_empty()
87 }
88
89 #[must_use]
91 pub fn len(&self) -> usize {
92 self.params.len()
93 }
94
95 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
97 self.params.iter().map(|(k, v)| (k.as_str(), v.as_str()))
98 }
99
100 #[must_use]
102 pub fn version(&self) -> Option<&str> {
103 self.get("version")
104 }
105
106 #[must_use]
108 pub fn ttl(&self) -> Option<u64> {
109 self.get("ttl").and_then(|s| s.parse().ok())
110 }
111
112 pub fn with_param(&self, key: &str, value: &str) -> Result<Self, QueryError> {
128 Self::validate_param_name(key)?;
129 for c in value.chars() {
131 if !c.is_ascii_alphanumeric() && !"-_.".contains(c) {
132 return Err(QueryError::InvalidParamValue {
133 name: key.to_string(),
134 value: value.to_string(),
135 reason: "value contains invalid character",
136 });
137 }
138 }
139
140 let mut params = self.params.clone();
141 params.insert(key.to_string(), value.to_string());
142 Ok(Self { params })
143 }
144
145 #[must_use]
158 pub fn without_param(&self, key: &str) -> Self {
159 let mut params = self.params.clone();
160 params.remove(key);
161 Self { params }
162 }
163
164 pub fn with_version(&self, v: &str) -> Result<Self, QueryError> {
180 self.with_param("version", v)
181 }
182
183 pub fn with_ttl(&self, ttl: u64) -> Result<Self, QueryError> {
199 self.with_param("ttl", &ttl.to_string())
200 }
201
202 fn validate_param_name(name: &str) -> Result<(), QueryError> {
203 if name.is_empty() {
204 return Err(QueryError::InvalidParamName {
205 name: name.to_string(),
206 reason: "parameter name cannot be empty",
207 });
208 }
209
210 for c in name.chars() {
211 if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
212 return Err(QueryError::InvalidParamName {
213 name: name.to_string(),
214 reason: "name must be alphanumeric, hyphen, or underscore",
215 });
216 }
217 }
218
219 Ok(())
220 }
221
222 fn decode_value(name: &str, value: &str) -> Result<String, QueryError> {
223 let mut decoded = String::with_capacity(value.len());
224 let mut chars = value.chars().peekable();
225
226 while let Some(c) = chars.next() {
227 if c == '%' {
228 let hex: String = chars.by_ref().take(2).collect();
229 if hex.len() != 2 {
230 return Err(QueryError::InvalidPercentEncoding {
231 value: value.to_string(),
232 });
233 }
234 let byte =
235 u8::from_str_radix(&hex, 16).map_err(|_| QueryError::InvalidPercentEncoding {
236 value: value.to_string(),
237 })?;
238 decoded.push(byte as char);
239 } else if c.is_ascii_alphanumeric() || "-_.".contains(c) {
240 decoded.push(c);
241 } else {
242 return Err(QueryError::InvalidParamValue {
243 name: name.to_string(),
244 value: value.to_string(),
245 reason: "contains invalid unencoded character",
246 });
247 }
248 }
249
250 Ok(decoded)
251 }
252}
253
254impl fmt::Display for QueryParams {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 let pairs: Vec<String> = self
257 .params
258 .iter()
259 .map(|(k, v)| {
260 if v.is_empty() {
261 k.clone()
262 } else {
263 format!("{k}={v}")
264 }
265 })
266 .collect();
267 write!(f, "{}", pairs.join("&"))
268 }
269}
270
271impl FromStr for QueryParams {
272 type Err = QueryError;
273
274 fn from_str(s: &str) -> Result<Self, Self::Err> {
275 Self::parse(s)
276 }
277}
278
279impl TryFrom<&str> for QueryParams {
280 type Error = QueryError;
281
282 fn try_from(s: &str) -> Result<Self, Self::Error> {
283 Self::parse(s)
284 }
285}
286
287impl PartialOrd for QueryParams {
288 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
289 Some(self.cmp(other))
290 }
291}
292
293impl Ord for QueryParams {
294 fn cmp(&self, other: &Self) -> Ordering {
295 self.to_string().cmp(&other.to_string())
296 }
297}
298
299#[cfg(feature = "serde")]
300impl serde::Serialize for QueryParams {
301 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
302 where
303 S: serde::Serializer,
304 {
305 serializer.serialize_str(&self.to_string())
306 }
307}
308
309#[cfg(feature = "serde")]
310impl<'de> serde::Deserialize<'de> for QueryParams {
311 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
312 where
313 D: serde::Deserializer<'de>,
314 {
315 let s = String::deserialize(deserializer)?;
316 Self::parse(&s).map_err(serde::de::Error::custom)
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn parse_empty() {
326 let params = QueryParams::parse("").unwrap();
327 assert!(params.is_empty());
328 }
329
330 #[test]
331 fn parse_single_param() {
332 let params = QueryParams::parse("version=2.0").unwrap();
333 assert_eq!(params.get("version"), Some("2.0"));
334 assert_eq!(params.len(), 1);
335 }
336
337 #[test]
338 fn parse_multiple_params() {
339 let params = QueryParams::parse("version=2.0&ttl=300").unwrap();
340 assert_eq!(params.get("version"), Some("2.0"));
341 assert_eq!(params.get("ttl"), Some("300"));
342 assert_eq!(params.len(), 2);
343 }
344
345 #[test]
346 fn parse_param_without_value() {
347 let params = QueryParams::parse("flag").unwrap();
348 assert_eq!(params.get("flag"), Some(""));
349 }
350
351 #[test]
352 fn parse_percent_encoded() {
353 let params = QueryParams::parse("name=%41%42%43").unwrap();
354 assert_eq!(params.get("name"), Some("ABC"));
355 }
356
357 #[test]
358 fn version_helper() {
359 let params = QueryParams::parse("version=2.0").unwrap();
360 assert_eq!(params.version(), Some("2.0"));
361 }
362
363 #[test]
364 fn ttl_helper() {
365 let params = QueryParams::parse("ttl=300").unwrap();
366 assert_eq!(params.ttl(), Some(300));
367 }
368
369 #[test]
370 fn ttl_invalid_returns_none() {
371 let params = QueryParams::parse("ttl=invalid").unwrap();
372 assert_eq!(params.ttl(), None);
373 }
374
375 #[test]
376 fn parse_duplicate_fails() {
377 let result = QueryParams::parse("version=1.0&version=2.0");
378 assert!(matches!(result, Err(QueryError::DuplicateParam { .. })));
379 }
380
381 #[test]
382 fn parse_invalid_name_fails() {
383 let result = QueryParams::parse("invalid@name=value");
384 assert!(matches!(result, Err(QueryError::InvalidParamName { .. })));
385 }
386
387 #[test]
388 fn parse_invalid_encoding_fails() {
389 let result = QueryParams::parse("name=%GG");
390 assert!(matches!(result, Err(QueryError::InvalidPercentEncoding { .. })));
391 }
392
393 #[test]
394 fn parse_invalid_value_char_fails() {
395 let result = QueryParams::parse("name=value with space");
396 assert!(matches!(result, Err(QueryError::InvalidParamValue { .. })));
397 }
398
399 #[test]
400 fn display_sorted() {
401 let params = QueryParams::parse("z=1&a=2").unwrap();
402 assert_eq!(params.to_string(), "a=2&z=1");
404 }
405
406 #[test]
407 fn iter_returns_all_params() {
408 let params = QueryParams::parse("a=1&b=2").unwrap();
409 let items: Vec<_> = params.iter().collect();
410 assert_eq!(items, vec![("a", "1"), ("b", "2")]);
411 }
412}