1use serde::{Deserialize, Serialize};
4
5use super::media::{Audio, Document, Image, Video};
6
7#[non_exhaustive]
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(tag = "type")]
25pub enum ContentPrimitive {
26 Text {
28 text: String,
30 },
31 Image(Image),
33 Document(Document),
35 Audio(Audio),
37 Video(Video),
39}
40
41#[non_exhaustive]
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(tag = "type")]
61pub enum Content {
62 Text {
64 text: String,
66 },
67 Image(Image),
69 Document(Document),
71 Audio(Audio),
73 Video(Video),
75 Multi {
77 parts: Vec<ContentPrimitive>,
79 },
80}
81
82impl Content {
83 #[must_use]
96 pub fn text(s: impl Into<String>) -> Self {
97 Self::Text { text: s.into() }
98 }
99
100 #[must_use]
110 pub const fn is_text(&self) -> bool {
111 matches!(self, Self::Text { .. })
112 }
113
114 #[must_use]
128 pub const fn as_text(&self) -> Option<&str> {
129 match self {
130 Self::Text { text } => Some(text.as_str()),
131 _ => None,
132 }
133 }
134}
135
136impl Default for Content {
141 fn default() -> Self {
151 Self::Text {
152 text: String::new(),
153 }
154 }
155}
156
157impl std::fmt::Display for Content {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 match self {
165 Self::Text { text } => f.write_str(text),
166 Self::Image(m) => write!(f, "[Image: {}]", m.mime_type),
167 Self::Document(m) => write!(f, "[Document: {}]", m.mime_type),
168 Self::Audio(m) => write!(f, "[Audio: {}]", m.mime_type),
169 Self::Video(m) => write!(f, "[Video: {}]", m.mime_type),
170 Self::Multi { parts } => write!(f, "[Multi: {} parts]", parts.len()),
171 }
172 }
173}
174
175impl From<&str> for Content {
180 fn from(s: &str) -> Self {
181 Self::Text { text: s.to_owned() }
182 }
183}
184
185impl From<String> for Content {
186 fn from(s: String) -> Self {
187 Self::Text { text: s }
188 }
189}
190
191impl From<Image> for Content {
192 fn from(img: Image) -> Self {
193 Self::Image(img)
194 }
195}
196
197impl From<Document> for Content {
198 fn from(doc: Document) -> Self {
199 Self::Document(doc)
200 }
201}
202
203impl From<Audio> for Content {
204 fn from(audio: Audio) -> Self {
205 Self::Audio(audio)
206 }
207}
208
209impl From<Video> for Content {
210 fn from(video: Video) -> Self {
211 Self::Video(video)
212 }
213}
214
215impl From<Vec<ContentPrimitive>> for Content {
216 fn from(parts: Vec<ContentPrimitive>) -> Self {
217 Self::Multi { parts }
218 }
219}
220
221impl From<ContentPrimitive> for Content {
222 fn from(prim: ContentPrimitive) -> Self {
223 match prim {
224 ContentPrimitive::Text { text } => Self::Text { text },
225 ContentPrimitive::Image(img) => Self::Image(img),
226 ContentPrimitive::Document(doc) => Self::Document(doc),
227 ContentPrimitive::Audio(audio) => Self::Audio(audio),
228 ContentPrimitive::Video(video) => Self::Video(video),
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn from_str_ref_creates_text_content() {
239 let content: Content = "hello".into();
240 assert_eq!(
241 content,
242 Content::Text {
243 text: "hello".to_string()
244 }
245 );
246 }
247
248 #[test]
249 fn from_string_creates_text_content() {
250 let content: Content = String::from("world").into();
251 assert_eq!(
252 content,
253 Content::Text {
254 text: "world".to_string()
255 }
256 );
257 }
258
259 #[test]
260 fn from_image_creates_image_content() {
261 let img = Image {
262 data: vec![0x89, 0x50, 0x4E, 0x47],
263 mime_type: "image/png".to_string(),
264 description: Some("test image".to_string()),
265 };
266 let content: Content = img.clone().into();
267 assert_eq!(content, Content::Image(img));
268 }
269
270 #[test]
271 fn from_document_creates_document_content() {
272 let doc = Document {
273 data: b"%PDF".to_vec(),
274 mime_type: "application/pdf".to_string(),
275 description: None,
276 };
277 let content: Content = doc.clone().into();
278 assert_eq!(content, Content::Document(doc));
279 }
280
281 #[test]
282 fn from_audio_creates_audio_content() {
283 let audio = Audio {
284 data: vec![0xFF, 0xFB],
285 mime_type: "audio/mp3".to_string(),
286 description: None,
287 };
288 let content: Content = audio.clone().into();
289 assert_eq!(content, Content::Audio(audio));
290 }
291
292 #[test]
293 fn from_video_creates_video_content() {
294 let video = Video {
295 data: vec![0x00, 0x00, 0x00, 0x1C],
296 mime_type: "video/mp4".to_string(),
297 description: Some("test video".to_string()),
298 };
299 let content: Content = video.clone().into();
300 assert_eq!(content, Content::Video(video));
301 }
302
303 #[test]
304 fn from_vec_creates_multi_content() {
305 let parts = vec![
306 ContentPrimitive::Text {
307 text: "describe this:".to_string(),
308 },
309 ContentPrimitive::Image(Image {
310 data: vec![1, 2, 3],
311 mime_type: "image/png".to_string(),
312 description: None,
313 }),
314 ];
315 let content: Content = parts.clone().into();
316 assert_eq!(content, Content::Multi { parts });
317 }
318
319 #[test]
322 fn content_text_serde_roundtrip() {
323 let content = Content::Text {
324 text: "hello".to_string(),
325 };
326 let json = serde_json::to_string(&content).unwrap();
327 let parsed: Content = serde_json::from_str(&json).unwrap();
328 assert_eq!(parsed, content);
329 }
330
331 #[test]
332 fn content_image_serde_roundtrip() {
333 let content = Content::Image(Image {
334 data: vec![0x89, 0x50, 0x4E, 0x47],
335 mime_type: "image/png".to_string(),
336 description: Some("a PNG".to_string()),
337 });
338 let json = serde_json::to_string(&content).unwrap();
339 let parsed: Content = serde_json::from_str(&json).unwrap();
340 assert_eq!(parsed, content);
341 }
342
343 #[test]
344 fn content_document_serde_roundtrip() {
345 let content = Content::Document(Document {
346 data: b"%PDF-1.4".to_vec(),
347 mime_type: "application/pdf".to_string(),
348 description: None,
349 });
350 let json = serde_json::to_string(&content).unwrap();
351 let parsed: Content = serde_json::from_str(&json).unwrap();
352 assert_eq!(parsed, content);
353 }
354
355 #[test]
356 fn content_audio_serde_roundtrip() {
357 let content = Content::Audio(Audio {
358 data: vec![0xFF, 0xFB, 0x90],
359 mime_type: "audio/mp3".to_string(),
360 description: None,
361 });
362 let json = serde_json::to_string(&content).unwrap();
363 let parsed: Content = serde_json::from_str(&json).unwrap();
364 assert_eq!(parsed, content);
365 }
366
367 #[test]
368 fn content_video_serde_roundtrip() {
369 let content = Content::Video(Video {
370 data: vec![0x00, 0x00, 0x00, 0x1C, 0x66],
371 mime_type: "video/mp4".to_string(),
372 description: Some("clip".to_string()),
373 });
374 let json = serde_json::to_string(&content).unwrap();
375 let parsed: Content = serde_json::from_str(&json).unwrap();
376 assert_eq!(parsed, content);
377 }
378
379 #[test]
380 fn content_multi_serde_roundtrip() {
381 let content = Content::Multi {
382 parts: vec![
383 ContentPrimitive::Text {
384 text: "look at this".to_string(),
385 },
386 ContentPrimitive::Image(Image {
387 data: vec![1, 2, 3],
388 mime_type: "image/jpeg".to_string(),
389 description: None,
390 }),
391 ],
392 };
393 let json = serde_json::to_string(&content).unwrap();
394 let parsed: Content = serde_json::from_str(&json).unwrap();
395 assert_eq!(parsed, content);
396 }
397
398 #[test]
399 fn content_primitive_text_serde_roundtrip() {
400 let prim = ContentPrimitive::Text {
401 text: "hi".to_string(),
402 };
403 let json = serde_json::to_string(&prim).unwrap();
404 let parsed: ContentPrimitive = serde_json::from_str(&json).unwrap();
405 assert_eq!(parsed, prim);
406 }
407
408 #[test]
409 fn content_primitive_image_serde_roundtrip() {
410 let prim = ContentPrimitive::Image(Image {
411 data: vec![9, 8, 7],
412 mime_type: "image/webp".to_string(),
413 description: Some("webp img".to_string()),
414 });
415 let json = serde_json::to_string(&prim).unwrap();
416 let parsed: ContentPrimitive = serde_json::from_str(&json).unwrap();
417 assert_eq!(parsed, prim);
418 }
419
420 #[test]
421 fn content_text_creates_text_variant() {
422 let c = Content::text("hello");
423 assert_eq!(
424 c,
425 Content::Text {
426 text: "hello".to_string()
427 }
428 );
429 }
430
431 #[test]
432 fn content_text_accepts_string() {
433 let c = Content::text(String::from("world"));
434 assert_eq!(
435 c,
436 Content::Text {
437 text: "world".to_string()
438 }
439 );
440 }
441
442 #[test]
443 fn content_is_text_returns_true_for_text() {
444 assert!(Content::text("hello").is_text());
445 }
446
447 #[test]
448 fn content_is_text_returns_false_for_image() {
449 let content = Content::Image(Image::png(vec![1]));
450 assert!(!content.is_text());
451 }
452
453 #[test]
454 fn content_is_text_returns_false_for_document() {
455 let content = Content::Document(Document::pdf(vec![1]));
456 assert!(!content.is_text());
457 }
458
459 #[test]
460 fn content_is_text_returns_false_for_audio() {
461 let content = Content::Audio(Audio::mp3(vec![1]));
462 assert!(!content.is_text());
463 }
464
465 #[test]
466 fn content_is_text_returns_false_for_video() {
467 let content = Content::Video(Video::mp4(vec![1]));
468 assert!(!content.is_text());
469 }
470
471 #[test]
472 fn content_is_text_returns_false_for_multi() {
473 let content = Content::Multi { parts: vec![] };
474 assert!(!content.is_text());
475 }
476
477 #[test]
478 fn content_as_text_returns_some_for_text() {
479 let c = Content::text("hello");
480 assert_eq!(c.as_text(), Some("hello"));
481 }
482
483 #[test]
484 fn content_as_text_returns_none_for_image() {
485 let c = Content::Image(Image::png(vec![1]));
486 assert_eq!(c.as_text(), None);
487 }
488
489 #[test]
490 fn content_as_text_returns_none_for_document() {
491 let c = Content::Document(Document::pdf(vec![1]));
492 assert_eq!(c.as_text(), None);
493 }
494
495 #[test]
496 fn content_as_text_returns_none_for_audio() {
497 let c = Content::Audio(Audio::mp3(vec![1]));
498 assert_eq!(c.as_text(), None);
499 }
500
501 #[test]
502 fn content_as_text_returns_none_for_video() {
503 let c = Content::Video(Video::mp4(vec![1]));
504 assert_eq!(c.as_text(), None);
505 }
506
507 #[test]
508 fn content_as_text_returns_none_for_multi() {
509 let c = Content::Multi { parts: vec![] };
510 assert_eq!(c.as_text(), None);
511 }
512
513 #[test]
516 fn display_text_renders_content() {
517 let c = Content::text("hello world");
518 assert_eq!(format!("{c}"), "hello world");
519 }
520
521 #[test]
522 fn display_image_shows_mime_type() {
523 let c = Content::Image(Image::png(vec![1]));
524 assert_eq!(format!("{c}"), "[Image: image/png]");
525 }
526
527 #[test]
528 fn display_document_shows_mime_type() {
529 let c = Content::Document(Document::pdf(vec![1]));
530 assert_eq!(format!("{c}"), "[Document: application/pdf]");
531 }
532
533 #[test]
534 fn display_audio_shows_mime_type() {
535 let c = Content::Audio(Audio::mp3(vec![1]));
536 assert_eq!(format!("{c}"), "[Audio: audio/mpeg]");
537 }
538
539 #[test]
540 fn display_video_shows_mime_type() {
541 let c = Content::Video(Video::mp4(vec![1]));
542 assert_eq!(format!("{c}"), "[Video: video/mp4]");
543 }
544
545 #[test]
546 fn display_multi_shows_part_count() {
547 let c = Content::Multi {
548 parts: vec![
549 ContentPrimitive::Text {
550 text: "a".to_string(),
551 },
552 ContentPrimitive::Text {
553 text: "b".to_string(),
554 },
555 ContentPrimitive::Text {
556 text: "c".to_string(),
557 },
558 ],
559 };
560 assert_eq!(format!("{c}"), "[Multi: 3 parts]");
561 }
562
563 #[test]
564 fn display_empty_text() {
565 let c = Content::text("");
566 assert_eq!(format!("{c}"), "");
567 }
568
569 #[test]
572 fn from_content_primitive_text() {
573 let prim = ContentPrimitive::Text {
574 text: "hello".to_string(),
575 };
576 let content: Content = prim.into();
577 assert_eq!(
578 content,
579 Content::Text {
580 text: "hello".to_string()
581 }
582 );
583 }
584
585 #[test]
586 fn from_content_primitive_image() {
587 let prim = ContentPrimitive::Image(Image::png(vec![1, 2, 3]));
588 let content: Content = prim.into();
589 assert!(matches!(content, Content::Image(_)));
590 }
591}