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::Map<String, 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 std::hash::Hash for MediaType {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
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(),
}
.hash(state)
}
}
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct Media {
#[serde(
flatten,
deserialize_with = "deserialize_media",
serialize_with = "serialize_media"
)]
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 mime_type: MediaType = match key.as_str() {
"text/plain"
| "text/html"
| "text/latex"
| "application/javascript"
| "text/markdown"
| "image/svg+xml" => {
let text: String = match value {
Value::String(s) => s,
Value::Array(arr) => arr
.into_iter()
.filter_map(|v| v.as_str().map(String::from))
.collect::<Vec<String>>()
.join(""),
_ => return Err(de::Error::custom("Invalid value for text-based media type")),
};
match key.as_str() {
"text/plain" => MediaType::Plain(text),
"text/html" => MediaType::Html(text),
"text/latex" => MediaType::Latex(text),
"application/javascript" => MediaType::Javascript(text),
"text/markdown" => MediaType::Markdown(text),
"image/svg+xml" => MediaType::Svg(text),
_ => unreachable!(),
}
}
_ => {
match serde_json::from_value(Value::Object(serde_json::Map::from_iter([
("type".to_string(), Value::String(key.clone())),
("data".to_string(), value.clone()),
]))) {
Ok(mediatype) => mediatype,
Err(_) => MediaType::Other((key, value)),
}
}
};
content.push(mime_type);
}
Ok(content)
}
fn serialize_media<S>(content: &Vec<MediaType>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = HashMap::new();
for media_type in content {
let serialized = serde_json::to_value(media_type);
if let Ok(Value::Object(obj)) = serialized {
if let Some(Value::String(key)) = obj.get("type") {
if let Some(data) = obj.get("data") {
map.insert(key.clone(), data.clone());
}
}
}
}
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],
}
}
}
pub type MimeBundle = Media;
pub type MimeType = MediaType;
#[cfg(test)]
mod test {
use datatable::TableSchemaField;
use serde_json::json;
use super::*;
#[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())));
}
}