use nepali_core::HostCache;
use redis::Commands;
use std::cell::RefCell;
pub struct RedisCache {
url: String,
conn: RefCell<Option<redis::Connection>>,
}
impl RedisCache {
pub fn new(url: String) -> Self {
RedisCache { url, conn: RefCell::new(None) }
}
fn with_conn<T>(
&self,
f: impl FnOnce(&mut redis::Connection) -> redis::RedisResult<T>,
) -> Result<T, String> {
let mut guard = self.conn.borrow_mut();
if guard.is_none() {
let client = redis::Client::open(self.url.as_str())
.map_err(|e| format!("क्यास: '{}' अवैध redis URL: {e}", self.url))?;
let conn = client
.get_connection()
.map_err(|e| format!("क्यास: '{}' मा redis-server सँग जोड्न सकिएन: {e}", self.url))?;
*guard = Some(conn);
}
let conn = guard.as_mut().expect("just set above");
f(conn).map_err(|e| e.to_string())
}
}
impl HostCache for RedisCache {
fn set(&self, key: &str, value: &str, ttl_seconds: u64) -> Result<(), String> {
if ttl_seconds > 0 {
self.with_conn(|conn| conn.set_ex::<_, _, ()>(key, value, ttl_seconds))
} else {
self.with_conn(|conn| conn.set::<_, _, ()>(key, value))
}
}
fn get(&self, key: &str) -> Result<Option<String>, String> {
self.with_conn(|conn| conn.get(key))
}
fn delete(&self, key: &str) -> Result<(), String> {
self.with_conn(|conn| conn.del::<_, ()>(key))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::{Child, Command, Stdio};
use std::time::Duration;
struct TestRedisServer {
child: Child,
port: u16,
}
impl TestRedisServer {
fn start() -> Self {
let port = 16399; let child = Command::new("redis-server")
.args(["--port", &port.to_string(), "--daemonize", "no", "--save", "", "--appendonly", "no"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect(
"redis-server isn't on $PATH - install it to run this test \
(e.g. `brew install redis` or `apt install redis-server`), \
the same real requirement this bridge has in production",
);
std::thread::sleep(Duration::from_millis(300)); TestRedisServer { child, port }
}
fn url(&self) -> String {
format!("redis://127.0.0.1:{}", self.port)
}
}
impl Drop for TestRedisServer {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
#[test]
fn real_redis_set_get_delete_and_ttl() {
let server = TestRedisServer::start();
let cache = RedisCache::new(server.url());
cache.set("greeting", "नमस्ते क्यास!", 0).unwrap();
assert_eq!(cache.get("greeting").unwrap(), Some("नमस्ते क्यास!".to_string()));
assert_eq!(cache.get("no-such-key").unwrap(), None, "a real miss must be None, not an error");
cache.delete("greeting").unwrap();
assert_eq!(cache.get("greeting").unwrap(), None);
cache.set("temp", "soon gone", 1).unwrap();
assert_eq!(cache.get("temp").unwrap(), Some("soon gone".to_string()));
std::thread::sleep(Duration::from_millis(1200));
assert_eq!(cache.get("temp").unwrap(), None, "real TTL expiry, not just delete");
}
#[test]
fn connecting_to_a_dead_server_is_a_real_error() {
let cache = RedisCache::new("redis://127.0.0.1:1".to_string());
assert!(cache.set("k", "v", 0).is_err());
}
}