Skip to main content

hub_codegen/
cache.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::path::PathBuf;
6
7/// Toolchain version information for cache invalidation
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ToolchainVersions {
10    #[serde(rename = "synapse-cc")]
11    pub synapse_cc: String,
12    pub synapse: String,
13    #[serde(rename = "hub-codegen")]
14    pub hub_codegen: String,
15}
16
17/// Cache entry for a single plugin's generated code
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct CodePluginCache {
20    /// Hash of the IR that generated this code
21    #[serde(rename = "irHash")]
22    pub ir_hash: String,
23
24    /// Per-file hashes for granular change detection
25    /// Map of relative file path -> hash
26    #[serde(rename = "fileHashes")]
27    pub file_hashes: HashMap<String, String>,
28
29    /// ISO 8601 timestamp when this was cached
30    #[serde(rename = "cachedAt")]
31    pub cached_at: String,
32}
33
34/// Code cache manifest (written to hub-codegen/{target}/{backend}/manifest.json)
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct CodeCacheManifest {
37    /// Manifest format version
38    pub version: String,
39
40    /// Target language (typescript, python, rust)
41    pub target: String,
42
43    /// Toolchain versions for invalidation
44    pub toolchain: ToolchainVersions,
45
46    /// ISO 8601 timestamp when manifest was last updated
47    #[serde(rename = "updatedAt")]
48    pub updated_at: String,
49
50    /// Cache entries per plugin
51    pub plugins: HashMap<String, CodePluginCache>,
52}
53
54impl CodeCacheManifest {
55    /// Create a new cache manifest
56    pub fn new(target: String, toolchain: ToolchainVersions) -> Self {
57        Self {
58            version: "2.0".to_string(),
59            target,
60            toolchain,
61            updated_at: current_timestamp(),
62            plugins: HashMap::new(),
63        }
64    }
65
66    /// Add or update a plugin cache entry
67    pub fn add_plugin(
68        &mut self,
69        plugin_name: String,
70        ir_hash: String,
71        file_hashes: HashMap<String, String>,
72    ) {
73        self.plugins.insert(
74            plugin_name,
75            CodePluginCache {
76                ir_hash,
77                file_hashes,
78                cached_at: current_timestamp(),
79            },
80        );
81        self.updated_at = current_timestamp();
82    }
83}
84
85/// Get current ISO 8601 timestamp
86fn current_timestamp() -> String {
87    use std::time::SystemTime;
88
89    let now = SystemTime::now()
90        .duration_since(SystemTime::UNIX_EPOCH)
91        .expect("Time went backwards");
92
93    // Format as ISO 8601: YYYY-MM-DDTHH:MM:SSZ
94    let secs = now.as_secs();
95    let datetime = time_to_iso8601(secs);
96    datetime
97}
98
99/// Convert Unix timestamp to ISO 8601 format
100fn time_to_iso8601(secs: u64) -> String {
101    const SECS_PER_DAY: u64 = 86400;
102    const SECS_PER_HOUR: u64 = 3600;
103    const SECS_PER_MIN: u64 = 60;
104
105    // Days since Unix epoch
106    let days = secs / SECS_PER_DAY;
107    let remaining = secs % SECS_PER_DAY;
108
109    // Calculate date (simple approximation - good enough for cache timestamps)
110    let year = 1970 + (days / 365);
111    let day_of_year = days % 365;
112    let month = 1 + (day_of_year / 30);
113    let day = 1 + (day_of_year % 30);
114
115    // Calculate time
116    let hours = remaining / SECS_PER_HOUR;
117    let remaining = remaining % SECS_PER_HOUR;
118    let minutes = remaining / SECS_PER_MIN;
119    let seconds = remaining % SECS_PER_MIN;
120
121    format!(
122        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
123        year, month, day, hours, minutes, seconds
124    )
125}
126
127/// Get cache directory path: ~/.cache/plexus-codegen/hub-codegen/{target}/{backend}
128pub fn get_cache_dir(target: &str, backend: &str) -> Result<PathBuf> {
129    let home = std::env::var("HOME")
130        .or_else(|_| std::env::var("USERPROFILE"))
131        .map_err(|_| anyhow::anyhow!("Cannot determine home directory"))?;
132
133    Ok(get_cache_dir_under(&PathBuf::from(home), target, backend))
134}
135
136/// Get cache directory path under a specific root directory
137pub fn get_cache_dir_under(root: &PathBuf, target: &str, backend: &str) -> PathBuf {
138    root.join(".cache")
139        .join("plexus-codegen")
140        .join("hub-codegen")
141        .join(target)
142        .join(backend)
143}
144
145/// Read cache manifest from disk
146pub fn read_cache_manifest(target: &str, backend: &str) -> Result<CodeCacheManifest> {
147    let cache_dir = get_cache_dir(target, backend)?;
148    read_cache_manifest_from(&cache_dir)
149}
150
151/// Read cache manifest from a specific cache directory
152pub fn read_cache_manifest_from(cache_dir: &PathBuf) -> Result<CodeCacheManifest> {
153    let manifest_path = cache_dir.join("manifest.json");
154
155    if !manifest_path.exists() {
156        anyhow::bail!("Cache manifest not found at {}", manifest_path.display());
157    }
158
159    let content = fs::read_to_string(&manifest_path)?;
160    let manifest: CodeCacheManifest = serde_json::from_str(&content)?;
161
162    Ok(manifest)
163}
164
165/// Write cache manifest to disk
166pub fn write_cache_manifest(
167    target: &str,
168    backend: &str,
169    manifest: &CodeCacheManifest,
170) -> Result<()> {
171    let cache_dir = get_cache_dir(target, backend)?;
172    write_cache_manifest_to(&cache_dir, manifest)
173}
174
175/// Write cache manifest to a specific cache directory
176pub fn write_cache_manifest_to(
177    cache_dir: &PathBuf,
178    manifest: &CodeCacheManifest,
179) -> Result<()> {
180    fs::create_dir_all(cache_dir)?;
181
182    let manifest_path = cache_dir.join("manifest.json");
183    let content = serde_json::to_string_pretty(&manifest)?;
184    fs::write(&manifest_path, content)?;
185
186    Ok(())
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_new_manifest() {
195        let toolchain = ToolchainVersions {
196            synapse_cc: "0.1.0.0".to_string(),
197            synapse: "0.2.0.0".to_string(),
198            hub_codegen: "0.1.0".to_string(),
199        };
200
201        let manifest = CodeCacheManifest::new("typescript".to_string(), toolchain);
202
203        assert_eq!(manifest.version, "2.0");
204        assert_eq!(manifest.target, "typescript");
205        assert_eq!(manifest.plugins.len(), 0);
206    }
207
208    #[test]
209    fn test_add_plugin() {
210        let toolchain = ToolchainVersions {
211            synapse_cc: "0.1.0.0".to_string(),
212            synapse: "0.2.0.0".to_string(),
213            hub_codegen: "0.1.0".to_string(),
214        };
215
216        let mut manifest = CodeCacheManifest::new("typescript".to_string(), toolchain);
217
218        let mut file_hashes = HashMap::new();
219        file_hashes.insert("types.ts".to_string(), "abc123".to_string());
220        file_hashes.insert("methods.ts".to_string(), "def456".to_string());
221
222        manifest.add_plugin("cone".to_string(), "ir_hash_123".to_string(), file_hashes);
223
224        assert_eq!(manifest.plugins.len(), 1);
225        assert!(manifest.plugins.contains_key("cone"));
226
227        let plugin = &manifest.plugins["cone"];
228        assert_eq!(plugin.ir_hash, "ir_hash_123");
229        assert_eq!(plugin.file_hashes.len(), 2);
230        assert_eq!(plugin.file_hashes["types.ts"], "abc123");
231    }
232
233    #[test]
234    fn test_serialization() {
235        let toolchain = ToolchainVersions {
236            synapse_cc: "0.1.0.0".to_string(),
237            synapse: "0.2.0.0".to_string(),
238            hub_codegen: "0.1.0".to_string(),
239        };
240
241        let manifest = CodeCacheManifest::new("typescript".to_string(), toolchain);
242
243        let json = serde_json::to_string_pretty(&manifest).unwrap();
244        let deserialized: CodeCacheManifest = serde_json::from_str(&json).unwrap();
245
246        assert_eq!(deserialized.version, "2.0");
247        assert_eq!(deserialized.target, "typescript");
248    }
249}