turbomcp_protocol/types/
domain.rs1use serde::{Deserialize, Serialize};
8use std::{fmt, ops::Deref};
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 Deref for Uri {
124 type Target = str;
125
126 fn deref(&self) -> &Self::Target {
127 &self.0
128 }
129}
130
131impl From<String> for Uri {
132 fn from(uri: String) -> Self {
133 Self::new_unchecked(uri)
134 }
135}
136
137impl From<&str> for Uri {
138 fn from(uri: &str) -> Self {
139 Self::new_unchecked(uri)
140 }
141}
142
143impl PartialEq<&str> for Uri {
144 fn eq(&self, other: &&str) -> bool {
145 self.as_str() == *other
146 }
147}
148
149impl PartialEq<Uri> for &str {
150 fn eq(&self, other: &Uri) -> bool {
151 *self == other.as_str()
152 }
153}
154
155impl From<Uri> for String {
156 fn from(uri: Uri) -> Self {
157 uri.0
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
177#[serde(transparent)]
178pub struct MimeType(String);
179
180impl MimeType {
181 pub fn new<S: Into<String>>(mime: S) -> Result<Self, MimeTypeError> {
187 let mime_string = mime.into();
188
189 if !mime_string.contains('/') {
191 return Err(MimeTypeError::InvalidFormat(mime_string));
192 }
193
194 let main_part = mime_string.split(';').next().unwrap_or(&mime_string);
196 let parts: Vec<&str> = main_part.split('/').collect();
197
198 if parts.len() != 2 {
199 return Err(MimeTypeError::InvalidFormat(mime_string));
200 }
201
202 let type_part = parts[0].trim();
203 let subtype = parts[1].trim();
204
205 if type_part.is_empty() {
206 return Err(MimeTypeError::EmptyType(mime_string));
207 }
208
209 if subtype.is_empty() {
210 return Err(MimeTypeError::EmptySubtype(mime_string));
211 }
212
213 Ok(Self(mime_string))
214 }
215
216 #[must_use]
218 pub fn new_unchecked<S: Into<String>>(mime: S) -> Self {
219 Self(mime.into())
220 }
221
222 #[must_use]
224 pub fn as_str(&self) -> &str {
225 &self.0
226 }
227
228 #[must_use]
230 pub fn type_part(&self) -> Option<&str> {
231 self.0
232 .split('/')
233 .next()
234 .map(|s| s.split(';').next().unwrap_or(s).trim())
235 }
236
237 #[must_use]
239 pub fn subtype(&self) -> Option<&str> {
240 self.0
241 .split('/')
242 .nth(1)
243 .map(|s| s.split(';').next().unwrap_or(s).trim())
244 }
245
246 #[must_use]
248 pub fn into_inner(self) -> String {
249 self.0
250 }
251}
252
253impl fmt::Display for MimeType {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 write!(f, "{}", self.0)
256 }
257}
258
259impl AsRef<str> for MimeType {
260 fn as_ref(&self) -> &str {
261 &self.0
262 }
263}
264
265impl Deref for MimeType {
266 type Target = str;
267
268 fn deref(&self) -> &Self::Target {
269 &self.0
270 }
271}
272
273impl From<String> for MimeType {
274 fn from(mime: String) -> Self {
275 Self::new_unchecked(mime)
276 }
277}
278
279impl From<&str> for MimeType {
280 fn from(mime: &str) -> Self {
281 Self::new_unchecked(mime)
282 }
283}
284
285impl PartialEq<&str> for MimeType {
286 fn eq(&self, other: &&str) -> bool {
287 self.as_str() == *other
288 }
289}
290
291impl PartialEq<MimeType> for &str {
292 fn eq(&self, other: &MimeType) -> bool {
293 *self == other.as_str()
294 }
295}
296
297impl From<MimeType> for String {
298 fn from(mime: MimeType) -> Self {
299 mime.0
300 }
301}
302
303#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
319#[serde(transparent)]
320pub struct Base64String(String);
321
322impl Base64String {
323 pub fn new<S: Into<String>>(data: S) -> Result<Self, Base64Error> {
329 let data_string = data.into();
330
331 if !data_string
333 .chars()
334 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '='))
335 {
336 return Err(Base64Error::InvalidCharacters(data_string));
337 }
338
339 if let Some(first_pad) = data_string.find('=')
341 && !data_string[first_pad..].chars().all(|c| c == '=')
342 {
343 return Err(Base64Error::InvalidPadding(data_string));
344 }
345
346 Ok(Self(data_string))
347 }
348
349 #[must_use]
351 pub fn new_unchecked<S: Into<String>>(data: S) -> Self {
352 Self(data.into())
353 }
354
355 #[must_use]
357 pub fn as_str(&self) -> &str {
358 &self.0
359 }
360
361 #[must_use]
363 pub fn into_inner(self) -> String {
364 self.0
365 }
366}
367
368impl fmt::Display for Base64String {
369 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370 write!(f, "{}", self.0)
371 }
372}
373
374impl AsRef<str> for Base64String {
375 fn as_ref(&self) -> &str {
376 &self.0
377 }
378}
379
380impl Deref for Base64String {
381 type Target = str;
382
383 fn deref(&self) -> &Self::Target {
384 &self.0
385 }
386}
387
388impl From<String> for Base64String {
389 fn from(data: String) -> Self {
390 Self::new_unchecked(data)
391 }
392}
393
394impl From<&str> for Base64String {
395 fn from(data: &str) -> Self {
396 Self::new_unchecked(data)
397 }
398}
399
400impl PartialEq<&str> for Base64String {
401 fn eq(&self, other: &&str) -> bool {
402 self.as_str() == *other
403 }
404}
405
406impl PartialEq<Base64String> for &str {
407 fn eq(&self, other: &Base64String) -> bool {
408 *self == other.as_str()
409 }
410}
411
412impl From<Base64String> for String {
413 fn from(b64: Base64String) -> Self {
414 b64.0
415 }
416}
417
418#[derive(Debug, Clone, thiserror::Error)]
420pub enum UriError {
421 #[error("URI missing scheme separator: {0}")]
423 MissingScheme(String),
424
425 #[error("URI has empty scheme: {0}")]
427 EmptyScheme(String),
428
429 #[error(
431 "URI has invalid scheme (must start with letter and contain only alphanumeric, +, ., -): {0}"
432 )]
433 InvalidScheme(String),
434}
435
436#[derive(Debug, Clone, thiserror::Error)]
438pub enum MimeTypeError {
439 #[error("Invalid MIME type format (must be type/subtype): {0}")]
441 InvalidFormat(String),
442
443 #[error("MIME type has empty type part: {0}")]
445 EmptyType(String),
446
447 #[error("MIME type has empty subtype part: {0}")]
449 EmptySubtype(String),
450}
451
452#[derive(Debug, Clone, thiserror::Error)]
454pub enum Base64Error {
455 #[error("Base64 string contains invalid characters: {0}")]
457 InvalidCharacters(String),
458
459 #[error("Base64 string has invalid padding (= must only appear at end): {0}")]
461 InvalidPadding(String),
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn test_uri_validation() {
470 assert!(Uri::new("file:///path/to/file").is_ok());
472 assert!(Uri::new("https://example.com").is_ok());
473 assert!(Uri::new("resource://test").is_ok());
474 assert!(Uri::new("custom+scheme://data").is_ok());
475
476 assert!(Uri::new("not-a-uri").is_err());
478 assert!(Uri::new(":no-scheme").is_err());
479 assert!(Uri::new("123://invalid-start").is_err());
480 }
481
482 #[test]
483 fn test_uri_scheme_extraction() {
484 let uri = Uri::new("https://example.com/path").unwrap();
485 assert_eq!(uri.scheme(), Some("https"));
486
487 let file_uri = Uri::new("file:///local/path").unwrap();
488 assert_eq!(file_uri.scheme(), Some("file"));
489 }
490
491 #[test]
492 fn test_mime_type_validation() {
493 assert!(MimeType::new("text/plain").is_ok());
495 assert!(MimeType::new("application/json").is_ok());
496 assert!(MimeType::new("text/html; charset=utf-8").is_ok());
497 assert!(MimeType::new("image/png").is_ok());
498
499 assert!(MimeType::new("invalid").is_err());
501 assert!(MimeType::new("/no-type").is_err());
502 assert!(MimeType::new("no-subtype/").is_err());
503 }
504
505 #[test]
506 fn test_mime_type_parts() {
507 let mime = MimeType::new("text/html; charset=utf-8").unwrap();
508 assert_eq!(mime.type_part(), Some("text"));
509 assert_eq!(mime.subtype(), Some("html"));
510 }
511
512 #[test]
513 fn test_base64_validation() {
514 assert!(Base64String::new("SGVsbG8gV29ybGQh").is_ok());
516 assert!(Base64String::new("YWJjMTIz").is_ok());
517 assert!(Base64String::new("dGVzdA==").is_ok());
518 assert!(Base64String::new("").is_ok()); assert!(Base64String::new("invalid!@#").is_err());
522 assert!(Base64String::new("test=data").is_err()); }
524
525 #[test]
526 fn test_domain_type_conversions() {
527 let uri = Uri::new("https://example.com").unwrap();
528 assert_eq!(uri.as_str(), "https://example.com");
529 assert_eq!(uri.to_string(), "https://example.com");
530
531 let mime = MimeType::new("text/plain").unwrap();
532 assert_eq!(mime.as_str(), "text/plain");
533
534 let b64 = Base64String::new("dGVzdA==").unwrap();
535 assert_eq!(b64.as_str(), "dGVzdA==");
536 }
537}