leviath_core/mime/
inbound.rs1use serde::{Deserialize, Serialize};
12
13use super::{Delivery, MimeType};
14
15#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
17pub struct InboundPart {
18 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub region: Option<String>,
23 pub name: String,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub mime_type: Option<MimeType>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub deliver: Option<Delivery>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub caption: Option<String>,
35 #[serde(with = "base64_bytes")]
37 pub data: Vec<u8>,
38}
39
40impl std::fmt::Debug for InboundPart {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("InboundPart")
43 .field("region", &self.region)
44 .field("name", &self.name)
45 .field("mime_type", &self.mime_type)
46 .field("deliver", &self.deliver)
47 .field("caption", &self.caption)
48 .field("data", &format_args!("{} bytes", self.data.len()))
49 .finish()
50 }
51}
52
53impl InboundPart {
54 pub fn from_bytes(name: impl Into<String>, data: Vec<u8>) -> Self {
56 Self {
57 region: None,
58 name: name.into(),
59 mime_type: None,
60 deliver: None,
61 caption: None,
62 data,
63 }
64 }
65
66 pub fn in_region(mut self, region: impl Into<String>) -> Self {
68 self.region = Some(region.into());
69 self
70 }
71
72 pub fn typed(mut self, mime_type: MimeType) -> Self {
74 self.mime_type = Some(mime_type);
75 self
76 }
77
78 pub fn delivered(mut self, deliver: Delivery) -> Self {
80 self.deliver = Some(deliver);
81 self
82 }
83
84 pub fn captioned(mut self, caption: impl Into<String>) -> Self {
86 self.caption = Some(caption.into());
87 self
88 }
89}
90
91mod base64_bytes {
93 use base64::Engine;
94 use serde::{Deserialize, Deserializer, Serializer};
95
96 pub fn serialize<S: Serializer>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error> {
97 s.serialize_str(&base64::engine::general_purpose::STANDARD.encode(bytes))
98 }
99
100 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
101 let text = String::deserialize(d)?;
102 base64::engine::general_purpose::STANDARD
103 .decode(text.as_bytes())
104 .map_err(serde::de::Error::custom)
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn builds_and_round_trips_as_base64() {
114 let part = InboundPart::from_bytes("a.png", vec![1, 2, 3])
115 .in_region("art")
116 .typed(MimeType::parse("image/png").unwrap())
117 .delivered(Delivery::Native)
118 .captioned("the hero");
119 let json = serde_json::to_string(&part).unwrap();
120 assert!(json.contains("\"data\":\"AQID\""), "{json}");
121 let back: InboundPart = serde_json::from_str(&json).unwrap();
122 assert_eq!(back, part);
123 assert!(format!("{part:?}").contains("3 bytes"));
124 assert!(!format!("{part:?}").contains("[1, 2, 3]"));
125 assert!(serde_json::from_str::<InboundPart>("{\"name\":\"x\",\"data\":\"!!\"}").is_err());
126 assert!(serde_json::from_str::<InboundPart>("{\"name\":\"x\",\"data\":5}").is_err());
127 let bare: InboundPart = serde_json::from_str("{\"name\":\"x\",\"data\":\"\"}").unwrap();
128 assert!(bare.data.is_empty() && bare.region.is_none());
129 assert_eq!(InboundPart::default().name, "");
130 }
131}