1use anyhow::{Context, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::PathBuf;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13const API_LIST_URL: &str = "https://www.toptal.com/developers/gitignore/api/list?format=json";
14
15#[derive(Debug, Serialize, Deserialize, Clone)]
17#[serde(default)]
18pub struct ManagerConfig {
19 pub last_updated: u64,
21 pub cache_duration: u64,
23}
24
25impl Default for ManagerConfig {
26 fn default() -> Self {
27 Self {
28 last_updated: 0,
29 cache_duration: 86_400, }
31 }
32}
33
34#[derive(Debug, Serialize, Deserialize, Clone)]
36pub struct Template {
37 pub key: String,
39 pub name: String,
41 pub contents: String,
43}
44
45#[derive(Debug, Deserialize)]
47struct ToptalEntry {
48 name: String,
49 #[serde(default)]
50 contents: String,
51}
52
53#[derive(Debug)]
55pub struct TemplateManager {
56 config_path: PathBuf,
57 templates_path: PathBuf,
58 config: ManagerConfig,
59 templates: HashMap<String, Template>,
60}
61
62impl TemplateManager {
63 pub fn new() -> Result<Self> {
71 let home_dir = dirs::home_dir().context("Could not determine home directory")?;
72 let flatten_dir = home_dir.join(".flatten");
73
74 std::fs::create_dir_all(&flatten_dir).context("Failed to create .flatten directory")?;
75
76 let config_path = flatten_dir.join("manager_config.json");
77 let templates_path = flatten_dir.join("templates_cache.json");
78
79 let mut manager = Self {
80 config_path,
81 templates_path,
82 config: ManagerConfig::default(),
83 templates: HashMap::new(),
84 };
85
86 manager.load_config()?;
87 manager.load_templates()?;
88
89 Ok(manager)
90 }
91
92 fn load_config(&mut self) -> Result<()> {
94 if self.config_path.exists() {
95 let content = std::fs::read_to_string(&self.config_path)
96 .context("Failed to read config file")?;
97 self.config = serde_json::from_str(&content).unwrap_or_default();
99 } else {
100 self.save_config()?;
101 }
102 Ok(())
103 }
104
105 fn save_config(&self) -> Result<()> {
107 let content =
108 serde_json::to_string_pretty(&self.config).context("Failed to serialize config")?;
109 std::fs::write(&self.config_path, content).context("Failed to write config file")?;
110 Ok(())
111 }
112
113 fn load_templates(&mut self) -> Result<()> {
115 if self.templates_path.exists() {
116 let content = std::fs::read_to_string(&self.templates_path)
117 .context("Failed to read templates file")?;
118 self.templates = serde_json::from_str(&content).unwrap_or_default();
120 }
121 Ok(())
122 }
123
124 fn save_templates(&self) -> Result<()> {
126 let content =
127 serde_json::to_string_pretty(&self.templates).context("Failed to serialize templates")?;
128 std::fs::write(&self.templates_path, content).context("Failed to write templates file")?;
129 Ok(())
130 }
131
132 fn needs_update(&self) -> bool {
134 let current_time = SystemTime::now()
135 .duration_since(UNIX_EPOCH)
136 .expect("Time went backwards")
137 .as_secs();
138
139 if self.templates.is_empty() {
141 return true;
142 }
143
144 current_time.saturating_sub(self.config.last_updated) > self.config.cache_duration
146 }
147
148 pub async fn update_if_needed(&mut self) -> Result<()> {
156 if !self.needs_update() {
157 return Ok(());
158 }
159
160 match self.fetch_templates().await {
162 Ok(()) => {
163 },
165 Err(e) => {
166 if self.templates.is_empty() {
167 return Err(e.context("Failed to fetch initial templates and cache is empty"));
169 } else {
170 }
173 }
174 }
175 Ok(())
176 }
177
178 pub async fn force_update(&mut self) -> Result<()> {
180 println!("🔄 Force updating exclusion templates from API...");
181 match self.fetch_templates().await {
182 Ok(_) => {
183 println!("✅ Templates updated successfully");
184 Ok(())
185 }
186 Err(e) => {
187 Err(e)
189 }
190 }
191 }
192
193 async fn fetch_templates(&mut self) -> Result<()> {
198 let client = reqwest::Client::builder()
199 .timeout(std::time::Duration::from_secs(15))
200 .build()?;
201
202 let response = client.get(API_LIST_URL)
203 .send()
204 .await
205 .context("Failed to connect to templates API")?;
206
207 let api_data: HashMap<String, ToptalEntry> = response
210 .json()
211 .await
212 .context("Failed to parse templates JSON")?;
213
214 if api_data.is_empty() {
215 return Err(anyhow::anyhow!("Received empty templates list from API"));
216 }
217
218 self.templates = api_data
220 .into_iter()
221 .map(|(key, entry)| {
222 (
223 key.clone(),
224 Template {
225 key,
226 name: entry.name,
227 contents: entry.contents,
228 },
229 )
230 })
231 .collect();
232
233 self.config.last_updated = SystemTime::now()
235 .duration_since(UNIX_EPOCH)
236 .expect("Time went backwards")
237 .as_secs();
238
239 self.save_templates()?;
240 self.save_config()?;
241 Ok(())
242 }
243
244 pub fn get_available_templates(&self) -> Vec<String> {
246 self.templates.keys().cloned().collect()
247 }
248
249 pub fn get_template_contents(&self, key: &str) -> Option<&str> {
251 self.templates.get(key).map(|t| t.contents.as_str())
252 }
253}