use serde::{Deserialize, Serialize};
use super::{Delivery, MimeType};
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct InboundPart {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mime_type: Option<MimeType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deliver: Option<Delivery>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub caption: Option<String>,
#[serde(with = "base64_bytes")]
pub data: Vec<u8>,
}
impl std::fmt::Debug for InboundPart {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InboundPart")
.field("region", &self.region)
.field("name", &self.name)
.field("mime_type", &self.mime_type)
.field("deliver", &self.deliver)
.field("caption", &self.caption)
.field("data", &format_args!("{} bytes", self.data.len()))
.finish()
}
}
impl InboundPart {
pub fn from_bytes(name: impl Into<String>, data: Vec<u8>) -> Self {
Self {
region: None,
name: name.into(),
mime_type: None,
deliver: None,
caption: None,
data,
}
}
pub fn in_region(mut self, region: impl Into<String>) -> Self {
self.region = Some(region.into());
self
}
pub fn typed(mut self, mime_type: MimeType) -> Self {
self.mime_type = Some(mime_type);
self
}
pub fn delivered(mut self, deliver: Delivery) -> Self {
self.deliver = Some(deliver);
self
}
pub fn captioned(mut self, caption: impl Into<String>) -> Self {
self.caption = Some(caption.into());
self
}
}
mod base64_bytes {
use base64::Engine;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&base64::engine::general_purpose::STANDARD.encode(bytes))
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let text = String::deserialize(d)?;
base64::engine::general_purpose::STANDARD
.decode(text.as_bytes())
.map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_and_round_trips_as_base64() {
let part = InboundPart::from_bytes("a.png", vec![1, 2, 3])
.in_region("art")
.typed(MimeType::parse("image/png").unwrap())
.delivered(Delivery::Native)
.captioned("the hero");
let json = serde_json::to_string(&part).unwrap();
assert!(json.contains("\"data\":\"AQID\""), "{json}");
let back: InboundPart = serde_json::from_str(&json).unwrap();
assert_eq!(back, part);
assert!(format!("{part:?}").contains("3 bytes"));
assert!(!format!("{part:?}").contains("[1, 2, 3]"));
assert!(serde_json::from_str::<InboundPart>("{\"name\":\"x\",\"data\":\"!!\"}").is_err());
assert!(serde_json::from_str::<InboundPart>("{\"name\":\"x\",\"data\":5}").is_err());
let bare: InboundPart = serde_json::from_str("{\"name\":\"x\",\"data\":\"\"}").unwrap();
assert!(bare.data.is_empty() && bare.region.is_none());
assert_eq!(InboundPart::default().name, "");
}
}