agent_uri/builder.rs
1//! Typestate builder for constructing [`AgentUri`] instances.
2//!
3//! This module provides a builder that uses phantom types to enforce
4//! at compile-time that components are added in the correct order.
5
6use std::marker::PhantomData;
7
8use crate::agent_id::AgentId;
9use crate::capability_path::CapabilityPath;
10use crate::error::{
11 AgentIdError, BuilderError, CapabilityPathError, FragmentError, ParseErrorKind, QueryError,
12 TrustRootError,
13};
14use crate::fragment::Fragment;
15use crate::query::QueryParams;
16use crate::trust_root::TrustRoot;
17use crate::uri::AgentUri;
18
19/// Marker: No components set yet.
20#[derive(Debug, Clone, Copy, Default)]
21pub struct Empty;
22
23/// Marker: Trust root has been set.
24#[derive(Debug, Clone, Copy, Default)]
25pub struct HasTrustRoot;
26
27/// Marker: Trust root and capability path have been set.
28#[derive(Debug, Clone, Copy, Default)]
29pub struct HasCapabilityPath;
30
31/// Marker: All required components are set, ready to build.
32#[derive(Debug, Clone, Copy, Default)]
33pub struct Ready;
34
35/// A typestate builder for constructing [`AgentUri`] instances.
36///
37/// This builder enforces at compile-time that components are added
38/// in the correct order: trust root, then capability path, then agent ID.
39/// Query and fragment are optional and can be added at any point.
40///
41/// # Type State
42///
43/// The builder uses phantom types to track which components have been set:
44/// - [`Empty`]: Initial state, no components set
45/// - [`HasTrustRoot`]: Trust root has been set
46/// - [`HasCapabilityPath`]: Trust root and capability path have been set
47/// - [`Ready`]: All required components set, can call `build()`
48///
49/// # Examples
50///
51/// ```
52/// use agent_uri::{AgentUriBuilder, TrustRoot, CapabilityPath, AgentId};
53///
54/// let uri = AgentUriBuilder::new()
55/// .trust_root(TrustRoot::parse("anthropic.com").unwrap())
56/// .capability_path(CapabilityPath::parse("assistant/chat").unwrap())
57/// .agent_id(AgentId::new("llm_chat"))
58/// .build()
59/// .unwrap();
60///
61/// assert_eq!(uri.trust_root().host_str(), "anthropic.com");
62/// ```
63///
64/// # Compile-Time Safety
65///
66/// Attempting to call methods out of order results in a compile error:
67///
68/// ```compile_fail
69/// use agent_uri::{AgentUriBuilder, CapabilityPath};
70///
71/// // Error: cannot call capability_path() before trust_root()
72/// let path = CapabilityPath::parse("chat").unwrap();
73/// let builder = AgentUriBuilder::new()
74/// .capability_path(path); // Compile error!
75/// ```
76///
77/// ```compile_fail
78/// use agent_uri::{AgentUriBuilder, TrustRoot};
79///
80/// // Error: cannot call build() without all required components
81/// let root = TrustRoot::parse("example.com").unwrap();
82/// let uri = AgentUriBuilder::new()
83/// .trust_root(root)
84/// .build(); // Compile error!
85/// ```
86#[derive(Debug, Clone)]
87pub struct AgentUriBuilder<State = Empty> {
88 trust_root: Option<TrustRoot>,
89 capability_path: Option<CapabilityPath>,
90 agent_id: Option<AgentId>,
91 query: QueryParams,
92 fragment: Option<Fragment>,
93 _state: PhantomData<State>,
94}
95
96impl AgentUriBuilder<Empty> {
97 /// Creates a new builder in the initial state.
98 ///
99 /// # Examples
100 ///
101 /// ```
102 /// use agent_uri::AgentUriBuilder;
103 ///
104 /// let builder = AgentUriBuilder::new();
105 /// ```
106 #[must_use]
107 pub fn new() -> Self {
108 Self {
109 trust_root: None,
110 capability_path: None,
111 agent_id: None,
112 query: QueryParams::new(),
113 fragment: None,
114 _state: PhantomData,
115 }
116 }
117
118 /// Sets the trust root and advances to the [`HasTrustRoot`] state.
119 ///
120 /// This must be called first before setting other required components.
121 ///
122 /// # Arguments
123 ///
124 /// * `trust_root` - The validated trust root (authority) for the URI
125 ///
126 /// # Examples
127 ///
128 /// ```
129 /// use agent_uri::{AgentUriBuilder, TrustRoot};
130 ///
131 /// let builder = AgentUriBuilder::new()
132 /// .trust_root(TrustRoot::parse("anthropic.com").unwrap());
133 /// ```
134 #[must_use]
135 pub fn trust_root(self, trust_root: TrustRoot) -> AgentUriBuilder<HasTrustRoot> {
136 AgentUriBuilder {
137 trust_root: Some(trust_root),
138 capability_path: self.capability_path,
139 agent_id: self.agent_id,
140 query: self.query,
141 fragment: self.fragment,
142 _state: PhantomData,
143 }
144 }
145
146 /// Parses and sets the trust root from a string.
147 ///
148 /// This is a convenience method that combines parsing and setting the trust root.
149 ///
150 /// # Errors
151 ///
152 /// Returns [`TrustRootError`] if the string is not a valid trust root.
153 ///
154 /// # Examples
155 ///
156 /// ```
157 /// use agent_uri::AgentUriBuilder;
158 ///
159 /// let builder = AgentUriBuilder::new()
160 /// .try_trust_root("anthropic.com")?;
161 /// # Ok::<(), agent_uri::TrustRootError>(())
162 /// ```
163 pub fn try_trust_root(self, s: &str) -> Result<AgentUriBuilder<HasTrustRoot>, TrustRootError> {
164 let trust_root = TrustRoot::parse(s)?;
165 Ok(self.trust_root(trust_root))
166 }
167}
168
169impl Default for AgentUriBuilder<Empty> {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175impl AgentUriBuilder<HasTrustRoot> {
176 /// Sets the capability path and advances to the [`HasCapabilityPath`] state.
177 ///
178 /// This must be called after `trust_root()` and before `agent_id()`.
179 ///
180 /// # Arguments
181 ///
182 /// * `capability_path` - The validated capability path describing the agent's function
183 ///
184 /// # Examples
185 ///
186 /// ```
187 /// use agent_uri::{AgentUriBuilder, TrustRoot, CapabilityPath};
188 ///
189 /// let builder = AgentUriBuilder::new()
190 /// .trust_root(TrustRoot::parse("anthropic.com").unwrap())
191 /// .capability_path(CapabilityPath::parse("assistant/chat").unwrap());
192 /// ```
193 #[must_use]
194 pub fn capability_path(
195 self,
196 capability_path: CapabilityPath,
197 ) -> AgentUriBuilder<HasCapabilityPath> {
198 AgentUriBuilder {
199 trust_root: self.trust_root,
200 capability_path: Some(capability_path),
201 agent_id: self.agent_id,
202 query: self.query,
203 fragment: self.fragment,
204 _state: PhantomData,
205 }
206 }
207
208 /// Parses and sets the capability path from a string.
209 ///
210 /// This is a convenience method that combines parsing and setting the capability path.
211 ///
212 /// # Errors
213 ///
214 /// Returns [`CapabilityPathError`] if the string is not a valid capability path.
215 ///
216 /// # Examples
217 ///
218 /// ```
219 /// use agent_uri::AgentUriBuilder;
220 ///
221 /// let builder = AgentUriBuilder::new()
222 /// .try_trust_root("anthropic.com")?
223 /// .try_capability_path("assistant/chat")?;
224 /// # Ok::<(), Box<dyn std::error::Error>>(())
225 /// ```
226 pub fn try_capability_path(
227 self,
228 s: &str,
229 ) -> Result<AgentUriBuilder<HasCapabilityPath>, CapabilityPathError> {
230 let capability_path = CapabilityPath::parse(s)?;
231 Ok(self.capability_path(capability_path))
232 }
233}
234
235impl AgentUriBuilder<HasCapabilityPath> {
236 /// Sets the agent ID and advances to the [`Ready`] state.
237 ///
238 /// After calling this, the builder is ready to call `build()`.
239 ///
240 /// # Arguments
241 ///
242 /// * `agent_id` - The validated agent identifier
243 ///
244 /// # Examples
245 ///
246 /// ```
247 /// use agent_uri::{AgentUriBuilder, TrustRoot, CapabilityPath, AgentId};
248 ///
249 /// let builder = AgentUriBuilder::new()
250 /// .trust_root(TrustRoot::parse("anthropic.com").unwrap())
251 /// .capability_path(CapabilityPath::parse("assistant/chat").unwrap())
252 /// .agent_id(AgentId::new("llm_chat"));
253 /// // Builder is now in Ready state and can call build()
254 /// ```
255 #[must_use]
256 pub fn agent_id(self, agent_id: AgentId) -> AgentUriBuilder<Ready> {
257 AgentUriBuilder {
258 trust_root: self.trust_root,
259 capability_path: self.capability_path,
260 agent_id: Some(agent_id),
261 query: self.query,
262 fragment: self.fragment,
263 _state: PhantomData,
264 }
265 }
266
267 /// Parses and sets the agent ID from a string.
268 ///
269 /// This is a convenience method that combines parsing and setting the agent ID.
270 ///
271 /// # Errors
272 ///
273 /// Returns [`AgentIdError`] if the string is not a valid agent ID.
274 ///
275 /// # Examples
276 ///
277 /// ```
278 /// use agent_uri::AgentUriBuilder;
279 ///
280 /// let builder = AgentUriBuilder::new()
281 /// .try_trust_root("anthropic.com")?
282 /// .try_capability_path("assistant/chat")?
283 /// .try_agent_id("llm_chat_01h455vb4pex5vsknk084sn02q")?;
284 /// # Ok::<(), Box<dyn std::error::Error>>(())
285 /// ```
286 pub fn try_agent_id(self, s: &str) -> Result<AgentUriBuilder<Ready>, AgentIdError> {
287 let agent_id = AgentId::parse(s)?;
288 Ok(self.agent_id(agent_id))
289 }
290}
291
292impl AgentUriBuilder<Ready> {
293 /// Builds the final [`AgentUri`].
294 ///
295 /// This method is only available when the builder is in the [`Ready`] state,
296 /// meaning all required components (trust root, capability path, agent ID)
297 /// have been set.
298 ///
299 /// # Errors
300 ///
301 /// Returns [`BuilderError::UriTooLong`] if the resulting URI would exceed
302 /// the maximum allowed length of 512 characters. Returns
303 /// [`BuilderError::InvalidUri`] with the underlying parse error if the
304 /// components cannot be assembled for any other reason.
305 ///
306 /// # Panics
307 ///
308 /// This method will not panic in practice because the typestate pattern
309 /// guarantees all required fields are set before `build()` can be called.
310 /// The internal `expect()` calls are for defense-in-depth only.
311 ///
312 /// # Examples
313 ///
314 /// ```
315 /// use agent_uri::{AgentUriBuilder, TrustRoot, CapabilityPath, AgentId};
316 ///
317 /// let uri = AgentUriBuilder::new()
318 /// .trust_root(TrustRoot::parse("anthropic.com").unwrap())
319 /// .capability_path(CapabilityPath::parse("assistant/chat").unwrap())
320 /// .agent_id(AgentId::new("llm_chat"))
321 /// .build()
322 /// .unwrap();
323 ///
324 /// assert_eq!(uri.trust_root().host_str(), "anthropic.com");
325 /// ```
326 pub fn build(self) -> Result<AgentUri, BuilderError> {
327 // SAFETY: These are guaranteed to be Some because the typestate
328 // ensures all required fields are set when we reach the Ready state.
329 // The only way to reach Ready is through the proper state transitions.
330 let trust_root = self
331 .trust_root
332 .expect("trust_root set in HasTrustRoot state");
333 let capability_path = self
334 .capability_path
335 .expect("capability_path set in HasCapabilityPath state");
336 let agent_id = self.agent_id.expect("agent_id set in Ready state");
337
338 AgentUri::new(
339 trust_root,
340 capability_path,
341 agent_id,
342 self.query,
343 self.fragment,
344 )
345 // `InvalidUri` is currently unreachable because `AgentUri::new` only returns `TooLong`;
346 // it preserves future failure modes instead of masking them as length errors.
347 .map_err(|e| match &e.kind {
348 ParseErrorKind::TooLong { max, actual } => BuilderError::UriTooLong {
349 max: *max,
350 actual: *actual,
351 },
352 _ => BuilderError::InvalidUri(e),
353 })
354 }
355}
356
357/// Methods available in all states after Empty for optional components.
358impl<State> AgentUriBuilder<State> {
359 /// Sets optional query parameters.
360 ///
361 /// This can be called at any point in the builder chain.
362 /// If called multiple times, the last value wins.
363 ///
364 /// # Arguments
365 ///
366 /// * `query` - The query parameters to include in the URI
367 ///
368 /// # Examples
369 ///
370 /// ```
371 /// use agent_uri::{AgentUriBuilder, TrustRoot, CapabilityPath, AgentId, QueryParams};
372 ///
373 /// let uri = AgentUriBuilder::new()
374 /// .trust_root(TrustRoot::parse("anthropic.com").unwrap())
375 /// .query(QueryParams::parse("version=2.0").unwrap())
376 /// .capability_path(CapabilityPath::parse("chat").unwrap())
377 /// .agent_id(AgentId::new("llm"))
378 /// .build()
379 /// .unwrap();
380 ///
381 /// assert_eq!(uri.query().version(), Some("2.0"));
382 /// ```
383 #[must_use]
384 pub fn query(mut self, query: QueryParams) -> Self {
385 self.query = query;
386 self
387 }
388
389 /// Sets the optional fragment.
390 ///
391 /// This can be called at any point in the builder chain.
392 /// If called multiple times, the last value wins.
393 ///
394 /// # Arguments
395 ///
396 /// * `fragment` - The fragment to include in the URI
397 ///
398 /// # Examples
399 ///
400 /// ```
401 /// use agent_uri::{AgentUriBuilder, TrustRoot, CapabilityPath, AgentId, Fragment};
402 ///
403 /// let uri = AgentUriBuilder::new()
404 /// .trust_root(TrustRoot::parse("anthropic.com").unwrap())
405 /// .capability_path(CapabilityPath::parse("chat").unwrap())
406 /// .fragment(Fragment::parse("summarization").unwrap())
407 /// .agent_id(AgentId::new("llm"))
408 /// .build()
409 /// .unwrap();
410 ///
411 /// assert_eq!(uri.fragment().map(|f| f.as_str()), Some("summarization"));
412 /// ```
413 #[must_use]
414 pub fn fragment(mut self, fragment: Fragment) -> Self {
415 self.fragment = Some(fragment);
416 self
417 }
418
419 /// Parses and sets the query parameters from a string.
420 ///
421 /// This is a convenience method that combines parsing and setting query parameters.
422 ///
423 /// # Errors
424 ///
425 /// Returns [`QueryError`] if the string is not a valid query string.
426 ///
427 /// # Examples
428 ///
429 /// ```
430 /// use agent_uri::AgentUriBuilder;
431 ///
432 /// let builder = AgentUriBuilder::new()
433 /// .try_query("version=2.0&ttl=300")?;
434 /// # Ok::<(), agent_uri::QueryError>(())
435 /// ```
436 pub fn try_query(self, s: &str) -> Result<Self, QueryError> {
437 let query = QueryParams::parse(s)?;
438 Ok(self.query(query))
439 }
440
441 /// Parses and sets the fragment from a string.
442 ///
443 /// This is a convenience method that combines parsing and setting the fragment.
444 ///
445 /// # Errors
446 ///
447 /// Returns [`FragmentError`] if the string is not a valid fragment.
448 ///
449 /// # Examples
450 ///
451 /// ```
452 /// use agent_uri::AgentUriBuilder;
453 ///
454 /// let builder = AgentUriBuilder::new()
455 /// .try_fragment("summarization")?;
456 /// # Ok::<(), agent_uri::FragmentError>(())
457 /// ```
458 pub fn try_fragment(self, s: &str) -> Result<Self, FragmentError> {
459 let fragment = Fragment::parse(s)?;
460 Ok(self.fragment(fragment))
461 }
462
463 /// Sets the query if provided, otherwise leaves it unchanged.
464 ///
465 /// This is useful when the query is optional and may be `None`.
466 ///
467 /// # Examples
468 ///
469 /// ```
470 /// use agent_uri::{AgentUriBuilder, QueryParams};
471 ///
472 /// let query = Some(QueryParams::parse("version=2.0").unwrap());
473 /// let builder = AgentUriBuilder::new()
474 /// .maybe_query(query);
475 /// ```
476 #[must_use]
477 pub fn maybe_query(self, query: Option<QueryParams>) -> Self {
478 match query {
479 Some(q) => self.query(q),
480 None => self,
481 }
482 }
483
484 /// Sets the fragment if provided, otherwise leaves it unchanged.
485 ///
486 /// This is useful when the fragment is optional and may be `None`.
487 ///
488 /// # Examples
489 ///
490 /// ```
491 /// use agent_uri::{AgentUriBuilder, Fragment};
492 ///
493 /// let fragment = Some(Fragment::parse("test").unwrap());
494 /// let builder = AgentUriBuilder::new()
495 /// .maybe_fragment(fragment);
496 /// ```
497 #[must_use]
498 pub fn maybe_fragment(self, fragment: Option<Fragment>) -> Self {
499 match fragment {
500 Some(f) => self.fragment(f),
501 None => self,
502 }
503 }
504
505 /// Parses and sets the query from a string if provided.
506 ///
507 /// This is useful when the query string is optional and may be `None`.
508 ///
509 /// # Errors
510 ///
511 /// Returns [`QueryError`] if the provided string is not a valid query string.
512 ///
513 /// # Examples
514 ///
515 /// ```
516 /// use agent_uri::AgentUriBuilder;
517 ///
518 /// let query_str = Some("version=2.0");
519 /// let builder = AgentUriBuilder::new()
520 /// .maybe_query_str(query_str)?;
521 /// # Ok::<(), agent_uri::QueryError>(())
522 /// ```
523 pub fn maybe_query_str(self, s: Option<&str>) -> Result<Self, QueryError> {
524 match s {
525 Some(s) => self.try_query(s),
526 None => Ok(self),
527 }
528 }
529
530 /// Parses and sets the fragment from a string if provided.
531 ///
532 /// This is useful when the fragment string is optional and may be `None`.
533 ///
534 /// # Errors
535 ///
536 /// Returns [`FragmentError`] if the provided string is not a valid fragment.
537 ///
538 /// # Examples
539 ///
540 /// ```
541 /// use agent_uri::AgentUriBuilder;
542 ///
543 /// let fragment_str = Some("summarization");
544 /// let builder = AgentUriBuilder::new()
545 /// .maybe_fragment_str(fragment_str)?;
546 /// # Ok::<(), agent_uri::FragmentError>(())
547 /// ```
548 pub fn maybe_fragment_str(self, s: Option<&str>) -> Result<Self, FragmentError> {
549 match s {
550 Some(s) => self.try_fragment(s),
551 None => Ok(self),
552 }
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 fn sample_trust_root() -> TrustRoot {
561 TrustRoot::parse("anthropic.com").unwrap()
562 }
563
564 fn sample_capability_path() -> CapabilityPath {
565 CapabilityPath::parse("assistant/chat").unwrap()
566 }
567
568 fn sample_agent_id() -> AgentId {
569 AgentId::new("llm_chat")
570 }
571
572 #[test]
573 fn new_creates_empty_builder() {
574 let builder = AgentUriBuilder::new();
575 assert!(builder.trust_root.is_none());
576 assert!(builder.capability_path.is_none());
577 assert!(builder.agent_id.is_none());
578 }
579
580 #[test]
581 fn trust_root_transitions_to_has_trust_root() {
582 let builder = AgentUriBuilder::new().trust_root(sample_trust_root());
583 assert!(builder.trust_root.is_some());
584 assert!(builder.capability_path.is_none());
585 }
586
587 #[test]
588 fn capability_path_transitions_to_has_capability_path() {
589 let builder = AgentUriBuilder::new()
590 .trust_root(sample_trust_root())
591 .capability_path(sample_capability_path());
592 assert!(builder.trust_root.is_some());
593 assert!(builder.capability_path.is_some());
594 assert!(builder.agent_id.is_none());
595 }
596
597 #[test]
598 fn agent_id_transitions_to_ready() {
599 let builder = AgentUriBuilder::new()
600 .trust_root(sample_trust_root())
601 .capability_path(sample_capability_path())
602 .agent_id(sample_agent_id());
603 assert!(builder.trust_root.is_some());
604 assert!(builder.capability_path.is_some());
605 assert!(builder.agent_id.is_some());
606 }
607
608 #[test]
609 fn build_creates_valid_uri() {
610 let uri = AgentUriBuilder::new()
611 .trust_root(sample_trust_root())
612 .capability_path(sample_capability_path())
613 .agent_id(sample_agent_id())
614 .build()
615 .unwrap();
616
617 assert_eq!(uri.trust_root().host_str(), "anthropic.com");
618 assert_eq!(uri.capability_path().as_str(), "assistant/chat");
619 assert_eq!(uri.agent_id().prefix().as_str(), "llm_chat");
620 }
621
622 #[test]
623 fn build_with_query_includes_query() {
624 let query = QueryParams::parse("version=2.0&ttl=300").unwrap();
625
626 let uri = AgentUriBuilder::new()
627 .trust_root(sample_trust_root())
628 .query(query)
629 .capability_path(sample_capability_path())
630 .agent_id(sample_agent_id())
631 .build()
632 .unwrap();
633
634 assert_eq!(uri.query().version(), Some("2.0"));
635 assert_eq!(uri.query().ttl(), Some(300));
636 }
637
638 #[test]
639 fn build_with_fragment_includes_fragment() {
640 let fragment = Fragment::parse("summarization").unwrap();
641
642 let uri = AgentUriBuilder::new()
643 .trust_root(sample_trust_root())
644 .capability_path(sample_capability_path())
645 .fragment(fragment)
646 .agent_id(sample_agent_id())
647 .build()
648 .unwrap();
649
650 assert_eq!(uri.fragment().map(Fragment::as_str), Some("summarization"));
651 }
652
653 #[test]
654 fn build_with_all_optionals() {
655 let query = QueryParams::parse("version=2.0").unwrap();
656 let fragment = Fragment::parse("test").unwrap();
657
658 let uri = AgentUriBuilder::new()
659 .trust_root(sample_trust_root())
660 .capability_path(sample_capability_path())
661 .agent_id(sample_agent_id())
662 .query(query)
663 .fragment(fragment)
664 .build()
665 .unwrap();
666
667 assert_eq!(uri.query().version(), Some("2.0"));
668 assert_eq!(uri.fragment().map(Fragment::as_str), Some("test"));
669 }
670
671 #[test]
672 fn build_too_long_returns_error() {
673 // Create components that are individually valid but together exceed 512 chars.
674 // The basic components (scheme + trust_root + path + agent_id) max out at ~484,
675 // so we need a long query string to push over 512.
676
677 // Trust root: ~115 chars (55 + 1 + 55 + 4)
678 let long_domain = format!("{}.{}.com", "a".repeat(55), "b".repeat(55));
679 let trust_root = TrustRoot::parse(&long_domain).unwrap();
680
681 // Capability path: ~251 chars (28 segments × 8 chars + 27 slashes = 224 + 27)
682 let long_path = (0..28).map(|_| "abcdefgh").collect::<Vec<_>>().join("/");
683 let capability_path = CapabilityPath::parse(&long_path).unwrap();
684
685 // Agent ID: ~63 chars (prefix + underscore + 26 char suffix)
686 let agent_id = AgentId::new("very_long_prefix_name_for_this_test");
687
688 // Long query parameter to push over 512
689 // Current components total 506 chars, need 7+ more to exceed 512
690 let long_query = format!("custom={}", "x".repeat(70));
691 let query = QueryParams::parse(&long_query).unwrap();
692
693 let result = AgentUriBuilder::new()
694 .trust_root(trust_root)
695 .capability_path(capability_path)
696 .agent_id(agent_id)
697 .query(query)
698 .build();
699
700 let Err(BuilderError::UriTooLong { max, actual }) = result else {
701 panic!("expected BuilderError::UriTooLong");
702 };
703 assert_eq!(max, 512);
704 assert!(actual > 512);
705 }
706
707 #[test]
708 fn query_can_be_set_at_any_state() {
709 let query = QueryParams::parse("version=1.0").unwrap();
710
711 // Set query after trust_root
712 let builder1 = AgentUriBuilder::new()
713 .trust_root(sample_trust_root())
714 .query(query.clone());
715 assert!(!builder1.query.is_empty());
716
717 // Set query after capability_path
718 let builder2 = AgentUriBuilder::new()
719 .trust_root(sample_trust_root())
720 .capability_path(sample_capability_path())
721 .query(query.clone());
722 assert!(!builder2.query.is_empty());
723
724 // Set query after agent_id (in Ready state)
725 let builder3 = AgentUriBuilder::new()
726 .trust_root(sample_trust_root())
727 .capability_path(sample_capability_path())
728 .agent_id(sample_agent_id())
729 .query(query);
730 assert!(!builder3.query.is_empty());
731 }
732
733 #[test]
734 fn fragment_can_be_set_at_any_state() {
735 let fragment = Fragment::parse("test").unwrap();
736
737 // Set fragment after trust_root
738 let builder1 = AgentUriBuilder::new()
739 .trust_root(sample_trust_root())
740 .fragment(fragment.clone());
741 assert!(builder1.fragment.is_some());
742
743 // Set fragment after capability_path
744 let builder2 = AgentUriBuilder::new()
745 .trust_root(sample_trust_root())
746 .capability_path(sample_capability_path())
747 .fragment(fragment.clone());
748 assert!(builder2.fragment.is_some());
749
750 // Set fragment after agent_id (in Ready state)
751 let builder3 = AgentUriBuilder::new()
752 .trust_root(sample_trust_root())
753 .capability_path(sample_capability_path())
754 .agent_id(sample_agent_id())
755 .fragment(fragment);
756 assert!(builder3.fragment.is_some());
757 }
758
759 #[test]
760 fn default_creates_empty_builder() {
761 let builder: AgentUriBuilder<Empty> = AgentUriBuilder::default();
762 assert!(builder.trust_root.is_none());
763 }
764
765 #[test]
766 fn clone_preserves_state() {
767 let builder = AgentUriBuilder::new()
768 .trust_root(sample_trust_root())
769 .capability_path(sample_capability_path());
770
771 let cloned = builder.clone();
772 assert!(cloned.trust_root.is_some());
773 assert!(cloned.capability_path.is_some());
774 }
775
776 #[test]
777 fn debug_output_is_useful() {
778 let builder = AgentUriBuilder::new().trust_root(sample_trust_root());
779
780 let debug_str = format!("{builder:?}");
781 assert!(debug_str.contains("AgentUriBuilder"));
782 assert!(debug_str.contains("trust_root"));
783 }
784}