use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileAttachment {
pub key: String,
pub filename: String,
pub created_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub original_filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
impl FileAttachment {
pub fn new(key: &str) -> Self {
let filename = key.split('/').next_back().unwrap_or(key).to_string();
Self {
key: key.to_string(),
filename,
created_at: chrono::Utc::now().to_rfc3339(),
original_filename: None,
size: None,
mime_type: None,
metadata: HashMap::new(),
}
}
pub fn with_metadata(
key: &str,
original_filename: Option<&str>,
size: Option<u64>,
mime_type: Option<&str>,
) -> Self {
let mut attachment = Self::new(key);
attachment.original_filename = original_filename.map(|s| s.to_string());
attachment.size = size;
attachment.mime_type = mime_type.map(|s| s.to_string());
attachment
}
pub fn add_metadata(mut self, key: &str, value: impl Into<serde_json::Value>) -> Self {
self.metadata.insert(key.to_string(), value.into());
self
}
#[inline]
pub fn url(&self, field_name: &str) -> String {
crate::config::Config::generate_file_url(field_name, self)
}
#[inline]
pub fn url_with_generator(&self, field_name: &str, generator: crate::config::FileUrlGenerator) -> String {
generator(field_name, self)
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::to_value(self).unwrap_or(serde_json::json!({}))
}
}
pub trait HasAttachments {
fn has_one_files() -> Vec<&'static str>;
fn has_many_files() -> Vec<&'static str>;
fn all_file_relations() -> Vec<&'static str> {
let mut relations = Self::has_one_files();
relations.extend(Self::has_many_files());
relations
}
fn is_has_one_relation(relation: &str) -> bool {
Self::has_one_files().contains(&relation)
}
fn is_has_many_relation(relation: &str) -> bool {
Self::has_many_files().contains(&relation)
}
fn get_files_data(&self) -> Result<FilesData, AttachmentError>;
fn set_files_data(&mut self, data: FilesData) -> Result<(), AttachmentError>;
fn attach(&mut self, relation: &str, file_key: &str) -> Result<(), AttachmentError> {
self.attach_with_metadata(relation, FileAttachment::new(file_key))
}
fn attach_with_metadata(&mut self, relation: &str, attachment: FileAttachment) -> Result<(), AttachmentError> {
self.validate_relation(relation)?;
let mut files = self.get_files_data()?;
if Self::is_has_one_relation(relation) {
files.set_one(relation, attachment);
} else {
files.add_many(relation, attachment);
}
self.set_files_data(files)
}
fn attach_many(&mut self, relation: &str, file_keys: Vec<&str>) -> Result<(), AttachmentError> {
if !Self::is_has_many_relation(relation) {
return Err(AttachmentError::InvalidRelation(
format!("'{}' is not a hasMany relation, use attach() instead", relation)
));
}
let mut files = self.get_files_data()?;
for key in file_keys {
files.add_many(relation, FileAttachment::new(key));
}
self.set_files_data(files)
}
fn detach(&mut self, relation: &str, file_key: Option<&str>) -> Result<(), AttachmentError> {
self.validate_relation(relation)?;
let mut files = self.get_files_data()?;
if Self::is_has_one_relation(relation) {
files.remove_one(relation);
} else if let Some(key) = file_key {
files.remove_from_many(relation, key);
} else {
files.clear_many(relation);
}
self.set_files_data(files)
}
fn detach_many(&mut self, relation: &str, file_keys: Vec<&str>) -> Result<(), AttachmentError> {
if !Self::is_has_many_relation(relation) {
return Err(AttachmentError::InvalidRelation(
format!("'{}' is not a hasMany relation", relation)
));
}
let mut files = self.get_files_data()?;
for key in file_keys {
files.remove_from_many(relation, key);
}
self.set_files_data(files)
}
fn sync(&mut self, relation: &str, file_keys: Vec<&str>) -> Result<(), AttachmentError> {
self.validate_relation(relation)?;
let mut files = self.get_files_data()?;
if Self::is_has_one_relation(relation) {
if file_keys.is_empty() {
files.remove_one(relation);
} else {
files.set_one(relation, FileAttachment::new(file_keys[0]));
}
} else {
files.clear_many(relation);
for key in file_keys {
files.add_many(relation, FileAttachment::new(key));
}
}
self.set_files_data(files)
}
fn sync_with_metadata(&mut self, relation: &str, attachments: Vec<FileAttachment>) -> Result<(), AttachmentError> {
self.validate_relation(relation)?;
let mut files = self.get_files_data()?;
if Self::is_has_one_relation(relation) {
if attachments.is_empty() {
files.remove_one(relation);
} else {
if let Some(first) = attachments.into_iter().next() {
files.set_one(relation, first);
}
}
} else {
files.clear_many(relation);
for attachment in attachments {
files.add_many(relation, attachment);
}
}
self.set_files_data(files)
}
fn get_file(&self, relation: &str) -> Result<Option<FileAttachment>, AttachmentError> {
let files = self.get_files_data()?;
Ok(files.get_one(relation))
}
fn get_files(&self, relation: &str) -> Result<Vec<FileAttachment>, AttachmentError> {
let files = self.get_files_data()?;
Ok(files.get_many(relation))
}
fn has_files(&self, relation: &str) -> Result<bool, AttachmentError> {
let files = self.get_files_data()?;
Ok(files.has_files(relation))
}
fn count_files(&self, relation: &str) -> Result<usize, AttachmentError> {
let files = self.get_files_data()?;
Ok(files.count_files(relation))
}
fn validate_relation(&self, relation: &str) -> Result<(), AttachmentError> {
if !Self::all_file_relations().contains(&relation) {
return Err(AttachmentError::InvalidRelation(
format!("Unknown file relation: '{}'. Available: {:?}", relation, Self::all_file_relations())
));
}
Ok(())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FilesData {
#[serde(flatten)]
data: HashMap<String, serde_json::Value>,
}
impl FilesData {
pub fn new() -> Self {
Self { data: HashMap::new() }
}
pub fn from_json(value: &serde_json::Value) -> Self {
match value {
serde_json::Value::Object(map) => {
let data: HashMap<String, serde_json::Value> = map
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
Self { data }
}
_ => Self::new(),
}
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::to_value(&self.data).unwrap_or(serde_json::json!({}))
}
pub fn set_one(&mut self, relation: &str, attachment: FileAttachment) {
self.data.insert(relation.to_string(), attachment.to_json());
}
pub fn remove_one(&mut self, relation: &str) {
self.data.insert(relation.to_string(), serde_json::Value::Null);
}
pub fn get_one(&self, relation: &str) -> Option<FileAttachment> {
self.data.get(relation)
.filter(|v| !v.is_null())
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
pub fn add_many(&mut self, relation: &str, attachment: FileAttachment) {
let mut array = self.get_many_raw(relation);
array.push(attachment.to_json());
self.data.insert(relation.to_string(), serde_json::Value::Array(array));
}
pub fn remove_from_many(&mut self, relation: &str, file_key: &str) {
let array: Vec<serde_json::Value> = self.get_many_raw(relation)
.into_iter()
.filter(|item| {
item.get("key").and_then(|k| k.as_str()) != Some(file_key)
})
.collect();
self.data.insert(relation.to_string(), serde_json::Value::Array(array));
}
pub fn clear_many(&mut self, relation: &str) {
self.data.insert(relation.to_string(), serde_json::Value::Array(vec![]));
}
pub fn get_many(&self, relation: &str) -> Vec<FileAttachment> {
self.get_many_raw(relation)
.into_iter()
.filter_map(|v| serde_json::from_value(v).ok())
.collect()
}
fn get_many_raw(&self, relation: &str) -> Vec<serde_json::Value> {
self.data.get(relation)
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default()
}
pub fn has_files(&self, relation: &str) -> bool {
match self.data.get(relation) {
Some(serde_json::Value::Null) => false,
Some(serde_json::Value::Array(arr)) => !arr.is_empty(),
Some(serde_json::Value::Object(_)) => true,
_ => false,
}
}
pub fn count_files(&self, relation: &str) -> usize {
match self.data.get(relation) {
Some(serde_json::Value::Null) => 0,
Some(serde_json::Value::Array(arr)) => arr.len(),
Some(serde_json::Value::Object(_)) => 1,
_ => 0,
}
}
}
#[derive(Debug, Clone)]
pub enum AttachmentError {
InvalidRelation(String),
ParseError(String),
NotSupported,
}
impl std::fmt::Display for AttachmentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AttachmentError::InvalidRelation(msg) => write!(f, "Invalid relation: {}", msg),
AttachmentError::ParseError(msg) => write!(f, "Parse error: {}", msg),
AttachmentError::NotSupported => write!(f, "Model does not support file attachments"),
}
}
}
impl std::error::Error for AttachmentError {}
impl From<AttachmentError> for crate::Error {
fn from(err: AttachmentError) -> Self {
crate::Error::query(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_attachment_creation() {
let attachment = FileAttachment::new("uploads/2024/01/image.jpg");
assert_eq!(attachment.key, "uploads/2024/01/image.jpg");
assert_eq!(attachment.filename, "image.jpg");
}
#[test]
fn test_file_attachment_with_metadata() {
let attachment = FileAttachment::with_metadata(
"uploads/doc.pdf",
Some("My Document.pdf"),
Some(1024),
Some("application/pdf"),
);
assert_eq!(attachment.original_filename, Some("My Document.pdf".to_string()));
assert_eq!(attachment.size, Some(1024));
assert_eq!(attachment.mime_type, Some("application/pdf".to_string()));
}
#[test]
fn test_files_data_has_one() {
let mut files = FilesData::new();
files.set_one("thumbnail", FileAttachment::new("thumb.jpg"));
assert!(files.has_files("thumbnail"));
assert_eq!(files.count_files("thumbnail"), 1);
let thumb = files.get_one("thumbnail").unwrap();
assert_eq!(thumb.key, "thumb.jpg");
files.remove_one("thumbnail");
assert!(!files.has_files("thumbnail"));
}
#[test]
fn test_files_data_has_many() {
let mut files = FilesData::new();
files.add_many("images", FileAttachment::new("img1.jpg"));
files.add_many("images", FileAttachment::new("img2.jpg"));
assert!(files.has_files("images"));
assert_eq!(files.count_files("images"), 2);
let images = files.get_many("images");
assert_eq!(images.len(), 2);
files.remove_from_many("images", "img1.jpg");
assert_eq!(files.count_files("images"), 1);
files.clear_many("images");
assert!(!files.has_files("images"));
}
}