1use serde::{Deserialize, Serialize};
22
23#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(untagged)]
26pub enum MessageContent {
27 Text(String),
29 Parts(Vec<ContentPart>),
31}
32
33impl MessageContent {
34 pub fn as_text(&self) -> Option<&str> {
41 match self {
42 Self::Text(text) => Some(text),
43 Self::Parts(_) => None,
44 }
45 }
46
47 pub fn to_text(&self) -> String {
51 match self {
52 Self::Text(text) => text.clone(),
53 Self::Parts(parts) => parts
54 .iter()
55 .filter_map(ContentPart::as_text)
56 .collect::<Vec<_>>()
57 .join("\n"),
58 }
59 }
60
61 pub fn parts(&self) -> impl Iterator<Item = &ContentPart> {
63 let iter: Box<dyn Iterator<Item = &ContentPart>> = match self {
67 Self::Text(_) => Box::new(std::iter::empty()),
68 Self::Parts(parts) => Box::new(parts.iter()),
69 };
70 iter
71 }
72
73 pub fn has_media(&self) -> bool {
80 self.parts()
81 .any(|part| !matches!(part, ContentPart::Text { .. }))
82 }
83}
84
85impl From<String> for MessageContent {
86 fn from(text: String) -> Self {
87 Self::Text(text)
88 }
89}
90
91impl From<&String> for MessageContent {
92 fn from(text: &String) -> Self {
93 Self::Text(text.clone())
94 }
95}
96
97impl From<&str> for MessageContent {
98 fn from(text: &str) -> Self {
99 Self::Text(text.to_string())
100 }
101}
102
103impl From<Vec<ContentPart>> for MessageContent {
104 fn from(parts: Vec<ContentPart>) -> Self {
105 Self::Parts(parts)
106 }
107}
108
109impl From<ContentPart> for MessageContent {
110 fn from(part: ContentPart) -> Self {
111 Self::Parts(vec![part])
112 }
113}
114
115impl std::fmt::Debug for MessageContent {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 match self {
120 Self::Text(text) => f.debug_tuple("Text").field(text).finish(),
121 Self::Parts(parts) => f.debug_tuple("Parts").field(parts).finish(),
122 }
123 }
124}
125
126#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(tag = "type", rename_all = "snake_case")]
132#[non_exhaustive]
133pub enum ContentPart {
134 Text { text: String },
136 Blob {
142 mime_type: String,
146 #[serde(with = "base64_bytes")]
147 data: Vec<u8>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
151 name: Option<String>,
152 },
153 Uri {
159 mime_type: String,
160 uri: String,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 name: Option<String>,
163 },
164}
165
166impl ContentPart {
167 pub fn text(text: impl Into<String>) -> Self {
168 Self::Text { text: text.into() }
169 }
170
171 pub fn blob(mime_type: impl Into<String>, data: Vec<u8>) -> Self {
172 Self::Blob {
173 mime_type: mime_type.into(),
174 data,
175 name: None,
176 }
177 }
178
179 pub fn uri(mime_type: impl Into<String>, uri: impl Into<String>) -> Self {
180 Self::Uri {
181 mime_type: mime_type.into(),
182 uri: uri.into(),
183 name: None,
184 }
185 }
186
187 pub fn named(mut self, file_name: impl Into<String>) -> Self {
190 match &mut self {
191 Self::Text { .. } => {}
192 Self::Blob { name, .. } | Self::Uri { name, .. } => *name = Some(file_name.into()),
193 }
194 self
195 }
196
197 pub fn as_text(&self) -> Option<&str> {
199 match self {
200 Self::Text { text } => Some(text),
201 _ => None,
202 }
203 }
204
205 pub fn mime_type(&self) -> Option<&str> {
207 match self {
208 Self::Text { .. } => None,
209 Self::Blob { mime_type, .. } | Self::Uri { mime_type, .. } => Some(mime_type),
210 }
211 }
212
213 pub fn name(&self) -> Option<&str> {
215 match self {
216 Self::Text { .. } => None,
217 Self::Blob { name, .. } | Self::Uri { name, .. } => name.as_deref(),
218 }
219 }
220
221 pub(crate) fn describe(&self) -> String {
225 match (self.name(), self.mime_type()) {
226 (Some(name), Some(mime)) => format!("{name} ({mime})"),
227 (Some(name), None) => name.to_string(),
228 (None, Some(mime)) => format!("unnamed {mime} file"),
229 (None, None) => "unnamed file".to_string(),
230 }
231 }
232}
233
234impl std::fmt::Debug for ContentPart {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 match self {
238 Self::Text { text } => f.debug_struct("Text").field("text", text).finish(),
239 Self::Blob {
240 mime_type,
241 data,
242 name,
243 } => f
244 .debug_struct("Blob")
245 .field("mime_type", mime_type)
246 .field("name", name)
247 .field("data", &format_args!("{} bytes", data.len()))
248 .finish(),
249 Self::Uri {
250 mime_type,
251 uri,
252 name,
253 } => f
254 .debug_struct("Uri")
255 .field("mime_type", mime_type)
256 .field("uri", uri)
257 .field("name", name)
258 .finish(),
259 }
260 }
261}
262
263mod base64_bytes {
267 use base64::Engine as _;
268 use serde::{Deserialize, Deserializer, Serializer, de::Error as _};
269
270 pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
271 serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(bytes))
272 }
273
274 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
275 let encoded = String::deserialize(deserializer)?;
276 base64::engine::general_purpose::STANDARD
277 .decode(encoded.as_bytes())
278 .map_err(D::Error::custom)
279 }
280}
281
282pub(crate) fn encode_base64(bytes: &[u8]) -> String {
284 use base64::Engine as _;
285 base64::engine::general_purpose::STANDARD.encode(bytes)
286}
287
288pub(crate) fn data_url(mime_type: &str, bytes: &[u8]) -> String {
290 format!("data:{mime_type};base64,{}", encode_base64(bytes))
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
301 fn text_content_is_a_bare_string_on_the_wire() {
302 let content = MessageContent::from("hei");
303 assert_eq!(
304 serde_json::to_value(&content).expect("serializes"),
305 serde_json::json!("hei")
306 );
307 let back: MessageContent =
308 serde_json::from_value(serde_json::json!("hei")).expect("deserializes");
309 assert_eq!(back, content);
310 }
311
312 #[test]
314 fn a_blob_is_base64_on_the_wire() {
315 let content = MessageContent::from(vec![
316 ContentPart::text("what is this"),
317 ContentPart::blob("image/png", vec![0, 1, 2]).named("shot.png"),
318 ]);
319 let json = serde_json::to_value(&content).expect("serializes");
320 assert_eq!(
321 json,
322 serde_json::json!([
323 { "type": "text", "text": "what is this" },
324 { "type": "blob", "mime_type": "image/png", "data": "AAEC", "name": "shot.png" },
325 ])
326 );
327 let back: MessageContent = serde_json::from_value(json).expect("deserializes");
328 assert_eq!(back, content);
329 }
330
331 #[test]
335 fn as_text_speaks_only_for_a_whole_text_message() {
336 assert_eq!(MessageContent::from("hei").as_text(), Some("hei"));
337 let parts = MessageContent::from(vec![ContentPart::text("hei")]);
338 assert_eq!(parts.as_text(), None);
339 assert_eq!(parts.to_text(), "hei");
340 }
341
342 #[test]
345 fn media_is_what_a_text_channel_cannot_carry() {
346 assert!(!MessageContent::from("hei").has_media());
347 assert!(!MessageContent::from(vec![ContentPart::text("hei")]).has_media());
348 assert!(MessageContent::from(ContentPart::blob("image/png", vec![1])).has_media());
349 assert!(MessageContent::from(ContentPart::uri("image/png", "https://x/y.png")).has_media());
350 }
351
352 #[test]
354 fn debug_does_not_print_the_bytes() {
355 let content = MessageContent::from(ContentPart::blob("application/pdf", vec![7; 4096]));
356 let rendered = format!("{content:?}");
357 assert!(rendered.contains("4096 bytes"), "{rendered}");
358 assert!(!rendered.contains(", 7,"), "{rendered}");
359 }
360}