use crate::message::Media;
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum StorageError {
#[error("no media with id '{0}'")]
NotFound(String),
#[error(transparent)]
Other(Box<dyn std::error::Error + Send + Sync>),
}
impl StorageError {
pub fn other(err: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
Self::Other(err.into())
}
}
#[async_trait]
pub trait MediaRead: Send + Sync + 'static {
async fn read(&self, id: &str) -> Result<Media, StorageError>;
}
#[async_trait]
pub trait MediaSave: Send + Sync + 'static {
async fn save(&self, media: &Media) -> Result<String, StorageError>;
}
pub trait MediaStorage: MediaRead + MediaSave {}
impl<T: MediaRead + MediaSave> MediaStorage for T {}
#[async_trait]
impl<T: MediaRead + ?Sized> MediaRead for Arc<T> {
async fn read(&self, id: &str) -> Result<Media, StorageError> {
(**self).read(id).await
}
}
#[async_trait]
impl<T: MediaSave + ?Sized> MediaSave for Arc<T> {
async fn save(&self, media: &Media) -> Result<String, StorageError> {
(**self).save(media).await
}
}
#[derive(Debug, Clone, Default)]
pub struct MemoryStorage {
inner: Arc<Mutex<MemoryInner>>,
}
#[derive(Debug, Default)]
struct MemoryInner {
count: usize,
media: HashMap<String, Media>,
}
impl MemoryStorage {
pub fn new() -> Self {
Self::default()
}
pub fn ids(&self) -> Vec<String> {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.media.keys().cloned().collect()
}
}
#[async_trait]
impl MediaRead for MemoryStorage {
async fn read(&self, id: &str) -> Result<Media, StorageError> {
let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner
.media
.get(id)
.cloned()
.ok_or_else(|| StorageError::NotFound(id.to_string()))
}
}
#[async_trait]
impl MediaSave for MemoryStorage {
async fn save(&self, media: &Media) -> Result<String, StorageError> {
let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
inner.count += 1;
let id = match &media.name {
Some(name) => format!("{}-{name}", inner.count),
None => format!("media-{}", inner.count),
};
inner.media.insert(id.clone(), media.clone());
Ok(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn memory_storage_reads_back_what_was_saved_under_distinct_ids() {
let storage = MemoryStorage::new();
let cat = Media::from_bytes("image/png", b"cat").with_name("a.png");
let dog = Media::from_bytes("image/png", b"dog").with_name("a.png");
let cat_id = storage.save(&cat).await.unwrap();
let dog_id = storage.save(&dog).await.unwrap();
let unnamed = storage
.save(&Media::from_bytes("image/png", b"x"))
.await
.unwrap();
assert_ne!(cat_id, dog_id);
assert_eq!(unnamed, "media-3");
assert_eq!(storage.read(&cat_id).await.unwrap(), cat);
assert_eq!(storage.read(&dog_id).await.unwrap(), dog);
assert!(matches!(
storage.read("missing").await,
Err(StorageError::NotFound(id)) if id == "missing"
));
}
}