turbomcp_protocol/types/
domain.rs1use serde::{Deserialize, Serialize};
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(transparent)]
30pub struct Uri(String);
31
32impl Uri {
33 pub fn new<S: Into<String>>(uri: S) -> Result<Self, UriError> {
39 let uri_string = uri.into();
40
41 if !uri_string.contains(':') {
43 return Err(UriError::MissingScheme(uri_string));
44 }
45
46 if let Some(scheme_end) = uri_string.find(':') {
48 let scheme = &uri_string[..scheme_end];
49
50 if scheme.is_empty() {
51 return Err(UriError::EmptyScheme(uri_string));
52 }
53
54 if !scheme
56 .chars()
57 .next()
58 .is_some_and(|c| c.is_ascii_alphabetic())
59 {
60 return Err(UriError::InvalidScheme(uri_string));
61 }
62
63 if !scheme
64 .chars()
65 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '.' | '-'))
66 {
67 return Err(UriError::InvalidScheme(uri_string));
68 }
69 }
70
71 Ok(Self(uri_string))
72 }
73
74 #[must_use]
79 pub fn new_unchecked<S: Into<String>>(uri: S) -> Self {
80 Self(uri.into())
81 }
82
83 #[must_use]
85 pub fn as_str(&self) -> &str {
86 &self.0
87 }
88
89 #[must_use]
100 pub fn scheme(&self) -> Option<&str> {
101 self.0.split(':').next()
102 }
103
104 #[must_use]
106 pub fn into_inner(self) -> String {
107 self.0
108 }
109}
110
111impl fmt::Display for Uri {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 write!(f, "{}", self.0)
114 }
115}
116
117impl AsRef<str> for Uri {
118 fn as_ref(&self) -> &str {
119 &self.0
120 }
121}
122
123impl From<Uri> for String {
124 fn from(uri: Uri) -> Self {
125 uri.0
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
145#[serde(transparent)]
146pub struct MimeType(String);
147
148impl MimeType {
149 pub fn new<S: Into<String>>(mime: S) -> Result<Self, MimeTypeError> {
155 let mime_string = mime.into();
156
157 if !mime_string.contains('/') {
159 return Err(MimeTypeError::InvalidFormat(mime_string));
160 }
161
162 let main_part = mime_string.split(';').next().unwrap_or(&mime_string);
164 let parts: Vec<&str> = main_part.split('/').collect();
165
166 if parts.len() != 2 {
167 return Err(MimeTypeError::InvalidFormat(mime_string));
168 }
169
170 let type_part = parts[0].trim();
171 let subtype = parts[1].trim();
172
173 if type_part.is_empty() {
174 return Err(MimeTypeError::EmptyType(mime_string));
175 }
176
177 if subtype.is_empty() {
178 return Err(MimeTypeError::EmptySubtype(mime_string));
179 }
180
181 Ok(Self(mime_string))
182 }
183
184 #[must_use]
186 pub fn new_unchecked<S: Into<String>>(mime: S) -> Self {
187 Self(mime.into())
188 }
189
190 #[must_use]
192 pub fn as_str(&self) -> &str {
193 &self.0
194 }
195
196 #[must_use]
198 pub fn type_part(&self) -> Option<&str> {
199 self.0
200 .split('/')
201 .next()
202 .map(|s| s.split(';').next().unwrap_or(s).trim())
203 }
204
205 #[must_use]
207 pub fn subtype(&self) -> Option<&str> {
208 self.0
209 .split('/')
210 .nth(1)
211 .map(|s| s.split(';').next().unwrap_or(s).trim())
212 }
213
214 #[must_use]
216 pub fn into_inner(self) -> String {
217 self.0
218 }
219}
220
221impl fmt::Display for MimeType {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 write!(f, "{}", self.0)
224 }
225}
226
227impl AsRef<str> for MimeType {
228 fn as_ref(&self) -> &str {
229 &self.0
230 }
231}
232
233impl From<MimeType> for String {
234 fn from(mime: MimeType) -> Self {
235 mime.0
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
255#[serde(transparent)]
256pub struct Base64String(String);
257
258impl Base64String {
259 pub fn new<S: Into<String>>(data: S) -> Result<Self, Base64Error> {
265 let data_string = data.into();
266
267 if !data_string
269 .chars()
270 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '='))
271 {
272 return Err(Base64Error::InvalidCharacters(data_string));
273 }
274
275 if let Some(first_pad) = data_string.find('=')
277 && !data_string[first_pad..].chars().all(|c| c == '=')
278 {
279 return Err(Base64Error::InvalidPadding(data_string));
280 }
281
282 Ok(Self(data_string))
283 }
284
285 #[must_use]
287 pub fn new_unchecked<S: Into<String>>(data: S) -> Self {
288 Self(data.into())
289 }
290
291 #[must_use]
293 pub fn as_str(&self) -> &str {
294 &self.0
295 }
296
297 #[must_use]
299 pub fn into_inner(self) -> String {
300 self.0
301 }
302}
303
304impl fmt::Display for Base64String {
305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306 write!(f, "{}", self.0)
307 }
308}
309
310impl AsRef<str> for Base64String {
311 fn as_ref(&self) -> &str {
312 &self.0
313 }
314}
315
316impl From<Base64String> for String {
317 fn from(b64: Base64String) -> Self {
318 b64.0
319 }
320}
321
322#[derive(Debug, Clone, thiserror::Error)]
324pub enum UriError {
325 #[error("URI missing scheme separator: {0}")]
327 MissingScheme(String),
328
329 #[error("URI has empty scheme: {0}")]
331 EmptyScheme(String),
332
333 #[error(
335 "URI has invalid scheme (must start with letter and contain only alphanumeric, +, ., -): {0}"
336 )]
337 InvalidScheme(String),
338}
339
340#[derive(Debug, Clone, thiserror::Error)]
342pub enum MimeTypeError {
343 #[error("Invalid MIME type format (must be type/subtype): {0}")]
345 InvalidFormat(String),
346
347 #[error("MIME type has empty type part: {0}")]
349 EmptyType(String),
350
351 #[error("MIME type has empty subtype part: {0}")]
353 EmptySubtype(String),
354}
355
356#[derive(Debug, Clone, thiserror::Error)]
358pub enum Base64Error {
359 #[error("Base64 string contains invalid characters: {0}")]
361 InvalidCharacters(String),
362
363 #[error("Base64 string has invalid padding (= must only appear at end): {0}")]
365 InvalidPadding(String),
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_uri_validation() {
374 assert!(Uri::new("file:///path/to/file").is_ok());
376 assert!(Uri::new("https://example.com").is_ok());
377 assert!(Uri::new("resource://test").is_ok());
378 assert!(Uri::new("custom+scheme://data").is_ok());
379
380 assert!(Uri::new("not-a-uri").is_err());
382 assert!(Uri::new(":no-scheme").is_err());
383 assert!(Uri::new("123://invalid-start").is_err());
384 }
385
386 #[test]
387 fn test_uri_scheme_extraction() {
388 let uri = Uri::new("https://example.com/path").unwrap();
389 assert_eq!(uri.scheme(), Some("https"));
390
391 let file_uri = Uri::new("file:///local/path").unwrap();
392 assert_eq!(file_uri.scheme(), Some("file"));
393 }
394
395 #[test]
396 fn test_mime_type_validation() {
397 assert!(MimeType::new("text/plain").is_ok());
399 assert!(MimeType::new("application/json").is_ok());
400 assert!(MimeType::new("text/html; charset=utf-8").is_ok());
401 assert!(MimeType::new("image/png").is_ok());
402
403 assert!(MimeType::new("invalid").is_err());
405 assert!(MimeType::new("/no-type").is_err());
406 assert!(MimeType::new("no-subtype/").is_err());
407 }
408
409 #[test]
410 fn test_mime_type_parts() {
411 let mime = MimeType::new("text/html; charset=utf-8").unwrap();
412 assert_eq!(mime.type_part(), Some("text"));
413 assert_eq!(mime.subtype(), Some("html"));
414 }
415
416 #[test]
417 fn test_base64_validation() {
418 assert!(Base64String::new("SGVsbG8gV29ybGQh").is_ok());
420 assert!(Base64String::new("YWJjMTIz").is_ok());
421 assert!(Base64String::new("dGVzdA==").is_ok());
422 assert!(Base64String::new("").is_ok()); assert!(Base64String::new("invalid!@#").is_err());
426 assert!(Base64String::new("test=data").is_err()); }
428
429 #[test]
430 fn test_domain_type_conversions() {
431 let uri = Uri::new("https://example.com").unwrap();
432 assert_eq!(uri.as_str(), "https://example.com");
433 assert_eq!(uri.to_string(), "https://example.com");
434
435 let mime = MimeType::new("text/plain").unwrap();
436 assert_eq!(mime.as_str(), "text/plain");
437
438 let b64 = Base64String::new("dGVzdA==").unwrap();
439 assert_eq!(b64.as_str(), "dGVzdA==");
440 }
441}