agent_skills/
compatibility.rs1use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum CompatibilityError {
11 Empty,
13 TooLong {
15 length: usize,
17 max: usize,
19 },
20}
21
22impl fmt::Display for CompatibilityError {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Self::Empty => write!(f, "compatibility string cannot be empty"),
26 Self::TooLong { length, max } => {
27 write!(
28 f,
29 "compatibility string is {length} characters; maximum is {max}"
30 )
31 }
32 }
33 }
34}
35
36impl std::error::Error for CompatibilityError {}
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
56pub struct Compatibility(String);
57
58impl Compatibility {
59 pub const MAX_LENGTH: usize = 500;
61
62 pub fn new(compat: impl Into<String>) -> Result<Self, CompatibilityError> {
68 let compat = compat.into();
69 validate_compatibility(&compat)?;
70 Ok(Self(compat))
71 }
72
73 #[must_use]
75 pub fn as_str(&self) -> &str {
76 &self.0
77 }
78}
79
80impl fmt::Display for Compatibility {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 write!(f, "{}", self.0)
83 }
84}
85
86impl FromStr for Compatibility {
87 type Err = CompatibilityError;
88
89 fn from_str(s: &str) -> Result<Self, Self::Err> {
90 Self::new(s)
91 }
92}
93
94impl AsRef<str> for Compatibility {
95 fn as_ref(&self) -> &str {
96 &self.0
97 }
98}
99
100impl Serialize for Compatibility {
101 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
102 where
103 S: Serializer,
104 {
105 self.0.serialize(serializer)
106 }
107}
108
109impl<'de> Deserialize<'de> for Compatibility {
110 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111 where
112 D: Deserializer<'de>,
113 {
114 let s = String::deserialize(deserializer)?;
115 Self::new(s).map_err(serde::de::Error::custom)
116 }
117}
118
119const fn validate_compatibility(compat: &str) -> Result<(), CompatibilityError> {
121 if compat.is_empty() {
123 return Err(CompatibilityError::Empty);
124 }
125
126 if compat.len() > Compatibility::MAX_LENGTH {
128 return Err(CompatibilityError::TooLong {
129 length: compat.len(),
130 max: Compatibility::MAX_LENGTH,
131 });
132 }
133
134 Ok(())
135}
136
137#[cfg(test)]
138#[allow(clippy::unwrap_used, clippy::expect_used)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn valid_compatibility_is_accepted() {
144 let compat = Compatibility::new("Requires git, docker, jq");
145 assert!(compat.is_ok());
146 }
147
148 #[test]
149 fn compatibility_at_max_length_is_accepted() {
150 let compat = "a".repeat(500);
151 assert!(Compatibility::new(compat).is_ok());
152 }
153
154 #[test]
155 fn empty_compatibility_is_rejected() {
156 let result = Compatibility::new("");
157 assert_eq!(result, Err(CompatibilityError::Empty));
158 }
159
160 #[test]
161 fn compatibility_exceeding_max_length_is_rejected() {
162 let long_compat = "a".repeat(501);
163 let result = Compatibility::new(long_compat);
164 assert!(matches!(
165 result,
166 Err(CompatibilityError::TooLong {
167 length: 501,
168 max: 500
169 })
170 ));
171 }
172
173 #[test]
174 fn display_returns_inner_string() {
175 let compat = Compatibility::new("Designed for Claude Code").unwrap();
176 assert_eq!(format!("{compat}"), "Designed for Claude Code");
177 }
178
179 #[test]
180 fn from_str_works() {
181 let compat: Compatibility = "Requires docker".parse().unwrap();
182 assert_eq!(compat.as_str(), "Requires docker");
183 }
184
185 #[test]
186 fn as_ref_works() {
187 let compat = Compatibility::new("Test").unwrap();
188 let s: &str = compat.as_ref();
189 assert_eq!(s, "Test");
190 }
191}