1use serde::{de, Deserialize, Serialize};
46use serde_json::Value;
47use std::collections::HashMap;
48
49pub mod datatable;
50
51pub use datatable::TabularDataResource;
52
53pub type JsonObject = serde_json::Map<String, serde_json::Value>;
54
55#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
58#[serde(rename_all = "snake_case")]
59#[serde(tag = "type", content = "data")]
60pub enum MediaType {
61 #[serde(rename = "text/plain")]
63 Plain(String),
64 #[serde(rename = "text/html")]
66 Html(String),
67 #[serde(rename = "text/latex")]
69 Latex(String),
70 #[serde(rename = "application/javascript")]
72 Javascript(String),
73 #[serde(rename = "text/markdown")]
75 Markdown(String),
76
77 #[serde(rename = "image/svg+xml")]
79 Svg(String),
80
81 #[serde(rename = "image/png")]
85 Png(String),
86 #[serde(rename = "image/jpeg")]
88 Jpeg(String),
89 #[serde(rename = "image/gif")]
91 Gif(String),
92
93 #[serde(rename = "application/json")]
95 Json(JsonObject),
96
97 #[serde(rename = "application/geo+json")]
99 GeoJson(JsonObject),
100 #[serde(rename = "application/vnd.dataresource+json")]
103 DataTable(Box<TabularDataResource>),
104 #[serde(rename = "application/vnd.plotly.v1+json")]
106 Plotly(JsonObject),
107 #[serde(rename = "application/vnd.jupyter.widget-view+json")]
109 WidgetView(JsonObject),
110 #[serde(rename = "application/vnd.jupyter.widget-state+json")]
112 WidgetState(JsonObject),
113 #[serde(rename = "application/vnd.vegalite.v2+json")]
115 VegaLiteV2(JsonObject),
116 #[serde(rename = "application/vnd.vegalite.v3+json")]
118 VegaLiteV3(JsonObject),
119 #[serde(rename = "application/vnd.vegalite.v4+json")]
121 VegaLiteV4(JsonObject),
122 #[serde(rename = "application/vnd.vegalite.v5+json")]
124 VegaLiteV5(JsonObject),
125 #[serde(rename = "application/vnd.vegalite.v6+json")]
127 VegaLiteV6(JsonObject),
128 #[serde(rename = "application/vnd.vega.v3+json")]
130 VegaV3(JsonObject),
131 #[serde(rename = "application/vnd.vega.v4+json")]
133 VegaV4(JsonObject),
134 #[serde(rename = "application/vnd.vega.v5+json")]
136 VegaV5(JsonObject),
137
138 #[serde(rename = "application/vdom.v1+json")]
140 Vdom(JsonObject),
141
142 Other((String, Value)),
145}
146
147impl std::hash::Hash for MediaType {
148 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
149 match &self {
150 MediaType::Plain(_) => "text/plain",
151 MediaType::Html(_) => "text/html",
152 MediaType::Latex(_) => "text/latex",
153 MediaType::Javascript(_) => "application/javascript",
154 MediaType::Markdown(_) => "text/markdown",
155 MediaType::Svg(_) => "image/svg+xml",
156 MediaType::Png(_) => "image/png",
157 MediaType::Jpeg(_) => "image/jpeg",
158 MediaType::Gif(_) => "image/gif",
159 MediaType::Json(_) => "application/json",
160 MediaType::GeoJson(_) => "application/geo+json",
161 MediaType::DataTable(_) => "application/vnd.dataresource+json",
162 MediaType::Plotly(_) => "application/vnd.plotly.v1+json",
163 MediaType::WidgetView(_) => "application/vnd.jupyter.widget-view+json",
164 MediaType::WidgetState(_) => "application/vnd.jupyter.widget-state+json",
165 MediaType::VegaLiteV2(_) => "application/vnd.vegalite.v2+json",
166 MediaType::VegaLiteV3(_) => "application/vnd.vegalite.v3+json",
167 MediaType::VegaLiteV4(_) => "application/vnd.vegalite.v4+json",
168 MediaType::VegaLiteV5(_) => "application/vnd.vegalite.v5+json",
169 MediaType::VegaLiteV6(_) => "application/vnd.vegalite.v6+json",
170 MediaType::VegaV3(_) => "application/vnd.vega.v3+json",
171 MediaType::VegaV4(_) => "application/vnd.vega.v4+json",
172 MediaType::VegaV5(_) => "application/vnd.vega.v5+json",
173 MediaType::Vdom(_) => "application/vdom.v1+json",
174 MediaType::Other((key, _)) => key.as_str(),
175 }
176 .hash(state)
177 }
178}
179
180#[derive(Default, Serialize, Deserialize, Debug, Clone)]
185pub struct Media {
186 #[serde(
188 flatten,
189 deserialize_with = "deserialize_media",
190 serialize_with = "serialize_media_for_wire"
191 )]
192 pub content: Vec<MediaType>,
193}
194
195fn deserialize_media<'de, D>(deserializer: D) -> Result<Vec<MediaType>, D::Error>
196where
197 D: serde::Deserializer<'de>,
198{
199 let map: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
202 let mut content = Vec::new();
203
204 for (key, value) in map {
205 if key.starts_with("application/") && key.ends_with("json") {
207 let media_type =
208 match serde_json::from_value(Value::Object(serde_json::Map::from_iter([
209 ("type".to_string(), Value::String(key.clone())),
210 ("data".to_string(), value.clone()),
211 ]))) {
212 Ok(mediatype) => mediatype,
213 Err(_) => MediaType::Other((key, value)),
214 };
215 content.push(media_type);
216 continue;
217 }
218
219 let text: String = match value.clone() {
221 Value::String(s) => s,
222 Value::Array(arr) => arr
223 .into_iter()
224 .filter_map(|v| v.as_str().map(String::from))
225 .collect::<Vec<String>>()
226 .join(""),
227 _ => return Err(de::Error::custom("Invalid value for text-based media type")),
228 };
229
230 if key.starts_with("image/") {
231 let mediatype: MediaType = match key.as_str() {
236 "image/png" => MediaType::Png(text),
237 "image/jpeg" => MediaType::Jpeg(text),
238 "image/gif" => MediaType::Gif(text),
239 _ => MediaType::Other((key.clone(), value)),
240 };
241 content.push(mediatype);
242 continue;
243 }
244
245 let mediatype: MediaType = match key.as_str() {
246 "text/plain" => MediaType::Plain(text),
247 "text/html" => MediaType::Html(text),
248 "text/latex" => MediaType::Latex(text),
249 "application/javascript" => MediaType::Javascript(text),
250 "text/markdown" => MediaType::Markdown(text),
251 "image/svg+xml" => MediaType::Svg(text),
252
253 _ => MediaType::Other((key.clone(), value)),
255 };
256
257 content.push(mediatype);
258 }
259
260 Ok(content)
261}
262
263pub fn serialize_media_for_wire<S>(
264 content: &Vec<MediaType>,
265 serializer: S,
266) -> Result<S::Ok, S::Error>
267where
268 S: serde::Serializer,
269{
270 serialize_media_with_options(content, serializer, false)
271}
272
273pub fn serialize_media_for_notebook<S>(media: &Media, serializer: S) -> Result<S::Ok, S::Error>
274where
275 S: serde::Serializer,
276{
277 serialize_media_with_options(&media.content, serializer, true)
278}
279
280pub fn serialize_media_with_options<S>(
281 content: &Vec<MediaType>,
282 serializer: S,
283 with_multiline: bool,
284) -> Result<S::Ok, S::Error>
285where
286 S: serde::Serializer,
287{
288 let mut map = HashMap::new();
289
290 for media_type in content {
291 let (key, value) = match media_type {
292 MediaType::Plain(text)
293 | MediaType::Html(text)
294 | MediaType::Latex(text)
295 | MediaType::Javascript(text)
296 | MediaType::Markdown(text)
297 | MediaType::Svg(text) => {
298 let key = match media_type {
299 MediaType::Plain(_) => "text/plain",
300 MediaType::Html(_) => "text/html",
301 MediaType::Latex(_) => "text/latex",
302 MediaType::Javascript(_) => "application/javascript",
303 MediaType::Markdown(_) => "text/markdown",
304 MediaType::Svg(_) => "image/svg+xml",
305 _ => unreachable!(),
306 };
307 let value = if with_multiline {
308 let lines: Vec<&str> = text.lines().collect();
309
310 if lines.len() > 1 {
311 let entries = lines
312 .iter()
313 .map(|line| Value::String(format!("{}\n", line)));
314
315 Value::Array(entries.collect())
316 } else {
317 Value::Array(vec![Value::String(text.clone())])
318 }
319 } else {
320 Value::String(text.clone())
321 };
322 (key.to_string(), value)
323 }
324 MediaType::Jpeg(text) | MediaType::Png(text) | MediaType::Gif(text) => {
332 let key = match media_type {
333 MediaType::Jpeg(_) => "image/jpeg",
334 MediaType::Png(_) => "image/png",
335 MediaType::Gif(_) => "image/gif",
336 _ => unreachable!(),
337 };
338 let value = if with_multiline {
339 let lines: Vec<&str> = text.lines().collect();
340
341 if lines.len() > 1 {
342 let entries = lines
343 .iter()
344 .map(|line| Value::String(format!("{}\n", line)));
345
346 Value::Array(entries.collect())
347 } else {
348 Value::String(text.clone())
349 }
350 } else {
351 Value::String(text.clone())
352 };
353
354 (key.to_string(), value)
355 }
356 MediaType::Other((key, value)) => (key.clone(), value.clone()),
358 _ => {
359 let serialized =
360 serde_json::to_value(media_type).map_err(serde::ser::Error::custom)?;
361 if let Value::Object(obj) = serialized {
362 if let (Some(Value::String(key)), Some(data)) =
363 (obj.get("type"), obj.get("data"))
364 {
365 (key.clone(), data.clone())
366 } else {
367 continue;
368 }
369 } else {
370 continue;
371 }
372 }
373 };
374 map.insert(key, value);
375 }
376
377 map.serialize(serializer)
378}
379
380impl Media {
381 pub fn richest(&self, ranker: fn(&MediaType) -> usize) -> Option<&MediaType> {
414 self.content
415 .iter()
416 .filter_map(|mediatype| {
417 let rank = ranker(mediatype);
418 if rank > 0 {
419 Some((rank, mediatype))
420 } else {
421 None
422 }
423 })
424 .max_by_key(|(rank, _)| *rank)
425 .map(|(_, mediatype)| mediatype)
426 }
427
428 pub fn new(content: Vec<MediaType>) -> Self {
429 Self { content }
430 }
431}
432
433impl From<MediaType> for Media {
434 fn from(media_type: MediaType) -> Self {
435 Media {
436 content: vec![media_type],
437 }
438 }
439}
440
441impl From<Vec<MediaType>> for Media {
442 fn from(content: Vec<MediaType>) -> Self {
443 Media { content }
444 }
445}
446
447pub type MimeBundle = Media;
449pub type MimeType = MediaType;
450
451#[cfg(test)]
452mod test {
453 use datatable::TableSchemaField;
454 use serde_json::json;
455
456 use super::*;
457
458 #[test]
459 fn richest_middle() {
460 let raw = r#"{
461 "text/plain": "Hello, world!",
462 "text/html": "<h1>Hello, world!</h1>",
463 "application/json": {
464 "name": "John Doe",
465 "age": 30
466 },
467 "application/vnd.dataresource+json": {
468 "data": [
469 {"name": "Alice", "age": 25},
470 {"name": "Bob", "age": 35}
471 ],
472 "schema": {
473 "fields": [
474 {"name": "name", "type": "string"},
475 {"name": "age", "type": "integer"}
476 ]
477 }
478 },
479 "application/octet-stream": "Binary data"
480 }"#;
481
482 let bundle: Media = serde_json::from_str(raw).unwrap();
483
484 let ranker = |mediatype: &MediaType| match mediatype {
485 MediaType::Plain(_) => 1,
486 MediaType::Html(_) => 2,
487 _ => 0,
488 };
489
490 match bundle.richest(ranker) {
491 Some(MediaType::Html(data)) => assert_eq!(data, "<h1>Hello, world!</h1>"),
492 _ => panic!("Unexpected media type"),
493 }
494 }
495
496 #[test]
497 fn find_table() {
498 let raw = r#"{
499 "text/plain": "Hello, world!",
500 "text/html": "<h1>Hello, world!</h1>",
501 "application/json": {
502 "name": "John Doe",
503 "age": 30
504 },
505 "application/vnd.dataresource+json": {
506 "data": [
507 {"name": "Alice", "age": 25},
508 {"name": "Bob", "age": 35}
509 ],
510 "schema": {
511 "fields": [
512 {"name": "name", "type": "string"},
513 {"name": "age", "type": "integer"}
514 ]
515 }
516 },
517 "application/octet-stream": "Binary data"
518 }"#;
519
520 let bundle: Media = serde_json::from_str(raw).unwrap();
521
522 let ranker = |mediatype: &MediaType| match mediatype {
523 MediaType::Html(_) => 1,
524 MediaType::Json(_) => 2,
525 MediaType::DataTable(_) => 3,
526 _ => 0,
527 };
528
529 let richest = bundle.richest(ranker);
530
531 match richest {
532 Some(MediaType::DataTable(table)) => {
533 assert_eq!(
534 table.data,
535 Some(vec![
536 json!({"name": "Alice", "age": 25}),
537 json!({"name": "Bob", "age": 35})
538 ])
539 );
540 assert_eq!(
541 table.schema.fields,
542 vec![
543 TableSchemaField {
544 name: "name".to_string(),
545 field_type: datatable::FieldType::String,
546 ..Default::default()
547 },
548 TableSchemaField {
549 name: "age".to_string(),
550 field_type: datatable::FieldType::Integer,
551 ..Default::default()
552 }
553 ]
554 );
555 }
556 _ => panic!("Unexpected mime type"),
557 }
558 }
559
560 #[test]
561 fn find_nothing_and_be_happy() {
562 let raw = r#"{
563 "application/fancy": "Too ✨ Fancy ✨ for you!"
564 }"#;
565
566 let bundle: Media = serde_json::from_str(raw).unwrap();
567
568 let ranker = |mediatype: &MediaType| match mediatype {
569 MediaType::Html(_) => 1,
570 MediaType::Json(_) => 2,
571 MediaType::DataTable(_) => 3,
572 _ => 0,
573 };
574
575 let richest = bundle.richest(ranker);
576
577 assert_eq!(richest, None);
578
579 assert!(bundle.content.contains(&MediaType::Other((
580 "application/fancy".to_string(),
581 json!("Too ✨ Fancy ✨ for you!")
582 ))));
583 }
584
585 #[test]
586 fn no_media_type_supported() {
587 let raw = r#"{
588 "text/plain": "Hello, world!",
589 "text/html": "<h1>Hello, world!</h1>",
590 "application/json": {
591 "name": "John Doe",
592 "age": 30
593 },
594 "application/vnd.dataresource+json": {
595 "data": [
596 {"name": "Alice", "age": 25},
597 {"name": "Bob", "age": 35}
598 ],
599 "schema": {
600 "fields": [
601 {"name": "name", "type": "string"},
602 {"name": "age", "type": "integer"}
603 ]
604 }
605 },
606 "application/octet-stream": "Binary data"
607 }"#;
608
609 let bundle: Media = serde_json::from_str(raw).unwrap();
610 let richest = bundle.richest(|_| 0);
611 assert_eq!(richest, None);
612 }
613
614 #[test]
615 fn ensure_array_of_text_processed() {
616 let raw = r#"{
617 "text/plain": ["Hello, world!"],
618 "text/html": "<h1>Hello, world!</h1>"
619 }"#;
620
621 let bundle: Media = serde_json::from_str(raw).unwrap();
622
623 assert_eq!(bundle.content.len(), 2);
624 assert!(bundle
625 .content
626 .contains(&MediaType::Plain("Hello, world!".to_string())));
627 assert!(bundle
628 .content
629 .contains(&MediaType::Html("<h1>Hello, world!</h1>".to_string())));
630
631 let raw = r#"{
632 "text/plain": ["Hello, world!\n", "Welcome to zombo.com"],
633 "text/html": ["<h1>\n", " Hello, world!\n", "</h1>"]
634 }"#;
635
636 let bundle: Media = serde_json::from_str(raw).unwrap();
637
638 assert_eq!(bundle.content.len(), 2);
639 assert!(bundle.content.contains(&MediaType::Plain(
640 "Hello, world!\nWelcome to zombo.com".to_string()
641 )));
642 assert!(bundle
643 .content
644 .contains(&MediaType::Html("<h1>\n Hello, world!\n</h1>".to_string())));
645 }
646}