agent_uri/
path_segment.rs1use 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub struct PathSegment(String);
29
30impl PathSegment {
31 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 #[must_use]
62 pub fn as_str(&self) -> &str {
63 &self.0
64 }
65
66 #[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}