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_BMP: &str = "image/bmp";
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 pub const TEXT_CSS: &str = "text/css";
167 pub const TEXT_CSV: &str = "text/csv";
169 pub const TEXT_HTML: &str = "text/html";
171 pub const TEXT_JAVASCRIPT: &str = "text/javascript";
173 pub const TEXT_RTF: &str = "text/rtf";
175 pub const TEXT_XML: &str = "text/xml";
177
178 pub const AUDIO_MPEG: &str = "audio/mpeg";
180 pub const AUDIO_WAV: &str = "audio/wav";
182 pub const AUDIO_OGG: &str = "audio/ogg";
184 pub const AUDIO_FLAC: &str = "audio/flac";
186 pub const AUDIO_AAC: &str = "audio/aac";
188 pub const AUDIO_OPUS: &str = "audio/opus";
190 pub const AUDIO_M4A: &str = "audio/m4a";
192
193 pub const VIDEO_MP4: &str = "video/mp4";
195 pub const VIDEO_WEBM: &str = "video/webm";
197 pub const VIDEO_3GPP: &str = "video/3gpp";
199 pub const VIDEO_AVI: &str = "video/avi";
201 pub const VIDEO_MPEG: &str = "video/mpeg";
203 pub const VIDEO_QUICKTIME: &str = "video/quicktime";
205 pub const VIDEO_WMV: &str = "video/wmv";
207 pub const VIDEO_X_FLV: &str = "video/x-flv";
209
210 #[must_use]
218 pub fn from_extension(ext: &str) -> Option<&'static str> {
219 match ext.to_ascii_lowercase().as_str() {
220 "png" => Some(IMAGE_PNG),
222 "jpg" | "jpeg" => Some(IMAGE_JPEG),
223 "bmp" => Some(IMAGE_BMP),
224 "webp" => Some(IMAGE_WEBP),
225 "pdf" => Some(APPLICATION_PDF),
227 "txt" => Some(TEXT_PLAIN),
228 "json" => Some(APPLICATION_JSON),
229 "css" => Some(TEXT_CSS),
230 "csv" => Some(TEXT_CSV),
231 "html" | "htm" => Some(TEXT_HTML),
232 "js" | "mjs" => Some(TEXT_JAVASCRIPT),
233 "rtf" => Some(TEXT_RTF),
234 "xml" => Some(TEXT_XML),
235 "mp3" => Some(AUDIO_MPEG),
237 "wav" => Some(AUDIO_WAV),
238 "ogg" | "oga" => Some(AUDIO_OGG),
239 "flac" => Some(AUDIO_FLAC),
240 "aac" => Some(AUDIO_AAC),
241 "opus" => Some(AUDIO_OPUS),
242 "m4a" => Some(AUDIO_M4A),
243 "mp4" | "m4v" => Some(VIDEO_MP4),
245 "webm" => Some(VIDEO_WEBM),
246 "3gp" | "3gpp" => Some(VIDEO_3GPP),
247 "avi" => Some(VIDEO_AVI),
248 "mpeg" | "mpg" => Some(VIDEO_MPEG),
249 "mov" => Some(VIDEO_QUICKTIME),
250 "wmv" => Some(VIDEO_WMV),
251 "flv" => Some(VIDEO_X_FLV),
252 _ => None,
253 }
254 }
255}
256
257fn from_file_inner(
262 path: &std::path::Path,
263 type_label: &str,
264 mime_prefixes: &[&str],
265) -> std::io::Result<(Vec<u8>, String)> {
266 let ext = path.extension().and_then(|e| e.to_str()).ok_or_else(|| {
267 std::io::Error::new(std::io::ErrorKind::InvalidInput, "missing file extension")
268 })?;
269 let mime_type = mime::from_extension(ext).ok_or_else(|| {
270 std::io::Error::new(
271 std::io::ErrorKind::InvalidInput,
272 format!("unrecognized {type_label} extension: {ext}"),
273 )
274 })?;
275 if !mime_prefixes
276 .iter()
277 .any(|prefix| mime_type.starts_with(prefix))
278 {
279 return Err(std::io::Error::new(
280 std::io::ErrorKind::InvalidInput,
281 format!("MIME type '{mime_type}' is not {type_label} type"),
282 ));
283 }
284 let data = std::fs::read(path)?;
285 Ok((data, mime_type.to_owned()))
286}
287
288impl Image {
293 pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
305 Self {
306 data,
307 mime_type: mime_type.into(),
308 description: None,
309 }
310 }
311
312 #[must_use]
323 pub fn png(data: Vec<u8>) -> Self {
324 Self::new(data, mime::IMAGE_PNG)
325 }
326
327 #[must_use]
337 pub fn jpeg(data: Vec<u8>) -> Self {
338 Self::new(data, mime::IMAGE_JPEG)
339 }
340
341 #[must_use]
343 pub fn webp(data: Vec<u8>) -> Self {
344 Self::new(data, mime::IMAGE_WEBP)
345 }
346
347 #[must_use]
349 pub fn bmp(data: Vec<u8>) -> Self {
350 Self::new(data, mime::IMAGE_BMP)
351 }
352
353 #[must_use]
355 pub fn with_description(mut self, description: impl Into<String>) -> Self {
356 self.description = Some(description.into());
357 self
358 }
359
360 pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
367 let (data, mime_type) = from_file_inner(path.as_ref(), "an image", &["image/"])?;
368 Ok(Self::new(data, mime_type))
369 }
370}
371
372impl Document {
373 pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
384 Self {
385 data,
386 mime_type: mime_type.into(),
387 description: None,
388 }
389 }
390
391 #[must_use]
401 pub fn pdf(data: Vec<u8>) -> Self {
402 Self::new(data, mime::APPLICATION_PDF)
403 }
404
405 #[must_use]
407 pub fn plain_text(data: Vec<u8>) -> Self {
408 Self::new(data, mime::TEXT_PLAIN)
409 }
410
411 #[must_use]
413 pub fn json(data: Vec<u8>) -> Self {
414 Self::new(data, mime::APPLICATION_JSON)
415 }
416
417 #[must_use]
419 pub fn with_description(mut self, description: impl Into<String>) -> Self {
420 self.description = Some(description.into());
421 self
422 }
423
424 pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
431 let (data, mime_type) =
432 from_file_inner(path.as_ref(), "a document", &["application/", "text/"])?;
433 Ok(Self::new(data, mime_type))
434 }
435}
436
437impl Audio {
438 pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
449 Self {
450 data,
451 mime_type: mime_type.into(),
452 description: None,
453 }
454 }
455
456 #[must_use]
466 pub fn mp3(data: Vec<u8>) -> Self {
467 Self::new(data, mime::AUDIO_MPEG)
468 }
469
470 #[must_use]
480 pub fn wav(data: Vec<u8>) -> Self {
481 Self::new(data, mime::AUDIO_WAV)
482 }
483
484 #[must_use]
486 pub fn ogg(data: Vec<u8>) -> Self {
487 Self::new(data, mime::AUDIO_OGG)
488 }
489
490 #[must_use]
492 pub fn flac(data: Vec<u8>) -> Self {
493 Self::new(data, mime::AUDIO_FLAC)
494 }
495
496 #[must_use]
498 pub fn with_description(mut self, description: impl Into<String>) -> Self {
499 self.description = Some(description.into());
500 self
501 }
502
503 pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
510 let (data, mime_type) = from_file_inner(path.as_ref(), "an audio", &["audio/"])?;
511 Ok(Self::new(data, mime_type))
512 }
513}
514
515impl Video {
516 pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
527 Self {
528 data,
529 mime_type: mime_type.into(),
530 description: None,
531 }
532 }
533
534 #[must_use]
544 pub fn mp4(data: Vec<u8>) -> Self {
545 Self::new(data, mime::VIDEO_MP4)
546 }
547
548 #[must_use]
550 pub fn webm(data: Vec<u8>) -> Self {
551 Self::new(data, mime::VIDEO_WEBM)
552 }
553
554 #[must_use]
556 pub fn with_description(mut self, description: impl Into<String>) -> Self {
557 self.description = Some(description.into());
558 self
559 }
560
561 pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
568 let (data, mime_type) = from_file_inner(path.as_ref(), "a video", &["video/"])?;
569 Ok(Self::new(data, mime_type))
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
578 fn image_struct_serde_roundtrip() {
579 let img = Image {
580 data: vec![10, 20, 30],
581 mime_type: "image/bmp".to_string(),
582 description: Some("bitmap".to_string()),
583 };
584 let json = serde_json::to_string(&img).unwrap();
585 let parsed: Image = serde_json::from_str(&json).unwrap();
586 assert_eq!(parsed, img);
587 }
588
589 #[test]
590 fn document_struct_serde_roundtrip() {
591 let doc = Document {
592 data: b"{}".to_vec(),
593 mime_type: "application/json".to_string(),
594 description: None,
595 };
596 let json = serde_json::to_string(&doc).unwrap();
597 let parsed: Document = serde_json::from_str(&json).unwrap();
598 assert_eq!(parsed, doc);
599 }
600
601 #[test]
602 fn audio_struct_serde_roundtrip() {
603 let audio = Audio {
604 data: vec![0xAA, 0xBB],
605 mime_type: "audio/wav".to_string(),
606 description: Some("beep".to_string()),
607 };
608 let json = serde_json::to_string(&audio).unwrap();
609 let parsed: Audio = serde_json::from_str(&json).unwrap();
610 assert_eq!(parsed, audio);
611 }
612
613 #[test]
614 fn video_struct_serde_roundtrip() {
615 let video = Video {
616 data: vec![0xCC, 0xDD, 0xEE],
617 mime_type: "video/webm".to_string(),
618 description: None,
619 };
620 let json = serde_json::to_string(&video).unwrap();
621 let parsed: Video = serde_json::from_str(&json).unwrap();
622 assert_eq!(parsed, video);
623 }
624
625 #[test]
626 fn image_description_defaults_to_none() {
627 let json = r#"{"data":[1,2,3],"mime_type":"image/png"}"#;
628 let img: Image = serde_json::from_str(json).unwrap();
629 assert!(img.description.is_none());
630 }
631
632 #[test]
633 fn image_new_creates_correct_image() {
634 let img = Image::new(vec![10, 20], "image/webp");
635 assert_eq!(img.data, vec![10, 20]);
636 assert_eq!(img.mime_type, "image/webp");
637 assert!(img.description.is_none());
638 }
639
640 #[test]
641 fn image_png_creates_correct_image() {
642 let img = Image::png(vec![1, 2, 3]);
643 assert_eq!(img.data, vec![1, 2, 3]);
644 assert_eq!(img.mime_type, "image/png");
645 assert!(img.description.is_none());
646 }
647
648 #[test]
649 fn image_jpeg_creates_correct_image() {
650 let img = Image::jpeg(vec![0xFF, 0xD8]);
651 assert_eq!(img.data, vec![0xFF, 0xD8]);
652 assert_eq!(img.mime_type, "image/jpeg");
653 assert!(img.description.is_none());
654 }
655
656 #[test]
657 fn document_new_creates_correct_document() {
658 let doc = Document::new(b"data".to_vec(), "text/plain");
659 assert_eq!(doc.data, b"data".to_vec());
660 assert_eq!(doc.mime_type, "text/plain");
661 assert!(doc.description.is_none());
662 }
663
664 #[test]
665 fn document_pdf_creates_correct_document() {
666 let doc = Document::pdf(b"%PDF-1.4".to_vec());
667 assert_eq!(doc.data, b"%PDF-1.4".to_vec());
668 assert_eq!(doc.mime_type, "application/pdf");
669 assert!(doc.description.is_none());
670 }
671
672 #[test]
673 fn audio_new_creates_correct_audio() {
674 let audio = Audio::new(vec![0xAA], "audio/ogg");
675 assert_eq!(audio.data, vec![0xAA]);
676 assert_eq!(audio.mime_type, "audio/ogg");
677 assert!(audio.description.is_none());
678 }
679
680 #[test]
681 fn audio_mp3_creates_correct_audio() {
682 let audio = Audio::mp3(vec![0xFF, 0xFB]);
683 assert_eq!(audio.data, vec![0xFF, 0xFB]);
684 assert_eq!(audio.mime_type, "audio/mpeg");
685 assert!(audio.description.is_none());
686 }
687
688 #[test]
689 fn audio_wav_creates_correct_audio() {
690 let audio = Audio::wav(vec![0x52, 0x49, 0x46, 0x46]);
691 assert_eq!(audio.data, vec![0x52, 0x49, 0x46, 0x46]);
692 assert_eq!(audio.mime_type, "audio/wav");
693 assert!(audio.description.is_none());
694 }
695
696 #[test]
697 fn video_new_creates_correct_video() {
698 let video = Video::new(vec![0x00], "video/webm");
699 assert_eq!(video.data, vec![0x00]);
700 assert_eq!(video.mime_type, "video/webm");
701 assert!(video.description.is_none());
702 }
703
704 #[test]
705 fn video_mp4_creates_correct_video() {
706 let video = Video::mp4(vec![0x00, 0x00, 0x00, 0x1C]);
707 assert_eq!(video.data, vec![0x00, 0x00, 0x00, 0x1C]);
708 assert_eq!(video.mime_type, "video/mp4");
709 assert!(video.description.is_none());
710 }
711
712 #[test]
713 fn image_new_accepts_string_type() {
714 let img = Image::new(vec![1], String::from("image/bmp"));
715 assert_eq!(img.mime_type, "image/bmp");
716 }
717
718 #[test]
721 fn image_with_description_sets_description() {
722 let img = Image::png(vec![1]).with_description("a logo");
723 assert_eq!(img.description.as_deref(), Some("a logo"));
724 assert_eq!(img.mime_type, "image/png");
725 }
726
727 #[test]
728 fn document_with_description_sets_description() {
729 let doc = Document::pdf(vec![1]).with_description("invoice");
730 assert_eq!(doc.description.as_deref(), Some("invoice"));
731 assert_eq!(doc.mime_type, "application/pdf");
732 }
733
734 #[test]
735 fn audio_with_description_sets_description() {
736 let audio = Audio::mp3(vec![1]).with_description("intro jingle");
737 assert_eq!(audio.description.as_deref(), Some("intro jingle"));
738 assert_eq!(audio.mime_type, "audio/mpeg");
739 }
740
741 #[test]
742 fn video_with_description_sets_description() {
743 let video = Video::mp4(vec![1]).with_description("demo clip");
744 assert_eq!(video.description.as_deref(), Some("demo clip"));
745 assert_eq!(video.mime_type, "video/mp4");
746 }
747
748 #[test]
751 fn image_webp_creates_correct_image() {
752 let img = Image::webp(vec![1, 2]);
753 assert_eq!(img.mime_type, "image/webp");
754 assert_eq!(img.data, vec![1, 2]);
755 assert!(img.description.is_none());
756 }
757
758 #[test]
759 fn image_bmp_creates_correct_image() {
760 let img = Image::bmp(vec![0x42, 0x4D]);
761 assert_eq!(img.mime_type, "image/bmp");
762 assert_eq!(img.data, vec![0x42, 0x4D]);
763 assert!(img.description.is_none());
764 }
765
766 #[test]
767 fn convenience_constructors_use_sdk_allowed_mimes() {
768 const SDK_IMAGE: &[&str] = &["image/bmp", "image/jpeg", "image/png", "image/webp"];
772 const SDK_DOCUMENT: &[&str] = &[
773 "application/pdf",
774 "application/json",
775 "text/css",
776 "text/csv",
777 "text/html",
778 "text/javascript",
779 "text/plain",
780 "text/rtf",
781 "text/xml",
782 ];
783 const SDK_AUDIO: &[&str] = &[
784 "audio/wav",
785 "audio/mp3",
786 "audio/aac",
787 "audio/ogg",
788 "audio/flac",
789 "audio/opus",
790 "audio/mpeg",
791 "audio/m4a",
792 "audio/l16",
793 ];
794 const SDK_VIDEO: &[&str] = &[
795 "video/3gpp",
796 "video/avi",
797 "video/mp4",
798 "video/mpeg",
799 "video/mpg",
800 "video/quicktime",
801 "video/webm",
802 "video/wmv",
803 "video/x-flv",
804 ];
805
806 let d = vec![0u8];
807 for m in [
808 Image::png(d.clone()).mime_type,
809 Image::jpeg(d.clone()).mime_type,
810 Image::webp(d.clone()).mime_type,
811 Image::bmp(d.clone()).mime_type,
812 ] {
813 assert!(
814 SDK_IMAGE.contains(&m.as_str()),
815 "image constructor mime {m} not in SDK allowlist"
816 );
817 }
818 for m in [
819 Document::pdf(d.clone()).mime_type,
820 Document::json(d.clone()).mime_type,
821 Document::plain_text(d.clone()).mime_type,
822 ] {
823 assert!(
824 SDK_DOCUMENT.contains(&m.as_str()),
825 "document constructor mime {m} not in SDK allowlist"
826 );
827 }
828 for m in [
829 Audio::mp3(d.clone()).mime_type,
830 Audio::wav(d.clone()).mime_type,
831 Audio::ogg(d.clone()).mime_type,
832 Audio::flac(d.clone()).mime_type,
833 ] {
834 assert!(
835 SDK_AUDIO.contains(&m.as_str()),
836 "audio constructor mime {m} not in SDK allowlist"
837 );
838 }
839 for m in [
840 Video::mp4(d.clone()).mime_type,
841 Video::webm(d.clone()).mime_type,
842 ] {
843 assert!(
844 SDK_VIDEO.contains(&m.as_str()),
845 "video constructor mime {m} not in SDK allowlist"
846 );
847 }
848
849 for ext in [
851 "png", "jpg", "jpeg", "bmp", "webp", "pdf", "txt", "json", "css", "csv", "html", "htm",
852 "js", "mjs", "rtf", "xml", "mp3", "wav", "ogg", "oga", "flac", "aac", "opus", "m4a",
853 "mp4", "m4v", "webm", "3gp", "3gpp", "avi", "mpeg", "mpg", "mov", "wmv", "flv",
854 ] {
855 let mime = mime::from_extension(ext).expect("known extension");
856 let allowed = SDK_IMAGE.contains(&mime)
857 || SDK_DOCUMENT.contains(&mime)
858 || SDK_AUDIO.contains(&mime)
859 || SDK_VIDEO.contains(&mime);
860 assert!(allowed, "extension .{ext} maps to non-SDK mime {mime}");
861 }
862 }
863
864 #[test]
865 fn audio_ogg_creates_correct_audio() {
866 let audio = Audio::ogg(vec![0x4F, 0x67]);
867 assert_eq!(audio.mime_type, "audio/ogg");
868 assert_eq!(audio.data, vec![0x4F, 0x67]);
869 assert!(audio.description.is_none());
870 }
871
872 #[test]
873 fn audio_flac_creates_correct_audio() {
874 let audio = Audio::flac(vec![0x66, 0x4C]);
875 assert_eq!(audio.mime_type, "audio/flac");
876 assert_eq!(audio.data, vec![0x66, 0x4C]);
877 assert!(audio.description.is_none());
878 }
879
880 #[test]
881 fn document_plain_text_creates_correct_document() {
882 let doc = Document::plain_text(b"hello".to_vec());
883 assert_eq!(doc.mime_type, "text/plain");
884 assert_eq!(doc.data, b"hello");
885 assert!(doc.description.is_none());
886 }
887
888 #[test]
889 fn document_json_creates_correct_document() {
890 let doc = Document::json(b"{}".to_vec());
891 assert_eq!(doc.mime_type, "application/json");
892 assert_eq!(doc.data, b"{}");
893 assert!(doc.description.is_none());
894 }
895
896 #[test]
897 fn video_webm_creates_correct_video() {
898 let video = Video::webm(vec![0x1A, 0x45]);
899 assert_eq!(video.mime_type, "video/webm");
900 assert_eq!(video.data, vec![0x1A, 0x45]);
901 assert!(video.description.is_none());
902 }
903
904 #[test]
907 fn image_from_file_success() {
908 let dir = tempfile::tempdir().unwrap();
909 let path = dir.path().join("photo.png");
910 std::fs::write(&path, b"\x89PNG").unwrap();
911 let img = Image::from_file(&path).unwrap();
912 assert_eq!(img.data, b"\x89PNG");
913 assert_eq!(img.mime_type, "image/png");
914 assert!(img.description.is_none());
915 }
916
917 #[test]
918 fn image_from_file_unknown_extension() {
919 let dir = tempfile::tempdir().unwrap();
920 let path = dir.path().join("photo.tiff");
921 std::fs::write(&path, b"II").unwrap();
922 let err = Image::from_file(&path).unwrap_err();
923 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
924 assert!(
925 err.to_string().contains("unrecognized"),
926 "expected 'unrecognized' in: {err}"
927 );
928 }
929
930 #[test]
931 fn image_from_file_wrong_mime_prefix() {
932 let dir = tempfile::tempdir().unwrap();
933 let path = dir.path().join("not_image.mp3");
934 std::fs::write(&path, b"\xFF\xFB").unwrap();
935 let err = Image::from_file(&path).unwrap_err();
936 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
937 assert!(
938 err.to_string().contains("not an image type"),
939 "expected MIME prefix error in: {err}"
940 );
941 }
942
943 #[test]
944 fn image_from_file_missing_extension() {
945 let dir = tempfile::tempdir().unwrap();
946 let path = dir.path().join("noext");
947 std::fs::write(&path, b"data").unwrap();
948 let err = Image::from_file(&path).unwrap_err();
949 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
950 assert!(
951 err.to_string().contains("missing file extension"),
952 "expected 'missing file extension' in: {err}"
953 );
954 }
955
956 #[test]
959 fn document_from_file_success() {
960 let dir = tempfile::tempdir().unwrap();
961 let path = dir.path().join("report.pdf");
962 std::fs::write(&path, b"%PDF-1.4").unwrap();
963 let doc = Document::from_file(&path).unwrap();
964 assert_eq!(doc.data, b"%PDF-1.4");
965 assert_eq!(doc.mime_type, "application/pdf");
966 assert!(doc.description.is_none());
967 }
968
969 #[test]
970 fn document_from_file_text_extension() {
971 let dir = tempfile::tempdir().unwrap();
972 let path = dir.path().join("notes.txt");
973 std::fs::write(&path, b"hello world").unwrap();
974 let doc = Document::from_file(&path).unwrap();
975 assert_eq!(doc.data, b"hello world");
976 assert_eq!(doc.mime_type, "text/plain");
977 }
978
979 #[test]
980 fn document_from_file_json_extension() {
981 let dir = tempfile::tempdir().unwrap();
982 let path = dir.path().join("config.json");
983 std::fs::write(&path, b"{}").unwrap();
984 let doc = Document::from_file(&path).unwrap();
985 assert_eq!(doc.data, b"{}");
986 assert_eq!(doc.mime_type, "application/json");
987 }
988
989 #[test]
990 fn document_from_file_unknown_extension() {
991 let dir = tempfile::tempdir().unwrap();
992 let path = dir.path().join("data.xyz");
993 std::fs::write(&path, b"stuff").unwrap();
994 let err = Document::from_file(&path).unwrap_err();
995 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
996 assert!(
997 err.to_string().contains("unrecognized"),
998 "expected 'unrecognized' in: {err}"
999 );
1000 }
1001
1002 #[test]
1003 fn document_from_file_wrong_mime_prefix() {
1004 let dir = tempfile::tempdir().unwrap();
1005 let path = dir.path().join("not_doc.png");
1006 std::fs::write(&path, b"\x89PNG").unwrap();
1007 let err = Document::from_file(&path).unwrap_err();
1008 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
1009 assert!(
1010 err.to_string().contains("not a document type"),
1011 "expected MIME prefix error in: {err}"
1012 );
1013 }
1014
1015 #[test]
1016 fn document_from_file_missing_extension() {
1017 let dir = tempfile::tempdir().unwrap();
1018 let path = dir.path().join("noext");
1019 std::fs::write(&path, b"data").unwrap();
1020 let err = Document::from_file(&path).unwrap_err();
1021 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
1022 assert!(
1023 err.to_string().contains("missing file extension"),
1024 "expected 'missing file extension' in: {err}"
1025 );
1026 }
1027
1028 #[test]
1031 fn audio_from_file_success() {
1032 let dir = tempfile::tempdir().unwrap();
1033 let path = dir.path().join("clip.mp3");
1034 std::fs::write(&path, b"\xFF\xFB\x90").unwrap();
1035 let audio = Audio::from_file(&path).unwrap();
1036 assert_eq!(audio.data, b"\xFF\xFB\x90");
1037 assert_eq!(audio.mime_type, "audio/mpeg");
1038 assert!(audio.description.is_none());
1039 }
1040
1041 #[test]
1042 fn audio_from_file_wav_extension() {
1043 let dir = tempfile::tempdir().unwrap();
1044 let path = dir.path().join("sample.wav");
1045 std::fs::write(&path, b"RIFF").unwrap();
1046 let audio = Audio::from_file(&path).unwrap();
1047 assert_eq!(audio.mime_type, "audio/wav");
1048 }
1049
1050 #[test]
1051 fn audio_from_file_unknown_extension() {
1052 let dir = tempfile::tempdir().unwrap();
1053 let path = dir.path().join("sound.mid");
1054 std::fs::write(&path, b"data").unwrap();
1055 let err = Audio::from_file(&path).unwrap_err();
1056 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
1057 assert!(
1058 err.to_string().contains("unrecognized"),
1059 "expected 'unrecognized' in: {err}"
1060 );
1061 }
1062
1063 #[test]
1064 fn audio_from_file_wrong_mime_prefix() {
1065 let dir = tempfile::tempdir().unwrap();
1066 let path = dir.path().join("not_audio.png");
1067 std::fs::write(&path, b"\x89PNG").unwrap();
1068 let err = Audio::from_file(&path).unwrap_err();
1069 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
1070 assert!(
1071 err.to_string().contains("not an audio type"),
1072 "expected MIME prefix error in: {err}"
1073 );
1074 }
1075
1076 #[test]
1077 fn audio_from_file_missing_extension() {
1078 let dir = tempfile::tempdir().unwrap();
1079 let path = dir.path().join("noext");
1080 std::fs::write(&path, b"data").unwrap();
1081 let err = Audio::from_file(&path).unwrap_err();
1082 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
1083 assert!(
1084 err.to_string().contains("missing file extension"),
1085 "expected 'missing file extension' in: {err}"
1086 );
1087 }
1088
1089 #[test]
1092 fn video_from_file_success() {
1093 let dir = tempfile::tempdir().unwrap();
1094 let path = dir.path().join("clip.mp4");
1095 std::fs::write(&path, b"\x00\x00\x00\x1Cftyp").unwrap();
1096 let video = Video::from_file(&path).unwrap();
1097 assert_eq!(video.data, b"\x00\x00\x00\x1Cftyp");
1098 assert_eq!(video.mime_type, "video/mp4");
1099 assert!(video.description.is_none());
1100 }
1101
1102 #[test]
1103 fn video_from_file_webm_extension() {
1104 let dir = tempfile::tempdir().unwrap();
1105 let path = dir.path().join("clip.webm");
1106 std::fs::write(&path, b"\x1A\x45\xDF\xA3").unwrap();
1107 let video = Video::from_file(&path).unwrap();
1108 assert_eq!(video.mime_type, "video/webm");
1109 }
1110
1111 #[test]
1112 fn video_from_file_unknown_extension() {
1113 let dir = tempfile::tempdir().unwrap();
1114 let path = dir.path().join("movie.mkv");
1115 std::fs::write(&path, b"RIFF").unwrap();
1116 let err = Video::from_file(&path).unwrap_err();
1117 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
1118 assert!(
1119 err.to_string().contains("unrecognized"),
1120 "expected 'unrecognized' in: {err}"
1121 );
1122 }
1123
1124 #[test]
1125 fn video_from_file_wrong_mime_prefix() {
1126 let dir = tempfile::tempdir().unwrap();
1127 let path = dir.path().join("not_video.png");
1128 std::fs::write(&path, b"\x89PNG").unwrap();
1129 let err = Video::from_file(&path).unwrap_err();
1130 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
1131 assert!(
1132 err.to_string().contains("not a video type"),
1133 "expected MIME prefix error in: {err}"
1134 );
1135 }
1136
1137 #[test]
1138 fn video_from_file_missing_extension() {
1139 let dir = tempfile::tempdir().unwrap();
1140 let path = dir.path().join("noext");
1141 std::fs::write(&path, b"data").unwrap();
1142 let err = Video::from_file(&path).unwrap_err();
1143 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
1144 assert!(
1145 err.to_string().contains("missing file extension"),
1146 "expected 'missing file extension' in: {err}"
1147 );
1148 }
1149
1150 #[test]
1153 fn image_from_file_nonexistent_file() {
1154 let err = Image::from_file("/tmp/agy_bridge_test_nonexistent_8f3a.png").unwrap_err();
1155 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
1156 }
1157
1158 #[test]
1159 fn document_from_file_nonexistent_file() {
1160 let err = Document::from_file("/tmp/agy_bridge_test_nonexistent_8f3a.pdf").unwrap_err();
1161 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
1162 }
1163
1164 #[test]
1165 fn audio_from_file_nonexistent_file() {
1166 let err = Audio::from_file("/tmp/agy_bridge_test_nonexistent_8f3a.mp3").unwrap_err();
1167 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
1168 }
1169
1170 #[test]
1171 fn video_from_file_nonexistent_file() {
1172 let err = Video::from_file("/tmp/agy_bridge_test_nonexistent_8f3a.mp4").unwrap_err();
1173 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
1174 }
1175
1176 #[test]
1179 fn mime_from_extension_case_insensitive() {
1180 assert_eq!(mime::from_extension("PNG"), Some("image/png"));
1181 assert_eq!(mime::from_extension("Jpeg"), Some("image/jpeg"));
1182 assert_eq!(mime::from_extension("MP4"), Some("video/mp4"));
1183 }
1184
1185 #[test]
1186 fn mime_from_extension_unknown_returns_none() {
1187 assert_eq!(mime::from_extension("tiff"), None);
1188 assert_eq!(mime::from_extension("tga"), None);
1189 assert_eq!(mime::from_extension(""), None);
1190 }
1191}