openapi_from_source/
detector.rs

1use crate::cli::Framework;
2use crate::parser::ParsedFile;
3use log::debug;
4use std::collections::HashSet;
5use syn::{Item, UseTree};
6
7/// Framework detector for identifying web frameworks used in a Rust project.
8///
9/// The `FrameworkDetector` analyzes parsed Rust files to automatically detect which
10/// web frameworks are being used. It does this by examining `use` statements for
11/// framework-specific imports.
12///
13/// Currently supports detection of:
14/// - Axum (via `use axum::...`)
15/// - Actix-Web (via `use actix_web::...`)
16pub struct FrameworkDetector;
17
18/// Result of framework detection.
19///
20/// Contains the list of all detected web frameworks in the project.
21pub struct DetectionResult {
22    /// List of detected frameworks
23    pub frameworks: Vec<Framework>,
24}
25
26impl FrameworkDetector {
27    /// Detects web frameworks used in the provided parsed files.
28    ///
29    /// This method scans all `use` statements in the parsed files to identify
30    /// framework imports. Multiple frameworks can be detected if the project
31    /// uses more than one.
32    ///
33    /// # Arguments
34    ///
35    /// * `parsed_files` - Slice of successfully parsed Rust files to analyze
36    ///
37    /// # Returns
38    ///
39    /// Returns a `DetectionResult` containing all detected frameworks.
40    ///
41    /// # Example
42    ///
43    /// ```no_run
44    /// use openapi_from_source::detector::FrameworkDetector;
45    /// use openapi_from_source::parser::AstParser;
46    /// use std::path::Path;
47    ///
48    /// let parsed = AstParser::parse_file(Path::new("src/main.rs")).unwrap();
49    /// let result = FrameworkDetector::detect(&[parsed]);
50    /// println!("Detected {} framework(s)", result.frameworks.len());
51    /// ```
52    pub fn detect(parsed_files: &[ParsedFile]) -> DetectionResult {
53        debug!("Detecting frameworks in {} files", parsed_files.len());
54        
55        let mut detected_frameworks = HashSet::new();
56        
57        for parsed_file in parsed_files {
58            // Check each item in the syntax tree
59            for item in &parsed_file.syntax_tree.items {
60                if let Item::Use(use_item) = item {
61                    Self::check_use_tree(&use_item.tree, &mut detected_frameworks);
62                }
63            }
64        }
65        
66        let frameworks: Vec<Framework> = detected_frameworks.into_iter().collect();
67        debug!("Detected frameworks: {:?}", frameworks);
68        
69        DetectionResult { frameworks }
70    }
71    
72    /// Recursively check use tree for framework imports
73    fn check_use_tree(tree: &UseTree, detected: &mut HashSet<Framework>) {
74        match tree {
75            UseTree::Path(path) => {
76                let ident = path.ident.to_string();
77                
78                // Check for axum
79                if ident == "axum" {
80                    detected.insert(Framework::Axum);
81                }
82                
83                // Check for actix_web
84                if ident == "actix_web" {
85                    detected.insert(Framework::ActixWeb);
86                }
87                
88                // Recursively check the rest of the path
89                Self::check_use_tree(&path.tree, detected);
90            }
91            UseTree::Group(group) => {
92                // Check all items in the group
93                for item in &group.items {
94                    Self::check_use_tree(item, detected);
95                }
96            }
97            UseTree::Rename(rename) => {
98                // Check the original name
99                let ident = rename.ident.to_string();
100                if ident == "axum" {
101                    detected.insert(Framework::Axum);
102                }
103                if ident == "actix_web" {
104                    detected.insert(Framework::ActixWeb);
105                }
106            }
107            UseTree::Name(name) => {
108                // Check the name
109                let ident = name.ident.to_string();
110                if ident == "axum" {
111                    detected.insert(Framework::Axum);
112                }
113                if ident == "actix_web" {
114                    detected.insert(Framework::ActixWeb);
115                }
116            }
117            UseTree::Glob(_) => {
118                // Glob imports don't help us identify the framework
119            }
120        }
121    }
122}
123
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::parser::AstParser;
129    use std::fs;
130    use std::io::Write;
131    use tempfile::TempDir;
132
133    /// Helper function to create a temporary file with content
134    fn create_temp_file(dir: &TempDir, name: &str, content: &str) -> std::path::PathBuf {
135        let file_path = dir.path().join(name);
136        let mut file = fs::File::create(&file_path).unwrap();
137        file.write_all(content.as_bytes()).unwrap();
138        file_path
139    }
140
141    /// Helper function to parse a file and return ParsedFile
142    fn parse_test_file(dir: &TempDir, name: &str, content: &str) -> ParsedFile {
143        let file_path = create_temp_file(dir, name, content);
144        AstParser::parse_file(&file_path).unwrap()
145    }
146
147    #[test]
148    fn test_detect_axum_framework() {
149        let temp_dir = TempDir::new().unwrap();
150        
151        let axum_code = r#"
152            use axum::{Router, routing::get};
153            use axum::extract::Path;
154            
155            pub async fn hello() -> &'static str {
156                "Hello, World!"
157            }
158            
159            pub fn app() -> Router {
160                Router::new().route("/", get(hello))
161            }
162        "#;
163        
164        let parsed = parse_test_file(&temp_dir, "axum.rs", axum_code);
165        let result = FrameworkDetector::detect(&[parsed]);
166        
167        assert_eq!(result.frameworks.len(), 1);
168        assert!(result.frameworks.contains(&Framework::Axum));
169    }
170
171    #[test]
172    fn test_detect_actix_web_framework() {
173        let temp_dir = TempDir::new().unwrap();
174        
175        let actix_code = r#"
176            use actix_web::{web, App, HttpResponse, HttpServer};
177            use actix_web::middleware::Logger;
178            
179            #[actix_web::get("/")]
180            async fn hello() -> HttpResponse {
181                HttpResponse::Ok().body("Hello, World!")
182            }
183            
184            pub fn main() {
185                HttpServer::new(|| {
186                    App::new().service(hello)
187                })
188            }
189        "#;
190        
191        let parsed = parse_test_file(&temp_dir, "actix.rs", actix_code);
192        let result = FrameworkDetector::detect(&[parsed]);
193        
194        assert_eq!(result.frameworks.len(), 1);
195        assert!(result.frameworks.contains(&Framework::ActixWeb));
196    }
197
198    #[test]
199    fn test_detect_mixed_frameworks() {
200        let temp_dir = TempDir::new().unwrap();
201        
202        let axum_code = r#"
203            use axum::Router;
204            
205            pub fn axum_app() -> Router {
206                Router::new()
207            }
208        "#;
209        
210        let actix_code = r#"
211            use actix_web::{web, App};
212            
213            pub fn actix_app() -> App {
214                App::new()
215            }
216        "#;
217        
218        let parsed_axum = parse_test_file(&temp_dir, "axum.rs", axum_code);
219        let parsed_actix = parse_test_file(&temp_dir, "actix.rs", actix_code);
220        
221        let result = FrameworkDetector::detect(&[parsed_axum, parsed_actix]);
222        
223        assert_eq!(result.frameworks.len(), 2);
224        assert!(result.frameworks.contains(&Framework::Axum));
225        assert!(result.frameworks.contains(&Framework::ActixWeb));
226    }
227
228    #[test]
229    fn test_detect_no_framework() {
230        let temp_dir = TempDir::new().unwrap();
231        
232        let plain_code = r#"
233            use std::collections::HashMap;
234            use serde::{Serialize, Deserialize};
235            
236            #[derive(Serialize, Deserialize)]
237            pub struct User {
238                pub id: u32,
239                pub name: String,
240            }
241            
242            pub fn process_user(user: User) -> User {
243                user
244            }
245        "#;
246        
247        let parsed = parse_test_file(&temp_dir, "plain.rs", plain_code);
248        let result = FrameworkDetector::detect(&[parsed]);
249        
250        assert_eq!(result.frameworks.len(), 0);
251    }
252
253    #[test]
254    fn test_detect_with_grouped_imports() {
255        let temp_dir = TempDir::new().unwrap();
256        
257        let grouped_code = r#"
258            use axum::{
259                Router,
260                routing::{get, post},
261                extract::{Path, Query},
262            };
263            
264            pub fn app() -> Router {
265                Router::new()
266            }
267        "#;
268        
269        let parsed = parse_test_file(&temp_dir, "grouped.rs", grouped_code);
270        let result = FrameworkDetector::detect(&[parsed]);
271        
272        assert_eq!(result.frameworks.len(), 1);
273        assert!(result.frameworks.contains(&Framework::Axum));
274    }
275
276    #[test]
277    fn test_detect_with_renamed_import() {
278        let temp_dir = TempDir::new().unwrap();
279        
280        let renamed_code = r#"
281            use actix_web as actix;
282            use actix::web;
283            
284            pub fn handler() {}
285        "#;
286        
287        let parsed = parse_test_file(&temp_dir, "renamed.rs", renamed_code);
288        let result = FrameworkDetector::detect(&[parsed]);
289        
290        assert_eq!(result.frameworks.len(), 1);
291        assert!(result.frameworks.contains(&Framework::ActixWeb));
292    }
293
294    #[test]
295    fn test_detect_multiple_files_same_framework() {
296        let temp_dir = TempDir::new().unwrap();
297        
298        let code1 = r#"
299            use axum::Router;
300        "#;
301        
302        let code2 = r#"
303            use axum::routing::get;
304        "#;
305        
306        let code3 = r#"
307            use axum::extract::Path;
308        "#;
309        
310        let parsed1 = parse_test_file(&temp_dir, "file1.rs", code1);
311        let parsed2 = parse_test_file(&temp_dir, "file2.rs", code2);
312        let parsed3 = parse_test_file(&temp_dir, "file3.rs", code3);
313        
314        let result = FrameworkDetector::detect(&[parsed1, parsed2, parsed3]);
315        
316        // Should only detect Axum once, not three times
317        assert_eq!(result.frameworks.len(), 1);
318        assert!(result.frameworks.contains(&Framework::Axum));
319    }
320
321    #[test]
322    fn test_detect_empty_file_list() {
323        let result = FrameworkDetector::detect(&[]);
324        
325        assert_eq!(result.frameworks.len(), 0);
326    }
327
328    #[test]
329    fn test_detect_with_nested_use_paths() {
330        let temp_dir = TempDir::new().unwrap();
331        
332        let nested_code = r#"
333            use actix_web::web::Json;
334            use actix_web::middleware::Logger;
335            
336            pub fn handler() {}
337        "#;
338        
339        let parsed = parse_test_file(&temp_dir, "nested.rs", nested_code);
340        let result = FrameworkDetector::detect(&[parsed]);
341        
342        assert_eq!(result.frameworks.len(), 1);
343        assert!(result.frameworks.contains(&Framework::ActixWeb));
344    }
345
346    #[test]
347    fn test_detect_with_glob_imports() {
348        let temp_dir = TempDir::new().unwrap();
349        
350        let glob_code = r#"
351            use axum::*;
352            use axum::routing::*;
353            
354            pub fn app() {}
355        "#;
356        
357        let parsed = parse_test_file(&temp_dir, "glob.rs", glob_code);
358        let result = FrameworkDetector::detect(&[parsed]);
359        
360        assert_eq!(result.frameworks.len(), 1);
361        assert!(result.frameworks.contains(&Framework::Axum));
362    }
363}