cstats-core 0.1.1

Core library for cstats - statistical analysis and metrics collection
Documentation
//! Caching functionality for cstats

use std::path::PathBuf;
use std::time::{Duration, SystemTime};

use serde::{Deserialize, Serialize};
use tokio::fs;

use crate::{config::CacheConfig, Result};

/// Cache entry with metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheEntry<T> {
    /// The cached data
    pub data: T,

    /// When the entry was created
    pub created_at: SystemTime,

    /// When the entry expires
    pub expires_at: SystemTime,

    /// Optional metadata
    pub metadata: Option<std::collections::HashMap<String, String>>,
}

/// File-based cache implementation
#[derive(Debug, Clone)]
pub struct FileCache {
    config: CacheConfig,
}

impl FileCache {
    /// Create a new file cache with the given configuration
    pub fn new(config: CacheConfig) -> Self {
        Self { config }
    }

    /// Initialize the cache directory
    pub async fn init(&self) -> Result<()> {
        fs::create_dir_all(&self.config.cache_dir).await?;
        Ok(())
    }

    /// Store data in the cache
    pub async fn store<T>(&self, key: &str, data: T) -> Result<()>
    where
        T: Serialize,
    {
        let entry = CacheEntry {
            data,
            created_at: SystemTime::now(),
            expires_at: SystemTime::now() + Duration::from_secs(self.config.ttl_seconds),
            metadata: None,
        };

        let path = self.cache_path(key);
        let content = serde_json::to_string(&entry)?;
        fs::write(path, content).await?;

        Ok(())
    }

    /// Retrieve data from the cache
    pub async fn get<T>(&self, key: &str) -> Result<Option<T>>
    where
        T: for<'de> Deserialize<'de>,
    {
        let path = self.cache_path(key);

        if !path.exists() {
            return Ok(None);
        }

        let content = fs::read_to_string(path).await?;
        let entry: CacheEntry<T> = serde_json::from_str(&content)?;

        // Check if entry has expired
        if entry.expires_at < SystemTime::now() {
            self.remove(key).await?;
            return Ok(None);
        }

        Ok(Some(entry.data))
    }

    /// Remove an entry from the cache
    pub async fn remove(&self, key: &str) -> Result<()> {
        let path = self.cache_path(key);
        if path.exists() {
            fs::remove_file(path).await?;
        }
        Ok(())
    }

    /// Clear all entries from the cache
    pub async fn clear(&self) -> Result<()> {
        if self.config.cache_dir.exists() {
            fs::remove_dir_all(&self.config.cache_dir).await?;
            fs::create_dir_all(&self.config.cache_dir).await?;
        }
        Ok(())
    }

    /// Check if a key exists in the cache and is not expired
    pub async fn exists(&self, key: &str) -> Result<bool> {
        let path = self.cache_path(key);

        if !path.exists() {
            return Ok(false);
        }

        let content = fs::read_to_string(path).await?;
        let entry: CacheEntry<serde_json::Value> = serde_json::from_str(&content)?;

        Ok(entry.expires_at > SystemTime::now())
    }

    /// Get cache statistics
    pub async fn stats(&self) -> Result<CacheStats> {
        let mut total_size = 0u64;
        let mut entry_count = 0u32;
        let mut expired_count = 0u32;

        let mut entries = fs::read_dir(&self.config.cache_dir).await?;
        while let Some(entry) = entries.next_entry().await? {
            if entry.file_type().await?.is_file() {
                entry_count += 1;
                let metadata = entry.metadata().await?;
                total_size += metadata.len();

                // Check if expired
                if let Ok(content) = fs::read_to_string(entry.path()).await {
                    if let Ok(cache_entry) =
                        serde_json::from_str::<CacheEntry<serde_json::Value>>(&content)
                    {
                        if cache_entry.expires_at < SystemTime::now() {
                            expired_count += 1;
                        }
                    }
                }
            }
        }

        Ok(CacheStats {
            total_size_bytes: total_size,
            entry_count,
            expired_count,
            cache_dir: self.config.cache_dir.clone(),
        })
    }

    /// Generate the file path for a cache key
    fn cache_path(&self, key: &str) -> PathBuf {
        // Use a hash of the key to avoid filesystem issues with special characters
        let key_hash = format!("{:x}", md5::compute(key.as_bytes()));
        self.config.cache_dir.join(format!("{}.json", key_hash))
    }
}

/// Cache statistics
#[derive(Debug, Clone)]
pub struct CacheStats {
    /// Total size of cache in bytes
    pub total_size_bytes: u64,

    /// Number of cache entries
    pub entry_count: u32,

    /// Number of expired entries
    pub expired_count: u32,

    /// Cache directory path
    pub cache_dir: PathBuf,
}