simple_agent_type/
validation.rs1use crate::error::{Result, ValidationError};
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use subtle::ConstantTimeEq;
9
10#[derive(Clone)]
28pub struct ApiKey(String);
29
30impl ApiKey {
31 pub fn new(key: impl Into<String>) -> Result<Self> {
49 let key = key.into();
50
51 if key.is_empty() {
52 return Err(ValidationError::Empty {
53 field: "api_key".to_string(),
54 }
55 .into());
56 }
57
58 if key.len() < 20 {
59 return Err(ValidationError::TooShort {
60 field: "api_key".to_string(),
61 min: 20,
62 }
63 .into());
64 }
65
66 if key.contains('\0') {
68 return Err(ValidationError::InvalidFormat {
69 field: "api_key".to_string(),
70 reason: "contains null bytes".to_string(),
71 }
72 .into());
73 }
74
75 Ok(Self(key))
76 }
77
78 pub fn expose(&self) -> &str {
92 &self.0
93 }
94
95 pub fn preview(&self) -> String {
109 let prefix = if self.0.len() >= 7 {
110 &self.0[..7]
111 } else {
112 &self.0
113 };
114 format!("{}*** ({} chars)", prefix, self.0.len())
115 }
116}
117
118impl fmt::Debug for ApiKey {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 write!(f, "ApiKey([REDACTED])")
122 }
123}
124
125impl Serialize for ApiKey {
127 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
128 where
129 S: serde::Serializer,
130 {
131 serializer.serialize_str("[REDACTED]")
132 }
133}
134
135impl<'de> Deserialize<'de> for ApiKey {
137 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
138 where
139 D: serde::Deserializer<'de>,
140 {
141 let s = String::deserialize(deserializer)?;
142 ApiKey::new(s).map_err(serde::de::Error::custom)
143 }
144}
145
146impl PartialEq for ApiKey {
148 fn eq(&self, other: &Self) -> bool {
149 self.0.as_bytes().ct_eq(other.0.as_bytes()).into()
150 }
151}
152
153impl Eq for ApiKey {}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_api_key_valid() {
161 let key = ApiKey::new("sk-1234567890abcdef1234567890");
162 assert!(key.is_ok());
163 }
164
165 #[test]
166 fn test_api_key_empty() {
167 let key = ApiKey::new("");
168 assert!(key.is_err());
169 assert!(matches!(
170 key.unwrap_err(),
171 crate::error::SimpleAgentsError::Validation(ValidationError::Empty { .. })
172 ));
173 }
174
175 #[test]
176 fn test_api_key_too_short() {
177 let key = ApiKey::new("short");
178 assert!(key.is_err());
179 assert!(matches!(
180 key.unwrap_err(),
181 crate::error::SimpleAgentsError::Validation(ValidationError::TooShort { .. })
182 ));
183 }
184
185 #[test]
186 fn test_api_key_null_byte() {
187 let key = ApiKey::new("sk-12345678901234567890\0extra");
188 assert!(key.is_err());
189 assert!(matches!(
190 key.unwrap_err(),
191 crate::error::SimpleAgentsError::Validation(ValidationError::InvalidFormat { .. })
192 ));
193 }
194
195 #[test]
196 fn test_api_key_expose() {
197 let key = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
198 assert_eq!(key.expose(), "sk-1234567890abcdef1234567890");
199 }
200
201 #[test]
202 fn test_api_key_debug_redacted() {
203 let key = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
204 let debug = format!("{:?}", key);
205 assert!(debug.contains("REDACTED"));
206 assert!(!debug.contains("sk-"));
207 assert!(!debug.contains("1234"));
208 }
209
210 #[test]
211 fn test_api_key_serialize_redacted() {
212 let key = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
213 let json = serde_json::to_string(&key).unwrap();
214 assert_eq!(json, "\"[REDACTED]\"");
215 assert!(!json.contains("sk-"));
216 }
217
218 #[test]
219 fn test_api_key_deserialize() {
220 let json = "\"sk-1234567890abcdef1234567890\"";
221 let key: ApiKey = serde_json::from_str(json).unwrap();
222 assert_eq!(key.expose(), "sk-1234567890abcdef1234567890");
223 }
224
225 #[test]
226 fn test_api_key_preview() {
227 let key = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
228 let preview = key.preview();
229 assert!(preview.contains("sk-"));
230 assert!(preview.contains("29 chars"));
231 assert!(!preview.contains("abcdef"));
232 }
233
234 #[test]
235 fn test_api_key_equality() {
236 let key1 = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
237 let key2 = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
238 let key3 = ApiKey::new("sk-differentkey1234567890").unwrap();
239
240 assert_eq!(key1, key2);
241 assert_ne!(key1, key3);
242 }
243
244 #[test]
245 fn test_api_key_constant_time_comparison() {
246 let key1 = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
248 let key2 = ApiKey::new("sk-1234567890abcdef1234567890").unwrap();
249 let key3 = ApiKey::new("sk-9999999999999999999999").unwrap();
250
251 assert_eq!(key1, key2);
253
254 assert_ne!(key1, key3);
256
257 let key4 = ApiKey::new("sk-1234567890abcdef12345678901").unwrap();
259 assert_ne!(key1, key4);
260 }
261}