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)]
#[path = "testing/attachments_tests.rs"]
mod tests;