Skip to main content

st/
integration.rs

1//! Integration helpers for easier Smart Tree usage in other applications
2//!
3//! This module provides simplified APIs and helper functions that make it easier
4//! to integrate Smart Tree functionality into other applications without dealing
5//! with all the low-level details.
6
7use anyhow::Result;
8use serde::{Deserialize, Serialize};
9use std::path::{Path, PathBuf};
10use std::time::SystemTime;
11
12use crate::{detect_project_context, FileNode, Scanner, ScannerConfig, TreeStats};
13
14/// Simplified project analysis result optimized for external integrations
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ProjectAnalysis {
17    pub project_path: PathBuf,
18    pub project_type: String,
19    pub project_name: String,
20    pub total_files: usize,
21    pub total_directories: usize,
22    pub total_size: u64,
23    pub key_files: Vec<String>,
24    pub recent_files: Vec<String>,
25    pub file_types: std::collections::HashMap<String, usize>,
26    pub insights: Vec<String>,
27}
28
29/// Quick project analyzer for integration use cases
30pub struct ProjectAnalyzer {
31    default_config: ScannerConfig,
32}
33
34impl Default for ProjectAnalyzer {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl ProjectAnalyzer {
41    /// Create a new project analyzer with sensible defaults for integration
42    pub fn new() -> Self {
43        let config = ScannerConfig {
44            max_depth: 10,
45            show_hidden: false,
46            respect_gitignore: true,
47            ..ScannerConfig::default()
48        };
49
50        Self {
51            default_config: config,
52        }
53    }
54
55    /// Analyze a project directory and return simplified results
56    pub fn analyze_project(&self, project_path: &Path) -> Result<ProjectAnalysis> {
57        let scanner = Scanner::new(project_path, self.default_config.clone())?;
58        let (nodes, stats) = scanner.scan()?;
59
60        // Detect project type
61        let project_type =
62            detect_project_context(project_path).unwrap_or_else(|| "Unknown".to_string());
63
64        // Get project name from directory
65        let project_name = project_path
66            .file_name()
67            .and_then(|n| n.to_str())
68            .unwrap_or("Unknown")
69            .to_string();
70
71        // Find key files
72        let key_files = Self::extract_key_files(&nodes);
73
74        // Find recent files (last hour)
75        let recent_files = Self::find_recent_files(&nodes, 1);
76
77        // Analyze file types
78        let file_types = Self::analyze_file_types(&nodes);
79
80        // Generate insights
81        let insights = Self::generate_insights(&stats, &project_type, &nodes);
82
83        Ok(ProjectAnalysis {
84            project_path: project_path.to_path_buf(),
85            project_type,
86            project_name,
87            total_files: stats.total_files as usize,
88            total_directories: stats.total_dirs as usize,
89            total_size: stats.total_size,
90            key_files,
91            recent_files,
92            file_types,
93            insights,
94        })
95    }
96
97    /// Quick analysis for dashboard use - limited depth and faster execution
98    pub fn quick_analysis(&self, project_path: &Path) -> Result<ProjectAnalysis> {
99        let mut config = self.default_config.clone();
100        config.max_depth = 2; // Very shallow for quick analysis
101
102        let scanner = Scanner::new(project_path, config)?;
103        let (_nodes, stats) = scanner.quick_scan()?;
104
105        let project_type =
106            detect_project_context(project_path).unwrap_or_else(|| "Unknown".to_string());
107
108        let project_name = project_path
109            .file_name()
110            .and_then(|n| n.to_str())
111            .unwrap_or("Unknown")
112            .to_string();
113
114        Ok(ProjectAnalysis {
115            project_path: project_path.to_path_buf(),
116            project_type: project_type.clone(),
117            project_name,
118            total_files: stats.total_files as usize,
119            total_directories: stats.total_dirs as usize,
120            total_size: stats.total_size,
121            key_files: vec![],                            // Skip for quick analysis
122            recent_files: vec![],                         // Skip for quick analysis
123            file_types: std::collections::HashMap::new(), // Skip for quick analysis
124            insights: vec![format!(
125                "{} project with {} files",
126                project_type, stats.total_files
127            )],
128        })
129    }
130
131    /// Find files modified within the last N hours
132    pub fn find_recent_activity(&self, project_path: &Path, hours: u64) -> Result<Vec<FileNode>> {
133        let scanner = Scanner::new(project_path, self.default_config.clone())?;
134        scanner.find_recent_files(hours)
135    }
136
137    /// Get only key project files for quick overview
138    pub fn get_key_files(&self, project_path: &Path) -> Result<Vec<FileNode>> {
139        let scanner = Scanner::new(project_path, self.default_config.clone())?;
140        scanner.find_key_files()
141    }
142
143    // Helper methods
144    fn extract_key_files(nodes: &[FileNode]) -> Vec<String> {
145        let important_patterns = [
146            "main.rs",
147            "lib.rs",
148            "mod.rs",
149            "package.json",
150            "Cargo.toml",
151            "requirements.txt",
152            "pyproject.toml",
153            "README.md",
154            "LICENSE",
155            "Makefile",
156            "CMakeLists.txt",
157            "index.js",
158            "app.js",
159            "server.js",
160            "main.js",
161            "main.py",
162            "__init__.py",
163            "setup.py",
164            "go.mod",
165            "main.go",
166        ];
167
168        let mut key_files = Vec::new();
169        for node in nodes {
170            if !node.is_dir {
171                let file_name = node.path.file_name().and_then(|n| n.to_str()).unwrap_or("");
172
173                for pattern in &important_patterns {
174                    if file_name == *pattern {
175                        key_files.push(node.path.to_string_lossy().to_string());
176                        break;
177                    }
178                }
179            }
180        }
181
182        key_files.sort();
183        key_files.dedup();
184        key_files
185    }
186
187    fn find_recent_files(nodes: &[FileNode], hours_ago: u64) -> Vec<String> {
188        let cutoff_time = SystemTime::now() - std::time::Duration::from_secs(hours_ago * 3600);
189
190        nodes
191            .iter()
192            .filter(|node| !node.is_dir && node.modified > cutoff_time)
193            .map(|node| node.path.to_string_lossy().to_string())
194            .collect()
195    }
196
197    fn analyze_file_types(nodes: &[FileNode]) -> std::collections::HashMap<String, usize> {
198        let mut types = std::collections::HashMap::new();
199
200        for node in nodes {
201            if !node.is_dir {
202                let category = format!("{:?}", node.category);
203                *types.entry(category).or_insert(0) += 1;
204            }
205        }
206
207        types
208    }
209
210    fn generate_insights(stats: &TreeStats, project_type: &str, nodes: &[FileNode]) -> Vec<String> {
211        let mut insights = Vec::new();
212
213        // Size insights
214        if stats.total_files > 1000 {
215            insights.push("Large codebase with extensive structure".to_string());
216        } else if stats.total_files > 100 {
217            insights.push("Medium-sized project".to_string());
218        } else {
219            insights.push("Focused project with concise structure".to_string());
220        }
221
222        // Technology insights
223        insights.push(format!("{} project", project_type));
224
225        // Feature detection
226        let has_tests = nodes.iter().any(|n| {
227            let path_str = n.path.to_string_lossy();
228            path_str.contains("test") || path_str.contains("spec")
229        });
230        if has_tests {
231            insights.push("Includes test suite".to_string());
232        }
233
234        let has_docs = nodes.iter().any(|n| {
235            let path_str = n.path.to_string_lossy();
236            path_str.contains("README") || path_str.contains("doc")
237        });
238        if has_docs {
239            insights.push("Well-documented project".to_string());
240        }
241
242        insights
243    }
244}
245
246/// Convenience function for one-off project analysis
247pub fn analyze_project(project_path: &Path) -> Result<ProjectAnalysis> {
248    let analyzer = ProjectAnalyzer::new();
249    analyzer.analyze_project(project_path)
250}
251
252/// Convenience function for quick project overview
253pub fn quick_project_overview(project_path: &Path) -> Result<String> {
254    let analyzer = ProjectAnalyzer::new();
255    let analysis = analyzer.quick_analysis(project_path)?;
256
257    Ok(format!(
258        "{} | {} ({} files, {} dirs)",
259        analysis.project_name,
260        analysis.project_type,
261        analysis.total_files,
262        analysis.total_directories
263    ))
264}