1use std::cmp::Ordering;
4use std::collections::BTreeMap;
5use std::fmt;
6use std::str::FromStr;
7
8use crate::constants::MAX_URI_LENGTH;
9use crate::error::QueryError;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
31pub struct QueryParams {
32 params: BTreeMap<String, String>,
33}
34
35impl QueryParams {
36 #[must_use]
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn parse(input: &str) -> Result<Self, QueryError> {
58 if input.len() > MAX_URI_LENGTH {
59 return Err(QueryError::TooLong {
60 max: MAX_URI_LENGTH,
61 actual: input.len(),
62 });
63 }
64
65 if input.is_empty() {
66 return Ok(Self::new());
67 }
68
69 let mut params = BTreeMap::new();
70
71 for pair in input.split('&') {
72 if pair.is_empty() {
73 continue;
74 }
75
76 let (name, value) = if let Some(eq_idx) = pair.find('=') {
77 let name = &pair[..eq_idx];
78 let value = &pair[eq_idx + 1..];
79 (name, value)
80 } else {
81 (pair, "")
82 };
83
84 Self::validate_param_name(name)?;
86
87 let decoded_value = Self::decode_value(name, value)?;
89
90 if params.contains_key(name) {
92 return Err(QueryError::DuplicateParam {
93 name: name.to_string(),
94 });
95 }
96
97 params.insert(name.to_string(), decoded_value);
98 }
99
100 Ok(Self { params })
101 }
102
103 #[must_use]
105 pub fn get(&self, name: &str) -> Option<&str> {
106 self.params.get(name).map(String::as_str)
107 }
108
109 #[must_use]
111 pub fn is_empty(&self) -> bool {
112 self.params.is_empty()
113 }
114
115 #[must_use]
117 pub fn len(&self) -> usize {
118 self.params.len()
119 }
120
121 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
123 self.params.iter().map(|(k, v)| (k.as_str(), v.as_str()))
124 }
125
126 #[must_use]
128 pub fn version(&self) -> Option<&str> {
129 self.get("version")
130 }
131
132 #[must_use]
134 pub fn ttl(&self) -> Option<u64> {
135 self.get("ttl").and_then(|s| s.parse().ok())
136 }
137
138 pub fn with_param(&self, key: &str, value: &str) -> Result<Self, QueryError> {
166 Self::validate_param_name(key)?;
167
168 let mut params = self.params.clone();
169 params.insert(key.to_string(), value.to_string());
170 Ok(Self { params })
171 }
172
173 #[must_use]
186 pub fn without_param(&self, key: &str) -> Self {
187 let mut params = self.params.clone();
188 params.remove(key);
189 Self { params }
190 }
191
192 pub fn with_version(&self, v: &str) -> Result<Self, QueryError> {
210 self.with_param("version", v)
211 }
212
213 pub fn with_ttl(&self, ttl: u64) -> Result<Self, QueryError> {
231 self.with_param("ttl", &ttl.to_string())
232 }
233
234 fn validate_param_name(name: &str) -> Result<(), QueryError> {
235 if name.is_empty() {
236 return Err(QueryError::InvalidParamName {
237 name: name.to_string(),
238 reason: "parameter name cannot be empty",
239 });
240 }
241
242 for c in name.chars() {
243 if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
244 return Err(QueryError::InvalidParamName {
245 name: name.to_string(),
246 reason: "name must be alphanumeric, hyphen, or underscore",
247 });
248 }
249 }
250
251 Ok(())
252 }
253
254 fn decode_value(name: &str, value: &str) -> Result<String, QueryError> {
255 let encoded = value.as_bytes();
256 let mut decoded = Vec::with_capacity(value.len());
257 let mut index = 0;
258
259 while let Some(&byte) = encoded.get(index) {
260 match byte {
261 b'%' => {
262 let digits = encoded
263 .get(index + 1)
264 .copied()
265 .and_then(Self::hex_digit)
266 .zip(encoded.get(index + 2).copied().and_then(Self::hex_digit));
267 let Some((high, low)) = digits else {
268 return Err(QueryError::InvalidPercentEncoding {
269 value: value.to_string(),
270 });
271 };
272 decoded.push((high << 4) | low);
273 index += 3;
274 }
275 byte if Self::is_unreserved(byte) => {
276 decoded.push(byte);
277 index += 1;
278 }
279 _ => {
280 return Err(QueryError::InvalidParamValue {
281 name: name.to_string(),
282 value: value.to_string(),
283 reason: "contains invalid unencoded character",
284 });
285 }
286 }
287 }
288
289 String::from_utf8(decoded).map_err(|_| QueryError::InvalidUtf8 {
290 value: value.to_string(),
291 })
292 }
293
294 fn encode_value(value: &str) -> String {
295 const HEX_DIGITS: &[u8; 16] = b"0123456789ABCDEF";
296
297 let mut encoded = String::with_capacity(value.len());
298 for &byte in value.as_bytes() {
299 if Self::is_unreserved(byte) {
300 encoded.push(char::from(byte));
301 } else {
302 encoded.push('%');
303 encoded.push(char::from(HEX_DIGITS[usize::from(byte >> 4)]));
304 encoded.push(char::from(HEX_DIGITS[usize::from(byte & 0x0f)]));
305 }
306 }
307 encoded
308 }
309
310 const fn hex_digit(byte: u8) -> Option<u8> {
311 match byte {
312 b'0'..=b'9' => Some(byte - b'0'),
313 b'A'..=b'F' => Some(byte - b'A' + 10),
314 b'a'..=b'f' => Some(byte - b'a' + 10),
315 _ => None,
316 }
317 }
318
319 const fn is_unreserved(byte: u8) -> bool {
320 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')
321 }
322}
323
324impl fmt::Display for QueryParams {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 let pairs: Vec<String> = self
327 .params
328 .iter()
329 .map(|(k, v)| {
330 if v.is_empty() {
331 k.clone()
332 } else {
333 let encoded_value = Self::encode_value(v);
334 format!("{k}={encoded_value}")
335 }
336 })
337 .collect();
338 write!(f, "{}", pairs.join("&"))
339 }
340}
341
342impl FromStr for QueryParams {
343 type Err = QueryError;
344
345 fn from_str(s: &str) -> Result<Self, Self::Err> {
346 Self::parse(s)
347 }
348}
349
350impl TryFrom<&str> for QueryParams {
351 type Error = QueryError;
352
353 fn try_from(s: &str) -> Result<Self, Self::Error> {
354 Self::parse(s)
355 }
356}
357
358impl PartialOrd for QueryParams {
359 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
360 Some(self.cmp(other))
361 }
362}
363
364impl Ord for QueryParams {
373 fn cmp(&self, other: &Self) -> Ordering {
374 self.params.cmp(&other.params)
375 }
376}
377
378#[cfg(feature = "serde")]
379impl serde::Serialize for QueryParams {
380 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
381 where
382 S: serde::Serializer,
383 {
384 serializer.serialize_str(&self.to_string())
385 }
386}
387
388#[cfg(feature = "serde")]
389impl<'de> serde::Deserialize<'de> for QueryParams {
390 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
391 where
392 D: serde::Deserializer<'de>,
393 {
394 let s = String::deserialize(deserializer)?;
395 Self::parse(&s).map_err(serde::de::Error::custom)
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn parse_empty() {
405 let params = QueryParams::parse("").unwrap();
406 assert!(params.is_empty());
407 }
408
409 #[test]
410 fn query_at_maximum_length_parses() {
411 let input = format!("v={}", "a".repeat(MAX_URI_LENGTH - 2));
412
413 let params = QueryParams::parse(&input).unwrap();
414
415 assert_eq!(input.len(), MAX_URI_LENGTH);
416 assert_eq!(params.get("v").unwrap().len(), MAX_URI_LENGTH - 2);
417 }
418
419 #[test]
420 fn query_over_maximum_length_reports_limit_and_actual_length() {
421 let input = format!("v={}", "a".repeat(MAX_URI_LENGTH - 1));
422
423 let error = QueryParams::parse(&input).unwrap_err();
424 let QueryError::TooLong { max, actual } = error else {
425 panic!("expected QueryError::TooLong");
426 };
427
428 assert_eq!(max, MAX_URI_LENGTH);
429 assert_eq!(actual, input.len());
430 }
431
432 #[test]
433 fn query_length_cap_precedes_character_validation() {
434 let input = format!("v={}", "@".repeat(MAX_URI_LENGTH));
435
436 assert!(matches!(
437 QueryParams::parse(&input),
438 Err(QueryError::TooLong {
439 max: MAX_URI_LENGTH,
440 actual
441 }) if actual == input.len()
442 ));
443 }
444
445 #[test]
446 fn whole_uri_length_cap_still_precedes_query_parsing() {
447 let base = "agent://example.com/chat/llm_01h455vb4pex5vsknk084sn02q?v=";
448 let input = format!("{base}{}", "a".repeat(MAX_URI_LENGTH + 1 - base.len()));
449
450 let error = crate::AgentUri::parse(&input).unwrap_err();
451 let crate::ParseErrorKind::TooLong { max, actual } = error.kind else {
452 panic!("expected ParseErrorKind::TooLong");
453 };
454
455 assert_eq!(input.len(), MAX_URI_LENGTH + 1);
456 assert_eq!(max, MAX_URI_LENGTH);
457 assert_eq!(actual, input.len());
458 }
459
460 #[test]
461 fn parse_single_param() {
462 let params = QueryParams::parse("version=2.0").unwrap();
463 assert_eq!(params.get("version"), Some("2.0"));
464 assert_eq!(params.len(), 1);
465 }
466
467 #[test]
468 fn parse_multiple_params() {
469 let params = QueryParams::parse("version=2.0&ttl=300").unwrap();
470 assert_eq!(params.get("version"), Some("2.0"));
471 assert_eq!(params.get("ttl"), Some("300"));
472 assert_eq!(params.len(), 2);
473 }
474
475 #[test]
476 fn parse_param_without_value() {
477 let params = QueryParams::parse("flag").unwrap();
478 assert_eq!(params.get("flag"), Some(""));
479 }
480
481 #[test]
482 fn ascii_percent_escapes_still_decode() {
483 let params = QueryParams::parse("name=%41%42%43").unwrap();
484 assert_eq!(params.get("name"), Some("ABC"));
485 }
486
487 #[test]
488 fn percent_decoded_multibyte_sequences_are_utf8() {
489 let acute = QueryParams::parse("name=%C3%A9").unwrap();
491 let euro = QueryParams::parse("name=%E2%82%AC").unwrap();
492 let crab = QueryParams::parse("name=%F0%9F%A6%80").unwrap();
493
494 assert_eq!(acute.get("name"), Some("é"));
495 assert_eq!(euro.get("name"), Some("€"));
496 assert_eq!(crab.get("name"), Some("🦀"));
497 }
498
499 #[test]
500 fn invalid_utf8_percent_escapes_are_rejected() {
501 for input in ["name=%FF", "name=%80", "name=%C3", "name=%ED%A0%80"] {
502 let result = QueryParams::parse(input);
503
504 assert!(
505 matches!(result, Err(QueryError::InvalidUtf8 { .. })),
506 "expected invalid UTF-8 error for {input}"
507 );
508 }
509 }
510
511 #[test]
512 fn version_helper() {
513 let params = QueryParams::parse("version=2.0").unwrap();
514 assert_eq!(params.version(), Some("2.0"));
515 }
516
517 #[test]
518 fn ttl_helper() {
519 let params = QueryParams::parse("ttl=300").unwrap();
520 assert_eq!(params.ttl(), Some(300));
521 }
522
523 #[test]
524 fn ttl_invalid_returns_none() {
525 let params = QueryParams::parse("ttl=invalid").unwrap();
526 assert_eq!(params.ttl(), None);
527 }
528
529 #[test]
530 fn parse_duplicate_fails() {
531 let result = QueryParams::parse("version=1.0&version=2.0");
532 assert!(matches!(result, Err(QueryError::DuplicateParam { .. })));
533 }
534
535 #[test]
536 fn parse_invalid_name_fails() {
537 let result = QueryParams::parse("invalid@name=value");
538 assert!(matches!(result, Err(QueryError::InvalidParamName { .. })));
539 }
540
541 #[test]
542 fn malformed_percent_escapes_still_report_encoding_error() {
543 for input in ["name=%GG", "name=%4"] {
544 let result = QueryParams::parse(input);
545
546 assert!(
547 matches!(result, Err(QueryError::InvalidPercentEncoding { .. })),
548 "expected invalid percent encoding error for {input}"
549 );
550 }
551 }
552
553 #[test]
554 fn parse_invalid_value_char_fails() {
555 let result = QueryParams::parse("name=value with space");
556 assert!(matches!(result, Err(QueryError::InvalidParamValue { .. })));
557 }
558
559 #[test]
560 fn display_sorted() {
561 let params = QueryParams::parse("z=1&a=2").unwrap();
562 assert_eq!(params.to_string(), "a=2&z=1");
564 }
565
566 #[test]
567 fn display_reencodes_non_unreserved_octets() {
568 let unicode = QueryParams::parse("name=%C3%A9").unwrap();
569 let space = QueryParams::parse("name=%20").unwrap();
570
571 assert_eq!(unicode.to_string(), "name=%C3%A9");
572 assert_eq!(space.to_string(), "name=%20");
573 }
574
575 #[test]
576 fn reserved_delimiters_survive_the_display_round_trip() {
577 let params = QueryParams::parse("a=%26b%3Dc").unwrap();
578 assert_eq!(params.get("a"), Some("&b=c"));
579 assert_eq!(params.len(), 1);
580
581 let rendered = params.to_string();
582 assert_eq!(rendered, "a=%26b%3Dc");
583
584 let reparsed = QueryParams::parse(&rendered).unwrap();
587 assert_eq!(reparsed.get("a"), Some("&b=c"));
588 assert_eq!(reparsed.len(), 1);
589 }
590
591 #[test]
592 fn with_param_accepts_values_that_parse_accepts() {
593 for value in ["&b=c", "é", "a b", "?x=1", "100%"] {
594 let params = QueryParams::new().with_param("v", value).unwrap();
595 assert_eq!(params.get("v"), Some(value));
596 }
597 }
598
599 #[test]
600 fn with_param_values_round_trip_through_display() {
601 for value in ["&b=c", "é", "a b", "?x=1", "100%"] {
602 let params = QueryParams::new().with_param("v", value).unwrap();
603 let rendered = params.to_string();
604 let reparsed = QueryParams::parse(&rendered).unwrap();
605
606 assert_eq!(reparsed.get("v"), Some(value));
607 assert_eq!(reparsed.len(), 1);
608 }
609 }
610
611 #[test]
612 fn with_param_still_rejects_invalid_names() {
613 let invalid_space = QueryParams::new().with_param("bad name", "1");
614 let empty = QueryParams::new().with_param("", "1");
615
616 assert!(matches!(
617 invalid_space,
618 Err(QueryError::InvalidParamName { .. })
619 ));
620 assert!(matches!(empty, Err(QueryError::InvalidParamName { .. })));
621 }
622
623 #[test]
624 fn oversized_query_values_are_still_rejected_at_the_uri_boundary() {
625 let params = QueryParams::new()
626 .with_param("v", &"&".repeat(300))
627 .unwrap();
628 let uri = crate::AgentUri::parse("agent://example.com/chat/llm_01h455vb4pex5vsknk084sn02q")
629 .unwrap();
630
631 let error = uri.with_query(params).unwrap_err();
632 assert!(matches!(
633 error.kind,
634 crate::ParseErrorKind::TooLong { max, actual }
635 if max == crate::MAX_URI_LENGTH && actual > max
636 ));
637 }
638
639 #[test]
640 fn uri_with_encoded_query_round_trips() {
641 let uri = crate::AgentUri::parse(
642 "agent://example.com/chat/llm_01h455vb4pex5vsknk084sn02q?name=%C3%A9",
643 )
644 .unwrap();
645
646 assert_eq!(uri.query().get("name"), Some("é"));
647
648 let reparsed = crate::AgentUri::parse(uri.as_str()).unwrap();
649 assert_eq!(reparsed.query().get("name"), Some("é"));
650 assert_eq!(reparsed.query().to_string(), uri.query().to_string());
651 }
652
653 #[test]
654 fn iter_returns_all_params() {
655 let params = QueryParams::parse("a=1&b=2").unwrap();
656 let items: Vec<_> = params.iter().collect();
657 assert_eq!(items, vec![("a", "1"), ("b", "2")]);
658 }
659
660 #[test]
661 fn ordering_agrees_with_equality_and_walks_keys_in_order() {
662 let empty = QueryParams::new();
663 let a = QueryParams::parse("a=1").unwrap();
664 let a_b = QueryParams::parse("a=1&b=2").unwrap();
665 let b = QueryParams::parse("b=1").unwrap();
666 let a_two = QueryParams::parse("a=2").unwrap();
667
668 assert!(empty < a);
669 assert!(a < a_b);
670 assert!(a_b < b);
671 assert!(a < a_two);
672
673 for left in [&empty, &a, &a_b, &b, &a_two] {
674 for right in [&empty, &a, &a_b, &b, &a_two] {
675 assert_eq!(left.cmp(right) == Ordering::Equal, left == right);
676 assert_eq!(left.cmp(right).reverse(), right.cmp(left));
677 }
678 }
679 }
680
681 #[test]
682 fn ordering_is_by_value_and_not_by_the_way_it_renders() {
683 let tilde = QueryParams::new().with_param("note", "~").unwrap();
687 let zed = QueryParams::new().with_param("note", "z").unwrap();
688
689 assert!(zed < tilde);
690 assert!(tilde.to_string() < zed.to_string());
691 }
692
693 #[test]
694 fn two_spellings_of_one_value_order_as_one() {
695 let written_encoded = QueryParams::parse("note=a%20b").unwrap();
696 let built_directly = QueryParams::new().with_param("note", "a b").unwrap();
697
698 assert_eq!(written_encoded, built_directly);
699 assert_eq!(written_encoded.cmp(&built_directly), Ordering::Equal);
700 }
701}