redis-cache 0.3.0

A shared cache designed for casbin-rs
use casbin::Cache;

use redis::{Client, Connection, IntoConnectionInfo, RedisResult};
use serde::{de::DeserializeOwned, Serialize};

use std::{borrow::Cow, hash::Hash, marker::PhantomData};

const CACHE_HKEY: &str = "casbin_cache";

pub struct RedisCache<K, V> {
    conn: Connection,
    cap: usize,
    _marker: PhantomData<(K, V)>,
}

impl<K, V> RedisCache<K, V> {
    pub fn new<U: IntoConnectionInfo>(url: U) -> RedisResult<RedisCache<K, V>> {
        let client = Client::open(url)?;
        let conn = client.get_connection()?;

        Ok(RedisCache {
            conn,
            cap: 1000,
            _marker: PhantomData,
        })
    }
}

impl<K, V> Cache<K, V> for RedisCache<K, V>
where
    K: Eq + Hash + Send + Sync + Serialize + 'static,
    V: Send + Sync + Clone + Serialize + DeserializeOwned + 'static,
{
    fn set_capacity(&mut self, cap: usize) {
        self.cap = cap;
    }

    fn get(&mut self, k: &K) -> Option<Cow<'_, V>> {
        if let Ok(field) = serde_json::to_string(&k) {
            if let Ok(res) = redis::cmd("HGET")
                .arg(CACHE_HKEY)
                .arg(field)
                .query::<String>(&mut self.conn)
            {
                return serde_json::from_str(&res).map(|x| Cow::Owned(x)).ok();
            }
        }

        None
    }

    fn has(&mut self, k: &K) -> bool {
        if let Ok(field) = serde_json::to_string(&k) {
            if let Ok(res) = redis::cmd("HEXISTS")
                .arg(CACHE_HKEY)
                .arg(field)
                .query::<bool>(&mut self.conn)
            {
                return res;
            }
        }

        false
    }

    fn set(&mut self, k: K, v: V) {
        if let Ok(keys) = redis::cmd("HKEYS")
            .arg(CACHE_HKEY)
            .query::<Vec<String>>(&mut self.conn)
        {
            if keys.len() < self.cap {
                if let (Ok(field), Ok(value)) =
                    (serde_json::to_string(&k), serde_json::to_string(&v))
                {
                    let _ = redis::cmd("HSET")
                        .arg(CACHE_HKEY)
                        .arg(field)
                        .arg(value)
                        .query::<bool>(&mut self.conn);
                }
            }
        }
    }

    fn clear(&mut self) {
        let _ = redis::cmd("DEL")
            .arg(CACHE_HKEY)
            .query::<bool>(&mut self.conn);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_set_has_get_clear() {
        let mut cache = RedisCache::new("redis://localhost:6379").unwrap();

        cache.set(vec!["alice", "/data1", "read"], false);
        assert!(cache.has(&vec!["alice", "/data1", "read"]));
        assert!(cache.get(&vec!["alice", "/data1", "read"]) == Some(Cow::Borrowed(&false)));
        cache.clear();

        assert!(cache.get(&vec!["alice", "/data1", "read"]) == None);
    }
}