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