tfmcp 0.1.9

Terraform Model Context Protocol Tool - A CLI tool to manage Terraform through MCP
Documentation
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;

#[derive(Clone)]
struct CachedItem<T> {
    value: T,
    inserted_at: Instant,
}

/// Simple in-memory cache with TTL support
pub struct SimpleCache<T: Clone> {
    storage: Arc<RwLock<HashMap<String, CachedItem<T>>>>,
    ttl: Duration,
}

impl<T: Clone> SimpleCache<T> {
    pub fn new(ttl: Duration) -> Self {
        Self {
            storage: Arc::new(RwLock::new(HashMap::new())),
            ttl,
        }
    }

    /// Get value from cache if it exists and hasn't expired
    pub async fn get(&self, key: &str) -> Option<T> {
        let storage = self.storage.read().await;

        if let Some(item) = storage.get(key) {
            if item.inserted_at.elapsed() < self.ttl {
                return Some(item.value.clone());
            }
        }

        None
    }

    /// Set value in cache
    pub async fn set(&self, key: String, value: T) {
        let mut storage = self.storage.write().await;
        storage.insert(
            key,
            CachedItem {
                value,
                inserted_at: Instant::now(),
            },
        );
    }

    /// Remove expired entries from cache
    #[allow(dead_code)]
    pub async fn cleanup_expired(&self) {
        let mut storage = self.storage.write().await;
        storage.retain(|_, item| item.inserted_at.elapsed() < self.ttl);
    }

    /// Get cache statistics
    #[allow(dead_code)]
    pub async fn stats(&self) -> CacheStats {
        let storage = self.storage.read().await;
        let total_entries = storage.len();
        let valid_entries = storage
            .values()
            .filter(|item| item.inserted_at.elapsed() < self.ttl)
            .count();
        let expired_entries = total_entries - valid_entries;

        CacheStats {
            total_entries,
            valid_entries,
            expired_entries,
        }
    }

    /// Clear all cache entries
    #[allow(dead_code)]
    pub async fn clear(&self) {
        let mut storage = self.storage.write().await;
        storage.clear();
    }

    /// Get cache hit rate (requires tracking hits and misses)
    #[allow(dead_code)]
    pub async fn size(&self) -> usize {
        let storage = self.storage.read().await;
        storage.len()
    }
}

#[derive(Debug)]
pub struct CacheStats {
    pub total_entries: usize,
    pub valid_entries: usize,
    pub expired_entries: usize,
}

impl<T: Clone> Default for SimpleCache<T> {
    fn default() -> Self {
        Self::new(Duration::from_secs(300)) // 5 minutes default TTL
    }
}

/// Specialized cache for documentation content
pub type DocumentationCache = SimpleCache<String>;

/// Specialized cache for provider information
pub type ProvidersCache = SimpleCache<String>;

/// Cache manager that handles multiple cache types
pub struct CacheManager {
    pub documentation_cache: DocumentationCache,
    pub providers_cache: ProvidersCache,
}

impl CacheManager {
    pub fn new() -> Self {
        Self {
            documentation_cache: SimpleCache::new(Duration::from_secs(1800)), // 30 minutes for docs
            providers_cache: SimpleCache::new(Duration::from_secs(600)), // 10 minutes for providers
        }
    }

    /// Get comprehensive cache statistics
    #[allow(dead_code)]
    pub async fn global_stats(&self) -> HashMap<String, CacheStats> {
        let mut stats = HashMap::new();
        stats.insert(
            "documentation".to_string(),
            self.documentation_cache.stats().await,
        );
        stats.insert("providers".to_string(), self.providers_cache.stats().await);
        stats
    }

    /// Cleanup expired entries from all caches
    #[allow(dead_code)]
    pub async fn cleanup_all(&self) {
        self.documentation_cache.cleanup_expired().await;
        self.providers_cache.cleanup_expired().await;
    }

    /// Clear all caches
    #[allow(dead_code)]
    pub async fn clear_all(&self) {
        self.documentation_cache.clear().await;
        self.providers_cache.clear().await;
    }
}

impl Default for CacheManager {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tokio::time::{Duration, sleep};

    #[tokio::test]
    async fn test_cache_basic_operations() {
        let cache = SimpleCache::new(Duration::from_millis(100));

        // Test set and get
        cache.set("key1".to_string(), "value1".to_string()).await;
        assert_eq!(cache.get("key1").await, Some("value1".to_string()));

        // Test non-existent key
        assert_eq!(cache.get("nonexistent").await, None);
    }

    #[tokio::test]
    async fn test_cache_expiration() {
        let cache = SimpleCache::new(Duration::from_millis(50));

        cache.set("key1".to_string(), "value1".to_string()).await;
        assert_eq!(cache.get("key1").await, Some("value1".to_string()));

        // Wait for expiration
        sleep(Duration::from_millis(60)).await;
        assert_eq!(cache.get("key1").await, None);
    }
}