Skip to main content

agent_uri/
path_segment.rs

1//! Path segment type for capability paths.
2
3use std::cmp::Ordering;
4use std::fmt;
5use std::ops::Deref;
6use std::str::FromStr;
7
8use crate::constants::MAX_PATH_SEGMENT_LENGTH;
9use crate::error::PathSegmentError;
10
11/// A validated path segment in a capability path.
12///
13/// Path segments are lowercase alphanumeric strings with hyphens,
14/// between 1 and 64 characters long.
15///
16/// # Examples
17///
18/// ```
19/// use agent_uri::PathSegment;
20///
21/// let seg = PathSegment::parse("chat").unwrap();
22/// assert_eq!(seg.as_str(), "chat");
23///
24/// let seg = PathSegment::parse("code-interpreter").unwrap();
25/// assert_eq!(seg.as_str(), "code-interpreter");
26/// ```
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub struct PathSegment(String);
29
30impl PathSegment {
31    /// Parses a path segment from a string.
32    ///
33    /// # Errors
34    ///
35    /// Returns `PathSegmentError` if:
36    /// - The segment is empty
37    /// - The segment exceeds 64 characters
38    /// - The segment contains invalid characters (only lowercase letters, digits, hyphens allowed)
39    pub fn parse(input: &str) -> Result<Self, PathSegmentError> {
40        if input.is_empty() {
41            return Err(PathSegmentError::Empty);
42        }
43
44        if input.len() > MAX_PATH_SEGMENT_LENGTH {
45            return Err(PathSegmentError::TooLong {
46                max: MAX_PATH_SEGMENT_LENGTH,
47                actual: input.len(),
48            });
49        }
50
51        for (i, c) in input.chars().enumerate() {
52            if !Self::is_valid_char(c) {
53                return Err(PathSegmentError::InvalidChar {
54                    char: c,
55                    position: i,
56                });
57            }
58        }
59
60        Ok(Self(input.to_string()))
61    }
62
63    /// Returns the segment as a string slice.
64    #[must_use]
65    pub fn as_str(&self) -> &str {
66        &self.0
67    }
68
69    /// Returns true if the character is valid for a path segment.
70    #[must_use]
71    pub const fn is_valid_char(c: char) -> bool {
72        c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'
73    }
74}
75
76impl fmt::Display for PathSegment {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(f, "{}", self.0)
79    }
80}
81
82impl FromStr for PathSegment {
83    type Err = PathSegmentError;
84
85    fn from_str(s: &str) -> Result<Self, Self::Err> {
86        Self::parse(s)
87    }
88}
89
90impl AsRef<str> for PathSegment {
91    fn as_ref(&self) -> &str {
92        &self.0
93    }
94}
95
96impl TryFrom<&str> for PathSegment {
97    type Error = PathSegmentError;
98
99    fn try_from(s: &str) -> Result<Self, Self::Error> {
100        Self::parse(s)
101    }
102}
103
104impl Deref for PathSegment {
105    type Target = str;
106
107    fn deref(&self) -> &Self::Target {
108        &self.0
109    }
110}
111
112impl PartialOrd for PathSegment {
113    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
114        Some(self.cmp(other))
115    }
116}
117
118impl Ord for PathSegment {
119    fn cmp(&self, other: &Self) -> Ordering {
120        self.0.cmp(&other.0)
121    }
122}
123
124#[cfg(feature = "serde")]
125impl serde::Serialize for PathSegment {
126    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
127    where
128        S: serde::Serializer,
129    {
130        serializer.serialize_str(&self.0)
131    }
132}
133
134#[cfg(feature = "serde")]
135impl<'de> serde::Deserialize<'de> for PathSegment {
136    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
137    where
138        D: serde::Deserializer<'de>,
139    {
140        let s = String::deserialize(deserializer)?;
141        Self::parse(&s).map_err(serde::de::Error::custom)
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn parse_valid_segment() {
151        let seg = PathSegment::parse("chat").unwrap();
152        assert_eq!(seg.as_str(), "chat");
153    }
154
155    #[test]
156    fn parse_segment_with_hyphen() {
157        let seg = PathSegment::parse("code-interpreter").unwrap();
158        assert_eq!(seg.as_str(), "code-interpreter");
159    }
160
161    #[test]
162    fn parse_segment_with_digits() {
163        let seg = PathSegment::parse("v2").unwrap();
164        assert_eq!(seg.as_str(), "v2");
165    }
166
167    #[test]
168    fn parse_empty_fails() {
169        let result = PathSegment::parse("");
170        assert!(matches!(result, Err(PathSegmentError::Empty)));
171    }
172
173    #[test]
174    fn parse_too_long_fails() {
175        let long = "a".repeat(65);
176        let result = PathSegment::parse(&long);
177        assert!(matches!(
178            result,
179            Err(PathSegmentError::TooLong {
180                max: 64,
181                actual: 65
182            })
183        ));
184    }
185
186    #[test]
187    fn parse_uppercase_fails() {
188        let result = PathSegment::parse("Chat");
189        assert!(matches!(
190            result,
191            Err(PathSegmentError::InvalidChar {
192                char: 'C',
193                position: 0
194            })
195        ));
196    }
197
198    #[test]
199    fn parse_underscore_fails() {
200        let result = PathSegment::parse("code_interpreter");
201        assert!(matches!(
202            result,
203            Err(PathSegmentError::InvalidChar {
204                char: '_',
205                position: 4
206            })
207        ));
208    }
209}