Skip to content

Commit

Permalink
feat: make CacheDriver use generics
Browse files Browse the repository at this point in the history
CacheDriver trait currently operates over `&str` which means it's
not really possible to use ints or guids or other fixed sized
keys.
  • Loading branch information
sevki committed Oct 14, 2024
1 parent 018e1ba commit f44f5dd
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 52 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ bb8 = { version = "0.8.1", optional = true }
[workspace.dependencies]
async-trait = { version = "0.1.74" }
axum = { version = "0.7.5", features = ["macros"] }
tower = "0.4"
tower = { version = "0.4", features = ["util"] }
tower-http = { version = "0.6.1", features = [
"trace",
"catch-panic",
Expand Down
54 changes: 34 additions & 20 deletions src/cache/drivers/inmem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::cache::CacheResult;
///
/// A boxed [`CacheDriver`] instance.
#[must_use]
pub fn new() -> Box<dyn CacheDriver> {
pub fn new() -> Box<dyn CacheDriver<Key = String, Value = String>> {
let cache = Cache::builder().max_capacity(32 * 1024 * 1024).build();
Inmem::from(cache)
}
Expand All @@ -34,19 +34,27 @@ impl Inmem {
///
/// A boxed [`CacheDriver`] instance.
#[must_use]
pub fn from(cache: Cache<String, String>) -> Box<dyn CacheDriver> {
pub fn from(
cache: Cache<String, String>,
) -> Box<dyn CacheDriver<Key = String, Value = String>> {
Box::new(Self { cache })
}
}

#[async_trait]
impl CacheDriver for Inmem {
/// The type used for cache keys. Must be serializable and deserializable.
type Key = String;

/// The type used for cache values. Must be serializable and deserializable.
type Value = String;

/// Checks if a key exists in the cache.
///
/// # Errors
///
/// Returns a `CacheError` if there is an error during the operation.
async fn contains_key(&self, key: &str) -> CacheResult<bool> {
async fn contains_key(&self, key: &Self::Key) -> CacheResult<bool> {
Ok(self.cache.contains_key(key))
}

Expand All @@ -55,7 +63,7 @@ impl CacheDriver for Inmem {
/// # Errors
///
/// Returns a `CacheError` if there is an error during the operation.
async fn get(&self, key: &str) -> CacheResult<Option<String>> {
async fn get(&self, key: &Self::Key) -> CacheResult<Option<Self::Value>> {
Ok(self.cache.get(key))
}

Expand All @@ -64,7 +72,7 @@ impl CacheDriver for Inmem {
/// # Errors
///
/// Returns a `CacheError` if there is an error during the operation.
async fn insert(&self, key: &str, value: &str) -> CacheResult<()> {
async fn insert(&self, key: &Self::Key, value: &Self::Value) -> CacheResult<()> {
self.cache
.insert(key.to_string(), Arc::new(value).to_string());
Ok(())
Expand All @@ -75,7 +83,7 @@ impl CacheDriver for Inmem {
/// # Errors
///
/// Returns a `CacheError` if there is an error during the operation.
async fn remove(&self, key: &str) -> CacheResult<()> {
async fn remove(&self, key: &Self::Key) -> CacheResult<()> {
self.cache.remove(key);
Ok(())
}
Expand All @@ -99,40 +107,46 @@ mod tests {
#[tokio::test]
async fn is_contains_key() {
let mem = new();
assert!(!mem.contains_key("key").await.unwrap());
assert!(mem.insert("key", "loco").await.is_ok());
assert!(mem.contains_key("key").await.unwrap());
let key = "key".to_string();
let value = "loco".to_string();
assert!(!mem.contains_key(&key).await.unwrap());
assert!(mem.insert(&key, &value).await.is_ok());
assert!(mem.contains_key(&key).await.unwrap());
}

#[tokio::test]
async fn can_get_key_value() {
let mem = new();
assert!(mem.insert("key", "loco").await.is_ok());
assert_eq!(mem.get("key").await.unwrap(), Some("loco".to_string()));
let key = "key".to_string();
let value = "loco".to_string();
assert!(mem.insert(&key, &value).await.is_ok());
assert_eq!(mem.get(&key).await.unwrap(), Some(value));

//try getting key that not exists
assert_eq!(mem.get("not-found").await.unwrap(), None);
let not_found_key = "not-found".to_string();
assert_eq!(mem.get(&not_found_key).await.unwrap(), None);
}

#[tokio::test]
async fn can_remove_key() {
let mem = new();
assert!(mem.insert("key", "loco").await.is_ok());
assert!(mem.contains_key("key").await.unwrap());
mem.remove("key").await.unwrap();
assert!(!mem.contains_key("key").await.unwrap());
let key = "key".to_string();
let value = "loco".to_string();
assert!(mem.insert(&key, &value).await.is_ok());
assert!(mem.contains_key(&key).await.unwrap());
mem.remove(&key).await.unwrap();
assert!(!mem.contains_key(&key).await.unwrap());
}

#[tokio::test]
async fn can_clear() {
let mem = new();

let keys = vec!["key", "key2", "key3"];
let keys = vec!["key".to_string(), "key2".to_string(), "key3".to_string()];
for key in &keys {
assert!(mem.insert(key, "loco").await.is_ok());
assert!(mem.insert(key, &"loco".to_string()).await.is_ok());
}
for key in &keys {
assert!(mem.contains_key(key).await.is_ok());
assert!(mem.contains_key(key).await.unwrap());
}
assert!(mem.clear().await.is_ok());
for key in &keys {
Expand Down
14 changes: 10 additions & 4 deletions src/cache/drivers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//!
//! This module defines traits and implementations for cache drivers.
use async_trait::async_trait;
use serde::{de::DeserializeOwned, Deserialize, Serialize};

use super::CacheResult;

Expand All @@ -12,37 +13,42 @@ pub mod null;
/// Trait representing a cache driver.
#[async_trait]
pub trait CacheDriver: Sync + Send {
/// The type used for cache keys. Must be serializable and deserializable.
type Key: Serialize + for<'de> Deserialize<'de> + Send + Sync;

/// The type used for cache values. Must be serializable and deserializable.
type Value: Serialize + for<'de> Deserialize<'de> + Send + Sync;
/// Checks if a key exists in the cache.
///
/// # Errors
///
/// Returns a [`super::CacheError`] if there is an error during the
/// operation.
async fn contains_key(&self, key: &str) -> CacheResult<bool>;
async fn contains_key(&self, key: &Self::Key) -> CacheResult<bool>;

/// Retrieves a value from the cache based on the provided key.
///
/// # Errors
///
/// Returns a [`super::CacheError`] if there is an error during the
/// operation.
async fn get(&self, key: &str) -> CacheResult<Option<String>>;
async fn get(&self, key: &Self::Key) -> CacheResult<Option<Self::Value>>;

/// Inserts a key-value pair into the cache.
///
/// # Errors
///
/// Returns a [`super::CacheError`] if there is an error during the
/// operation.
async fn insert(&self, key: &str, value: &str) -> CacheResult<()>;
async fn insert(&self, key: &Self::Key, value: &Self::Value) -> CacheResult<()>;

/// Removes a key-value pair from the cache.
///
/// # Errors
///
/// Returns a [`super::CacheError`] if there is an error during the
/// operation.
async fn remove(&self, key: &str) -> CacheResult<()>;
async fn remove(&self, key: &Self::Key) -> CacheResult<()>;

/// Clears all key-value pairs from the cache.
///
Expand Down
12 changes: 7 additions & 5 deletions src/cache/drivers/null.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,20 @@ pub struct Null {}
///
/// A boxed [`CacheDriver`] instance.
#[must_use]
pub fn new() -> Box<dyn CacheDriver> {
pub fn new() -> Box<dyn CacheDriver<Key = String, Value = String>> {
Box::new(Null {})
}

#[async_trait]
impl CacheDriver for Null {
type Key = String;
type Value = String;
/// Checks if a key exists in the cache.
///
/// # Errors
///
/// Returns always error
async fn contains_key(&self, _key: &str) -> CacheResult<bool> {
async fn contains_key(&self, _key: &Self::Key) -> CacheResult<bool> {
Err(CacheError::Any(
"Operation not supported by null cache".into(),
))
Expand All @@ -41,7 +43,7 @@ impl CacheDriver for Null {
/// # Errors
///
/// Returns always error
async fn get(&self, _key: &str) -> CacheResult<Option<String>> {
async fn get(&self, _key: &Self::Key) -> CacheResult<Option<Self::Value>> {
Ok(None)
}

Expand All @@ -50,7 +52,7 @@ impl CacheDriver for Null {
/// # Errors
///
/// Returns always error
async fn insert(&self, _key: &str, _value: &str) -> CacheResult<()> {
async fn insert(&self, _key: &Self::Key, _value: &Self::Value) -> CacheResult<()> {
Err(CacheError::Any(
"Operation not supported by null cache".into(),
))
Expand All @@ -61,7 +63,7 @@ impl CacheDriver for Null {
/// # Errors
///
/// Returns always error
async fn remove(&self, _key: &str) -> CacheResult<()> {
async fn remove(&self, _key: &Self::Key) -> CacheResult<()> {
Err(CacheError::Any(
"Operation not supported by null cache".into(),
))
Expand Down
53 changes: 33 additions & 20 deletions src/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ pub mod drivers;

use std::future::Future;

use serde::{Deserialize, Serialize};

use self::drivers::CacheDriver;
use crate::Result as LocoResult;

Expand All @@ -17,17 +19,28 @@ pub enum CacheError {
}

pub type CacheResult<T> = std::result::Result<T, CacheError>;
/// The type used for cache keys. Must be serializable and deserializable.

/// Represents a cache instance
pub struct Cache {
pub struct Cache<Key = String, Value = String>
where
Key: Serialize + for<'de> Deserialize<'de> + Send + Sync,

Value: Serialize + for<'de> Deserialize<'de> + Send + Sync,
{
/// The cache driver used for underlying operations
pub driver: Box<dyn CacheDriver>,
pub driver: Box<dyn CacheDriver<Key = Key, Value = Value>>,
}

impl Cache {
impl<Key, Value> Cache<Key, Value>
where
Key: Serialize + for<'de> Deserialize<'de> + Send + Sync,

Value: Serialize + for<'de> Deserialize<'de> + Send + Sync,
{
/// Creates a new cache instance with the specified cache driver.
#[must_use]
pub fn new(driver: Box<dyn CacheDriver>) -> Self {
pub fn new(driver: Box<dyn CacheDriver<Key = Key, Value = Value>>) -> Self {
Self { driver }
}

Expand All @@ -39,13 +52,13 @@ impl Cache {
///
/// pub async fn contains_key() -> CacheResult<bool> {
/// let cache = cache::Cache::new(cache::drivers::inmem::new());
/// cache.contains_key("key").await
/// cache.contains_key(&"key".to_string()).await
/// }
/// ```
///
/// # Errors
/// A [`CacheResult`] indicating whether the key exists in the cache.
pub async fn contains_key(&self, key: &str) -> CacheResult<bool> {
pub async fn contains_key(&self, key: &Key) -> CacheResult<bool> {
self.driver.contains_key(key).await
}

Expand All @@ -57,14 +70,14 @@ impl Cache {
///
/// pub async fn get_key() -> CacheResult<Option<String>> {
/// let cache = cache::Cache::new(cache::drivers::inmem::new());
/// cache.get("key").await
/// cache.get(&"key".to_string()).await
/// }
/// ```
///
/// # Errors
/// A [`CacheResult`] containing an `Option` representing the retrieved
/// value.
pub async fn get(&self, key: &str) -> CacheResult<Option<String>> {
pub async fn get(&self, key: &Key) -> CacheResult<Option<Value>> {
self.driver.get(key).await
}

Expand All @@ -76,14 +89,14 @@ impl Cache {
///
/// pub async fn insert() -> CacheResult<()> {
/// let cache = cache::Cache::new(cache::drivers::inmem::new());
/// cache.insert("key", "value").await
/// cache.insert(&"key".to_string(), &"value".to_string()).await
/// }
/// ```
///
/// # Errors
///
/// A [`CacheResult`] indicating the success of the operation.
pub async fn insert(&self, key: &str, value: &str) -> CacheResult<()> {
pub async fn insert(&self, key: &Key, value: &Value) -> CacheResult<()> {
self.driver.insert(key, value).await
}

Expand All @@ -98,19 +111,19 @@ impl Cache {
///
/// pub async fn get_or_insert(){
/// let app_ctx = get_app_context().await;
/// let res = app_ctx.cache.get_or_insert("key", async {
/// let res = app_ctx.cache.get_or_insert(&"key".to_string(), async {
/// Ok("value".to_string())
/// }).await.unwrap();
/// assert_eq!(res, "value");
/// assert_eq!(res, "value".to_string());
/// }
/// ```
///
/// # Errors
///
/// A [`LocoResult`] indicating the success of the operation.
pub async fn get_or_insert<F>(&self, key: &str, f: F) -> LocoResult<String>
pub async fn get_or_insert<F>(&self, key: &Key, f: F) -> LocoResult<Value>
where
F: Future<Output = LocoResult<String>> + Send,
F: Future<Output = LocoResult<Value>> + Send,
{
if let Some(value) = self.driver.get(key).await? {
Ok(value)
Expand All @@ -129,14 +142,14 @@ impl Cache {
///
/// pub async fn remove() -> CacheResult<()> {
/// let cache = cache::Cache::new(cache::drivers::inmem::new());
/// cache.remove("key").await
/// cache.remove(&"key".to_string()).await
/// }
/// ```
///
/// # Errors
///
/// A [`CacheResult`] indicating the success of the operation.
pub async fn remove(&self, key: &str) -> CacheResult<()> {
pub async fn remove(&self, key: &Key) -> CacheResult<()> {
self.driver.remove(key).await
}

Expand Down Expand Up @@ -168,19 +181,19 @@ mod tests {
#[tokio::test]
async fn can_get_or_insert() {
let app_ctx = tests_cfg::app::get_app_context().await;
let get_key = "loco";
let get_key = "loco".to_string();

assert_eq!(app_ctx.cache.get(get_key).await.unwrap(), None);
assert_eq!(app_ctx.cache.get(&get_key).await.unwrap(), None);

let result = app_ctx
.cache
.get_or_insert(get_key, async { Ok("loco-cache-value".to_string()) })
.get_or_insert(&get_key, async { Ok("loco-cache-value".to_string()) })
.await
.unwrap();

assert_eq!(result, "loco-cache-value".to_string());
assert_eq!(
app_ctx.cache.get(get_key).await.unwrap(),
app_ctx.cache.get(&get_key).await.unwrap(),
Some("loco-cache-value".to_string())
);
}
Expand Down
Loading

0 comments on commit f44f5dd

Please sign in to comment.