Skip to content

Commit e67c3e2

Browse files
feat!: Add Redis Cache store (#410)
1 parent c8f76b5 commit e67c3e2

File tree

8 files changed

+1020
-79
lines changed

8 files changed

+1020
-79
lines changed

compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ services:
3232
redis:
3333
image: redis:8-alpine
3434
container_name: cot-redis
35+
command: ["redis-server", "--databases", "100"]
3536
ports:
3637
- "6379:6379"
3738
healthcheck:

cot-macros/src/cache.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use proc_macro2::TokenStream;
2+
use quote::{format_ident, quote};
3+
use syn::ItemFn;
4+
5+
pub(super) fn fn_to_cache_test(test_fn: &ItemFn) -> TokenStream {
6+
let test_fn_name = &test_fn.sig.ident;
7+
let memory_ident = format_ident!("{}_memory", test_fn_name);
8+
let redis_ident = format_ident!("{}_redis", test_fn_name);
9+
10+
let result = quote! {
11+
#[::cot::test]
12+
async fn #memory_ident() {
13+
let mut cache = cot::test::TestCache::new_memory();
14+
#test_fn_name(&mut cache).await;
15+
16+
#test_fn
17+
}
18+
19+
20+
#[ignore = "Tests that use Redis are ignored by default"]
21+
#[::cot::test]
22+
#[cfg(feature="redis")]
23+
async fn #redis_ident() {
24+
let mut cache = cot::test::TestCache::new_redis().await.unwrap();
25+
26+
#test_fn_name(&mut cache).await;
27+
28+
cache.cleanup().await.unwrap_or_else(|err| panic!("Failed to cleanup: {err:?}"));
29+
30+
#test_fn
31+
}
32+
};
33+
result
34+
}

cot-macros/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod admin;
22
mod api_response_enum;
3+
mod cache;
34
mod dbtest;
45
mod form;
56
mod from_request;
@@ -158,6 +159,12 @@ pub fn main(_args: TokenStream, input: TokenStream) -> TokenStream {
158159
.into()
159160
}
160161

162+
#[proc_macro_attribute]
163+
pub fn cachetest(_args: TokenStream, input: TokenStream) -> TokenStream {
164+
let fn_input = parse_macro_input!(input as ItemFn);
165+
cache::fn_to_cache_test(&fn_input).into()
166+
}
167+
161168
/// An attribute macro that defines an `async` test function for a Cot-powered
162169
/// app.
163170
///

cot/src/cache.rs

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ use serde::de::DeserializeOwned;
122122
use thiserror::Error;
123123

