flatten_rust/
config.rs

1//! Gitignore templates management module
2//!
3//! This module provides functionality for managing gitignore templates
4//! from the toptal.com API with caching and user overrides.
5
6use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12/// Configuration for gitignore templates
13#[derive(Debug, Serialize, Deserialize, Clone)]
14pub struct GitignoreConfig {
15    /// Last update timestamp
16    pub last_updated: u64,
17    /// Cache duration in seconds (default: 24 hours)
18    pub cache_duration: u64,
19    /// Custom user overrides
20    pub user_overrides: HashMap<String, Vec<String>>,
21    /// Whether to check for internet connectivity
22    pub check_internet: bool,
23}
24
25impl Default for GitignoreConfig {
26    fn default() -> Self {
27        Self {
28            last_updated: 0,
29            cache_duration: 86400, // 24 hours
30            user_overrides: HashMap::new(),
31            check_internet: true,
32        }
33    }
34}
35
36/// Gitignore template data
37#[derive(Debug, Serialize, Deserialize, Clone)]
38pub struct GitignoreTemplate {
39    pub key: String,
40    pub name: String,
41    pub contents: String,
42    pub file_name: String,
43}
44
45/// Gitignore templates manager
46pub struct GitignoreManager {
47    config_path: PathBuf,
48    templates_path: PathBuf,
49    config: GitignoreConfig,
50    templates: HashMap<String, GitignoreTemplate>,
51}
52
53impl GitignoreManager {
54    /// Creates a new GitignoreManager instance
55    ///
56    /// # Errors
57    /// Returns an error if the home directory cannot be determined or
58    /// if the configuration files cannot be created/loaded
59    pub fn new() -> Result<Self> {
60        let home_dir = dirs::home_dir()
61            .context("Could not determine home directory")?;
62        let flatten_dir = home_dir.join(".flatten");
63        
64        // Create .flatten directory if it doesn't exist
65        if !flatten_dir.exists() {
66            std::fs::create_dir_all(&flatten_dir)
67                .context("Failed to create .flatten directory")?;
68        }
69        
70        let config_path = flatten_dir.join("config.json");
71        let templates_path = flatten_dir.join("templates.json");
72        
73        let mut manager = Self {
74            config_path,
75            templates_path,
76            config: GitignoreConfig::default(),
77            templates: HashMap::new(),
78        };
79        
80        // Load existing config or create default
81        manager.load_config()?;
82        
83        // Load templates
84        manager.load_templates()?;
85        
86        Ok(manager)
87    }
88    
89    /// Check if internet connectivity is available
90    ///
91    /// # Examples
92    /// ```no_run
93    /// use flatten_rust::config::GitignoreManager;
94    /// 
95    /// # async fn example() -> anyhow::Result<()> {
96    /// let manager = GitignoreManager::new()?;
97    /// if manager.check_internet_connectivity().await {
98    ///     println!("Internet is available");
99    /// }
100    /// # Ok(())
101    /// # }
102    /// ```
103    pub async fn check_internet_connectivity(&self) -> bool {
104        // Try to connect to a reliable, fast service
105        let client = reqwest::Client::builder()
106            .timeout(std::time::Duration::from_secs(5))
107            .build();
108            
109        match client {
110            Ok(client) => {
111                match client.get("https://www.google.com/generate_204").send().await {
112                    Ok(response) => response.status().is_success(),
113                    Err(_) => false,
114                }
115            }
116            Err(_) => false,
117        }
118    }
119    
120    /// Load configuration from file
121    fn load_config(&mut self) -> Result<()> {
122        if self.config_path.exists() {
123            let content = std::fs::read_to_string(&self.config_path)
124                .context("Failed to read config file")?;
125            self.config = serde_json::from_str(&content)
126                .context("Failed to parse config file")?;
127        } else {
128            // Save default config
129            self.save_config()?;
130        }
131        Ok(())
132    }
133    
134    /// Save configuration to file
135    fn save_config(&self) -> Result<()> {
136        let content = serde_json::to_string_pretty(&self.config)
137            .context("Failed to serialize config")?;
138        std::fs::write(&self.config_path, content)
139            .context("Failed to write config file")?;
140        Ok(())
141    }
142    
143    /// Load templates from file
144    fn load_templates(&mut self) -> Result<()> {
145        if self.templates_path.exists() {
146            let content = std::fs::read_to_string(&self.templates_path)
147                .context("Failed to read templates file")?;
148            self.templates = serde_json::from_str(&content)
149                .context("Failed to parse templates file")?;
150        }
151        Ok(())
152    }
153    
154    /// Save templates to file
155    fn save_templates(&self) -> Result<()> {
156        let content = serde_json::to_string_pretty(&self.templates)
157            .context("Failed to serialize templates")?;
158        std::fs::write(&self.templates_path, content)
159            .context("Failed to write templates file")?;
160        Ok(())
161    }
162    
163    /// Check if templates need update
164    fn needs_update(&self) -> bool {
165        let current_time = SystemTime::now()
166            .duration_since(UNIX_EPOCH)
167            .unwrap_or_default()
168            .as_secs();
169        
170        current_time.saturating_sub(self.config.last_updated) > self.config.cache_duration
171    }
172    
173    /// Fetch templates from API
174    async fn fetch_templates(&mut self) -> Result<()> {
175        let client = reqwest::Client::new();
176        
177        // Get list of available templates
178        let list_url = "https://www.toptal.com/developers/gitignore/api/list?format=json";
179        let list_response = client.get(list_url)
180            .send()
181            .await
182            .context("Failed to fetch template list")?;
183            
184        let template_list: HashMap<String, serde_json::Value> = list_response
185            .json()
186            .await
187            .context("Failed to parse template list")?;
188        
189        // Fetch each template
190        for (key, _) in template_list {
191            let template_url = format!("https://www.toptal.com/developers/gitignore/api/{}", key);
192            
193            match client.get(&template_url).send().await {
194                Ok(response) => {
195                    if let Ok(content) = response.text().await {
196                        let template = GitignoreTemplate {
197                            key: key.clone(),
198                            name: key.clone(), // Simple name for now
199                            contents: content,
200                            file_name: format!("{}.gitignore", key),
201                        };
202                        self.templates.insert(key, template);
203                    }
204                }
205                Err(e) => {
206                    eprintln!("Warning: Failed to fetch template {}: {}", key, e);
207                }
208            }
209            
210            // Small delay to be respectful to the API
211            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
212        }
213        
214        // Update timestamp
215        self.config.last_updated = SystemTime::now()
216            .duration_since(UNIX_EPOCH)
217            .unwrap_or_default()
218            .as_secs();
219        
220        // Save updated templates and config
221        self.save_templates()?;
222        self.save_config()?;
223        
224        Ok(())
225    }
226    
227    /// Update templates if needed
228    ///
229    /// # Examples
230    /// ```no_run
231    /// use flatten_rust::config::GitignoreManager;
232    /// 
233    /// # async fn example() -> anyhow::Result<()> {
234    /// let mut manager = GitignoreManager::new()?;
235    /// manager.update_if_needed().await?;
236    /// # Ok(())
237    /// # }
238    /// ```
239    pub async fn update_if_needed(&mut self) -> Result<()> {
240        if self.needs_update() {
241            println!("🔄 Updating gitignore templates...");
242            
243            // Check internet connectivity if configured
244            if self.config.check_internet && !self.check_internet_connectivity().await {
245                println!("⚠️  No internet connection available, using cached templates");
246                return Ok(());
247            }
248            
249            if let Err(e) = self.fetch_templates().await {
250                eprintln!("Warning: Failed to update templates: {}", e);
251                // Continue with cached templates if available
252                if self.templates.is_empty() {
253                    return Err(e);
254                }
255            } else {
256                println!("✅ Templates updated successfully");
257            }
258        }
259        Ok(())
260    }
261    
262    /// Get all available template keys
263    pub fn get_available_templates(&self) -> Vec<&str> {
264        self.templates.keys().map(|k| k.as_str()).collect()
265    }
266    
267    /// Get patterns for specific templates
268    pub fn get_patterns_for_templates(&self, template_keys: &[String]) -> Vec<String> {
269        let mut patterns = Vec::new();
270        
271        for key in template_keys {
272            if let Some(template) = self.templates.get(key) {
273                // Parse gitignore patterns from template content
274                let template_patterns = self.parse_gitignore_patterns(&template.contents);
275                patterns.extend(template_patterns);
276            }
277        }
278        
279        // Add user overrides
280        for override_patterns in self.config.user_overrides.values() {
281            patterns.extend(override_patterns.clone());
282        }
283        
284        patterns
285    }
286    
287    /// Parse gitignore patterns from template content
288    fn parse_gitignore_patterns(&self, content: &str) -> Vec<String> {
289        content
290            .lines()
291            .filter(|line| {
292                // Skip empty lines and comments
293                !line.trim().is_empty() && !line.trim().starts_with('#') && !line.trim().starts_with("###")
294            })
295            .map(|line| line.trim().to_string())
296            .collect()
297    }
298    
299    
300    
301    /// Set internet connectivity checking
302    ///
303    /// # Arguments
304    /// * `check` - Whether to check for internet connectivity
305    ///
306    /// # Examples
307    /// ```no_run
308    /// use flatten_rust::config::GitignoreManager;
309    /// 
310    /// # fn main() -> anyhow::Result<()> {
311    /// let mut manager = GitignoreManager::new()?;
312    /// manager.set_check_internet(false)?; // Disable internet checks
313    /// # Ok(())
314    /// # }
315    /// ```
316    pub fn set_check_internet(&mut self, check: bool) -> Result<()> {
317        self.config.check_internet = check;
318        self.save_config()?;
319        Ok(())
320    }
321    
322    /// Force update templates
323    ///
324    /// # Examples
325    /// ```no_run
326    /// use flatten_rust::config::GitignoreManager;
327    /// 
328    /// # async fn example() -> anyhow::Result<()> {
329    /// let mut manager = GitignoreManager::new()?;
330    /// manager.force_update().await?;
331    /// # Ok(())
332    /// # }
333    /// ```
334    pub async fn force_update(&mut self) -> Result<()> {
335        self.config.last_updated = 0; // Force update
336        self.update_if_needed().await
337    }
338}