use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, Serialize)]
pub struct UploadedFile {
pub filename: Option<String>,
pub content_type: Option<String>,
pub bytes: Vec<u8>,
}
impl UploadedFile {
pub fn len(&self) -> usize {
self.bytes.len()
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
}
impl<'de> Deserialize<'de> for UploadedFile {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct Wire {
#[serde(rename = "__veer_uploaded_file__")]
marker: bool,
filename: Option<String>,
content_type: Option<String>,
bytes_b64: String,
}
let w = Wire::deserialize(d)?;
if !w.marker {
return Err(D::Error::custom(
"UploadedFile: missing __veer_uploaded_file__ marker — \
this type can only be deserialized from a multipart/form-data \
field, not from JSON or form-urlencoded input",
));
}
let bytes = {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(w.bytes_b64.as_bytes())
.map_err(D::Error::custom)?
};
Ok(UploadedFile {
filename: w.filename,
content_type: w.content_type,
bytes,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine;
use serde_json::json;
#[test]
fn deserializes_from_marker_shape() {
let bytes = b"hello".to_vec();
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
let v = json!({
"__veer_uploaded_file__": true,
"filename": "greeting.txt",
"content_type": "text/plain",
"bytes_b64": b64,
});
let file: UploadedFile = serde_json::from_value(v).unwrap();
assert_eq!(file.bytes, bytes);
assert_eq!(file.filename.as_deref(), Some("greeting.txt"));
assert_eq!(file.content_type.as_deref(), Some("text/plain"));
}
#[test]
fn rejects_plain_string_value() {
let v = json!("just a string");
let r: Result<UploadedFile, _> = serde_json::from_value(v);
assert!(r.is_err());
}
}