Skip to main content

agent_uri/
fragment.rs

1//! Fragment type for sub-agent addressing.
2
3use std::cmp::Ordering;
4use std::fmt;
5use std::ops::Deref;
6use std::str::FromStr;
7
8use crate::error::FragmentError;
9
10/// A validated fragment from an agent URI.
11///
12/// The fragment addresses sub-agent functionality or capability subsets.
13///
14/// # Examples
15///
16/// ```
17/// use agent_uri::Fragment;
18///
19/// let frag = Fragment::parse("summarization").unwrap();
20/// assert_eq!(frag.as_str(), "summarization");
21/// ```
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct Fragment(String);
24
25impl Fragment {
26    /// Parses a fragment from a string (without leading '#').
27    ///
28    /// # Errors
29    ///
30    /// Returns `FragmentError` if the fragment contains invalid characters.
31    pub fn parse(input: &str) -> Result<Self, FragmentError> {
32        for (i, c) in input.chars().enumerate() {
33            if !Self::is_valid_char(c) {
34                return Err(FragmentError::InvalidChar { char: c, position: i });
35            }
36        }
37        Ok(Self(input.to_string()))
38    }
39
40    /// Returns the fragment as a string.
41    #[must_use]
42    pub fn as_str(&self) -> &str {
43        &self.0
44    }
45
46    /// Returns true if the character is valid for a fragment.
47    #[must_use]
48    pub const fn is_valid_char(c: char) -> bool {
49        c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/' | ':')
50    }
51}
52
53impl fmt::Display for Fragment {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(f, "{}", self.0)
56    }
57}
58
59impl FromStr for Fragment {
60    type Err = FragmentError;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        Self::parse(s)
64    }
65}
66
67impl AsRef<str> for Fragment {
68    fn as_ref(&self) -> &str {
69        &self.0
70    }
71}
72
73impl TryFrom<&str> for Fragment {
74    type Error = FragmentError;
75
76    fn try_from(s: &str) -> Result<Self, Self::Error> {
77        Self::parse(s)
78    }
79}
80
81impl Deref for Fragment {
82    type Target = str;
83
84    fn deref(&self) -> &Self::Target {
85        &self.0
86    }
87}
88
89impl PartialOrd for Fragment {
90    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
91        Some(self.cmp(other))
92    }
93}
94
95impl Ord for Fragment {
96    fn cmp(&self, other: &Self) -> Ordering {
97        self.0.cmp(&other.0)
98    }
99}
100
101#[cfg(feature = "serde")]
102impl serde::Serialize for Fragment {
103    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
104    where
105        S: serde::Serializer,
106    {
107        serializer.serialize_str(&self.0)
108    }
109}
110
111#[cfg(feature = "serde")]
112impl<'de> serde::Deserialize<'de> for Fragment {
113    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
114    where
115        D: serde::Deserializer<'de>,
116    {
117        let s = String::deserialize(deserializer)?;
118        Self::parse(&s).map_err(serde::de::Error::custom)
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn parse_simple_fragment() {
128        let frag = Fragment::parse("summarization").unwrap();
129        assert_eq!(frag.as_str(), "summarization");
130    }
131
132    #[test]
133    fn parse_fragment_with_special_chars() {
134        let frag = Fragment::parse("sub/path:v2").unwrap();
135        assert_eq!(frag.as_str(), "sub/path:v2");
136    }
137
138    #[test]
139    fn parse_fragment_with_hyphen() {
140        let frag = Fragment::parse("sub-capability").unwrap();
141        assert_eq!(frag.as_str(), "sub-capability");
142    }
143
144    #[test]
145    fn parse_fragment_with_underscore() {
146        let frag = Fragment::parse("sub_capability").unwrap();
147        assert_eq!(frag.as_str(), "sub_capability");
148    }
149
150    #[test]
151    fn parse_fragment_with_dot() {
152        let frag = Fragment::parse("v1.0").unwrap();
153        assert_eq!(frag.as_str(), "v1.0");
154    }
155
156    #[test]
157    fn parse_empty_fragment() {
158        // Empty fragments are valid (stripped by URI parser)
159        let frag = Fragment::parse("").unwrap();
160        assert_eq!(frag.as_str(), "");
161    }
162
163    #[test]
164    fn parse_invalid_char_fails() {
165        let result = Fragment::parse("test@value");
166        assert!(matches!(result, Err(FragmentError::InvalidChar { char: '@', position: 4 })));
167    }
168
169    #[test]
170    fn parse_space_fails() {
171        let result = Fragment::parse("test value");
172        assert!(matches!(result, Err(FragmentError::InvalidChar { char: ' ', position: 4 })));
173    }
174}