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, PartialEq, Eq)]
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 AgentUri {
65 pub fn parse(input: &str) -> Result<Self, ParseError> {
75 Self::parse_inner(input).map_err(|kind| ParseError {
76 input: input.to_string(),
77 kind,
78 })
79 }
80
81 pub fn new(
87 trust_root: TrustRoot,
88 capability_path: CapabilityPath,
89 agent_id: AgentId,
90 query: QueryParams,
91 fragment: Option<Fragment>,
92 ) -> Result<Self, ParseError> {
93 let normalized = Self::normalize(&trust_root, &capability_path, &agent_id, &query, fragment.as_ref());
94 let len = normalized.len();
95
96 if len > MAX_URI_LENGTH {
97 return Err(ParseError {
98 input: normalized,
99 kind: ParseErrorKind::TooLong {
100 max: MAX_URI_LENGTH,
101 actual: len,
102 },
103 });
104 }
105
106 Ok(Self {
107 trust_root,
108 capability_path,
109 agent_id,
110 query,
111 fragment,
112 normalized,
113 })
114 }
115
116 #[must_use]
118 pub const fn trust_root(&self) -> &TrustRoot {
119 &self.trust_root
120 }
121
122 #[must_use]
124 pub const fn capability_path(&self) -> &CapabilityPath {
125 &self.capability_path
126 }
127
128 #[must_use]
130 pub const fn agent_id(&self) -> &AgentId {
131 &self.agent_id
132 }
133
134 #[must_use]
136 pub const fn query(&self) -> &QueryParams {
137 &self.query
138 }
139
140 #[must_use]
142 pub const fn fragment(&self) -> Option<&Fragment> {
143 self.fragment.as_ref()
144 }
145
146 #[must_use]
148 pub fn as_str(&self) -> &str {
149 &self.normalized
150 }
151
152 #[must_use]
154 pub fn canonical(&self) -> String {
155 format!(
156 "{SCHEME}://{}/{}/{}",
157 self.trust_root, self.capability_path, self.agent_id
158 )
159 }
160
161 #[must_use]
163 pub fn is_localhost(&self) -> bool {
164 self.trust_root.is_localhost()
165 }
166
167 pub fn with_query(&self, query: QueryParams) -> Result<Self, ParseError> {
184 Self::new(
185 self.trust_root.clone(),
186 self.capability_path.clone(),
187 self.agent_id.clone(),
188 query,
189 self.fragment.clone(),
190 )
191 }
192
193 pub fn with_query_str(&self, s: &str) -> Result<Self, ParseError> {
211 let query = QueryParams::parse(s).map_err(|e| ParseError {
212 input: s.to_string(),
213 kind: ParseErrorKind::InvalidQuery(e),
214 })?;
215 self.with_query(query)
216 }
217
218 pub fn without_query(&self) -> Result<Self, ParseError> {
235 self.with_query(QueryParams::new())
236 }
237
238 pub fn with_fragment(&self, fragment: Fragment) -> Result<Self, ParseError> {
255 Self::new(
256 self.trust_root.clone(),
257 self.capability_path.clone(),
258 self.agent_id.clone(),
259 self.query.clone(),
260 Some(fragment),
261 )
262 }
263
264 pub fn with_fragment_str(&self, s: &str) -> Result<Self, ParseError> {
281 let fragment = Fragment::parse(s).map_err(|e| ParseError {
282 input: s.to_string(),
283 kind: ParseErrorKind::InvalidFragment(e),
284 })?;
285 self.with_fragment(fragment)
286 }
287
288 pub fn without_fragment(&self) -> Result<Self, ParseError> {
305 Self::new(
306 self.trust_root.clone(),
307 self.capability_path.clone(),
308 self.agent_id.clone(),
309 self.query.clone(),
310 None,
311 )
312 }
313
314 fn parse_inner(input: &str) -> Result<Self, ParseErrorKind> {
315 if input.is_empty() {
316 return Err(ParseErrorKind::Empty);
317 }
318
319 if input.len() > MAX_URI_LENGTH {
320 return Err(ParseErrorKind::TooLong {
321 max: MAX_URI_LENGTH,
322 actual: input.len(),
323 });
324 }
325
326 let scheme_prefix = format!("{SCHEME}://");
328 if !input.starts_with(&scheme_prefix) {
329 let found = input.split("://").next().map(str::to_string);
330 return Err(ParseErrorKind::InvalidScheme { found });
331 }
332 let rest = &input[scheme_prefix.len()..];
333
334 let (rest, fragment) = Self::split_fragment(rest)?;
336
337 let (rest, query) = Self::split_query(rest)?;
339
340 let (trust_root_str, path_with_id) = Self::split_trust_root(rest)?;
342
343 let trust_root =
345 TrustRoot::parse(trust_root_str).map_err(ParseErrorKind::InvalidTrustRoot)?;
346
347 let (cap_path_str, agent_id_str) = Self::split_path_and_id(path_with_id)?;
349
350 let capability_path =
352 CapabilityPath::parse(cap_path_str).map_err(ParseErrorKind::InvalidCapabilityPath)?;
353
354 let agent_id = AgentId::parse(agent_id_str).map_err(ParseErrorKind::InvalidAgentId)?;
356
357 let normalized =
358 Self::normalize(&trust_root, &capability_path, &agent_id, &query, fragment.as_ref());
359
360 Ok(Self {
361 trust_root,
362 capability_path,
363 agent_id,
364 query,
365 fragment,
366 normalized,
367 })
368 }
369
370 fn split_fragment(input: &str) -> Result<(&str, Option<Fragment>), ParseErrorKind> {
371 if let Some(hash_idx) = input.find('#') {
372 let rest = &input[..hash_idx];
373 let frag_str = &input[hash_idx + 1..];
374 if frag_str.is_empty() {
375 Ok((rest, None)) } else {
377 let fragment =
378 Fragment::parse(frag_str).map_err(ParseErrorKind::InvalidFragment)?;
379 Ok((rest, Some(fragment)))
380 }
381 } else {
382 Ok((input, None))
383 }
384 }
385
386 fn split_query(input: &str) -> Result<(&str, QueryParams), ParseErrorKind> {
387 if let Some(q_idx) = input.find('?') {
388 let rest = &input[..q_idx];
389 let query_str = &input[q_idx + 1..];
390 if query_str.is_empty() {
391 Ok((rest, QueryParams::new())) } else {
393 let query = QueryParams::parse(query_str).map_err(ParseErrorKind::InvalidQuery)?;
394 Ok((rest, query))
395 }
396 } else {
397 Ok((input, QueryParams::new()))
398 }
399 }
400
401 fn split_trust_root(input: &str) -> Result<(&str, &str), ParseErrorKind> {
402 let slash_idx = input.find('/').ok_or(ParseErrorKind::MissingComponent {
404 component: "capability path",
405 })?;
406
407 let trust_root = &input[..slash_idx];
408 let path = &input[slash_idx + 1..];
409
410 if trust_root.is_empty() {
411 return Err(ParseErrorKind::MissingComponent {
412 component: "trust root",
413 });
414 }
415
416 if path.is_empty() {
417 return Err(ParseErrorKind::MissingComponent {
418 component: "capability path",
419 });
420 }
421
422 Ok((trust_root, path))
423 }
424
425 fn split_path_and_id(input: &str) -> Result<(&str, &str), ParseErrorKind> {
426 let last_slash_idx = input.rfind('/').ok_or(ParseErrorKind::MissingComponent {
428 component: "agent ID",
429 })?;
430
431 let path = &input[..last_slash_idx];
432 let agent_id = &input[last_slash_idx + 1..];
433
434 if path.is_empty() {
435 return Err(ParseErrorKind::MissingComponent {
436 component: "capability path",
437 });
438 }
439
440 if agent_id.is_empty() {
441 return Err(ParseErrorKind::MissingComponent {
442 component: "agent ID",
443 });
444 }
445
446 Ok((path, agent_id))
447 }
448
449 fn normalize(
450 trust_root: &TrustRoot,
451 capability_path: &CapabilityPath,
452 agent_id: &AgentId,
453 query: &QueryParams,
454 fragment: Option<&Fragment>,
455 ) -> String {
456 let mut result = format!("{SCHEME}://{trust_root}/{capability_path}/{agent_id}");
457
458 if !query.is_empty() {
459 result.push('?');
460 result.push_str(&query.to_string());
461 }
462
463 if let Some(frag) = fragment {
464 result.push('#');
465 result.push_str(frag.as_str());
466 }
467
468 result
469 }
470}
471
472impl fmt::Display for AgentUri {
473 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474 write!(f, "{}", self.normalized)
475 }
476}
477
478impl FromStr for AgentUri {
479 type Err = ParseError;
480
481 fn from_str(s: &str) -> Result<Self, Self::Err> {
482 Self::parse(s)
483 }
484}
485
486impl AsRef<str> for AgentUri {
487 fn as_ref(&self) -> &str {
488 &self.normalized
489 }
490}
491
492impl TryFrom<&str> for AgentUri {
493 type Error = ParseError;
494
495 fn try_from(s: &str) -> Result<Self, Self::Error> {
496 Self::parse(s)
497 }
498}
499
500impl PartialOrd for AgentUri {
501 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
502 Some(self.cmp(other))
503 }
504}
505
506impl Ord for AgentUri {
507 fn cmp(&self, other: &Self) -> Ordering {
508 self.normalized.cmp(&other.normalized)
509 }
510}
511
512#[cfg(feature = "serde")]
513impl serde::Serialize for AgentUri {
514 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
515 where
516 S: serde::Serializer,
517 {
518 serializer.serialize_str(&self.normalized)
519 }
520}
521
522#[cfg(feature = "serde")]
523impl<'de> serde::Deserialize<'de> for AgentUri {
524 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
525 where
526 D: serde::Deserializer<'de>,
527 {
528 let s = String::deserialize(deserializer)?;
529 Self::parse(&s).map_err(serde::de::Error::custom)
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536
537 #[test]
538 fn parse_valid_uri() {
539 let input = "agent://anthropic.com/assistant/chat/llm_chat_01h455vb4pex5vsknk084sn02q";
540 let uri = AgentUri::parse(input).unwrap();
541
542 assert_eq!(uri.trust_root().host_str(), "anthropic.com");
543 assert_eq!(uri.capability_path().as_str(), "assistant/chat");
544 assert_eq!(uri.agent_id().prefix().as_str(), "llm_chat");
545 }
546
547 #[test]
548 fn parse_empty_returns_error() {
549 let result = AgentUri::parse("");
550 assert!(matches!(
551 result,
552 Err(ParseError {
553 kind: ParseErrorKind::Empty,
554 ..
555 })
556 ));
557 }
558
559 #[test]
560 fn parse_too_long_returns_error() {
561 let long_path = "a".repeat(500);
562 let input = format!("agent://x.com/{long_path}/llm_01h455vb4pex5vsknk084sn02q");
563 let result = AgentUri::parse(&input);
564 assert!(matches!(
565 result,
566 Err(ParseError {
567 kind: ParseErrorKind::TooLong { .. },
568 ..
569 })
570 ));
571 }
572
573 #[test]
574 fn parse_wrong_scheme_returns_error() {
575 let result =
576 AgentUri::parse("http://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q");
577 assert!(matches!(
578 result,
579 Err(ParseError {
580 kind: ParseErrorKind::InvalidScheme { .. },
581 ..
582 })
583 ));
584 }
585
586 #[test]
587 fn parse_with_query_params() {
588 let input =
589 "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q?version=2.0&ttl=300";
590 let uri = AgentUri::parse(input).unwrap();
591
592 assert_eq!(uri.query().version(), Some("2.0"));
593 assert_eq!(uri.query().ttl(), Some(300));
594 }
595
596 #[test]
597 fn parse_with_fragment() {
598 let input = "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q#summarization";
599 let uri = AgentUri::parse(input).unwrap();
600
601 assert_eq!(
602 uri.fragment().map(crate::Fragment::as_str),
603 Some("summarization")
604 );
605 }
606
607 #[test]
608 fn empty_query_is_stripped() {
609 let input = "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q?";
610 let uri = AgentUri::parse(input).unwrap();
611 assert!(uri.query().is_empty());
612 }
613
614 #[test]
615 fn empty_fragment_is_stripped() {
616 let input = "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q#";
617 let uri = AgentUri::parse(input).unwrap();
618 assert!(uri.fragment().is_none());
619 }
620
621 #[test]
622 fn canonical_strips_query_and_fragment() {
623 let input =
624 "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q?version=2.0#test";
625 let uri = AgentUri::parse(input).unwrap();
626 let canonical = uri.canonical();
627
628 assert!(!canonical.contains('?'));
629 assert!(!canonical.contains('#'));
630 }
631
632 #[test]
633 fn is_localhost() {
634 let uri = AgentUri::parse(
635 "agent://localhost:8472/test/llm_01h455vb4pex5vsknk084sn02q",
636 )
637 .unwrap();
638 assert!(uri.is_localhost());
639
640 let uri =
641 AgentUri::parse("agent://127.0.0.1/test/llm_01h455vb4pex5vsknk084sn02q").unwrap();
642 assert!(uri.is_localhost());
643
644 let uri =
645 AgentUri::parse("agent://anthropic.com/test/llm_01h455vb4pex5vsknk084sn02q").unwrap();
646 assert!(!uri.is_localhost());
647 }
648
649 #[test]
650 fn parse_missing_capability_path() {
651 let result = AgentUri::parse("agent://anthropic.com/llm_01h455vb4pex5vsknk084sn02q");
652 assert!(matches!(
653 result,
654 Err(ParseError {
655 kind: ParseErrorKind::MissingComponent { component: "agent ID" },
656 ..
657 })
658 ));
659 }
660
661 #[test]
662 fn display_roundtrip() {
663 let input = "agent://anthropic.com/assistant/chat/llm_chat_01h455vb4pex5vsknk084sn02q";
664 let uri = AgentUri::parse(input).unwrap();
665 assert_eq!(uri.to_string(), input);
666 }
667
668 #[test]
669 fn new_from_components() {
670 let trust_root = TrustRoot::parse("anthropic.com").unwrap();
671 let cap_path = CapabilityPath::parse("assistant/chat").unwrap();
672 let agent_id = AgentId::parse("llm_01h455vb4pex5vsknk084sn02q").unwrap();
673
674 let uri = AgentUri::new(
675 trust_root,
676 cap_path,
677 agent_id,
678 QueryParams::new(),
679 None,
680 )
681 .unwrap();
682
683 assert_eq!(uri.trust_root().host_str(), "anthropic.com");
684 assert_eq!(uri.capability_path().as_str(), "assistant/chat");
685 }
686}