use async_trait::async_trait;
use serde::{de::DeserializeOwned, Serialize};
use sqlx::PgPool;
use std::sync::Arc;
use uuid::Uuid;
use crate::cache::RedisCache;
use crate::error::Result;
#[derive(Debug, Clone)]
pub struct Pagination {
pub page: u64,
pub page_size: u64,
}
impl Default for Pagination {
fn default() -> Self {
Self {
page: 0,
page_size: 20,
}
}
}
impl Pagination {
pub fn new(page: u64, page_size: u64) -> Self {
Self { page, page_size }
}
pub fn limit(&self) -> i64 {
self.page_size as i64
}
pub fn offset(&self) -> i64 {
(self.page * self.page_size) as i64
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Page<T> {
pub items: Vec<T>,
pub total: u64,
pub page: u64,
pub page_size: u64,
pub total_pages: u64,
}
impl<T> Page<T> {
pub fn new(items: Vec<T>, total: u64, pagination: &Pagination) -> Self {
let total_pages = if pagination.page_size > 0 {
total.div_ceil(pagination.page_size)
} else {
0
};
Self {
items,
total,
page: pagination.page,
page_size: pagination.page_size,
total_pages,
}
}
pub fn has_next(&self) -> bool {
self.page + 1 < self.total_pages
}
pub fn has_prev(&self) -> bool {
self.page > 0
}
}
#[async_trait]
pub trait Repository<T>: Send + Sync
where
T: Send + Sync + Clone + Serialize + DeserializeOwned,
{
fn entity_name(&self) -> &str;
async fn find_by_id(&self, id: Uuid) -> Result<Option<T>>;
async fn find_all(&self, pagination: Pagination) -> Result<Page<T>>;
async fn create(&self, entity: &T) -> Result<T>;
async fn update(&self, id: Uuid, entity: &T) -> Result<T>;
async fn delete(&self, id: Uuid) -> Result<bool>;
async fn count(&self) -> Result<u64>;
async fn exists(&self, id: Uuid) -> Result<bool> {
Ok(self.find_by_id(id).await?.is_some())
}
}
#[allow(dead_code)]
pub struct CachedGenericRepository<T>
where
T: Send + Sync + Clone + Serialize + DeserializeOwned,
{
pool: Arc<PgPool>,
cache: Option<Arc<RedisCache>>,
entity_name: String,
_phantom: std::marker::PhantomData<T>,
}
impl<T> CachedGenericRepository<T>
where
T: Send + Sync + Clone + Serialize + DeserializeOwned,
{
pub fn new(pool: Arc<PgPool>, cache: Option<Arc<RedisCache>>, entity_name: String) -> Self {
Self {
pool,
cache,
entity_name,
_phantom: std::marker::PhantomData,
}
}
#[allow(dead_code)]
fn cache_key(&self, id: Uuid) -> String {
format!("{}:{}", self.entity_name, id)
}
#[allow(dead_code)]
async fn get_from_cache(&self, id: Uuid) -> Result<Option<T>> {
if let Some(cache) = &self.cache {
cache.get(&self.cache_key(id)).await
} else {
Ok(None)
}
}
#[allow(dead_code)]
async fn set_in_cache(&self, id: Uuid, entity: &T, ttl_secs: u64) -> Result<()> {
if let Some(cache) = &self.cache {
cache.set(&self.cache_key(id), entity, ttl_secs).await?;
}
Ok(())
}
#[allow(dead_code)]
async fn invalidate_cache(&self, id: Uuid) -> Result<()> {
if let Some(cache) = &self.cache {
cache.delete(&self.cache_key(id)).await?;
}
Ok(())
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pagination_default() {
let pagination = Pagination::default();
assert_eq!(pagination.page, 0);
assert_eq!(pagination.page_size, 20);
assert_eq!(pagination.limit(), 20);
assert_eq!(pagination.offset(), 0);
}
#[test]
fn test_pagination_offset() {
let pagination = Pagination::new(2, 10);
assert_eq!(pagination.limit(), 10);
assert_eq!(pagination.offset(), 20);
}
#[test]
fn test_page_creation() {
let items = vec![1, 2, 3];
let pagination = Pagination::new(0, 10);
let page = Page::new(items, 25, &pagination);
assert_eq!(page.items.len(), 3);
assert_eq!(page.total, 25);
assert_eq!(page.page, 0);
assert_eq!(page.page_size, 10);
assert_eq!(page.total_pages, 3);
}
#[test]
fn test_page_has_next() {
let items = vec![1, 2, 3];
let pagination = Pagination::new(0, 10);
let page = Page::new(items, 25, &pagination);
assert!(page.has_next());
assert!(!page.has_prev());
}
#[test]
fn test_page_has_prev() {
let items = vec![1, 2, 3];
let pagination = Pagination::new(1, 10);
let page = Page::new(items, 25, &pagination);
assert!(page.has_next());
assert!(page.has_prev());
}
#[test]
fn test_page_last_page() {
let items = vec![1, 2, 3];
let pagination = Pagination::new(2, 10);
let page = Page::new(items, 25, &pagination);
assert!(!page.has_next());
assert!(page.has_prev());
}
}