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
204fn from_file_inner(
209 path: &std::path::Path,
210 type_label: &str,
211 mime_prefixes: &[&str],
212) -> std::io::Result<(Vec<u8>, String)> {
213 let ext = path.extension().and_then(|e| e.to_str()).ok_or_else(|| {
214 std::io::Error::new(std::io::ErrorKind::InvalidInput, "missing file extension")
215 })?;
216 let mime_type = mime::from_extension(ext).ok_or_else(|| {
217 std::io::Error::new(
218 std::io::ErrorKind::InvalidInput,
219 format!("unrecognized {type_label} extension: {ext}"),
220 )
221 })?;
222 if !mime_prefixes
223 .iter()
224 .any(|prefix| mime_type.starts_with(prefix))
225 {
226 return Err(std::io::Error::new(
227 std::io::ErrorKind::InvalidInput,
228 format!("MIME type '{mime_type}' is not {type_label} type"),
229 ));
230 }
231 let data = std::fs::read(path)?;
232 Ok((data, mime_type.to_owned()))
233}
234
235impl Image {
240 pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
252 Self {
253 data,
254 mime_type: mime_type.into(),
255 description: None,
256 }
257 }
258
259 #[must_use]
270 pub fn png(data: Vec<u8>) -> Self {
271 Self::new(data, mime::IMAGE_PNG)
272 }
273
274 #[must_use]
284 pub fn jpeg(data: Vec<u8>) -> Self {
285 Self::new(data, mime::IMAGE_JPEG)
286 }
287
288 #[must_use]
290 pub fn webp(data: Vec<u8>) -> Self {
291 Self::new(data, mime::IMAGE_WEBP)
292 }
293
294 #[must_use]
296 pub fn gif(data: Vec<u8>) -> Self {
297 Self::new(data, mime::IMAGE_GIF)
298 }
299
300 #[must_use]
302 pub fn with_description(mut self, description: impl Into<String>) -> Self {
303 self.description = Some(description.into());
304 self
305 }
306
307 pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
314 let (data, mime_type) = from_file_inner(path.as_ref(), "an image", &["image/"])?;
315 Ok(Self::new(data, mime_type))
316 }
317}
318
319impl Document {
320 pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
331 Self {
332 data,
333 mime_type: mime_type.into(),
334 description: None,
335 }
336 }
337
338 #[must_use]
348 pub fn pdf(data: Vec<u8>) -> Self {
349 Self::new(data, mime::APPLICATION_PDF)
350 }
351
352 #[must_use]
354 pub fn plain_text(data: Vec<u8>) -> Self {
355 Self::new(data, mime::TEXT_PLAIN)
356 }
357
358 #[must_use]
360 pub fn json(data: Vec<u8>) -> Self {
361 Self::new(data, mime::APPLICATION_JSON)
362 }
363
364 #[must_use]
366 pub fn with_description(mut self, description: impl Into<String>) -> Self {
367 self.description = Some(description.into());
368 self
369 }
370
371 pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
378 let (data, mime_type) =
379 from_file_inner(path.as_ref(), "a document", &["application/", "text/"])?;
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 (data, mime_type) = from_file_inner(path.as_ref(), "an audio", &["audio/"])?;
458 Ok(Self::new(data, mime_type))
459 }
460}
461
462impl Video {
463 pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
474 Self {
475 data,
476 mime_type: mime_type.into(),
477 description: None,
478 }
479 }
480
481 #[must_use]
491 pub fn mp4(data: Vec<u8>) -> Self {
492 Self::new(data, mime::VIDEO_MP4)
493 }
494
495 #[must_use]
497 pub fn webm(data: Vec<u8>) -> Self {
498 Self::new(data, mime::VIDEO_WEBM)
499 }
500
501 #[must_use]
503 pub fn with_description(mut self, description: impl Into<String>) -> Self {
504 self.description = Some(description.into());
505 self
506 }
507
508 pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
515 let (data, mime_type) = from_file_inner(path.as_ref(), "a video", &["video/"])?;
516 Ok(Self::new(data, mime_type))
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523
524 #[test]
525 fn image_struct_serde_roundtrip() {
526 let img = Image {
527 data: vec![10, 20, 30],
528 mime_type: "image/bmp".to_string(),
529 description: Some("bitmap".to_string()),
530 };
531 let json = serde_json::to_string(&img).unwrap();
532 let parsed: Image = serde_json::from_str(&json).unwrap();
533 assert_eq!(parsed, img);
534 }
535
536 #[test]
537 fn document_struct_serde_roundtrip() {
538 let doc = Document {
539 data: b"{}".to_vec(),
540 mime_type: "application/json".to_string(),
541 description: None,
542 };
543 let json = serde_json::to_string(&doc).unwrap();
544 let parsed: Document = serde_json::from_str(&json).unwrap();
545 assert_eq!(parsed, doc);
546 }
547
548 #[test]
549 fn audio_struct_serde_roundtrip() {
550 let audio = Audio {
551 data: vec![0xAA, 0xBB],
552 mime_type: "audio/wav".to_string(),
553 description: Some("beep".to_string()),
554 };
555 let json = serde_json::to_string(&audio).unwrap();
556 let parsed: Audio = serde_json::from_str(&json).unwrap();
557 assert_eq!(parsed, audio);
558 }
559
560 #[test]
561 fn video_struct_serde_roundtrip() {
562 let video = Video {
563 data: vec![0xCC, 0xDD, 0xEE],
564 mime_type: "video/webm".to_string(),
565 description: None,
566 };
567 let json = serde_json::to_string(&video).unwrap();
568 let parsed: Video = serde_json::from_str(&json).unwrap();
569 assert_eq!(parsed, video);
570 }
571
572 #[test]
573 fn image_description_defaults_to_none() {
574 let json = r#"{"data":[1,2,3],"mime_type":"image/png"}"#;
575 let img: Image = serde_json::from_str(json).unwrap();
576 assert!(img.description.is_none());
577 }
578
579 #[test]
580 fn image_new_creates_correct_image() {
581 let img = Image::new(vec![10, 20], "image/webp");
582 assert_eq!(img.data, vec![10, 20]);
583 assert_eq!(img.mime_type, "image/webp");
584 assert!(img.description.is_none());
585 }
586
587 #[test]
588 fn image_png_creates_correct_image() {
589 let img = Image::png(vec![1, 2, 3]);
590 assert_eq!(img.data, vec![1, 2, 3]);
591 assert_eq!(img.mime_type, "image/png");
592 assert!(img.description.is_none());
593 }
594
595 #[test]
596 fn image_jpeg_creates_correct_image() {
597 let img = Image::jpeg(vec![0xFF, 0xD8]);
598 assert_eq!(img.data, vec![0xFF, 0xD8]);
599 assert_eq!(img.mime_type, "image/jpeg");
600 assert!(img.description.is_none());
601 }
602
603 #[test]
604 fn document_new_creates_correct_document() {
605 let doc = Document::new(b"data".to_vec(), "text/plain");
606 assert_eq!(doc.data, b"data".to_vec());
607 assert_eq!(doc.mime_type, "text/plain");
608 assert!(doc.description.is_none());
609 }
610
611 #[test]
612 fn document_pdf_creates_correct_document() {
613 let doc = Document::pdf(b"%PDF-1.4".to_vec());
614 assert_eq!(doc.data, b"%PDF-1.4".to_vec());
615 assert_eq!(doc.mime_type, "application/pdf");
616 assert!(doc.description.is_none());
617 }
618
619 #[test]
620 fn audio_new_creates_correct_audio() {
621 let audio = Audio::new(vec![0xAA], "audio/ogg");
622 assert_eq!(audio.data, vec![0xAA]);
623 assert_eq!(audio.mime_type, "audio/ogg");
624 assert!(audio.description.is_none());
625 }
626
627 #[test]
628 fn audio_mp3_creates_correct_audio() {
629 let audio = Audio::mp3(vec![0xFF, 0xFB]);
630 assert_eq!(audio.data, vec![0xFF, 0xFB]);
631 assert_eq!(audio.mime_type, "audio/mpeg");
632 assert!(audio.description.is_none());
633 }
634
635 #[test]
636 fn audio_wav_creates_correct_audio() {
637 let audio = Audio::wav(vec![0x52, 0x49, 0x46, 0x46]);
638 assert_eq!(audio.data, vec![0x52, 0x49, 0x46, 0x46]);
639 assert_eq!(audio.mime_type, "audio/wav");
640 assert!(audio.description.is_none());
641 }
642
643 #[test]
644 fn video_new_creates_correct_video() {
645 let video = Video::new(vec![0x00], "video/webm");
646 assert_eq!(video.data, vec![0x00]);
647 assert_eq!(video.mime_type, "video/webm");
648 assert!(video.description.is_none());
649 }
650
651 #[test]
652 fn video_mp4_creates_correct_video() {
653 let video = Video::mp4(vec![0x00, 0x00, 0x00, 0x1C]);
654 assert_eq!(video.data, vec![0x00, 0x00, 0x00, 0x1C]);
655 assert_eq!(video.mime_type, "video/mp4");
656 assert!(video.description.is_none());
657 }
658
659 #[test]
660 fn image_new_accepts_string_type() {
661 let img = Image::new(vec![1], String::from("image/gif"));
662 assert_eq!(img.mime_type, "image/gif");
663 }
664
665 #[test]
668 fn image_with_description_sets_description() {
669 let img = Image::png(vec![1]).with_description("a logo");
670 assert_eq!(img.description.as_deref(), Some("a logo"));
671 assert_eq!(img.mime_type, "image/png");
672 }
673
674 #[test]
675 fn document_with_description_sets_description() {
676 let doc = Document::pdf(vec![1]).with_description("invoice");
677 assert_eq!(doc.description.as_deref(), Some("invoice"));
678 assert_eq!(doc.mime_type, "application/pdf");
679 }
680
681 #[test]
682 fn audio_with_description_sets_description() {
683 let audio = Audio::mp3(vec![1]).with_description("intro jingle");
684 assert_eq!(audio.description.as_deref(), Some("intro jingle"));
685 assert_eq!(audio.mime_type, "audio/mpeg");
686 }
687
688 #[test]
689 fn video_with_description_sets_description() {
690 let video = Video::mp4(vec![1]).with_description("demo clip");
691 assert_eq!(video.description.as_deref(), Some("demo clip"));
692 assert_eq!(video.mime_type, "video/mp4");
693 }
694
695 #[test]
698 fn image_webp_creates_correct_image() {
699 let img = Image::webp(vec![1, 2]);
700 assert_eq!(img.mime_type, "image/webp");
701 assert_eq!(img.data, vec![1, 2]);
702 assert!(img.description.is_none());
703 }
704
705 #[test]
706 fn image_gif_creates_correct_image() {
707 let img = Image::gif(vec![0x47, 0x49, 0x46]);
708 assert_eq!(img.mime_type, "image/gif");
709 assert_eq!(img.data, vec![0x47, 0x49, 0x46]);
710 assert!(img.description.is_none());
711 }
712
713 #[test]
714 fn audio_ogg_creates_correct_audio() {
715 let audio = Audio::ogg(vec![0x4F, 0x67]);
716 assert_eq!(audio.mime_type, "audio/ogg");
717 assert_eq!(audio.data, vec![0x4F, 0x67]);
718 assert!(audio.description.is_none());
719 }
720
721 #[test]
722 fn audio_flac_creates_correct_audio() {
723 let audio = Audio::flac(vec![0x66, 0x4C]);
724 assert_eq!(audio.mime_type, "audio/flac");
725 assert_eq!(audio.data, vec![0x66, 0x4C]);
726 assert!(audio.description.is_none());
727 }
728
729 #[test]
730 fn document_plain_text_creates_correct_document() {
731 let doc = Document::plain_text(b"hello".to_vec());
732 assert_eq!(doc.mime_type, "text/plain");
733 assert_eq!(doc.data, b"hello");
734 assert!(doc.description.is_none());
735 }
736
737 #[test]
738 fn document_json_creates_correct_document() {
739 let doc = Document::json(b"{}".to_vec());
740 assert_eq!(doc.mime_type, "application/json");
741 assert_eq!(doc.data, b"{}");
742 assert!(doc.description.is_none());
743 }
744
745 #[test]
746 fn video_webm_creates_correct_video() {
747 let video = Video::webm(vec![0x1A, 0x45]);
748 assert_eq!(video.mime_type, "video/webm");
749 assert_eq!(video.data, vec![0x1A, 0x45]);
750 assert!(video.description.is_none());
751 }
752
753 #[test]
756 fn image_from_file_success() {
757 let dir = tempfile::tempdir().unwrap();
758 let path = dir.path().join("photo.png");
759 std::fs::write(&path, b"\x89PNG").unwrap();
760 let img = Image::from_file(&path).unwrap();
761 assert_eq!(img.data, b"\x89PNG");
762 assert_eq!(img.mime_type, "image/png");
763 assert!(img.description.is_none());
764 }
765
766 #[test]
767 fn image_from_file_unknown_extension() {
768 let dir = tempfile::tempdir().unwrap();
769 let path = dir.path().join("photo.bmp");
770 std::fs::write(&path, b"BM").unwrap();
771 let err = Image::from_file(&path).unwrap_err();
772 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
773 assert!(
774 err.to_string().contains("unrecognized"),
775 "expected 'unrecognized' in: {err}"
776 );
777 }
778
779 #[test]
780 fn image_from_file_wrong_mime_prefix() {
781 let dir = tempfile::tempdir().unwrap();
782 let path = dir.path().join("not_image.mp3");
783 std::fs::write(&path, b"\xFF\xFB").unwrap();
784 let err = Image::from_file(&path).unwrap_err();
785 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
786 assert!(
787 err.to_string().contains("not an image type"),
788 "expected MIME prefix error in: {err}"
789 );
790 }
791
792 #[test]
793 fn image_from_file_missing_extension() {
794 let dir = tempfile::tempdir().unwrap();
795 let path = dir.path().join("noext");
796 std::fs::write(&path, b"data").unwrap();
797 let err = Image::from_file(&path).unwrap_err();
798 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
799 assert!(
800 err.to_string().contains("missing file extension"),
801 "expected 'missing file extension' in: {err}"
802 );
803 }
804
805 #[test]
808 fn document_from_file_success() {
809 let dir = tempfile::tempdir().unwrap();
810 let path = dir.path().join("report.pdf");
811 std::fs::write(&path, b"%PDF-1.4").unwrap();
812 let doc = Document::from_file(&path).unwrap();
813 assert_eq!(doc.data, b"%PDF-1.4");
814 assert_eq!(doc.mime_type, "application/pdf");
815 assert!(doc.description.is_none());
816 }
817
818 #[test]
819 fn document_from_file_text_extension() {
820 let dir = tempfile::tempdir().unwrap();
821 let path = dir.path().join("notes.txt");
822 std::fs::write(&path, b"hello world").unwrap();
823 let doc = Document::from_file(&path).unwrap();
824 assert_eq!(doc.data, b"hello world");
825 assert_eq!(doc.mime_type, "text/plain");
826 }
827
828 #[test]
829 fn document_from_file_json_extension() {
830 let dir = tempfile::tempdir().unwrap();
831 let path = dir.path().join("config.json");
832 std::fs::write(&path, b"{}").unwrap();
833 let doc = Document::from_file(&path).unwrap();
834 assert_eq!(doc.data, b"{}");
835 assert_eq!(doc.mime_type, "application/json");
836 }
837
838 #[test]
839 fn document_from_file_unknown_extension() {
840 let dir = tempfile::tempdir().unwrap();
841 let path = dir.path().join("data.xyz");
842 std::fs::write(&path, b"stuff").unwrap();
843 let err = Document::from_file(&path).unwrap_err();
844 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
845 assert!(
846 err.to_string().contains("unrecognized"),
847 "expected 'unrecognized' in: {err}"
848 );
849 }
850
851 #[test]
852 fn document_from_file_wrong_mime_prefix() {
853 let dir = tempfile::tempdir().unwrap();
854 let path = dir.path().join("not_doc.png");
855 std::fs::write(&path, b"\x89PNG").unwrap();
856 let err = Document::from_file(&path).unwrap_err();
857 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
858 assert!(
859 err.to_string().contains("not a document type"),
860 "expected MIME prefix error in: {err}"
861 );
862 }
863
864 #[test]
865 fn document_from_file_missing_extension() {
866 let dir = tempfile::tempdir().unwrap();
867 let path = dir.path().join("noext");
868 std::fs::write(&path, b"data").unwrap();
869 let err = Document::from_file(&path).unwrap_err();
870 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
871 assert!(
872 err.to_string().contains("missing file extension"),
873 "expected 'missing file extension' in: {err}"
874 );
875 }
876
877 #[test]
880 fn audio_from_file_success() {
881 let dir = tempfile::tempdir().unwrap();
882 let path = dir.path().join("clip.mp3");
883 std::fs::write(&path, b"\xFF\xFB\x90").unwrap();
884 let audio = Audio::from_file(&path).unwrap();
885 assert_eq!(audio.data, b"\xFF\xFB\x90");
886 assert_eq!(audio.mime_type, "audio/mpeg");
887 assert!(audio.description.is_none());
888 }
889
890 #[test]
891 fn audio_from_file_wav_extension() {
892 let dir = tempfile::tempdir().unwrap();
893 let path = dir.path().join("sample.wav");
894 std::fs::write(&path, b"RIFF").unwrap();
895 let audio = Audio::from_file(&path).unwrap();
896 assert_eq!(audio.mime_type, "audio/wav");
897 }
898
899 #[test]
900 fn audio_from_file_unknown_extension() {
901 let dir = tempfile::tempdir().unwrap();
902 let path = dir.path().join("sound.aac");
903 std::fs::write(&path, b"data").unwrap();
904 let err = Audio::from_file(&path).unwrap_err();
905 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
906 assert!(
907 err.to_string().contains("unrecognized"),
908 "expected 'unrecognized' in: {err}"
909 );
910 }
911
912 #[test]
913 fn audio_from_file_wrong_mime_prefix() {
914 let dir = tempfile::tempdir().unwrap();
915 let path = dir.path().join("not_audio.png");
916 std::fs::write(&path, b"\x89PNG").unwrap();
917 let err = Audio::from_file(&path).unwrap_err();
918 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
919 assert!(
920 err.to_string().contains("not an audio type"),
921 "expected MIME prefix error in: {err}"
922 );
923 }
924
925 #[test]
926 fn audio_from_file_missing_extension() {
927 let dir = tempfile::tempdir().unwrap();
928 let path = dir.path().join("noext");
929 std::fs::write(&path, b"data").unwrap();
930 let err = Audio::from_file(&path).unwrap_err();
931 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
932 assert!(
933 err.to_string().contains("missing file extension"),
934 "expected 'missing file extension' in: {err}"
935 );
936 }
937
938 #[test]
941 fn video_from_file_success() {
942 let dir = tempfile::tempdir().unwrap();
943 let path = dir.path().join("clip.mp4");
944 std::fs::write(&path, b"\x00\x00\x00\x1Cftyp").unwrap();
945 let video = Video::from_file(&path).unwrap();
946 assert_eq!(video.data, b"\x00\x00\x00\x1Cftyp");
947 assert_eq!(video.mime_type, "video/mp4");
948 assert!(video.description.is_none());
949 }
950
951 #[test]
952 fn video_from_file_webm_extension() {
953 let dir = tempfile::tempdir().unwrap();
954 let path = dir.path().join("clip.webm");
955 std::fs::write(&path, b"\x1A\x45\xDF\xA3").unwrap();
956 let video = Video::from_file(&path).unwrap();
957 assert_eq!(video.mime_type, "video/webm");
958 }
959
960 #[test]
961 fn video_from_file_unknown_extension() {
962 let dir = tempfile::tempdir().unwrap();
963 let path = dir.path().join("movie.avi");
964 std::fs::write(&path, b"RIFF").unwrap();
965 let err = Video::from_file(&path).unwrap_err();
966 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
967 assert!(
968 err.to_string().contains("unrecognized"),
969 "expected 'unrecognized' in: {err}"
970 );
971 }
972
973 #[test]
974 fn video_from_file_wrong_mime_prefix() {
975 let dir = tempfile::tempdir().unwrap();
976 let path = dir.path().join("not_video.png");
977 std::fs::write(&path, b"\x89PNG").unwrap();
978 let err = Video::from_file(&path).unwrap_err();
979 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
980 assert!(
981 err.to_string().contains("not a video type"),
982 "expected MIME prefix error in: {err}"
983 );
984 }
985
986 #[test]
987 fn video_from_file_missing_extension() {
988 let dir = tempfile::tempdir().unwrap();
989 let path = dir.path().join("noext");
990 std::fs::write(&path, b"data").unwrap();
991 let err = Video::from_file(&path).unwrap_err();
992 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
993 assert!(
994 err.to_string().contains("missing file extension"),
995 "expected 'missing file extension' in: {err}"
996 );
997 }
998
999 #[test]
1002 fn image_from_file_nonexistent_file() {
1003 let err = Image::from_file("/tmp/agy_bridge_test_nonexistent_8f3a.png").unwrap_err();
1004 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
1005 }
1006
1007 #[test]
1008 fn document_from_file_nonexistent_file() {
1009 let err = Document::from_file("/tmp/agy_bridge_test_nonexistent_8f3a.pdf").unwrap_err();
1010 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
1011 }
1012
1013 #[test]
1014 fn audio_from_file_nonexistent_file() {
1015 let err = Audio::from_file("/tmp/agy_bridge_test_nonexistent_8f3a.mp3").unwrap_err();
1016 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
1017 }
1018
1019 #[test]
1020 fn video_from_file_nonexistent_file() {
1021 let err = Video::from_file("/tmp/agy_bridge_test_nonexistent_8f3a.mp4").unwrap_err();
1022 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
1023 }
1024
1025 #[test]
1028 fn mime_from_extension_case_insensitive() {
1029 assert_eq!(mime::from_extension("PNG"), Some("image/png"));
1030 assert_eq!(mime::from_extension("Jpeg"), Some("image/jpeg"));
1031 assert_eq!(mime::from_extension("MP4"), Some("video/mp4"));
1032 }
1033
1034 #[test]
1035 fn mime_from_extension_unknown_returns_none() {
1036 assert_eq!(mime::from_extension("bmp"), None);
1037 assert_eq!(mime::from_extension("avi"), None);
1038 assert_eq!(mime::from_extension(""), None);
1039 }
1040}