1use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use url::Url;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub enum Attachment {
18 Local(PathBuf),
20
21 Remote(Url),
26
27 InMemory {
29 bytes: Vec<u8>,
31 file_name: Option<String>,
33 mime_type: Option<String>,
35 },
36}
37
38impl Attachment {
39 pub fn local(path: impl Into<PathBuf>) -> Self {
50 Self::Local(path.into())
51 }
52
53 pub fn remote(url: &str) -> Self {
68 Self::Remote(Url::parse(url).expect("Invalid URL"))
69 }
70
71 pub fn in_memory(bytes: Vec<u8>) -> Self {
82 Self::InMemory {
83 bytes,
84 file_name: None,
85 mime_type: None,
86 }
87 }
88
89 pub fn in_memory_with_meta(
104 bytes: Vec<u8>,
105 file_name: Option<String>,
106 mime_type: Option<String>,
107 ) -> Self {
108 Self::InMemory {
109 bytes,
110 file_name,
111 mime_type,
112 }
113 }
114
115 pub fn file_name(&self) -> Option<String> {
131 match self {
132 Self::Local(path) => path
133 .file_name()
134 .and_then(|n| n.to_str())
135 .map(|s| s.to_string()),
136 Self::Remote(url) => url
137 .path_segments()
138 .and_then(|mut segments| segments.next_back())
139 .filter(|s| !s.is_empty())
140 .map(|s| s.to_string()),
141 Self::InMemory { file_name, .. } => file_name.clone(),
142 }
143 }
144
145 pub fn mime_type(&self) -> Option<String> {
161 match self {
162 Self::InMemory { mime_type, .. } => mime_type.clone(),
163 Self::Local(path) => Self::infer_mime_type_from_path(path),
164 Self::Remote(_) => None,
165 }
166 }
167
168 fn infer_mime_type_from_path(path: &std::path::Path) -> Option<String> {
170 mime_guess::from_path(path)
171 .first()
172 .map(|mime| mime.to_string())
173 }
174
175 #[cfg(feature = "agent")]
203 pub async fn load_bytes(&self) -> Result<Vec<u8>, std::io::Error> {
204 match self {
205 Self::Local(path) => tokio::fs::read(path).await,
206 Self::InMemory { bytes, .. } => Ok(bytes.clone()),
207 Self::Remote(_url) => Err(std::io::Error::new(
208 std::io::ErrorKind::Unsupported,
209 "Remote attachment loading not yet implemented",
210 )),
211 }
212 }
213}
214
215pub trait ToAttachments {
244 fn to_attachments(&self) -> Vec<(String, Attachment)>;
250}
251
252pub trait AttachmentSchema {
302 fn attachment_keys() -> &'static [&'static str];
304}
305
306impl ToAttachments for Vec<u8> {
309 fn to_attachments(&self) -> Vec<(String, Attachment)> {
310 vec![("data".to_string(), Attachment::in_memory(self.clone()))]
311 }
312}
313
314impl ToAttachments for PathBuf {
315 fn to_attachments(&self) -> Vec<(String, Attachment)> {
316 vec![("file".to_string(), Attachment::local(self.clone()))]
317 }
318}
319
320impl ToAttachments for Attachment {
321 fn to_attachments(&self) -> Vec<(String, Attachment)> {
322 vec![("attachment".to_string(), self.clone())]
323 }
324}
325
326impl<T: ToAttachments> ToAttachments for Option<T> {
327 fn to_attachments(&self) -> Vec<(String, Attachment)> {
328 match self {
329 Some(inner) => inner.to_attachments(),
330 None => Vec::new(),
331 }
332 }
333}
334
335impl<T: ToAttachments> ToAttachments for Vec<T> {
336 fn to_attachments(&self) -> Vec<(String, Attachment)> {
337 self.iter()
338 .enumerate()
339 .flat_map(|(i, item)| {
340 item.to_attachments()
341 .into_iter()
342 .map(move |(key, attachment)| (format!("{}_{}", key, i), attachment))
343 })
344 .collect()
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
355 fn test_local_attachment_creation() {
356 let path = PathBuf::from("/path/to/file.png");
357 let attachment = Attachment::local(path.clone());
358
359 match attachment {
360 Attachment::Local(p) => assert_eq!(p, path),
361 _ => panic!("Expected Local variant"),
362 }
363 }
364
365 #[test]
366 fn test_remote_attachment_creation() {
367 let url = "https://example.com/image.png";
368 let attachment = Attachment::remote(url);
369
370 match attachment {
371 Attachment::Remote(u) => assert_eq!(u.as_str(), url),
372 _ => panic!("Expected Remote variant"),
373 }
374 }
375
376 #[test]
377 fn test_in_memory_attachment_creation() {
378 let data = vec![1, 2, 3, 4];
379 let attachment = Attachment::in_memory(data.clone());
380
381 match attachment {
382 Attachment::InMemory {
383 bytes,
384 file_name,
385 mime_type,
386 } => {
387 assert_eq!(bytes, data);
388 assert_eq!(file_name, None);
389 assert_eq!(mime_type, None);
390 }
391 _ => panic!("Expected InMemory variant"),
392 }
393 }
394
395 #[test]
396 fn test_in_memory_attachment_with_metadata() {
397 let data = vec![1, 2, 3, 4];
398 let name = Some("test.png".to_string());
399 let mime = Some("image/png".to_string());
400
401 let attachment = Attachment::in_memory_with_meta(data.clone(), name.clone(), mime.clone());
402
403 match attachment {
404 Attachment::InMemory {
405 bytes,
406 file_name,
407 mime_type,
408 } => {
409 assert_eq!(bytes, data);
410 assert_eq!(file_name, name);
411 assert_eq!(mime_type, mime);
412 }
413 _ => panic!("Expected InMemory variant"),
414 }
415 }
416
417 #[test]
418 fn test_file_name_extraction_local() {
419 let attachment = Attachment::local(PathBuf::from("/path/to/file.png"));
420 assert_eq!(attachment.file_name(), Some("file.png".to_string()));
421 }
422
423 #[test]
424 fn test_file_name_extraction_local_no_extension() {
425 let attachment = Attachment::local(PathBuf::from("/path/to/file"));
426 assert_eq!(attachment.file_name(), Some("file".to_string()));
427 }
428
429 #[test]
430 fn test_file_name_extraction_remote() {
431 let attachment = Attachment::remote("https://example.com/path/to/image.jpg");
432 assert_eq!(attachment.file_name(), Some("image.jpg".to_string()));
433 }
434
435 #[test]
436 fn test_file_name_extraction_remote_trailing_slash() {
437 let attachment = Attachment::remote("https://example.com/path/to/");
439 assert_eq!(attachment.file_name(), None);
440 }
441
442 #[test]
443 fn test_file_name_extraction_in_memory() {
444 let attachment =
445 Attachment::in_memory_with_meta(vec![1, 2, 3], Some("chart.png".to_string()), None);
446 assert_eq!(attachment.file_name(), Some("chart.png".to_string()));
447 }
448
449 #[test]
450 fn test_file_name_extraction_in_memory_none() {
451 let attachment = Attachment::in_memory(vec![1, 2, 3]);
452 assert_eq!(attachment.file_name(), None);
453 }
454
455 #[test]
456 fn test_mime_type_inference_png() {
457 let attachment = Attachment::local(PathBuf::from("/path/to/file.png"));
458 assert_eq!(attachment.mime_type(), Some("image/png".to_string()));
459 }
460
461 #[test]
462 fn test_mime_type_inference_jpg() {
463 let attachment = Attachment::local(PathBuf::from("/path/to/file.jpg"));
464 assert_eq!(attachment.mime_type(), Some("image/jpeg".to_string()));
465 }
466
467 #[test]
468 fn test_mime_type_inference_jpeg() {
469 let attachment = Attachment::local(PathBuf::from("/path/to/file.jpeg"));
470 assert_eq!(attachment.mime_type(), Some("image/jpeg".to_string()));
471 }
472
473 #[test]
474 fn test_mime_type_inference_pdf() {
475 let attachment = Attachment::local(PathBuf::from("/path/to/document.pdf"));
476 assert_eq!(attachment.mime_type(), Some("application/pdf".to_string()));
477 }
478
479 #[test]
480 fn test_mime_type_inference_json() {
481 let attachment = Attachment::local(PathBuf::from("/path/to/data.json"));
482 assert_eq!(attachment.mime_type(), Some("application/json".to_string()));
483 }
484
485 #[test]
486 fn test_mime_type_inference_unknown_extension() {
487 let attachment = Attachment::local(PathBuf::from("/path/to/file.unknown"));
488 assert_eq!(attachment.mime_type(), None);
489 }
490
491 #[test]
492 fn test_mime_type_inference_no_extension() {
493 let attachment = Attachment::local(PathBuf::from("/path/to/file"));
494 assert_eq!(attachment.mime_type(), None);
495 }
496
497 #[test]
498 fn test_mime_type_in_memory_with_type() {
499 let attachment = Attachment::in_memory_with_meta(
500 vec![1, 2, 3],
501 None,
502 Some("application/octet-stream".to_string()),
503 );
504 assert_eq!(
505 attachment.mime_type(),
506 Some("application/octet-stream".to_string())
507 );
508 }
509
510 #[test]
511 fn test_mime_type_in_memory_without_type() {
512 let attachment = Attachment::in_memory(vec![1, 2, 3]);
513 assert_eq!(attachment.mime_type(), None);
514 }
515
516 #[test]
517 fn test_mime_type_remote() {
518 let attachment = Attachment::remote("https://example.com/file.png");
519 assert_eq!(attachment.mime_type(), None);
520 }
521
522 #[cfg(feature = "agent")]
523 #[tokio::test]
524 async fn test_load_bytes_in_memory() {
525 let data = vec![1, 2, 3, 4, 5];
526 let attachment = Attachment::in_memory(data.clone());
527
528 let loaded = attachment.load_bytes().await.unwrap();
529 assert_eq!(loaded, data);
530 }
531
532 #[cfg(feature = "agent")]
533 #[tokio::test]
534 async fn test_load_bytes_remote_unsupported() {
535 let attachment = Attachment::remote("https://example.com/file.png");
536
537 let result = attachment.load_bytes().await;
538 assert!(result.is_err());
539 assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::Unsupported);
540 }
541
542 #[test]
543 fn test_attachment_clone() {
544 let attachment = Attachment::in_memory_with_meta(
545 vec![1, 2, 3],
546 Some("test.bin".to_string()),
547 Some("application/octet-stream".to_string()),
548 );
549
550 let cloned = attachment.clone();
551 assert_eq!(attachment, cloned);
552 }
553
554 #[test]
555 fn test_attachment_debug() {
556 let attachment = Attachment::local(PathBuf::from("/test/path.txt"));
557 let debug_str = format!("{:?}", attachment);
558 assert!(debug_str.contains("Local"));
559 assert!(debug_str.contains("path.txt"));
560 }
561
562 #[test]
565 fn test_to_attachments_vec_u8() {
566 let data = vec![1, 2, 3, 4, 5];
567 let attachments = data.to_attachments();
568
569 assert_eq!(attachments.len(), 1);
570 assert_eq!(attachments[0].0, "data");
571 match &attachments[0].1 {
572 Attachment::InMemory { bytes, .. } => assert_eq!(bytes, &data),
573 _ => panic!("Expected InMemory attachment"),
574 }
575 }
576
577 #[test]
578 fn test_to_attachments_pathbuf() {
579 let path = PathBuf::from("/test/file.txt");
580 let attachments = path.to_attachments();
581
582 assert_eq!(attachments.len(), 1);
583 assert_eq!(attachments[0].0, "file");
584 match &attachments[0].1 {
585 Attachment::Local(p) => assert_eq!(p, &path),
586 _ => panic!("Expected Local attachment"),
587 }
588 }
589
590 #[test]
591 fn test_to_attachments_attachment() {
592 let attachment = Attachment::remote("https://example.com/file.pdf");
593 let attachments = attachment.to_attachments();
594
595 assert_eq!(attachments.len(), 1);
596 assert_eq!(attachments[0].0, "attachment");
597 }
598
599 #[test]
600 fn test_to_attachments_option_some() {
601 let data = Some(vec![1, 2, 3]);
602 let attachments = data.to_attachments();
603
604 assert_eq!(attachments.len(), 1);
605 assert_eq!(attachments[0].0, "data");
606 }
607
608 #[test]
609 fn test_to_attachments_option_none() {
610 let data: Option<Vec<u8>> = None;
611 let attachments = data.to_attachments();
612
613 assert_eq!(attachments.len(), 0);
614 }
615
616 #[test]
617 fn test_to_attachments_vec() {
618 let items = vec![vec![1, 2, 3], vec![4, 5, 6]];
619 let attachments = items.to_attachments();
620
621 assert_eq!(attachments.len(), 2);
622 assert_eq!(attachments[0].0, "data_0");
623 assert_eq!(attachments[1].0, "data_1");
624 }
625
626 #[test]
627 fn test_to_attachments_custom_implementation() {
628 struct MyOutput {
629 chart: Vec<u8>,
630 thumbnail: Vec<u8>,
631 }
632
633 impl ToAttachments for MyOutput {
634 fn to_attachments(&self) -> Vec<(String, Attachment)> {
635 vec![
636 (
637 "chart".to_string(),
638 Attachment::in_memory(self.chart.clone()),
639 ),
640 (
641 "thumbnail".to_string(),
642 Attachment::in_memory(self.thumbnail.clone()),
643 ),
644 ]
645 }
646 }
647
648 let output = MyOutput {
649 chart: vec![1, 2, 3],
650 thumbnail: vec![4, 5, 6],
651 };
652
653 let attachments = output.to_attachments();
654 assert_eq!(attachments.len(), 2);
655 assert_eq!(attachments[0].0, "chart");
656 assert_eq!(attachments[1].0, "thumbnail");
657 }
658
659 #[test]
662 fn test_attachment_schema_keys() {
663 struct TestOutput;
664
665 impl AttachmentSchema for TestOutput {
666 fn attachment_keys() -> &'static [&'static str] {
667 &["image", "data"]
668 }
669 }
670
671 let keys = TestOutput::attachment_keys();
672 assert_eq!(keys.len(), 2);
673 assert_eq!(keys[0], "image");
674 assert_eq!(keys[1], "data");
675 }
676
677 #[test]
678 fn test_attachment_schema_empty_keys() {
679 struct EmptyOutput;
680
681 impl AttachmentSchema for EmptyOutput {
682 fn attachment_keys() -> &'static [&'static str] {
683 &[]
684 }
685 }
686
687 let keys = EmptyOutput::attachment_keys();
688 assert_eq!(keys.len(), 0);
689 }
690}