agent_uri/
agent_prefix.rs1use std::cmp::Ordering;
4use std::fmt;
5use std::ops::Deref;
6use std::str::FromStr;
7
8use crate::constants::MAX_AGENT_PREFIX_LENGTH;
9use crate::error::AgentPrefixError;
10use crate::type_class::TypeClass;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct AgentPrefix {
39 type_class: TypeClass,
40 modifiers: Vec<String>,
41 normalized: String,
43}
44
45impl AgentPrefix {
46 pub fn parse(input: &str) -> Result<Self, AgentPrefixError> {
58 if input.is_empty() {
59 return Err(AgentPrefixError::Empty);
60 }
61
62 if input.len() > MAX_AGENT_PREFIX_LENGTH {
63 return Err(AgentPrefixError::TooLong {
64 max: MAX_AGENT_PREFIX_LENGTH,
65 actual: input.len(),
66 });
67 }
68
69 let Some(first) = input.chars().next() else {
71 return Err(AgentPrefixError::Empty);
72 };
73 if !first.is_ascii_lowercase() {
74 return Err(AgentPrefixError::MustStartWithLetter { found: first });
75 }
76
77 let Some(last) = input.chars().next_back() else {
79 return Err(AgentPrefixError::Empty);
80 };
81 if !last.is_ascii_lowercase() {
82 return Err(AgentPrefixError::MustEndWithLetter { found: last });
83 }
84
85 for (i, c) in input.chars().enumerate() {
87 if c.is_ascii_digit() {
88 return Err(AgentPrefixError::ContainsDigit { position: i });
89 }
90 if !c.is_ascii_lowercase() && c != '_' {
91 return Err(AgentPrefixError::InvalidChar { char: c, position: i });
92 }
93 }
94
95 let parts: Vec<&str> = input.split('_').collect();
97 let type_class = parts[0].parse::<TypeClass>().map_err(|_| {
98 AgentPrefixError::InvalidChar {
99 char: parts[0].chars().next().unwrap_or(' '),
100 position: 0,
101 }
102 })?;
103
104 let modifiers = parts[1..].iter().map(|s| (*s).to_string()).collect();
105
106 Ok(Self {
107 type_class,
108 modifiers,
109 normalized: input.to_string(),
110 })
111 }
112
113 #[must_use]
115 pub const fn type_class(&self) -> &TypeClass {
116 &self.type_class
117 }
118
119 #[must_use]
121 pub fn modifiers(&self) -> &[String] {
122 &self.modifiers
123 }
124
125 #[must_use]
127 pub fn as_str(&self) -> &str {
128 &self.normalized
129 }
130}
131
132impl fmt::Display for AgentPrefix {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 write!(f, "{}", self.normalized)
135 }
136}
137
138impl FromStr for AgentPrefix {
139 type Err = AgentPrefixError;
140
141 fn from_str(s: &str) -> Result<Self, Self::Err> {
142 Self::parse(s)
143 }
144}
145
146impl AsRef<str> for AgentPrefix {
147 fn as_ref(&self) -> &str {
148 &self.normalized
149 }
150}
151
152impl TryFrom<&str> for AgentPrefix {
153 type Error = AgentPrefixError;
154
155 fn try_from(s: &str) -> Result<Self, Self::Error> {
156 Self::parse(s)
157 }
158}
159
160impl Deref for AgentPrefix {
161 type Target = str;
162
163 fn deref(&self) -> &Self::Target {
164 &self.normalized
165 }
166}
167
168impl PartialOrd for AgentPrefix {
169 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
170 Some(self.cmp(other))
171 }
172}
173
174impl Ord for AgentPrefix {
175 fn cmp(&self, other: &Self) -> Ordering {
176 self.normalized.cmp(&other.normalized)
177 }
178}
179
180#[cfg(feature = "serde")]
181impl serde::Serialize for AgentPrefix {
182 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
183 where
184 S: serde::Serializer,
185 {
186 serializer.serialize_str(&self.normalized)
187 }
188}
189
190#[cfg(feature = "serde")]
191impl<'de> serde::Deserialize<'de> for AgentPrefix {
192 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
193 where
194 D: serde::Deserializer<'de>,
195 {
196 let s = String::deserialize(deserializer)?;
197 Self::parse(&s).map_err(serde::de::Error::custom)
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn parse_simple_class() {
207 let prefix = AgentPrefix::parse("llm").unwrap();
208 assert_eq!(prefix.type_class().as_str(), "llm");
209 assert!(prefix.modifiers().is_empty());
210 }
211
212 #[test]
213 fn parse_with_modifiers() {
214 let prefix = AgentPrefix::parse("llm_chat_streaming").unwrap();
215 assert_eq!(prefix.type_class().as_str(), "llm");
216 assert_eq!(prefix.modifiers(), &["chat", "streaming"]);
217 }
218
219 #[test]
220 fn parse_empty_fails() {
221 let result = AgentPrefix::parse("");
222 assert!(matches!(result, Err(AgentPrefixError::Empty)));
223 }
224
225 #[test]
226 fn parse_too_long_fails() {
227 let long = "a".repeat(64);
228 let result = AgentPrefix::parse(&long);
229 assert!(matches!(result, Err(AgentPrefixError::TooLong { .. })));
230 }
231
232 #[test]
233 fn parse_with_digit_fails() {
234 let result = AgentPrefix::parse("ll2m");
236 assert!(matches!(
237 result,
238 Err(AgentPrefixError::ContainsDigit { position: 2 })
239 ));
240 }
241
242 #[test]
243 fn parse_uppercase_fails() {
244 let result = AgentPrefix::parse("LLM");
245 assert!(matches!(result, Err(AgentPrefixError::MustStartWithLetter { found: 'L' })));
246 }
247
248 #[test]
249 fn parse_ends_with_underscore_fails() {
250 let result = AgentPrefix::parse("llm_");
251 assert!(matches!(result, Err(AgentPrefixError::MustEndWithLetter { found: '_' })));
252 }
253
254 #[test]
255 fn parse_starts_with_underscore_fails() {
256 let result = AgentPrefix::parse("_llm");
257 assert!(matches!(result, Err(AgentPrefixError::MustStartWithLetter { found: '_' })));
258 }
259}