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 {
54 char: c,
55 position: i,
56 });
57 }
58 }
59
60 Ok(Self(input.to_string()))
61 }
62
63 #[must_use]
65 pub fn as_str(&self) -> &str {
66 &self.0
67 }
68
69 #[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}