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 { char: c, position: i });
54            }
55        }
56
57        Ok(Self(input.to_string()))
58    }
59
60    /// Returns the segment as a string slice.
61    #[must_use]
62    pub fn as_str(&self) -> &str {
63        &self.0
64    }
65
66    /// Returns true if the character is valid for a path segment.
67    #[must_use]
68    pub const fn is_valid_char(c: char) -> bool {
69        c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'
70    }
71}
72
73impl fmt::Display for PathSegment {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        write!(f, "{}", self.0)
76    }
77}
78
79impl FromStr for PathSegment {
80    type Err = PathSegmentError;
81
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        Self::parse(s)
84    }
85}
86
87impl AsRef<str> for PathSegment {
88    fn as_ref(&self) -> &str {
89        &self.0
90    }
91}
92
93impl TryFrom<&str> for PathSegment {
94    type Error = PathSegmentError;
95
96    fn try_from(s: &str) -> Result<Self, Self::Error> {
97        Self::parse(s)
98    }
99}
100
101impl Deref for PathSegment {
102    type Target = str;
103
104    fn deref(&self) -> &Self::Target {
105        &self.0
106    }
107}
108
109impl PartialOrd for PathSegment {
110    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
111        Some(self.cmp(other))
112    }
113}
114
115impl Ord for PathSegment {
116    fn cmp(&self, other: &Self) -> Ordering {
117        self.0.cmp(&other.0)
118    }
119}
120
121#[cfg(feature = "serde")]
122impl serde::Serialize for PathSegment {
123    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
124    where
125        S: serde::Serializer,
126    {
127        serializer.serialize_str(&self.0)
128    }
129}
130
131#[cfg(feature = "serde")]
132impl<'de> serde::Deserialize<'de> for PathSegment {
133    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
134    where
135        D: serde::Deserializer<'de>,
136    {
137        let s = String::deserialize(deserializer)?;
138        Self::parse(&s).map_err(serde::de::Error::custom)
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn parse_valid_segment() {
148        let seg = PathSegment::parse("chat").unwrap();
149        assert_eq!(seg.as_str(), "chat");
150    }
151
152    #[test]
153    fn parse_segment_with_hyphen() {
154        let seg = PathSegment::parse("code-interpreter").unwrap();
155        assert_eq!(seg.as_str(), "code-interpreter");
156    }
157
158    #[test]
159    fn parse_segment_with_digits() {
160        let seg = PathSegment::parse("v2").unwrap();
161        assert_eq!(seg.as_str(), "v2");
162    }
163
164    #[test]
165    fn parse_empty_fails() {
166        let result = PathSegment::parse("");
167        assert!(matches!(result, Err(PathSegmentError::Empty)));
168    }
169
170    #[test]
171    fn parse_too_long_fails() {
172        let long = "a".repeat(65);
173        let result = PathSegment::parse(&long);
174        assert!(matches!(result, Err(PathSegmentError::TooLong { max: 64, actual: 65 })));
175    }
176
177    #[test]
178    fn parse_uppercase_fails() {
179        let result = PathSegment::parse("Chat");
180        assert!(matches!(result, Err(PathSegmentError::InvalidChar { char: 'C', position: 0 })));
181    }
182
183    #[test]
184    fn parse_underscore_fails() {
185        let result = PathSegment::parse("code_interpreter");
186        assert!(matches!(result, Err(PathSegmentError::InvalidChar { char: '_', position: 4 })));
187    }
188}