flatten_rust/
config.rs

1//! Модуль для управления шаблонами исключений.
2//!
3//! Предоставляет функциональность для загрузки, кэширования и обновления
4//! шаблонов в формате gitignore из внешнего API (toptal.com).
5//! Управление конфигурацией и кэшем происходит в директории `~/.flatten/`.
6
7use 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/// Конфигурация менеджера шаблонов.
16#[derive(Debug, Serialize, Deserialize, Clone)]
17#[serde(default)]
18pub struct ManagerConfig {
19    /// Временная метка последнего обновления в секундах (Unix time).
20    pub last_updated: u64,
21    /// Продолжительность хранения кэша в секундах.
22    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, // 24 часа
30        }
31    }
32}
33
34/// Представление шаблона исключений.
35#[derive(Debug, Serialize, Deserialize, Clone)]
36pub struct Template {
37    /// Уникальный ключ шаблона (например, "rust").
38    pub key: String,
39    /// Имя шаблона.
40    pub name: String,
41    /// Содержимое шаблона (в формате gitignore).
42    pub contents: String,
43}
44
45/// Вспомогательная структура для парсинга ответа от Toptal API.
46#[derive(Debug, Deserialize)]
47struct ToptalEntry {
48    name: String,
49    #[serde(default)]
50    contents: String,
51}
52
53/// Управляет получением, кэшированием и доступом к шаблонам исключений.
54#[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    /// Создает новый экземпляр `TemplateManager`.
64    ///
65    /// Инициализирует пути, загружает конфигурацию и кэшированные шаблоны.
66    ///
67    /// # Ошибки
68    /// Возвращает ошибку, если не удается определить домашнюю директорию
69    /// или создать/прочитать файлы конфигурации.
70    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    /// Загружает конфигурацию из файла или создает новую, если файл отсутствует.
93    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            // Если конфиг поврежден, используем дефолтный, не падаем
98            self.config = serde_json::from_str(&content).unwrap_or_default();
99        } else {
100            self.save_config()?;
101        }
102        Ok(())
103    }
104
105    /// Сохраняет текущую конфигурацию в файл.
106    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    /// Загружает кэшированные шаблоны из файла.
114    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            // Если кэш поврежден, инициализируем пустой картой
119            self.templates = serde_json::from_str(&content).unwrap_or_default();
120        }
121        Ok(())
122    }
123
124    /// Сохраняет текущий набор шаблонов в кэш-файл.
125    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    /// Проверяет, истек ли срок действия кэша шаблонов.
133    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        // Если шаблонов нет совсем, обновление обязательно нужно
140        if self.templates.is_empty() {
141            return true;
142        }
143
144        // Иначе проверяем TTL
145        current_time.saturating_sub(self.config.last_updated) > self.config.cache_duration
146    }
147
148    /// Загружает шаблоны из API, если кэш устарел или отсутствует.
149    ///
150    /// # Логика обновления
151    /// 1. Если кэш актуален -> ничего не делаем.
152    /// 2. Если кэш устарел, пробуем скачать.
153    /// 3. Если скачивание не удалось, но есть старый кэш -> используем его (soft fail).
154    /// 4. Если шаблонов нет вообще и скачивание не удалось -> возвращаем ошибку.
155    pub async fn update_if_needed(&mut self) -> Result<()> {
156        if !self.needs_update() {
157            return Ok(());
158        }
159
160        // Пытаемся обновить
161        match self.fetch_templates().await {
162            Ok(()) => {
163                // Успех, ничего не пишем в консоль, чтобы не спамить
164            },
165            Err(e) => {
166                if self.templates.is_empty() {
167                    // Критическая ошибка: нет ни кэша, ни сети
168                    return Err(e.context("Failed to fetch initial templates and cache is empty"));
169                } else {
170                    // Не критично, используем кэш. Ошибку сети можно было бы залогировать в debug.
171                    // Для пользователя это должно быть прозрачно.
172                }
173            }
174        }
175        Ok(())
176    }
177    
178    /// Принудительно обновляет шаблоны из API.
179    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                // При явном обновлении ошибку нужно показать
188                Err(e)
189            }
190        }
191    }
192
193    /// Получает шаблоны из API toptal.com.
194    ///
195    /// Использует endpoint `list?format=json`, который возвращает полный список
196    /// шаблонов с их содержимым, что позволяет избежать N+1 запросов.
197    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        // Парсим ответ как HashMap, где ключ - ID шаблона, значение - структура с содержимым
208        // Это решает проблему неправильного парсинга строк
209        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        // Конвертируем во внутренний формат
219        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        // Обновляем метку времени только при успехе
234        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    /// Возвращает список ключей всех доступных шаблонов.
245    pub fn get_available_templates(&self) -> Vec<String> {
246        self.templates.keys().cloned().collect()
247    }
248
249    /// Возвращает содержимое шаблона по его ключу.
250    pub fn get_template_contents(&self, key: &str) -> Option<&str> {
251        self.templates.get(key).map(|t| t.contents.as_str())
252    }
253}