1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct Image {
19 pub data: Vec<u8>,
21 pub mime_type: String,
23 #[serde(default)]
25 pub description: Option<String>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct Document {
33 pub data: Vec<u8>,
35 pub mime_type: String,
37 #[serde(default)]
39 pub description: Option<String>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct Audio {
47 pub data: Vec<u8>,
49 pub mime_type: String,
51 #[serde(default)]
53 pub description: Option<String>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct Video {
61 pub data: Vec<u8>,
63 pub mime_type: String,
65 #[serde(default)]
67 pub description: Option<String>,
68}
69
70pub trait MediaContent {
80 const TYPE_NAME: &'static str;
83
84 fn data(&self) -> &[u8];
86 fn mime_type(&self) -> &str;
88 fn description(&self) -> Option<&str>;
90}
91
92impl MediaContent for Image {
93 const TYPE_NAME: &'static str = "Image";
94 fn data(&self) -> &[u8] {
95 &self.data
96 }
97 fn mime_type(&self) -> &str {
98 &self.mime_type
99 }
100 fn description(&self) -> Option<&str> {
101 self.description.as_deref()
102 }
103}
104
105impl MediaContent for Document {
106 const TYPE_NAME: &'static str = "Document";
107 fn data(&self) -> &[u8] {
108 &self.data
109 }
110 fn mime_type(&self) -> &str {
111 &self.mime_type
112 }
113 fn description(&self) -> Option<&str> {
114 self.description.as_deref()
115 }
116}
117
118impl MediaContent for Audio {
119 const TYPE_NAME: &'static str = "Audio";
120 fn data(&self) -> &[u8] {
121 &self.data
122 }
123 fn mime_type(&self) -> &str {
124 &self.mime_type
125 }
126 fn description(&self) -> Option<&str> {
127 self.description.as_deref()
128 }
129}
130
131impl MediaContent for Video {
132 const TYPE_NAME: &'static str = "Video";
133 fn data(&self) -> &[u8] {
134 &self.data
135 }
136 fn mime_type(&self) -> &str {
137 &self.mime_type
138 }
139 fn description(&self) -> Option<&str> {
140 self.description.as_deref()
141 }
142}
143
144pub mod mime {
150 pub const IMAGE_PNG: &str = "image/png";
152 pub const IMAGE_JPEG: &str = "image/jpeg";
154 pub const IMAGE_GIF: &str = "image/gif";
156 pub const IMAGE_WEBP: &str = "image/webp";
158
159 pub const APPLICATION_PDF: &str = "application/pdf";
161 pub const TEXT_PLAIN: &str = "text/plain";
163 pub const APPLICATION_JSON: &str = "application/json";
165
166 pub const AUDIO_MPEG: &str = "audio/mpeg";
168 pub const AUDIO_WAV: &str = "audio/wav";
170 pub const AUDIO_OGG: &str = "audio/ogg";
172 pub const AUDIO_FLAC: &str = "audio/flac";
174
175 pub const VIDEO_MP4: &str = "video/mp4";
177 pub const VIDEO_WEBM: &str = "video/webm";
179
180 #[must_use]
184 pub fn from_extension(ext: &str) -> Option<&'static str> {
185 match ext.to_ascii_lowercase().as_str() {
186 "png" => Some(IMAGE_PNG),
187 "jpg" | "jpeg" => Some(IMAGE_JPEG),
188 "gif" => Some(IMAGE_GIF),
189 "webp" => Some(IMAGE_WEBP),
190 "pdf" => Some(APPLICATION_PDF),
191 "txt" => Some(TEXT_PLAIN),
192 "json" => Some(APPLICATION_JSON),
193 "mp3" => Some(AUDIO_MPEG),
194 "wav" => Some(AUDIO_WAV),
195 "ogg" => Some(AUDIO_OGG),
196 "flac" => Some(AUDIO_FLAC),
197 "mp4" => Some(VIDEO_MP4),
198 "webm" => Some(VIDEO_WEBM),
199 _ => None,
200 }
201 }
202}
203
204impl Image {
209 pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
221 Self {
222 data,
223 mime_type: mime_type.into(),
224 description: None,
225 }
226 }
227
228 #[must_use]
239 pub fn png(data: Vec<u8>) -> Self {
240 Self::new(data, mime::IMAGE_PNG)
241 }
242
243 #[must_use]
253 pub fn jpeg(data: Vec<u8>) -> Self {
254 Self::new(data, mime::IMAGE_JPEG)
255 }
256
257 #[must_use]
259 pub fn webp(data: Vec<u8>) -> Self {
260 Self::new(data, mime::IMAGE_WEBP)
261 }
262
263 #[must_use]
265 pub fn gif(data: Vec<u8>) -> Self {
266 Self::new(data, mime::IMAGE_GIF)
267 }
268
269 #[must_use]
271 pub fn with_description(mut self, description: impl Into<String>) -> Self {
272 self.description = Some(description.into());
273 self
274 }
275
276 pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
283 let path = path.as_ref();
284 let ext = path.extension().and_then(|e| e.to_str()).ok_or_else(|| {
285 std::io::Error::new(std::io::ErrorKind::InvalidInput, "missing file extension")
286 })?;
287 let mime_type = mime::from_extension(ext).ok_or_else(|| {
288 std::io::Error::new(
289 std::io::ErrorKind::InvalidInput,
290 format!("unrecognized image extension: {ext}"),
291 )
292 })?;
293 if !mime_type.starts_with("image/") {
294 return Err(std::io::Error::new(
295 std::io::ErrorKind::InvalidInput,
296 format!("MIME type '{mime_type}' is not an image type"),
297 ));
298 }
299 let data = std::fs::read(path)?;
300 Ok(Self::new(data, mime_type))
301 }
302}
303
304impl Document {
305 pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
316 Self {
317 data,
318 mime_type: mime_type.into(),
319 description: None,
320 }
321 }
322
323 #[must_use]
333 pub fn pdf(data: Vec<u8>) -> Self {
334 Self::new(data, mime::APPLICATION_PDF)
335 }
336
337 #[must_use]
339 pub fn plain_text(data: Vec<u8>) -> Self {
340 Self::new(data, mime::TEXT_PLAIN)
341 }
342
343 #[must_use]
345 pub fn json(data: Vec<u8>) -> Self {
346 Self::new(data, mime::APPLICATION_JSON)
347 }
348
349 #[must_use]
351 pub fn with_description(mut self, description: impl Into<String>) -> Self {
352 self.description = Some(description.into());
353 self
354 }
355
356 pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
363 let path = path.as_ref();
364 let ext = path.extension().and_then(|e| e.to_str()).ok_or_else(|| {
365 std::io::Error::new(std::io::ErrorKind::InvalidInput, "missing file extension")
366 })?;
367 let mime_type = mime::from_extension(ext).ok_or_else(|| {
368 std::io::Error::new(
369 std::io::ErrorKind::InvalidInput,
370 format!("unrecognized document extension: {ext}"),
371 )
372 })?;
373 if !mime_type.starts_with("application/") && !mime_type.starts_with("text/") {
374 return Err(std::io::Error::new(
375 std::io::ErrorKind::InvalidInput,
376 format!("MIME type '{mime_type}' is not a document type"),
377 ));
378 }
379 let data = std::fs::read(path)?;
380 Ok(Self::new(data, mime_type))
381 }
382}
383
384impl Audio {
385 pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
396 Self {
397 data,
398 mime_type: mime_type.into(),
399 description: None,
400 }
401 }
402
403 #[must_use]
413 pub fn mp3(data: Vec<u8>) -> Self {
414 Self::new(data, mime::AUDIO_MPEG)
415 }
416
417 #[must_use]
427 pub fn wav(data: Vec<u8>) -> Self {
428 Self::new(data, mime::AUDIO_WAV)
429 }
430
431 #[must_use]
433 pub fn ogg(data: Vec<u8>) -> Self {
434 Self::new(data, mime::AUDIO_OGG)
435 }
436
437 #[must_use]
439 pub fn flac(data: Vec<u8>) -> Self {
440 Self::new(data, mime::AUDIO_FLAC)
441 }
442
443 #[must_use]
445 pub fn with_description(mut self, description: impl Into<String>) -> Self {
446 self.description = Some(description.into());
447 self
448 }
449
450 pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
457 let path = path.as_ref();
458 let ext = path.extension().and_then(|e| e.to_str()).ok_or_else(|| {
459 std::io::Error::new(std::io::ErrorKind::InvalidInput, "missing file extension")
460 })?;
461 let mime_type = mime::from_extension(ext).ok_or_else(|| {
462 std::io::Error::new(
463 std::io::ErrorKind::InvalidInput,
464 format!("unrecognized audio extension: {ext}"),
465 )
466 })?;
467 if !mime_type.starts_with("audio/") {
468 return Err(std::io::Error::new(
469 std::io::ErrorKind::InvalidInput,
470 format!("MIME type '{mime_type}' is not an audio type"),
471 ));
472 }
473 let data = std::fs::read(path)?;
474 Ok(Self::new(data, mime_type))
475 }
476}
477
478impl Video {
479 pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
490 Self {
491 data,
492 mime_type: mime_type.into(),
493 description: None,
494 }
495 }
496
497 #[must_use]
507 pub fn mp4(data: Vec<u8>) -> Self {
508 Self::new(data, mime::VIDEO_MP4)
509 }
510
511 #[must_use]
513 pub fn webm(data: Vec<u8>) -> Self {
514 Self::new(data, mime::VIDEO_WEBM)
515 }
516
517 #[must_use]
519 pub fn with_description(mut self, description: impl Into<String>) -> Self {
520 self.description = Some(description.into());
521 self
522 }
523
524 pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
531 let path = path.as_ref();
532 let ext = path.extension().and_then(|e| e.to_str()).ok_or_else(|| {
533 std::io::Error::new(std::io::ErrorKind::InvalidInput, "missing file extension")
534 })?;
535 let mime_type = mime::from_extension(ext).ok_or_else(|| {
536 std::io::Error::new(
537 std::io::ErrorKind::InvalidInput,
538 format!("unrecognized video extension: {ext}"),
539 )
540 })?;
541 if !mime_type.starts_with("video/") {
542 return Err(std::io::Error::new(
543 std::io::ErrorKind::InvalidInput,
544 format!("MIME type '{mime_type}' is not a video type"),
545 ));
546 }
547 let data = std::fs::read(path)?;
548 Ok(Self::new(data, mime_type))
549 }
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555
556 #[test]
557 fn image_struct_serde_roundtrip() {
558 let img = Image {
559 data: vec![10, 20, 30],
560 mime_type: "image/bmp".to_string(),
561 description: Some("bitmap".to_string()),
562 };
563 let json = serde_json::to_string(&img).unwrap();
564 let parsed: Image = serde_json::from_str(&json).unwrap();
565 assert_eq!(parsed, img);
566 }
567
568 #[test]
569 fn document_struct_serde_roundtrip() {
570 let doc = Document {
571 data: b"{}".to_vec(),
572 mime_type: "application/json".to_string(),
573 description: None,
574 };
575 let json = serde_json::to_string(&doc).unwrap();
576 let parsed: Document = serde_json::from_str(&json).unwrap();
577 assert_eq!(parsed, doc);
578 }
579
580 #[test]
581 fn audio_struct_serde_roundtrip() {
582 let audio = Audio {
583 data: vec![0xAA, 0xBB],
584 mime_type: "audio/wav".to_string(),
585 description: Some("beep".to_string()),
586 };
587 let json = serde_json::to_string(&audio).unwrap();
588 let parsed: Audio = serde_json::from_str(&json).unwrap();
589 assert_eq!(parsed, audio);
590 }
591
592 #[test]
593 fn video_struct_serde_roundtrip() {
594 let video = Video {
595 data: vec![0xCC, 0xDD, 0xEE],
596 mime_type: "video/webm".to_string(),
597 description: None,
598 };
599 let json = serde_json::to_string(&video).unwrap();
600 let parsed: Video = serde_json::from_str(&json).unwrap();
601 assert_eq!(parsed, video);
602 }
603
604 #[test]
605 fn image_description_defaults_to_none() {
606 let json = r#"{"data":[1,2,3],"mime_type":"image/png"}"#;
607 let img: Image = serde_json::from_str(json).unwrap();
608 assert!(img.description.is_none());
609 }
610
611 #[test]
612 fn image_new_creates_correct_image() {
613 let img = Image::new(vec![10, 20], "image/webp");
614 assert_eq!(img.data, vec![10, 20]);
615 assert_eq!(img.mime_type, "image/webp");
616 assert!(img.description.is_none());
617 }
618
619 #[test]
620 fn image_png_creates_correct_image() {
621 let img = Image::png(vec![1, 2, 3]);
622 assert_eq!(img.data, vec![1, 2, 3]);
623 assert_eq!(img.mime_type, "image/png");
624 assert!(img.description.is_none());
625 }
626
627 #[test]
628 fn image_jpeg_creates_correct_image() {
629 let img = Image::jpeg(vec![0xFF, 0xD8]);
630 assert_eq!(img.data, vec![0xFF, 0xD8]);
631 assert_eq!(img.mime_type, "image/jpeg");
632 assert!(img.description.is_none());
633 }
634
635 #[test]
636 fn document_new_creates_correct_document() {
637 let doc = Document::new(b"data".to_vec(), "text/plain");
638 assert_eq!(doc.data, b"data".to_vec());
639 assert_eq!(doc.mime_type, "text/plain");
640 assert!(doc.description.is_none());
641 }
642
643 #[test]
644 fn document_pdf_creates_correct_document() {
645 let doc = Document::pdf(b"%PDF-1.4".to_vec());
646 assert_eq!(doc.data, b"%PDF-1.4".to_vec());
647 assert_eq!(doc.mime_type, "application/pdf");
648 assert!(doc.description.is_none());
649 }
650
651 #[test]
652 fn audio_new_creates_correct_audio() {
653 let audio = Audio::new(vec![0xAA], "audio/ogg");
654 assert_eq!(audio.data, vec![0xAA]);
655 assert_eq!(audio.mime_type, "audio/ogg");
656 assert!(audio.description.is_none());
657 }
658
659 #[test]
660 fn audio_mp3_creates_correct_audio() {
661 let audio = Audio::mp3(vec![0xFF, 0xFB]);
662 assert_eq!(audio.data, vec![0xFF, 0xFB]);
663 assert_eq!(audio.mime_type, "audio/mpeg");
664 assert!(audio.description.is_none());
665 }
666
667 #[test]
668 fn audio_wav_creates_correct_audio() {
669 let audio = Audio::wav(vec![0x52, 0x49, 0x46, 0x46]);
670 assert_eq!(audio.data, vec![0x52, 0x49, 0x46, 0x46]);
671 assert_eq!(audio.mime_type, "audio/wav");
672 assert!(audio.description.is_none());
673 }
674
675 #[test]
676 fn video_new_creates_correct_video() {
677 let video = Video::new(vec![0x00], "video/webm");
678 assert_eq!(video.data, vec![0x00]);
679 assert_eq!(video.mime_type, "video/webm");
680 assert!(video.description.is_none());
681 }
682
683 #[test]
684 fn video_mp4_creates_correct_video() {
685 let video = Video::mp4(vec![0x00, 0x00, 0x00, 0x1C]);
686 assert_eq!(video.data, vec![0x00, 0x00, 0x00, 0x1C]);
687 assert_eq!(video.mime_type, "video/mp4");
688 assert!(video.description.is_none());
689 }
690
691 #[test]
692 fn image_new_accepts_string_type() {
693 let img = Image::new(vec![1], String::from("image/gif"));
694 assert_eq!(img.mime_type, "image/gif");
695 }
696}