124124
use crate::cache::store::memory::Memory;
125+
#[cfg(feature = "redis")]
126+
use crate::cache::store::redis::Redis;
125127
use crate::cache::store::{BoxCacheStore, CacheStore};
126128
use crate::config::{CacheConfig, Timeout};
127129
use crate::error::error_impl::impl_into_cot_error;
@@ -771,6 +773,11 @@ impl Cache {
771773
let mem_store = Memory::new();
772774
Self::new(mem_store, config.prefix.clone(), config.timeout)
773775
}
776+
#[cfg(feature = "redis")]
777+
CacheStoreTypeConfig::Redis { ref url, pool_size } => {
778+
let redis_store = Redis::new(url, pool_size)?;
779+
Self::new(redis_store, config.prefix.clone(), config.timeout)
780+
}
774781
_ => {
775782
unimplemented!();
776783
}
@@ -786,11 +793,13 @@ mod tests {
786793
use std::fmt::Debug;
787794
use std::time::Duration;
788795

796+
use cot::config::CacheUrl;
789797
use serde::{Deserialize, Serialize};
790798

791799
use super::*;
792800
use crate::cache::store::memory::Memory;
793801
use crate::config::Timeout;
802+
use crate::test::TestCache;
794803

795804
#[derive(Serialize, Deserialize, Debug, PartialEq)]
796805
struct User {
@@ -799,12 +808,14 @@ mod tests {
799808
email: String,
800809
}
801810

802-
#[cot::test]
803-
async fn test_cache_basic_operations() {
804-
let store = Memory::new();
805-
let cache = Cache::new(store, None, Timeout::After(Duration::from_secs(60)));
811+
#[cot_macros::cachetest]
812+
async fn test_cache_basic_operations(test_cache: &mut TestCache) {
813+
let cache = test_cache.cache();
806814

807-
cache.insert("user:1", "John Doe").await.unwrap();
815+
cache
816+
.insert("user:1", "John Doe".to_string())
817+
.await
818+
.unwrap();
808819
let user: Option<String> = cache.get("user:1").await.unwrap();
809820
assert_eq!(user, Some("John Doe".to_string()));
810821

@@ -827,10 +838,9 @@ mod tests {
827838
assert_eq!(user, Some("John Doe".to_string()));
828839
}
829840

830-
#[cot::test]
831-
async fn test_cache_complex_objects() {
832-
let store = Memory::new();
833-
let cache = Cache::new(store, None, Timeout::After(Duration::from_secs(60)));
841+
#[cot_macros::cachetest]
842+
async fn test_cache_complex_objects(test_cache: &mut TestCache) {
843+
let cache = test_cache.cache();
834844

835845
let user = User {
836846
id: 1,
@@ -843,10 +853,9 @@ mod tests {
843853
assert_eq!(cached_user, Some(user));
844854
}
845855

846-
#[cot::test]
847-
async fn test_cache_insert_expiring() {
848-
let store = Memory::new();
849-
let cache = Cache::new(store, None, Timeout::After(Duration::from_secs(60)));
856+
#[cot_macros::cachetest]
857+
async fn test_cache_insert_expiring(test_cache: &mut TestCache) {
858+
let cache = test_cache.cache();
850859

851860
cache
852861
.insert_expiring(
@@ -861,13 +870,11 @@ mod tests {
861870
assert_eq!(value, Some("temporary".to_string()));
862871
}
863872

864-
#[cot::test]
865-
async fn test_cache_get_or_insert_with() {
866-
let store = Memory::new();
867-
let cache = Cache::new(store, None, Timeout::After(Duration::from_secs(60)));
873+
#[cot_macros::cachetest]
874+
async fn test_cache_get_or_insert_with(test_cache: &mut TestCache) {
875+
let cache = test_cache.cache();
868876

869877
let mut call_count = 0;
870-
871878
let value1: String = cache
872879
.get_or_insert_with("expensive", || async {
873880
call_count += 1;
@@ -883,15 +890,13 @@ mod tests {
883890
})
884891
.await
885892
.unwrap();
886-
887893
assert_eq!(value1, value2);
888894
assert_eq!(call_count, 1);
889895
}
890896

891-
#[cot::test]
892-
async fn test_cache_get_or_insert_with_expiring() {
893-
let store = Memory::new();
894-
let cache = Cache::new(store, None, Timeout::After(Duration::from_secs(60)));
897+
#[cot_macros::cachetest]
898+
async fn test_cache_get_or_insert_with_expiring(test_cache: &mut TestCache) {
899+
let cache = test_cache.cache();
895900

896901
let mut call_count = 0;
897902

@@ -923,10 +928,9 @@ mod tests {
923928
assert_eq!(call_count, 1);
924929
}
925930

926-
#[cot::test]
927-
async fn test_cache_statistics() {
928-
let store = Memory::new();
929-
let cache = Cache::new(store, None, Timeout::After(Duration::from_secs(60)));
931+
#[cot_macros::cachetest]
932+
async fn test_cache_statistics(test_cache: &mut TestCache) {
933+
let cache = test_cache.cache();
930934

931935
assert_eq!(cache.approx_size().await.unwrap(), 0);
932936

@@ -939,14 +943,34 @@ mod tests {
939943
assert_eq!(cache.approx_size().await.unwrap(), 0);
940944
}
941945

942-
#[cot::test]
943-
async fn test_cache_contains_key() {
944-
let store = Memory::new();
945-
let cache = Cache::new(store, None, Timeout::After(Duration::from_secs(60)));
946+
#[cot_macros::cachetest]
947+
async fn test_cache_contains_key(test_cache: &mut TestCache) {
948+
let cache = test_cache.cache();
946949

947950
assert!(!cache.contains_key("nonexistent").await.unwrap());
948951

949952
cache.insert("existing", "value").await.unwrap();
950953
assert!(cache.contains_key("existing").await.unwrap());
951954
}
955+
956+
#[cfg(feature = "redis")]
957+
#[cot::test]
958+
async fn test_cache_from_config_redis() {
959+
use crate::config::{CacheConfig, CacheStoreConfig, CacheStoreTypeConfig};
960+
let url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost".to_string());
961+
let url = CacheUrl::from(url);
962+
963+
let config = CacheConfig::builder()
964+
.store(
965+
CacheStoreConfig::builder()
966+
.store_type(CacheStoreTypeConfig::Redis { url, pool_size: 5 })
967+
.build(),
968+
)
969+
.prefix("test_redis")
970+
.timeout(Timeout::After(Duration::from_secs(60)))
971+
.build();
972+
973+
let result = Cache::from_config(&config).await;
974+
assert!(result.is_ok());
975+
}
952976
}

cot/src/cache/store.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
//! cached values, optionally with expiration policies.
77
88
pub mod memory;
9+
#[cfg(feature = "redis")]
10+
pub mod redis;
911

1012
use std::fmt::Debug;
1113
use std::pin::Pin;
@@ -14,24 +16,27 @@ use serde_json::Value;
1416
use thiserror::Error;
1517

1618
use crate::config::Timeout;
19+
use crate::error::error_impl::impl_into_cot_error;
1720

18-
const CACHE_STORE_ERROR_PREFIX: &str = "Cache store error: ";
21+
const CACHE_STORE_ERROR_PREFIX: &str = "cache store error:";
1922

2023
/// Errors that can occur when interacting with a cache store.
2124
#[derive(Debug, Error)]
2225
#[non_exhaustive]
2326
pub enum CacheStoreError {
2427
/// The underlying cache backend returned an error.
25-
#[error("{CACHE_STORE_ERROR_PREFIX} Cache store backend error: {0}")]
28+
#[error("{CACHE_STORE_ERROR_PREFIX} backend error: {0}")]
2629
Backend(String),
2730
/// Failed to serialize a value for storage.
28-
#[error("{CACHE_STORE_ERROR_PREFIX} Serialization error: {0}")]
31+
#[error("{CACHE_STORE_ERROR_PREFIX} serialization error: {0}")]
2932
Serialize(String),
3033
/// Failed to deserialize a stored value.
31-
#[error("{CACHE_STORE_ERROR_PREFIX} Deserialization error: {0}")]
34+
#[error("{CACHE_STORE_ERROR_PREFIX} deserialization error: {0}")]
3235
Deserialize(String),
3336
}
3437

38+
impl_into_cot_error!(CacheStoreError);
39+
3540
/// Convenience alias for results returned by cache store operations.
3641
pub type CacheStoreResult<T> = Result<T, CacheStoreError>;
3742

0 commit comments

Comments
 (0)