use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum StorageConfig {
Memory(MemoryConfig),
#[cfg(feature = "redis")]
Redis(RedisConfig),
#[cfg(feature = "postgres")]
Postgres(PostgresConfig),
}
impl Default for StorageConfig {
fn default() -> Self {
Self::Memory(MemoryConfig::default())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemoryConfig {
pub max_jobs: Option<usize>,
#[deprecated(
note = "auto_cleanup is a no-op; CleanupWorker handles expiration. Will be removed."
)]
pub auto_cleanup: bool,
#[deprecated(
note = "cleanup_interval is a no-op; configure CleanupWorker via ServerConfig instead."
)]
pub cleanup_interval: Option<Duration>,
}
impl Default for MemoryConfig {
fn default() -> Self {
#[allow(deprecated)]
Self {
max_jobs: Some(10_000),
auto_cleanup: true,
cleanup_interval: Some(Duration::from_secs(300)),
}
}
}
impl MemoryConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_jobs(mut self, max_jobs: usize) -> Self {
self.max_jobs = Some(max_jobs);
self
}
pub fn unlimited(mut self) -> Self {
self.max_jobs = None;
self
}
#[deprecated(
note = "auto_cleanup is a no-op; CleanupWorker handles expiration. Will be removed."
)]
pub fn with_auto_cleanup(mut self, enabled: bool) -> Self {
#[allow(deprecated)]
{
self.auto_cleanup = enabled;
}
self
}
#[deprecated(
note = "cleanup_interval is a no-op; configure CleanupWorker via ServerConfig instead."
)]
pub fn with_cleanup_interval(mut self, interval: Duration) -> Self {
#[allow(deprecated)]
{
self.cleanup_interval = Some(interval);
}
self
}
}
#[cfg(feature = "redis")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RedisConfig {
pub url: String,
pub pool_size: u32,
pub connection_timeout: Duration,
pub command_timeout: Duration,
pub key_prefix: String,
pub database: Option<u8>,
pub username: Option<String>,
pub password: Option<String>,
pub tls: bool,
pub completed_job_ttl: Option<Duration>,
pub failed_job_ttl: Option<Duration>,
}
#[cfg(feature = "redis")]
impl Default for RedisConfig {
fn default() -> Self {
Self {
url: std::env::var("REDIS_URL")
.unwrap_or_else(|_| "redis://localhost:6379".to_string()),
pool_size: 10,
connection_timeout: Duration::from_secs(5),
command_timeout: Duration::from_secs(5),
key_prefix: "qml".to_string(),
database: None,
username: std::env::var("REDIS_USERNAME")
.ok()
.filter(|s| !s.is_empty()),
password: std::env::var("REDIS_PASSWORD")
.ok()
.filter(|s| !s.is_empty()),
tls: false,
completed_job_ttl: None,
failed_job_ttl: None,
}
}
}
#[cfg(feature = "redis")]
impl RedisConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_url<S: Into<String>>(mut self, url: S) -> Self {
self.url = url.into();
self
}
pub fn with_pool_size(mut self, size: u32) -> Self {
self.pool_size = size;
self
}
pub fn with_connection_timeout(mut self, timeout: Duration) -> Self {
self.connection_timeout = timeout;
self
}
pub fn with_command_timeout(mut self, timeout: Duration) -> Self {
self.command_timeout = timeout;
self
}
pub fn with_key_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
self.key_prefix = prefix.into();
self
}
pub fn with_database(mut self, database: u8) -> Self {
self.database = Some(database);
self
}
pub fn with_credentials<U: Into<String>, P: Into<String>>(
mut self,
username: U,
password: P,
) -> Self {
self.username = Some(username.into());
self.password = Some(password.into());
self
}
pub fn with_password<P: Into<String>>(mut self, password: P) -> Self {
self.password = Some(password.into());
self
}
pub fn with_tls(mut self, enabled: bool) -> Self {
self.tls = enabled;
self
}
pub fn with_completed_job_ttl(mut self, ttl: Duration) -> Self {
self.completed_job_ttl = Some(ttl);
self
}
pub fn with_failed_job_ttl(mut self, ttl: Duration) -> Self {
self.failed_job_ttl = Some(ttl);
self
}
pub fn no_completed_job_ttl(mut self) -> Self {
self.completed_job_ttl = None;
self
}
pub fn no_failed_job_ttl(mut self) -> Self {
self.failed_job_ttl = None;
self
}
pub fn full_url(&self) -> String {
let mut url = self.url.clone();
if let (Some(username), Some(password)) = (&self.username, &self.password) {
url = url.replace("redis://", &format!("redis://{}:{}@", username, password));
} else if let Some(password) = &self.password {
url = url.replace("redis://", &format!("redis://:{}@", password));
}
if let Some(db) = self.database {
if !url.ends_with('/') {
url.push('/');
}
url.push_str(&db.to_string());
}
url
}
}
#[cfg(feature = "postgres")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PostgresConfig {
pub database_url: String,
pub max_connections: u32,
pub min_connections: u32,
pub connect_timeout: Duration,
pub command_timeout: Duration,
pub table_name: String,
pub schema_name: String,
pub auto_migrate: bool,
pub idle_timeout: Duration,
pub max_lifetime: Option<Duration>,
pub require_ssl: bool,
}
#[cfg(feature = "postgres")]
impl Default for PostgresConfig {
fn default() -> Self {
Self {
database_url: std::env::var("DATABASE_URL").unwrap_or_else(|_| {
"postgresql://postgres:password@localhost:5432/qml".to_string()
}),
max_connections: 20,
min_connections: 1,
connect_timeout: Duration::from_secs(30),
command_timeout: Duration::from_secs(30),
table_name: "qml_jobs".to_string(),
schema_name: "qml".to_string(),
auto_migrate: true,
idle_timeout: Duration::from_secs(600),
max_lifetime: Some(Duration::from_secs(1800)),
require_ssl: false,
}
}
}
#[cfg(feature = "postgres")]
impl PostgresConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_defaults() -> Self {
Self {
database_url: String::new(), max_connections: 20,
min_connections: 1,
connect_timeout: Duration::from_secs(30),
command_timeout: Duration::from_secs(30),
table_name: "qml_jobs".to_string(),
schema_name: "qml".to_string(),
auto_migrate: true,
idle_timeout: Duration::from_secs(600),
max_lifetime: Some(Duration::from_secs(1800)),
require_ssl: false,
}
}
pub fn with_database_url<S: Into<String>>(mut self, url: S) -> Self {
self.database_url = url.into();
self
}
pub fn with_max_connections(mut self, max: u32) -> Self {
self.max_connections = max;
self
}
pub fn with_min_connections(mut self, min: u32) -> Self {
self.min_connections = min;
self
}
pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = timeout;
self
}
pub fn with_command_timeout(mut self, timeout: Duration) -> Self {
self.command_timeout = timeout;
self
}
pub fn with_table_name<S: Into<String>>(mut self, name: S) -> Self {
self.table_name = name.into();
self
}
pub fn with_schema_name<S: Into<String>>(mut self, name: S) -> Self {
self.schema_name = name.into();
self
}
pub fn with_auto_migrate(mut self, enabled: bool) -> Self {
self.auto_migrate = enabled;
self
}
pub fn with_idle_timeout(mut self, timeout: Duration) -> Self {
self.idle_timeout = timeout;
self
}
pub fn with_max_lifetime(mut self, lifetime: Duration) -> Self {
self.max_lifetime = Some(lifetime);
self
}
pub fn without_max_lifetime(mut self) -> Self {
self.max_lifetime = None;
self
}
pub fn with_ssl(mut self, require_ssl: bool) -> Self {
self.require_ssl = require_ssl;
self
}
pub fn full_table_name(&self) -> String {
format!("{}.{}", self.schema_name, self.table_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(deprecated)]
fn test_memory_config_default() {
let config = MemoryConfig::default();
assert_eq!(config.max_jobs, Some(10_000));
assert!(config.auto_cleanup);
assert_eq!(config.cleanup_interval, Some(Duration::from_secs(300)));
}
#[test]
#[allow(deprecated)]
fn test_memory_config_builder() {
let config = MemoryConfig::new()
.with_max_jobs(5_000)
.with_auto_cleanup(false)
.with_cleanup_interval(Duration::from_secs(600));
assert_eq!(config.max_jobs, Some(5_000));
assert!(!config.auto_cleanup);
assert_eq!(config.cleanup_interval, Some(Duration::from_secs(600)));
}
#[test]
#[cfg(feature = "redis")]
fn test_redis_config_default() {
let config = RedisConfig::default();
assert_eq!(config.url, "redis://localhost:6379");
assert_eq!(config.pool_size, 10);
assert_eq!(config.key_prefix, "qml");
assert!(!config.tls);
}
#[test]
#[cfg(feature = "redis")]
fn test_redis_config_builder() {
let config = RedisConfig::new()
.with_url("redis://localhost:6380")
.with_pool_size(20)
.with_key_prefix("test")
.with_database(1)
.with_credentials("user", "pass")
.with_tls(true);
assert_eq!(config.url, "redis://localhost:6380");
assert_eq!(config.pool_size, 20);
assert_eq!(config.key_prefix, "test");
assert_eq!(config.database, Some(1));
assert_eq!(config.username, Some("user".to_string()));
assert_eq!(config.password, Some("pass".to_string()));
assert!(config.tls);
}
#[test]
#[cfg(feature = "redis")]
fn test_redis_full_url() {
let config = RedisConfig::new()
.with_url("redis://localhost:6379")
.with_credentials("user", "pass")
.with_database(5);
assert_eq!(config.full_url(), "redis://user:pass@localhost:6379/5");
}
#[test]
#[cfg(feature = "redis")]
fn test_redis_full_url_password_only() {
let config = RedisConfig::new()
.with_url("redis://localhost:6379")
.with_password("pass")
.with_database(2);
assert_eq!(config.full_url(), "redis://:pass@localhost:6379/2");
}
#[test]
#[cfg(feature = "redis")]
fn test_storage_config_serialization() {
let memory_config = StorageConfig::Memory(MemoryConfig::default());
let redis_config = StorageConfig::Redis(RedisConfig::default());
let memory_json = serde_json::to_string(&memory_config).unwrap();
let redis_json = serde_json::to_string(&redis_config).unwrap();
let _: StorageConfig = serde_json::from_str(&memory_json).unwrap();
let _: StorageConfig = serde_json::from_str(&redis_json).unwrap();
}
#[test]
#[cfg(feature = "postgres")]
fn test_postgres_config_default() {
let expected_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgresql://postgres:password@localhost:5432/qml".to_string());
let config = PostgresConfig::default();
assert_eq!(config.database_url, expected_url);
assert_eq!(config.max_connections, 20);
assert_eq!(config.min_connections, 1);
assert_eq!(config.table_name, "qml_jobs");
assert_eq!(config.schema_name, "qml");
assert!(config.auto_migrate);
assert!(!config.require_ssl);
}
#[test]
#[cfg(feature = "postgres")]
fn test_postgres_config_builder() {
let config = PostgresConfig::new()
.with_database_url("postgresql://user:pass@localhost:5433/testdb")
.with_max_connections(50)
.with_min_connections(5)
.with_table_name("custom_jobs")
.with_schema_name("qml")
.with_auto_migrate(false)
.with_ssl(true);
assert_eq!(
config.database_url,
"postgresql://user:pass@localhost:5433/testdb"
);
assert_eq!(config.max_connections, 50);
assert_eq!(config.min_connections, 5);
assert_eq!(config.table_name, "custom_jobs");
assert_eq!(config.schema_name, "qml");
assert!(!config.auto_migrate);
assert!(config.require_ssl);
}
#[test]
#[cfg(feature = "postgres")]
fn test_postgres_full_table_name() {
let config = PostgresConfig::new()
.with_schema_name("qml")
.with_table_name("jobs");
assert_eq!(config.full_table_name(), "qml.jobs");
}
#[test]
#[cfg(feature = "postgres")]
fn test_postgres_config_serialization() {
let postgres_config = StorageConfig::Postgres(PostgresConfig::default());
let postgres_json = serde_json::to_string(&postgres_config).unwrap();
let _: StorageConfig = serde_json::from_str(&postgres_json).unwrap();
}
}