Skip to main content

actr_cli/core/components/
cache_manager.rs

1//! Default CacheManager implementation
2//!
3//! Proto files are cached to the project's `protos/remote/` folder (not ~/.actr/cache)
4//! following the documentation spec for dependency management.
5
6use anyhow::Result;
7use async_trait::async_trait;
8use std::path::PathBuf;
9
10use super::{CacheManager, CacheStats, CachedProto, Fingerprint, ProtoFile};
11
12/// Default cache manager (file-based, project-local)
13///
14/// Caches proto files to `{project_root}/protos/remote/{service_name}/` directory
15/// following the documentation spec.
16pub struct DefaultCacheManager {
17    /// Project root directory (where Actr.toml is located)
18    project_root: PathBuf,
19}
20
21impl DefaultCacheManager {
22    pub fn new() -> Self {
23        Self {
24            project_root: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
25        }
26    }
27
28    pub fn with_project_root(project_root: PathBuf) -> Self {
29        Self { project_root }
30    }
31
32    /// Get the proto cache directory for a service
33    /// Returns: {project_root}/protos/remote/{service_name}/
34    fn get_service_proto_dir(&self, service_name: &str) -> PathBuf {
35        self.project_root
36            .join("protos")
37            .join("remote")
38            .join(service_name)
39    }
40}
41
42impl Default for DefaultCacheManager {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48#[async_trait]
49impl CacheManager for DefaultCacheManager {
50    async fn get_cached_proto(&self, service_name: &str) -> Result<Option<CachedProto>> {
51        let cache_path = self.get_service_proto_dir(service_name);
52
53        if !cache_path.exists() {
54            return Ok(None);
55        }
56
57        let mut files = Vec::new();
58        for entry in std::fs::read_dir(&cache_path)? {
59            let entry = entry?;
60            let path = entry.path();
61            if path.extension().map(|e| e == "proto").unwrap_or(false) {
62                let content = std::fs::read_to_string(&path)?;
63                files.push(ProtoFile {
64                    name: path.file_name().unwrap().to_string_lossy().to_string(),
65                    path,
66                    content,
67                    services: Vec::new(),
68                });
69            }
70        }
71
72        if files.is_empty() {
73            Ok(None)
74        } else {
75            Ok(Some(CachedProto {
76                files,
77                fingerprint: Fingerprint {
78                    algorithm: "sha256".to_string(),
79                    value: "cached".to_string(),
80                },
81                cached_at: std::time::SystemTime::now(),
82                expires_at: None,
83            }))
84        }
85    }
86
87    async fn cache_proto(&self, service_name: &str, files: &[ProtoFile]) -> Result<()> {
88        let cache_path = self.get_service_proto_dir(service_name);
89        std::fs::create_dir_all(&cache_path)?;
90
91        for file in files {
92            // Use the proto file name directly (e.g., echo.v1.proto)
93            let file_name = if file.name.ends_with(".proto") {
94                file.name.clone()
95            } else {
96                format!("{}.proto", file.name)
97            };
98            let file_path = cache_path.join(&file_name);
99            std::fs::write(&file_path, &file.content)?;
100            tracing::debug!(
101                "Cached proto file: {} -> {}",
102                file.name,
103                file_path.display()
104            );
105        }
106
107        tracing::info!(
108            "Cached {} proto files to protos/remote/{}/",
109            files.len(),
110            service_name
111        );
112        Ok(())
113    }
114
115    async fn invalidate_cache(&self, service_name: &str) -> Result<()> {
116        let cache_path = self.get_service_proto_dir(service_name);
117        if cache_path.exists() {
118            std::fs::remove_dir_all(&cache_path)?;
119        }
120        Ok(())
121    }
122
123    async fn clear_cache(&self) -> Result<()> {
124        let proto_dir = self.project_root.join("protos");
125        if proto_dir.exists() {
126            std::fs::remove_dir_all(&proto_dir)?;
127        }
128        Ok(())
129    }
130
131    async fn get_cache_stats(&self) -> Result<CacheStats> {
132        let proto_dir = self.project_root.join("protos");
133        let mut total_size = 0u64;
134        let mut entry_count = 0usize;
135
136        if proto_dir.exists() {
137            for entry in std::fs::read_dir(&proto_dir)? {
138                entry_count += 1;
139                let entry = entry?;
140                if entry.path().is_dir() {
141                    for file in std::fs::read_dir(entry.path())? {
142                        let file = file?;
143                        total_size += file.metadata()?.len();
144                    }
145                }
146            }
147        }
148
149        Ok(CacheStats {
150            total_entries: entry_count,
151            total_size_bytes: total_size,
152            hit_rate: 0.0,
153            miss_rate: 0.0,
154        })
155    }
156}