use serde::{de, Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
pub mod datatable;
pub use datatable::TabularDataResource;
pub type JsonObject = serde_json::Value;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type", content = "data")]
pub enum MediaType {
#[serde(rename = "text/plain")]
Plain(String),
#[serde(rename = "text/html")]
Html(String),
#[serde(rename = "text/latex")]
Latex(String),
#[serde(rename = "application/javascript")]
Javascript(String),
#[serde(rename = "text/markdown")]
Markdown(String),
#[serde(rename = "image/svg+xml")]
Svg(String),
#[serde(rename = "image/png")]
Png(String),
#[serde(rename = "image/jpeg")]
Jpeg(String),
#[serde(rename = "image/gif")]
Gif(String),
#[serde(rename = "application/json")]
Json(JsonObject),
#[serde(rename = "application/geo+json")]
GeoJson(JsonObject),
#[serde(rename = "application/vnd.dataresource+json")]
DataTable(Box<TabularDataResource>),
#[serde(rename = "application/vnd.plotly.v1+json")]
Plotly(JsonObject),
#[serde(rename = "application/vnd.jupyter.widget-view+json")]
WidgetView(JsonObject),
#[serde(rename = "application/vnd.jupyter.widget-state+json")]
WidgetState(JsonObject),
#[serde(rename = "application/vnd.vegalite.v2+json")]
VegaLiteV2(JsonObject),
#[serde(rename = "application/vnd.vegalite.v3+json")]
VegaLiteV3(JsonObject),
#[serde(rename = "application/vnd.vegalite.v4+json")]
VegaLiteV4(JsonObject),
#[serde(rename = "application/vnd.vegalite.v5+json")]
VegaLiteV5(JsonObject),
#[serde(rename = "application/vnd.vegalite.v6+json")]
VegaLiteV6(JsonObject),
#[serde(rename = "application/vnd.vega.v3+json")]
VegaV3(JsonObject),
#[serde(rename = "application/vnd.vega.v4+json")]
VegaV4(JsonObject),
#[serde(rename = "application/vnd.vega.v5+json")]
VegaV5(JsonObject),
#[serde(rename = "application/vdom.v1+json")]
Vdom(JsonObject),
Other((String, Value)),
}
impl MediaType {
pub fn mime_type(&self) -> &str {
match &self {
MediaType::Plain(_) => "text/plain",
MediaType::Html(_) => "text/html",
MediaType::Latex(_) => "text/latex",
MediaType::Javascript(_) => "application/javascript",
MediaType::Markdown(_) => "text/markdown",
MediaType::Svg(_) => "image/svg+xml",
MediaType::Png(_) => "image/png",
MediaType::Jpeg(_) => "image/jpeg",
MediaType::Gif(_) => "image/gif",
MediaType::Json(_) => "application/json",
MediaType::GeoJson(_) => "application/geo+json",
MediaType::DataTable(_) => "application/vnd.dataresource+json",
MediaType::Plotly(_) => "application/vnd.plotly.v1+json",
MediaType::WidgetView(_) => "application/vnd.jupyter.widget-view+json",
MediaType::WidgetState(_) => "application/vnd.jupyter.widget-state+json",
MediaType::VegaLiteV2(_) => "application/vnd.vegalite.v2+json",
MediaType::VegaLiteV3(_) => "application/vnd.vegalite.v3+json",
MediaType::VegaLiteV4(_) => "application/vnd.vegalite.v4+json",
MediaType::VegaLiteV5(_) => "application/vnd.vegalite.v5+json",
MediaType::VegaLiteV6(_) => "application/vnd.vegalite.v6+json",
MediaType::VegaV3(_) => "application/vnd.vega.v3+json",
MediaType::VegaV4(_) => "application/vnd.vega.v4+json",
MediaType::VegaV5(_) => "application/vnd.vega.v5+json",
MediaType::Vdom(_) => "application/vdom.v1+json",
MediaType::Other((key, _)) => key.as_str(),
}
}
}
impl std::hash::Hash for MediaType {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.mime_type().hash(state)
}
}
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct Media {
#[serde(
flatten,
deserialize_with = "deserialize_media",
serialize_with = "serialize_media_for_wire"
)]
pub content: Vec<MediaType>,
}
fn deserialize_media<'de, D>(deserializer: D) -> Result<Vec<MediaType>, D::Error>
where
D: serde::Deserializer<'de>,
{
let map: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
let mut content = Vec::new();
for (key, value) in map {
let mediatype = match key.as_str() {
"text/plain" => MediaType::Plain(value_to_text::<D>(value)?),
"text/html" => MediaType::Html(value_to_text::<D>(value)?),
"text/latex" => MediaType::Latex(value_to_text::<D>(value)?),
"application/javascript" => MediaType::Javascript(value_to_text::<D>(value)?),
"text/markdown" => MediaType::Markdown(value_to_text::<D>(value)?),
"image/svg+xml" => MediaType::Svg(value_to_text::<D>(value)?),
"image/png" => MediaType::Png(value_to_text::<D>(value)?),
"image/jpeg" => MediaType::Jpeg(value_to_text::<D>(value)?),
"image/gif" => MediaType::Gif(value_to_text::<D>(value)?),
"application/json" => MediaType::Json(value),
"application/geo+json" => MediaType::GeoJson(value),
"application/vnd.dataresource+json" => {
let table: TabularDataResource =
serde_json::from_value(value).map_err(de::Error::custom)?;
MediaType::DataTable(Box::new(table))
}
"application/vnd.plotly.v1+json" => MediaType::Plotly(value),
"application/vnd.jupyter.widget-view+json" => MediaType::WidgetView(value),
"application/vnd.jupyter.widget-state+json" => MediaType::WidgetState(value),
"application/vnd.vegalite.v2+json" => MediaType::VegaLiteV2(value),
"application/vnd.vegalite.v3+json" => MediaType::VegaLiteV3(value),
"application/vnd.vegalite.v4+json" => MediaType::VegaLiteV4(value),
"application/vnd.vegalite.v5+json" => MediaType::VegaLiteV5(value),
"application/vnd.vegalite.v6+json" => MediaType::VegaLiteV6(value),
"application/vnd.vega.v3+json" => MediaType::VegaV3(value),
"application/vnd.vega.v4+json" => MediaType::VegaV4(value),
"application/vnd.vega.v5+json" => MediaType::VegaV5(value),
"application/vdom.v1+json" => MediaType::Vdom(value),
_ => MediaType::Other((key, value)),
};
content.push(mediatype);
}
Ok(content)
}
fn value_to_text<'de, D>(value: Value) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
match value {
Value::String(s) => Ok(s),
Value::Array(arr) => {
let text = arr
.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<&str>>()
.join("");
Ok(text)
}
_ => Err(de::Error::custom("Invalid value for text-based media type")),
}
}
pub fn serialize_media_for_wire<S>(
content: &Vec<MediaType>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serialize_media_with_options(content, serializer, false)
}
pub fn serialize_media_for_notebook<S>(media: &Media, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serialize_media_with_options(&media.content, serializer, true)
}
pub fn serialize_media_with_options<S>(
content: &Vec<MediaType>,
serializer: S,
with_multiline: bool,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = HashMap::new();
for media_type in content {
let key = media_type.mime_type().to_string();
let value = match media_type {
MediaType::Plain(text)
| MediaType::Html(text)
| MediaType::Latex(text)
| MediaType::Javascript(text)
| MediaType::Markdown(text)
| MediaType::Svg(text) => {
if with_multiline {
let lines: Vec<&str> = text.lines().collect();
if lines.len() > 1 {
let entries = lines
.iter()
.map(|line| Value::String(format!("{}\n", line)));
Value::Array(entries.collect())
} else {
Value::Array(vec![Value::String(text.clone())])
}
} else {
Value::String(text.clone())
}
}
MediaType::Jpeg(text) | MediaType::Png(text) | MediaType::Gif(text) => {
if with_multiline {
let lines: Vec<&str> = text.lines().collect();
if lines.len() > 1 {
let entries = lines
.iter()
.map(|line| Value::String(format!("{}\n", line)));
Value::Array(entries.collect())
} else {
Value::String(text.clone())
}
} else {
Value::String(text.clone())
}
}
MediaType::Other((_, value)) => value.clone(),
_ => {
let serialized =
serde_json::to_value(media_type).map_err(serde::ser::Error::custom)?;
if let Value::Object(mut obj) = serialized {
if let Some(data) = obj.remove("data") {
data
} else {
continue;
}
} else {
continue;
}
}
};
map.insert(key, value);
}
map.serialize(serializer)
}
impl Media {
pub fn richest(&self, ranker: fn(&MediaType) -> usize) -> Option<&MediaType> {
self.content
.iter()
.filter_map(|mediatype| {
let rank = ranker(mediatype);
if rank > 0 {
Some((rank, mediatype))
} else {
None
}
})
.max_by_key(|(rank, _)| *rank)
.map(|(_, mediatype)| mediatype)
}
pub fn new(content: Vec<MediaType>) -> Self {
Self { content }
}
}
impl From<MediaType> for Media {
fn from(media_type: MediaType) -> Self {
Media {
content: vec![media_type],
}
}
}
impl From<Vec<MediaType>> for Media {
fn from(content: Vec<MediaType>) -> Self {
Media { content }
}
}
pub type MimeBundle = Media;
pub type MimeType = MediaType;
#[cfg(test)]
mod test {
use datatable::TableSchemaField;
use serde_json::json;
use super::*;
#[test]
fn svg_deserialized_correctly() {
let raw = r#"{
"image/svg+xml": "<svg xmlns=\"http://www.w3.org/2000/svg\"><circle cx=\"50\" cy=\"50\" r=\"40\"/></svg>",
"text/plain": "<IPython.core.display.SVG object>"
}"#;
let bundle: Media = serde_json::from_str(raw).unwrap();
assert_eq!(bundle.content.len(), 2);
assert!(bundle
.content
.contains(&MediaType::Svg("<svg xmlns=\"http://www.w3.org/2000/svg\"><circle cx=\"50\" cy=\"50\" r=\"40\"/></svg>".to_string())));
assert!(bundle.content.contains(&MediaType::Plain(
"<IPython.core.display.SVG object>".to_string()
)));
}
#[test]
fn svg_array_deserialized_correctly() {
let raw = r#"{
"image/svg+xml": ["<svg>\n", " <circle/>\n", "</svg>"],
"text/plain": "svg"
}"#;
let bundle: Media = serde_json::from_str(raw).unwrap();
assert_eq!(bundle.content.len(), 2);
assert!(bundle
.content
.contains(&MediaType::Svg("<svg>\n <circle/>\n</svg>".to_string())));
}
#[test]
fn richest_middle() {
let raw = r#"{
"text/plain": "Hello, world!",
"text/html": "<h1>Hello, world!</h1>",
"application/json": {
"name": "John Doe",
"age": 30
},
"application/vnd.dataresource+json": {
"data": [
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 35}
],
"schema": {
"fields": [
{"name": "name", "type": "string"},
{"name": "age", "type": "integer"}
]
}
},
"application/octet-stream": "Binary data"
}"#;
let bundle: Media = serde_json::from_str(raw).unwrap();
let ranker = |mediatype: &MediaType| match mediatype {
MediaType::Plain(_) => 1,
MediaType::Html(_) => 2,
_ => 0,
};
match bundle.richest(ranker) {
Some(MediaType::Html(data)) => assert_eq!(data, "<h1>Hello, world!</h1>"),
_ => panic!("Unexpected media type"),
}
}
#[test]
fn find_table() {
let raw = r#"{
"text/plain": "Hello, world!",
"text/html": "<h1>Hello, world!</h1>",
"application/json": {
"name": "John Doe",
"age": 30
},
"application/vnd.dataresource+json": {
"data": [
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 35}
],
"schema": {
"fields": [
{"name": "name", "type": "string"},
{"name": "age", "type": "integer"}
]
}
},
"application/octet-stream": "Binary data"
}"#;
let bundle: Media = serde_json::from_str(raw).unwrap();
let ranker = |mediatype: &MediaType| match mediatype {
MediaType::Html(_) => 1,
MediaType::Json(_) => 2,
MediaType::DataTable(_) => 3,
_ => 0,
};
let richest = bundle.richest(ranker);
match richest {
Some(MediaType::DataTable(table)) => {
assert_eq!(
table.data,
Some(vec![
json!({"name": "Alice", "age": 25}),
json!({"name": "Bob", "age": 35})
])
);
assert_eq!(
table.schema.fields,
vec![
TableSchemaField {
name: "name".to_string(),
field_type: datatable::FieldType::String,
..Default::default()
},
TableSchemaField {
name: "age".to_string(),
field_type: datatable::FieldType::Integer,
..Default::default()
}
]
);
}
_ => panic!("Unexpected mime type"),
}
}
#[test]
fn find_nothing_and_be_happy() {
let raw = r#"{
"application/fancy": "Too ✨ Fancy ✨ for you!"
}"#;
let bundle: Media = serde_json::from_str(raw).unwrap();
let ranker = |mediatype: &MediaType| match mediatype {
MediaType::Html(_) => 1,
MediaType::Json(_) => 2,
MediaType::DataTable(_) => 3,
_ => 0,
};
let richest = bundle.richest(ranker);
assert_eq!(richest, None);
assert!(bundle.content.contains(&MediaType::Other((
"application/fancy".to_string(),
json!("Too ✨ Fancy ✨ for you!")
))));
}
#[test]
fn no_media_type_supported() {
let raw = r#"{
"text/plain": "Hello, world!",
"text/html": "<h1>Hello, world!</h1>",
"application/json": {
"name": "John Doe",
"age": 30
},
"application/vnd.dataresource+json": {
"data": [
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 35}
],
"schema": {
"fields": [
{"name": "name", "type": "string"},
{"name": "age", "type": "integer"}
]
}
},
"application/octet-stream": "Binary data"
}"#;
let bundle: Media = serde_json::from_str(raw).unwrap();
let richest = bundle.richest(|_| 0);
assert_eq!(richest, None);
}
#[test]
fn ensure_array_of_text_processed() {
let raw = r#"{
"text/plain": ["Hello, world!"],
"text/html": "<h1>Hello, world!</h1>"
}"#;
let bundle: Media = serde_json::from_str(raw).unwrap();
assert_eq!(bundle.content.len(), 2);
assert!(bundle
.content
.contains(&MediaType::Plain("Hello, world!".to_string())));
assert!(bundle
.content
.contains(&MediaType::Html("<h1>Hello, world!</h1>".to_string())));
let raw = r#"{
"text/plain": ["Hello, world!\n", "Welcome to zombo.com"],
"text/html": ["<h1>\n", " Hello, world!\n", "</h1>"]
}"#;
let bundle: Media = serde_json::from_str(raw).unwrap();
assert_eq!(bundle.content.len(), 2);
assert!(bundle.content.contains(&MediaType::Plain(
"Hello, world!\nWelcome to zombo.com".to_string()
)));
assert!(bundle
.content
.contains(&MediaType::Html("<h1>\n Hello, world!\n</h1>".to_string())));
}
#[test]
fn deserialize_unknown_json_media_type() {
let raw = r#"{
"application/x-custom+json": {"custom_key": "custom_value"}
}"#;
let bundle: Media = serde_json::from_str(raw).unwrap();
println!("{:?}", bundle);
assert_eq!(bundle.content.len(), 1);
assert!(bundle.content.contains(&MediaType::Other((
"application/x-custom+json".to_string(),
json!({"custom_key": "custom_value"})
))));
}
#[test]
fn deserialize_json_media_type() {
let raw_json: &str = r#"{
"application/json": {"id": 1, "key": "value"}
}"#;
let bundle: Media = serde_json::from_str(raw_json).unwrap();
assert!(bundle
.content
.contains(&MediaType::Json(json!({"id": 1, "key": "value"}))));
let raw_array: &str = r#"{
"application/json": [
{"id": 1, "key": "value"},
{"id": 2, "key": "another value"}
]
}"#;
let bundle: Media = serde_json::from_str(raw_array).unwrap();
assert!(bundle.content.contains(&MediaType::Json(json!([
{"id": 1, "key": "value"},
{"id": 2, "key": "another value"}
]))));
let raw_string: &str = r#"{
"application/json": "Just a plain string"
}"#;
let bundle: Media = serde_json::from_str(raw_string).unwrap();
assert!(bundle
.content
.contains(&MediaType::Json(json!("Just a plain string"))));
let raw_number: &str = r#"{
"application/json": 42
}"#;
let bundle: Media = serde_json::from_str(raw_number).unwrap();
assert!(bundle.content.contains(&MediaType::Json(json!(42))));
}
}