use serde::{Deserialize, Serialize};
use crate::error::{Result, SurqlError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CacheBackendKind {
#[default]
Memory,
Redis,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheConfig {
pub enabled: bool,
pub backend: CacheBackendKind,
pub default_ttl_secs: u64,
pub max_size: usize,
pub redis_url: String,
pub key_prefix: String,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
enabled: true,
backend: CacheBackendKind::Memory,
default_ttl_secs: 300,
max_size: 1000,
redis_url: "redis://localhost:6379".into(),
key_prefix: "surql:".into(),
}
}
}
impl CacheConfig {
pub fn builder() -> CacheConfigBuilder {
CacheConfigBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheConfigBuilder {
inner: CacheConfig,
}
impl CacheConfigBuilder {
pub fn enabled(mut self, enabled: bool) -> Self {
self.inner.enabled = enabled;
self
}
pub fn backend(mut self, backend: CacheBackendKind) -> Self {
self.inner.backend = backend;
self
}
pub fn default_ttl_secs(mut self, secs: u64) -> Self {
self.inner.default_ttl_secs = secs;
self
}
pub fn max_size(mut self, n: usize) -> Self {
self.inner.max_size = n;
self
}
pub fn redis_url(mut self, url: impl Into<String>) -> Self {
self.inner.redis_url = url.into();
self
}
pub fn key_prefix(mut self, prefix: impl Into<String>) -> Self {
self.inner.key_prefix = prefix.into();
self
}
pub fn build(self) -> CacheConfig {
self.inner
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheOptions {
pub ttl_secs: Option<u64>,
pub key: Option<String>,
pub invalidate_on: Vec<String>,
}
impl CacheOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_ttl_secs(mut self, ttl: u64) -> Result<Self> {
if ttl == 0 {
return Err(SurqlError::Validation {
reason: "TTL must be a positive integer".into(),
});
}
self.ttl_secs = Some(ttl);
Ok(self)
}
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn with_tables<I, S>(mut self, tables: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.invalidate_on = tables.into_iter().map(Into::into).collect();
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_match_python_port() {
let cfg = CacheConfig::default();
assert!(cfg.enabled);
assert_eq!(cfg.backend, CacheBackendKind::Memory);
assert_eq!(cfg.default_ttl_secs, 300);
assert_eq!(cfg.max_size, 1000);
assert_eq!(cfg.redis_url, "redis://localhost:6379");
assert_eq!(cfg.key_prefix, "surql:");
}
#[test]
fn builder_overrides_fields() {
let cfg = CacheConfig::builder()
.enabled(false)
.backend(CacheBackendKind::Redis)
.default_ttl_secs(60)
.max_size(42)
.redis_url("redis://remote:6379")
.key_prefix("test:")
.build();
assert!(!cfg.enabled);
assert_eq!(cfg.backend, CacheBackendKind::Redis);
assert_eq!(cfg.default_ttl_secs, 60);
assert_eq!(cfg.max_size, 42);
assert_eq!(cfg.redis_url, "redis://remote:6379");
assert_eq!(cfg.key_prefix, "test:");
}
#[test]
fn options_validate_ttl() {
assert!(CacheOptions::new().with_ttl_secs(0).is_err());
assert!(CacheOptions::new().with_ttl_secs(30).is_ok());
}
#[test]
fn options_chain() {
let opts = CacheOptions::new()
.with_key("k")
.with_tables(["user", "role"]);
assert_eq!(opts.key.as_deref(), Some("k"));
assert_eq!(opts.invalidate_on, vec!["user", "role"]);
}
#[test]
fn serde_roundtrip() {
let cfg = CacheConfig::default();
let json = serde_json::to_string(&cfg).unwrap();
let back: CacheConfig = serde_json::from_str(&json).unwrap();
assert_eq!(cfg, back);
}
}