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
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn from_str_ref_creates_text_content() {
227 let content: Content = "hello".into();
228 assert_eq!(
229 content,
230 Content::Text {
231 text: "hello".to_string()
232 }
233 );
234 }
235
236 #[test]
237 fn from_string_creates_text_content() {
238 let content: Content = String::from("world").into();
239 assert_eq!(
240 content,
241 Content::Text {
242 text: "world".to_string()
243 }
244 );
245 }
246
247 #[test]
248 fn from_image_creates_image_content() {
249 let img = Image {
250 data: vec![0x89, 0x50, 0x4E, 0x47],
251 mime_type: "image/png".to_string(),
252 description: Some("test image".to_string()),
253 };
254 let content: Content = img.clone().into();
255 assert_eq!(content, Content::Image(img));
256 }
257
258 #[test]
259 fn from_document_creates_document_content() {
260 let doc = Document {
261 data: b"%PDF".to_vec(),
262 mime_type: "application/pdf".to_string(),
263 description: None,
264 };
265 let content: Content = doc.clone().into();
266 assert_eq!(content, Content::Document(doc));
267 }
268
269 #[test]
270 fn from_audio_creates_audio_content() {
271 let audio = Audio {
272 data: vec![0xFF, 0xFB],
273 mime_type: "audio/mp3".to_string(),
274 description: None,
275 };
276 let content: Content = audio.clone().into();
277 assert_eq!(content, Content::Audio(audio));
278 }
279
280 #[test]
281 fn from_video_creates_video_content() {
282 let video = Video {
283 data: vec![0x00, 0x00, 0x00, 0x1C],
284 mime_type: "video/mp4".to_string(),
285 description: Some("test video".to_string()),
286 };
287 let content: Content = video.clone().into();
288 assert_eq!(content, Content::Video(video));
289 }
290
291 #[test]
292 fn from_vec_creates_multi_content() {
293 let parts = vec![
294 ContentPrimitive::Text {
295 text: "describe this:".to_string(),
296 },
297 ContentPrimitive::Image(Image {
298 data: vec![1, 2, 3],
299 mime_type: "image/png".to_string(),
300 description: None,
301 }),
302 ];
303 let content: Content = parts.clone().into();
304 assert_eq!(content, Content::Multi { parts });
305 }
306
307 #[test]
310 fn content_text_serde_roundtrip() {
311 let content = Content::Text {
312 text: "hello".to_string(),
313 };
314 let json = serde_json::to_string(&content).unwrap();
315 let parsed: Content = serde_json::from_str(&json).unwrap();
316 assert_eq!(parsed, content);
317 }
318
319 #[test]
320 fn content_image_serde_roundtrip() {
321 let content = Content::Image(Image {
322 data: vec![0x89, 0x50, 0x4E, 0x47],
323 mime_type: "image/png".to_string(),
324 description: Some("a PNG".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_document_serde_roundtrip() {
333 let content = Content::Document(Document {
334 data: b"%PDF-1.4".to_vec(),
335 mime_type: "application/pdf".to_string(),
336 description: None,
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_audio_serde_roundtrip() {
345 let content = Content::Audio(Audio {
346 data: vec![0xFF, 0xFB, 0x90],
347 mime_type: "audio/mp3".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_video_serde_roundtrip() {
357 let content = Content::Video(Video {
358 data: vec![0x00, 0x00, 0x00, 0x1C, 0x66],
359 mime_type: "video/mp4".to_string(),
360 description: Some("clip".to_string()),
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_multi_serde_roundtrip() {
369 let content = Content::Multi {
370 parts: vec![
371 ContentPrimitive::Text {
372 text: "look at this".to_string(),
373 },
374 ContentPrimitive::Image(Image {
375 data: vec![1, 2, 3],
376 mime_type: "image/jpeg".to_string(),
377 description: None,
378 }),
379 ],
380 };
381 let json = serde_json::to_string(&content).unwrap();
382 let parsed: Content = serde_json::from_str(&json).unwrap();
383 assert_eq!(parsed, content);
384 }
385
386 #[test]
387 fn content_primitive_text_serde_roundtrip() {
388 let prim = ContentPrimitive::Text {
389 text: "hi".to_string(),
390 };
391 let json = serde_json::to_string(&prim).unwrap();
392 let parsed: ContentPrimitive = serde_json::from_str(&json).unwrap();
393 assert_eq!(parsed, prim);
394 }
395
396 #[test]
397 fn content_primitive_image_serde_roundtrip() {
398 let prim = ContentPrimitive::Image(Image {
399 data: vec![9, 8, 7],
400 mime_type: "image/webp".to_string(),
401 description: Some("webp img".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_text_creates_text_variant() {
410 let c = Content::text("hello");
411 assert_eq!(
412 c,
413 Content::Text {
414 text: "hello".to_string()
415 }
416 );
417 }
418
419 #[test]
420 fn content_text_accepts_string() {
421 let c = Content::text(String::from("world"));
422 assert_eq!(
423 c,
424 Content::Text {
425 text: "world".to_string()
426 }
427 );
428 }
429
430 #[test]
431 fn content_is_text_returns_true_for_text() {
432 assert!(Content::text("hello").is_text());
433 }
434
435 #[test]
436 fn content_is_text_returns_false_for_image() {
437 let content = Content::Image(Image::png(vec![1]));
438 assert!(!content.is_text());
439 }
440
441 #[test]
442 fn content_is_text_returns_false_for_document() {
443 let content = Content::Document(Document::pdf(vec![1]));
444 assert!(!content.is_text());
445 }
446
447 #[test]
448 fn content_is_text_returns_false_for_audio() {
449 let content = Content::Audio(Audio::mp3(vec![1]));
450 assert!(!content.is_text());
451 }
452
453 #[test]
454 fn content_is_text_returns_false_for_video() {
455 let content = Content::Video(Video::mp4(vec![1]));
456 assert!(!content.is_text());
457 }
458
459 #[test]
460 fn content_is_text_returns_false_for_multi() {
461 let content = Content::Multi { parts: vec![] };
462 assert!(!content.is_text());
463 }
464
465 #[test]
466 fn content_as_text_returns_some_for_text() {
467 let c = Content::text("hello");
468 assert_eq!(c.as_text(), Some("hello"));
469 }
470
471 #[test]
472 fn content_as_text_returns_none_for_image() {
473 let c = Content::Image(Image::png(vec![1]));
474 assert_eq!(c.as_text(), None);
475 }
476
477 #[test]
478 fn content_as_text_returns_none_for_document() {
479 let c = Content::Document(Document::pdf(vec![1]));
480 assert_eq!(c.as_text(), None);
481 }
482
483 #[test]
484 fn content_as_text_returns_none_for_audio() {
485 let c = Content::Audio(Audio::mp3(vec![1]));
486 assert_eq!(c.as_text(), None);
487 }
488
489 #[test]
490 fn content_as_text_returns_none_for_video() {
491 let c = Content::Video(Video::mp4(vec![1]));
492 assert_eq!(c.as_text(), None);
493 }
494
495 #[test]
496 fn content_as_text_returns_none_for_multi() {
497 let c = Content::Multi { parts: vec![] };
498 assert_eq!(c.as_text(), None);
499 }
500}