agent_skills/
description.rs1use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum SkillDescriptionError {
11 Empty,
13 TooLong {
15 length: usize,
17 max: usize,
19 },
20}
21
22impl fmt::Display for SkillDescriptionError {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Self::Empty => write!(f, "skill description cannot be empty or whitespace-only"),
26 Self::TooLong { length, max } => {
27 write!(
28 f,
29 "skill description is {length} characters; maximum is {max}"
30 )
31 }
32 }
33 }
34}
35
36impl std::error::Error for SkillDescriptionError {}
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
60pub struct SkillDescription(String);
61
62impl SkillDescription {
63 pub const MAX_LENGTH: usize = 1024;
65
66 pub fn new(description: impl Into<String>) -> Result<Self, SkillDescriptionError> {
72 let description = description.into();
73 validate_skill_description(&description)?;
74 Ok(Self(description))
75 }
76
77 #[must_use]
79 pub fn as_str(&self) -> &str {
80 &self.0
81 }
82}
83
84impl fmt::Display for SkillDescription {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 write!(f, "{}", self.0)
87 }
88}
89
90impl FromStr for SkillDescription {
91 type Err = SkillDescriptionError;
92
93 fn from_str(s: &str) -> Result<Self, Self::Err> {
94 Self::new(s)
95 }
96}
97
98impl AsRef<str> for SkillDescription {
99 fn as_ref(&self) -> &str {
100 &self.0
101 }
102}
103
104impl Serialize for SkillDescription {
105 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
106 where
107 S: Serializer,
108 {
109 self.0.serialize(serializer)
110 }
111}
112
113impl<'de> Deserialize<'de> for SkillDescription {
114 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
115 where
116 D: Deserializer<'de>,
117 {
118 let s = String::deserialize(deserializer)?;
119 Self::new(s).map_err(serde::de::Error::custom)
120 }
121}
122
123fn validate_skill_description(description: &str) -> Result<(), SkillDescriptionError> {
125 if description.trim().is_empty() {
127 return Err(SkillDescriptionError::Empty);
128 }
129
130 if description.len() > SkillDescription::MAX_LENGTH {
132 return Err(SkillDescriptionError::TooLong {
133 length: description.len(),
134 max: SkillDescription::MAX_LENGTH,
135 });
136 }
137
138 Ok(())
139}
140
141#[cfg(test)]
142#[allow(clippy::unwrap_used, clippy::expect_used)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn valid_description_is_accepted() {
148 let desc = SkillDescription::new("Extracts text from PDF files.");
149 assert!(desc.is_ok());
150 }
151
152 #[test]
153 fn description_at_max_length_is_accepted() {
154 let desc = "a".repeat(1024);
155 assert!(SkillDescription::new(desc).is_ok());
156 }
157
158 #[test]
159 fn empty_description_is_rejected() {
160 let result = SkillDescription::new("");
161 assert_eq!(result, Err(SkillDescriptionError::Empty));
162 }
163
164 #[test]
165 fn whitespace_only_description_is_rejected() {
166 let result = SkillDescription::new(" \n\t ");
167 assert_eq!(result, Err(SkillDescriptionError::Empty));
168 }
169
170 #[test]
171 fn description_exceeding_max_length_is_rejected() {
172 let long_desc = "a".repeat(1025);
173 let result = SkillDescription::new(long_desc);
174 assert!(matches!(
175 result,
176 Err(SkillDescriptionError::TooLong {
177 length: 1025,
178 max: 1024
179 })
180 ));
181 }
182
183 #[test]
184 fn description_with_leading_trailing_whitespace_is_accepted() {
185 let desc = SkillDescription::new(" Some description ");
187 assert!(desc.is_ok());
188 }
189
190 #[test]
191 fn display_returns_inner_string() {
192 let desc = SkillDescription::new("My description").unwrap();
193 assert_eq!(format!("{desc}"), "My description");
194 }
195
196 #[test]
197 fn from_str_works() {
198 let desc: SkillDescription = "My description".parse().unwrap();
199 assert_eq!(desc.as_str(), "My description");
200 }
201
202 #[test]
203 fn as_ref_works() {
204 let desc = SkillDescription::new("My description").unwrap();
205 let s: &str = desc.as_ref();
206 assert_eq!(s, "My description");
207 }
208
209 #[test]
210 fn error_display_is_helpful() {
211 let err = SkillDescriptionError::TooLong {
212 length: 2000,
213 max: 1024,
214 };
215 let msg = err.to_string();
216 assert!(msg.contains("2000"));
217 assert!(msg.contains("1024"));
218 }
219}