1use std::cmp::Ordering;
16use std::fmt;
17use std::str::FromStr;
18
19use crate::agent_id::AgentId;
20use crate::capability_path::CapabilityPath;
21use crate::constants::{MAX_URI_LENGTH, SCHEME};
22use crate::error::{ParseError, ParseErrorKind};
23use crate::fragment::Fragment;
24use crate::query::QueryParams;
25use crate::trust_root::TrustRoot;
26
27#[derive(Debug, Clone)]
54pub struct AgentUri {
55 trust_root: TrustRoot,
56 capability_path: CapabilityPath,
57 agent_id: AgentId,
58 query: QueryParams,
59 fragment: Option<Fragment>,
60 normalized: String,
62}
63
64impl PartialEq for AgentUri {
65 fn eq(&self, other: &Self) -> bool {
66 self.trust_root == other.trust_root
67 && self.capability_path == other.capability_path
68 && self.agent_id == other.agent_id
69 }
70}
71
72impl Eq for AgentUri {}
73
74impl AgentUri {
75 pub fn parse(input: &str) -> Result<Self, ParseError> {
85 Self::parse_inner(input).map_err(|kind| ParseError {
86 input: input.to_string(),
87 kind,
88 })
89 }
90
91 pub fn new(
97 trust_root: TrustRoot,
98 capability_path: CapabilityPath,
99 agent_id: AgentId,
100 query: QueryParams,
101 fragment: Option<Fragment>,
102 ) -> Result<Self, ParseError> {
103 let normalized = Self::normalize(
104 &trust_root,
105 &capability_path,
106 &agent_id,
107 &query,
108 fragment.as_ref(),
109 );
110 let len = normalized.len();
111
112 if len > MAX_URI_LENGTH {
113 return Err(ParseError {
114 input: normalized,
115 kind: ParseErrorKind::TooLong {
116 max: MAX_URI_LENGTH,
117 actual: len,
118 },
119 });
120 }
121
122 Ok(Self {
123 trust_root,
124 capability_path,
125 agent_id,
126 query,
127 fragment,
128 normalized,
129 })
130 }
131
132 #[must_use]
134 pub const fn trust_root(&self) -> &TrustRoot {
135 &self.trust_root
136 }
137
138 #[must_use]
140 pub const fn capability_path(&self) -> &CapabilityPath {
141 &self.capability_path
142 }
143
144 #[must_use]
146 pub const fn agent_id(&self) -> &AgentId {
147 &self.agent_id
148 }
149
150 #[must_use]
152 pub const fn query(&self) -> &QueryParams {
153 &self.query
154 }
155
156 #[must_use]
158 pub const fn fragment(&self) -> Option<&Fragment> {
159 self.fragment.as_ref()
160 }
161
162 #[must_use]
164 pub fn as_str(&self) -> &str {
165 &self.normalized
166 }
167
168 #[must_use]
170 pub fn canonical(&self) -> String {
171 format!(
172 "{SCHEME}://{}/{}/{}",
173 self.trust_root, self.capability_path, self.agent_id
174 )
175 }
176
177 #[must_use]
179 pub fn is_localhost(&self) -> bool {
180 self.trust_root.is_localhost()
181 }
182
183 pub fn with_query(&self, query: QueryParams) -> Result<Self, ParseError> {
200 Self::new(
201 self.trust_root.clone(),
202 self.capability_path.clone(),
203 self.agent_id.clone(),
204 query,
205 self.fragment.clone(),
206 )
207 }
208
209 pub fn with_query_str(&self, s: &str) -> Result<Self, ParseError> {
227 let query = QueryParams::parse(s).map_err(|e| ParseError {
228 input: s.to_string(),
229 kind: ParseErrorKind::InvalidQuery(e),
230 })?;
231 self.with_query(query)
232 }
233
234 pub fn without_query(&self) -> Result<Self, ParseError> {
251 self.with_query(QueryParams::new())
252 }
253
254 pub fn with_fragment(&self, fragment: Fragment) -> Result<Self, ParseError> {
271 Self::new(
272 self.trust_root.clone(),
273 self.capability_path.clone(),
274 self.agent_id.clone(),
275 self.query.clone(),
276 Some(fragment),
277 )
278 }
279
280 pub fn with_fragment_str(&self, s: &str) -> Result<Self, ParseError> {
297 let fragment = Fragment::parse(s).map_err(|e| ParseError {
298 input: s.to_string(),
299 kind: ParseErrorKind::InvalidFragment(e),
300 })?;
301 self.with_fragment(fragment)
302 }
303
304 pub fn without_fragment(&self) -> Result<Self, ParseError> {
321 Self::new(
322 self.trust_root.clone(),
323 self.capability_path.clone(),
324 self.agent_id.clone(),
325 self.query.clone(),
326 None,
327 )
328 }
329
330 fn parse_inner(input: &str) -> Result<Self, ParseErrorKind> {
331 if input.is_empty() {
332 return Err(ParseErrorKind::Empty);
333 }
334
335 if input.len() > MAX_URI_LENGTH {
336 return Err(ParseErrorKind::TooLong {
337 max: MAX_URI_LENGTH,
338 actual: input.len(),
339 });
340 }
341
342 let scheme_prefix = format!("{SCHEME}://");
344 if !input.starts_with(&scheme_prefix) {
345 let found = input.split("://").next().map(str::to_string);
346 return Err(ParseErrorKind::InvalidScheme { found });
347 }
348 let rest = &input[scheme_prefix.len()..];
349
350 let (rest, fragment) = Self::split_fragment(rest)?;
352
353 let (rest, query) = Self::split_query(rest)?;
355
356 let (trust_root_str, path_with_id) = Self::split_trust_root(rest)?;
358
359 let trust_root =
361 TrustRoot::parse(trust_root_str).map_err(ParseErrorKind::InvalidTrustRoot)?;
362
363 let (cap_path_str, agent_id_str) = Self::split_path_and_id(path_with_id)?;
365
366 let capability_path =
368 CapabilityPath::parse(cap_path_str).map_err(ParseErrorKind::InvalidCapabilityPath)?;
369
370 let agent_id = AgentId::parse(agent_id_str).map_err(ParseErrorKind::InvalidAgentId)?;
372
373 let normalized = Self::normalize(
374 &trust_root,
375 &capability_path,
376 &agent_id,
377 &query,
378 fragment.as_ref(),
379 );
380
381 Ok(Self {
382 trust_root,
383 capability_path,
384 agent_id,
385 query,
386 fragment,
387 normalized,
388 })
389 }
390
391 fn split_fragment(input: &str) -> Result<(&str, Option<Fragment>), ParseErrorKind> {
392 if let Some(hash_idx) = input.find('#') {
393 let rest = &input[..hash_idx];
394 let frag_str = &input[hash_idx + 1..];
395 if frag_str.is_empty() {
396 Ok((rest, None)) } else {
398 let fragment =
399 Fragment::parse(frag_str).map_err(ParseErrorKind::InvalidFragment)?;
400 Ok((rest, Some(fragment)))
401 }
402 } else {
403 Ok((input, None))
404 }
405 }
406
407 fn split_query(input: &str) -> Result<(&str, QueryParams), ParseErrorKind> {
408 if let Some(q_idx) = input.find('?') {
409 let rest = &input[..q_idx];
410 let query_str = &input[q_idx + 1..];
411 if query_str.is_empty() {
412 Ok((rest, QueryParams::new())) } else {
414 let query = QueryParams::parse(query_str).map_err(ParseErrorKind::InvalidQuery)?;
415 Ok((rest, query))
416 }
417 } else {
418 Ok((input, QueryParams::new()))
419 }
420 }
421
422 fn split_trust_root(input: &str) -> Result<(&str, &str), ParseErrorKind> {
423 let slash_idx = input.find('/').ok_or(ParseErrorKind::MissingComponent {
425 component: "capability path",
426 })?;
427
428 let trust_root = &input[..slash_idx];
429 let path = &input[slash_idx + 1..];
430
431 if trust_root.is_empty() {
432 return Err(ParseErrorKind::MissingComponent {
433 component: "trust root",
434 });
435 }
436
437 if path.is_empty() {
438 return Err(ParseErrorKind::MissingComponent {
439 component: "capability path",
440 });
441 }
442
443 Ok((trust_root, path))
444 }
445
446 fn split_path_and_id(input: &str) -> Result<(&str, &str), ParseErrorKind> {
447 let last_slash_idx = input.rfind('/').ok_or(ParseErrorKind::MissingComponent {
449 component: "agent ID",
450 })?;
451
452 let path = &input[..last_slash_idx];
453 let agent_id = &input[last_slash_idx + 1..];
454
455 if path.is_empty() {
456 return Err(ParseErrorKind::MissingComponent {
457 component: "capability path",
458 });
459 }
460
461 if agent_id.is_empty() {
462 return Err(ParseErrorKind::MissingComponent {
463 component: "agent ID",
464 });
465 }
466
467 Ok((path, agent_id))
468 }
469
470 fn normalize(
471 trust_root: &TrustRoot,
472 capability_path: &CapabilityPath,
473 agent_id: &AgentId,
474 query: &QueryParams,
475 fragment: Option<&Fragment>,
476 ) -> String {
477 let mut result = format!("{SCHEME}://{trust_root}/{capability_path}/{agent_id}");
478
479 if !query.is_empty() {
480 result.push('?');
481 result.push_str(&query.to_string());
482 }
483
484 if let Some(frag) = fragment {
485 result.push('#');
486 result.push_str(frag.as_str());
487 }
488
489 result
490 }
491}
492
493impl fmt::Display for AgentUri {
494 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
495 write!(f, "{}", self.normalized)
496 }
497}
498
499impl FromStr for AgentUri {
500 type Err = ParseError;
501
502 fn from_str(s: &str) -> Result<Self, Self::Err> {
503 Self::parse(s)
504 }
505}
506
507impl AsRef<str> for AgentUri {
508 fn as_ref(&self) -> &str {
509 &self.normalized
510 }
511}
512
513impl TryFrom<&str> for AgentUri {
514 type Error = ParseError;
515
516 fn try_from(s: &str) -> Result<Self, Self::Error> {
517 Self::parse(s)
518 }
519}
520
521impl PartialOrd for AgentUri {
522 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
523 Some(self.cmp(other))
524 }
525}
526
527impl Ord for AgentUri {
528 fn cmp(&self, other: &Self) -> Ordering {
529 self.canonical().cmp(&other.canonical())
530 }
531}
532
533#[cfg(feature = "serde")]
534impl serde::Serialize for AgentUri {
535 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
536 where
537 S: serde::Serializer,
538 {
539 serializer.serialize_str(&self.normalized)
540 }
541}
542
543#[cfg(feature = "serde")]
544impl<'de> serde::Deserialize<'de> for AgentUri {
545 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
546 where
547 D: serde::Deserializer<'de>,
548 {
549 let s = String::deserialize(deserializer)?;
550 Self::parse(&s).map_err(serde::de::Error::custom)
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn parse_valid_uri() {
560 let input = "agent://anthropic.com/assistant/chat/llm_chat_01h455vb4pex5vsknk084sn02q";
561 let uri = AgentUri::parse(input).unwrap();
562
563 assert_eq!(uri.trust_root().host_str(), "anthropic.com");
564 assert_eq!(uri.capability_path().as_str(), "assistant/chat");
565 assert_eq!(uri.agent_id().prefix().as_str(), "llm_chat");
566 }
567
568 #[test]
569 fn parse_empty_returns_error() {
570 let result = AgentUri::parse("");
571 assert!(matches!(
572 result,
573 Err(ParseError {
574 kind: ParseErrorKind::Empty,
575 ..
576 })
577 ));
578 }
579
580 #[test]
581 fn parse_too_long_returns_error() {
582 let long_path = "a".repeat(500);
583 let input = format!("agent://x.com/{long_path}/llm_01h455vb4pex5vsknk084sn02q");
584 let result = AgentUri::parse(&input);
585 assert!(matches!(
586 result,
587 Err(ParseError {
588 kind: ParseErrorKind::TooLong { .. },
589 ..
590 })
591 ));
592 }
593
594 #[test]
595 fn parse_wrong_scheme_returns_error() {
596 let result = AgentUri::parse("http://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q");
597 assert!(matches!(
598 result,
599 Err(ParseError {
600 kind: ParseErrorKind::InvalidScheme { .. },
601 ..
602 })
603 ));
604 }
605
606 #[test]
607 fn parse_with_query_params() {
608 let input = "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q?version=2.0&ttl=300";
609 let uri = AgentUri::parse(input).unwrap();
610
611 assert_eq!(uri.query().version(), Some("2.0"));
612 assert_eq!(uri.query().ttl(), Some(300));
613 }
614
615 #[test]
616 fn parse_with_fragment() {
617 let input = "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q#summarization";
618 let uri = AgentUri::parse(input).unwrap();
619
620 assert_eq!(
621 uri.fragment().map(crate::Fragment::as_str),
622 Some("summarization")
623 );
624 }
625
626 #[test]
627 fn empty_query_is_stripped() {
628 let input = "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q?";
629 let uri = AgentUri::parse(input).unwrap();
630 assert!(uri.query().is_empty());
631 }
632
633 #[test]
634 fn empty_fragment_is_stripped() {
635 let input = "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q#";
636 let uri = AgentUri::parse(input).unwrap();
637 assert!(uri.fragment().is_none());
638 }
639
640 #[test]
641 fn canonical_strips_query_and_fragment() {
642 let input = "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q?version=2.0#test";
643 let uri = AgentUri::parse(input).unwrap();
644 let canonical = uri.canonical();
645
646 assert!(!canonical.contains('?'));
647 assert!(!canonical.contains('#'));
648 }
649
650 #[test]
651 fn query_and_fragment_do_not_change_identity_equality() {
652 let base =
653 AgentUri::parse("agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q").unwrap();
654 let decorated = AgentUri::parse(
655 "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q?version=2.0#task",
656 )
657 .unwrap();
658
659 assert_eq!(base, decorated);
660 assert_eq!(base.cmp(&decorated), Ordering::Equal);
661 }
662
663 #[test]
664 fn is_localhost() {
665 let uri =
666 AgentUri::parse("agent://localhost:8472/test/llm_01h455vb4pex5vsknk084sn02q").unwrap();
667 assert!(uri.is_localhost());
668
669 let uri = AgentUri::parse("agent://127.0.0.1/test/llm_01h455vb4pex5vsknk084sn02q").unwrap();
670 assert!(uri.is_localhost());
671
672 let uri =
673 AgentUri::parse("agent://anthropic.com/test/llm_01h455vb4pex5vsknk084sn02q").unwrap();
674 assert!(!uri.is_localhost());
675 }
676
677 #[test]
678 fn parse_missing_capability_path() {
679 let result = AgentUri::parse("agent://anthropic.com/llm_01h455vb4pex5vsknk084sn02q");
680 assert!(matches!(
681 result,
682 Err(ParseError {
683 kind: ParseErrorKind::MissingComponent {
684 component: "agent ID"
685 },
686 ..
687 })
688 ));
689 }
690
691 #[test]
692 fn display_roundtrip() {
693 let input = "agent://anthropic.com/assistant/chat/llm_chat_01h455vb4pex5vsknk084sn02q";
694 let uri = AgentUri::parse(input).unwrap();
695 assert_eq!(uri.to_string(), input);
696 }
697
698 #[test]
699 fn new_from_components() {
700 let trust_root = TrustRoot::parse("anthropic.com").unwrap();
701 let cap_path = CapabilityPath::parse("assistant/chat").unwrap();
702 let agent_id = AgentId::parse("llm_01h455vb4pex5vsknk084sn02q").unwrap();
703
704 let uri = AgentUri::new(trust_root, cap_path, agent_id, QueryParams::new(), None).unwrap();
705
706 assert_eq!(uri.trust_root().host_str(), "anthropic.com");
707 assert_eq!(uri.capability_path().as_str(), "assistant/chat");
708 }
709}