Skip to main content

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}