use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Image {
pub data: Vec<u8>,
pub mime_type: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Document {
pub data: Vec<u8>,
pub mime_type: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Audio {
pub data: Vec<u8>,
pub mime_type: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Video {
pub data: Vec<u8>,
pub mime_type: String,
#[serde(default)]
pub description: Option<String>,
}
pub trait MediaContent {
const TYPE_NAME: &'static str;
fn data(&self) -> &[u8];
fn mime_type(&self) -> &str;
fn description(&self) -> Option<&str>;
}
impl MediaContent for Image {
const TYPE_NAME: &'static str = "Image";
fn data(&self) -> &[u8] {
&self.data
}
fn mime_type(&self) -> &str {
&self.mime_type
}
fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}
impl MediaContent for Document {
const TYPE_NAME: &'static str = "Document";
fn data(&self) -> &[u8] {
&self.data
}
fn mime_type(&self) -> &str {
&self.mime_type
}
fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}
impl MediaContent for Audio {
const TYPE_NAME: &'static str = "Audio";
fn data(&self) -> &[u8] {
&self.data
}
fn mime_type(&self) -> &str {
&self.mime_type
}
fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}
impl MediaContent for Video {
const TYPE_NAME: &'static str = "Video";
fn data(&self) -> &[u8] {
&self.data
}
fn mime_type(&self) -> &str {
&self.mime_type
}
fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}
pub mod mime {
pub const IMAGE_PNG: &str = "image/png";
pub const IMAGE_JPEG: &str = "image/jpeg";
pub const IMAGE_BMP: &str = "image/bmp";
pub const IMAGE_WEBP: &str = "image/webp";
pub const APPLICATION_PDF: &str = "application/pdf";
pub const TEXT_PLAIN: &str = "text/plain";
pub const APPLICATION_JSON: &str = "application/json";
pub const TEXT_CSS: &str = "text/css";
pub const TEXT_CSV: &str = "text/csv";
pub const TEXT_HTML: &str = "text/html";
pub const TEXT_JAVASCRIPT: &str = "text/javascript";
pub const TEXT_RTF: &str = "text/rtf";
pub const TEXT_XML: &str = "text/xml";
pub const AUDIO_MPEG: &str = "audio/mpeg";
pub const AUDIO_WAV: &str = "audio/wav";
pub const AUDIO_OGG: &str = "audio/ogg";
pub const AUDIO_FLAC: &str = "audio/flac";
pub const AUDIO_AAC: &str = "audio/aac";
pub const AUDIO_OPUS: &str = "audio/opus";
pub const AUDIO_M4A: &str = "audio/m4a";
pub const VIDEO_MP4: &str = "video/mp4";
pub const VIDEO_WEBM: &str = "video/webm";
pub const VIDEO_3GPP: &str = "video/3gpp";
pub const VIDEO_AVI: &str = "video/avi";
pub const VIDEO_MPEG: &str = "video/mpeg";
pub const VIDEO_QUICKTIME: &str = "video/quicktime";
pub const VIDEO_WMV: &str = "video/wmv";
pub const VIDEO_X_FLV: &str = "video/x-flv";
#[must_use]
pub fn from_extension(ext: &str) -> Option<&'static str> {
match ext.to_ascii_lowercase().as_str() {
"png" => Some(IMAGE_PNG),
"jpg" | "jpeg" => Some(IMAGE_JPEG),
"bmp" => Some(IMAGE_BMP),
"webp" => Some(IMAGE_WEBP),
"pdf" => Some(APPLICATION_PDF),
"txt" => Some(TEXT_PLAIN),
"json" => Some(APPLICATION_JSON),
"css" => Some(TEXT_CSS),
"csv" => Some(TEXT_CSV),
"html" | "htm" => Some(TEXT_HTML),
"js" | "mjs" => Some(TEXT_JAVASCRIPT),
"rtf" => Some(TEXT_RTF),
"xml" => Some(TEXT_XML),
"mp3" => Some(AUDIO_MPEG),
"wav" => Some(AUDIO_WAV),
"ogg" | "oga" => Some(AUDIO_OGG),
"flac" => Some(AUDIO_FLAC),
"aac" => Some(AUDIO_AAC),
"opus" => Some(AUDIO_OPUS),
"m4a" => Some(AUDIO_M4A),
"mp4" | "m4v" => Some(VIDEO_MP4),
"webm" => Some(VIDEO_WEBM),
"3gp" | "3gpp" => Some(VIDEO_3GPP),
"avi" => Some(VIDEO_AVI),
"mpeg" | "mpg" => Some(VIDEO_MPEG),
"mov" => Some(VIDEO_QUICKTIME),
"wmv" => Some(VIDEO_WMV),
"flv" => Some(VIDEO_X_FLV),
_ => None,
}
}
}
fn from_file_inner(
path: &std::path::Path,
type_label: &str,
mime_prefixes: &[&str],
) -> std::io::Result<(Vec<u8>, String)> {
let ext = path.extension().and_then(|e| e.to_str()).ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "missing file extension")
})?;
let mime_type = mime::from_extension(ext).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unrecognized {type_label} extension: {ext}"),
)
})?;
if !mime_prefixes
.iter()
.any(|prefix| mime_type.starts_with(prefix))
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("MIME type '{mime_type}' is not {type_label} type"),
));
}
let data = std::fs::read(path)?;
Ok((data, mime_type.to_owned()))
}
impl Image {
pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
Self {
data,
mime_type: mime_type.into(),
description: None,
}
}
#[must_use]
pub fn png(data: Vec<u8>) -> Self {
Self::new(data, mime::IMAGE_PNG)
}
#[must_use]
pub fn jpeg(data: Vec<u8>) -> Self {
Self::new(data, mime::IMAGE_JPEG)
}
#[must_use]
pub fn webp(data: Vec<u8>) -> Self {
Self::new(data, mime::IMAGE_WEBP)
}
#[must_use]
pub fn bmp(data: Vec<u8>) -> Self {
Self::new(data, mime::IMAGE_BMP)
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let (data, mime_type) = from_file_inner(path.as_ref(), "an image", &["image/"])?;
Ok(Self::new(data, mime_type))
}
}
impl Document {
pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
Self {
data,
mime_type: mime_type.into(),
description: None,
}
}
#[must_use]
pub fn pdf(data: Vec<u8>) -> Self {
Self::new(data, mime::APPLICATION_PDF)
}
#[must_use]
pub fn plain_text(data: Vec<u8>) -> Self {
Self::new(data, mime::TEXT_PLAIN)
}
#[must_use]
pub fn json(data: Vec<u8>) -> Self {
Self::new(data, mime::APPLICATION_JSON)
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let (data, mime_type) =
from_file_inner(path.as_ref(), "a document", &["application/", "text/"])?;
Ok(Self::new(data, mime_type))
}
}
impl Audio {
pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
Self {
data,
mime_type: mime_type.into(),
description: None,
}
}
#[must_use]
pub fn mp3(data: Vec<u8>) -> Self {
Self::new(data, mime::AUDIO_MPEG)
}
#[must_use]
pub fn wav(data: Vec<u8>) -> Self {
Self::new(data, mime::AUDIO_WAV)
}
#[must_use]
pub fn ogg(data: Vec<u8>) -> Self {
Self::new(data, mime::AUDIO_OGG)
}
#[must_use]
pub fn flac(data: Vec<u8>) -> Self {
Self::new(data, mime::AUDIO_FLAC)
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let (data, mime_type) = from_file_inner(path.as_ref(), "an audio", &["audio/"])?;
Ok(Self::new(data, mime_type))
}
}
impl Video {
pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
Self {
data,
mime_type: mime_type.into(),
description: None,
}
}
#[must_use]
pub fn mp4(data: Vec<u8>) -> Self {
Self::new(data, mime::VIDEO_MP4)
}
#[must_use]
pub fn webm(data: Vec<u8>) -> Self {
Self::new(data, mime::VIDEO_WEBM)
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let (data, mime_type) = from_file_inner(path.as_ref(), "a video", &["video/"])?;
Ok(Self::new(data, mime_type))
}
}
#[cfg(test)]
mod tests;