1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::path::PathBuf;
6
7#[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#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct CodePluginCache {
20 #[serde(rename = "irHash")]
22 pub ir_hash: String,
23
24 #[serde(rename = "fileHashes")]
27 pub file_hashes: HashMap<String, String>,
28
29 #[serde(rename = "cachedAt")]
31 pub cached_at: String,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct CodeCacheManifest {
37 pub version: String,
39
40 pub target: String,
42
43 pub toolchain: ToolchainVersions,
45
46 #[serde(rename = "updatedAt")]
48 pub updated_at: String,
49
50 pub plugins: HashMap<String, CodePluginCache>,
52}
53
54impl CodeCacheManifest {
55 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 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
85fn 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 let secs = now.as_secs();
95 let datetime = time_to_iso8601(secs);
96 datetime
97}
98
99fn 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 let days = secs / SECS_PER_DAY;
107 let remaining = secs % SECS_PER_DAY;
108
109 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 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
127pub 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
136pub 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
145pub 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
151pub 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
165pub 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
175pub 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}