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";
14const API_TEMPLATE_URL_BASE: &str = "https://www.toptal.com/developers/gitignore/api/";
15
16/// Конфигурация менеджера шаблонов.
17#[derive(Debug, Serialize, Deserialize, Clone)]
18#[serde(default)]
19pub struct ManagerConfig {
20    /// Временная метка последнего обновления в секундах (Unix time).
21    pub last_updated: u64,
22    /// Продолжительность хранения кэша в секундах.
23    pub cache_duration: u64,
24}
25
26impl Default for ManagerConfig {
27    fn default() -> Self {
28        Self {
29            last_updated: 0,
30            cache_duration: 86_400, // 24 часа
31        }
32    }
33}
34
35/// Представление шаблона исключений.
36#[derive(Debug, Serialize, Deserialize, Clone)]
37pub struct Template {
38    /// Уникальный ключ шаблона (например, "rust").
39    pub key: String,
40    /// Имя шаблона.
41    pub name: String,
42    /// Содержимое шаблона (в формате gitignore).
43    pub contents: String,
44}
45
46/// Управляет получением, кэшированием и доступом к шаблонам исключений.
47#[derive(Debug)]
48pub struct TemplateManager {
49    config_path: PathBuf,
50    templates_path: PathBuf,
51    config: ManagerConfig,
52    templates: HashMap<String, Template>,
53}
54
55impl TemplateManager {
56    /// Создает новый экземпляр `TemplateManager`.
57    ///
58    /// Инициализирует пути, загружает конфигурацию и кэшированные шаблоны.
59    ///
60    /// # Ошибки
61    /// Возвращает ошибку, если не удается определить домашнюю директорию
62    /// или создать/прочитать файлы конфигурации.
63    ///
64    /// # Examples
65    /// ```no_run
66    /// # use flatten_rust::config::TemplateManager;
67    /// # use anyhow::Result;
68    /// # async fn example() -> Result<()> {
69    /// let mut manager = TemplateManager::new()?;
70    /// manager.update_if_needed().await?;
71    /// # Ok(())
72    /// # }
73    /// ```
74    pub fn new() -> Result<Self> {
75        let home_dir = dirs::home_dir().context("Could not determine home directory")?;
76        let flatten_dir = home_dir.join(".flatten");
77
78        std::fs::create_dir_all(&flatten_dir).context("Failed to create .flatten directory")?;
79
80        let config_path = flatten_dir.join("manager_config.json");
81        let templates_path = flatten_dir.join("templates_cache.json");
82
83        let mut manager = Self {
84            config_path,
85            templates_path,
86            config: ManagerConfig::default(),
87            templates: HashMap::new(),
88        };
89
90        manager.load_config()?;
91        manager.load_templates()?;
92
93        Ok(manager)
94    }
95
96    /// Загружает конфигурацию из файла или создает новую, если файл отсутствует.
97    fn load_config(&mut self) -> Result<()> {
98        if self.config_path.exists() {
99            let content = std::fs::read_to_string(&self.config_path)
100                .context("Failed to read config file")?;
101            self.config =
102                serde_json::from_str(&content).context("Failed to parse config file")?;
103        } else {
104            self.save_config()?;
105        }
106        Ok(())
107    }
108
109    /// Сохраняет текущую конфигурацию в файл.
110    fn save_config(&self) -> Result<()> {
111        let content =
112            serde_json::to_string_pretty(&self.config).context("Failed to serialize config")?;
113        std::fs::write(&self.config_path, content).context("Failed to write config file")?;
114        Ok(())
115    }
116
117    /// Загружает кэшированные шаблоны из файла.
118    fn load_templates(&mut self) -> Result<()> {
119        if self.templates_path.exists() {
120            let content = std::fs::read_to_string(&self.templates_path)
121                .context("Failed to read templates file")?;
122            self.templates =
123                serde_json::from_str(&content).context("Failed to parse templates file")?;
124        }
125        Ok(())
126    }
127
128    /// Сохраняет текущий набор шаблонов в кэш-файл.
129    fn save_templates(&self) -> Result<()> {
130        let content =
131            serde_json::to_string_pretty(&self.templates).context("Failed to serialize templates")?;
132        std::fs::write(&self.templates_path, content).context("Failed to write templates file")?;
133        Ok(())
134    }
135
136    /// Проверяет, истек ли срок действия кэша шаблонов.
137    fn needs_update(&self) -> bool {
138        let current_time = SystemTime::now()
139            .duration_since(UNIX_EPOCH)
140            .expect("Time went backwards")
141            .as_secs();
142        current_time.saturating_sub(self.config.last_updated) > self.config.cache_duration
143    }
144
145    /// Загружает шаблоны из API, если кэш устарел.
146    pub async fn update_if_needed(&mut self) -> Result<()> {
147        if self.needs_update() || self.templates.is_empty() {
148            println!("🔄 Updating exclusion templates...");
149            if let Err(e) = self.fetch_templates().await {
150                eprintln!("Warning: Failed to update templates: {}. Using cached version if available.", e);
151            } else {
152                println!("✅ Templates updated successfully");
153            }
154        }
155        Ok(())
156    }
157    
158    /// Принудительно обновляет шаблоны из API.
159    pub async fn force_update(&mut self) -> Result<()> {
160        self.config.last_updated = 0; // Сброс времени для принудительного обновления
161        println!("🔄 Force updating exclusion templates...");
162        match self.fetch_templates().await {
163             Ok(()) => {
164                println!("✅ Templates updated successfully");
165                Ok(())
166             },
167             Err(e) => {
168                eprintln!("Error: Failed to update templates: {}", e);
169                Err(e)
170             }
171        }
172    }
173
174    /// Получает шаблоны из API toptal.com.
175    async fn fetch_templates(&mut self) -> Result<()> {
176        let client = reqwest::Client::new();
177        let list_response = client.get(API_LIST_URL).send().await?.text().await?;
178        let template_keys: Vec<&str> = list_response.lines().collect();
179
180        for key in template_keys {
181            let template_url = format!("{}{}", API_TEMPLATE_URL_BASE, key);
182            match client.get(&template_url).send().await {
183                Ok(response) => {
184                    if let Ok(content) = response.text().await {
185                        let template = Template {
186                            key: key.to_string(),
187                            name: key.to_string(),
188                            contents: content,
189                        };
190                        self.templates.insert(key.to_string(), template);
191                    }
192                }
193                Err(e) => {
194                    eprintln!("Warning: Failed to fetch template '{}': {}", key, e);
195                }
196            }
197        }
198
199        self.config.last_updated = SystemTime::now()
200            .duration_since(UNIX_EPOCH)
201            .expect("Time went backwards")
202            .as_secs();
203
204        self.save_templates()?;
205        self.save_config()?;
206        Ok(())
207    }
208
209    /// Возвращает список ключей всех доступных шаблонов.
210    pub fn get_available_templates(&self) -> Vec<String> {
211        self.templates.keys().cloned().collect()
212    }
213
214    /// Возвращает содержимое шаблона по его ключу.
215    pub fn get_template_contents(&self, key: &str) -> Option<&str> {
216        self.templates.get(key).map(|t| t.contents.as_str())
217    }
218}