ricecoder_research/
architectural_patterns.rs

1//! Architectural pattern detection
2
3use crate::codebase_scanner::ScanResult;
4use crate::models::{DetectedPattern, PatternCategory};
5use crate::ResearchError;
6use std::collections::HashMap;
7
8/// Detects architectural patterns in a codebase
9pub struct ArchitecturalPatternDetector;
10
11impl ArchitecturalPatternDetector {
12    /// Detects layered architecture pattern
13    ///
14    /// Layered architecture is characterized by:
15    /// - Domain layer (business logic)
16    /// - Application layer (use cases, services)
17    /// - Infrastructure layer (persistence, external services)
18    /// - Interfaces layer (API, CLI, UI)
19    pub fn detect_layered_architecture(
20        scan_result: &ScanResult,
21    ) -> Result<Option<DetectedPattern>, ResearchError> {
22        let mut layer_indicators = HashMap::new();
23
24        for file in &scan_result.files {
25            let path_str = file.path.to_string_lossy().to_lowercase();
26
27            if path_str.contains("domain") {
28                *layer_indicators.entry("domain").or_insert(0) += 1;
29            }
30            if path_str.contains("application") {
31                *layer_indicators.entry("application").or_insert(0) += 1;
32            }
33            if path_str.contains("infrastructure") {
34                *layer_indicators.entry("infrastructure").or_insert(0) += 1;
35            }
36            if path_str.contains("interface") {
37                *layer_indicators.entry("interface").or_insert(0) += 1;
38            }
39        }
40
41        let layer_count = layer_indicators.len();
42        if layer_count >= 2 {
43            let confidence = (layer_count as f32) / 4.0;
44            let locations = scan_result
45                .files
46                .iter()
47                .filter(|f| {
48                    let path_str = f.path.to_string_lossy().to_lowercase();
49                    path_str.contains("domain")
50                        || path_str.contains("application")
51                        || path_str.contains("infrastructure")
52                        || path_str.contains("interface")
53                })
54                .map(|f| f.path.clone())
55                .collect();
56
57            return Ok(Some(DetectedPattern {
58                name: "Layered Architecture".to_string(),
59                category: PatternCategory::Architectural,
60                confidence,
61                locations,
62                description: format!(
63                    "Layered architecture detected with {} layers: {}",
64                    layer_count,
65                    layer_indicators
66                        .keys()
67                        .cloned()
68                        .collect::<Vec<_>>()
69                        .join(", ")
70                ),
71            }));
72        }
73
74        Ok(None)
75    }
76
77    /// Detects microservices pattern
78    ///
79    /// Microservices architecture is characterized by:
80    /// - Multiple service modules/directories
81    /// - Service-specific configuration files
82    /// - Independent deployment units
83    pub fn detect_microservices_pattern(
84        scan_result: &ScanResult,
85    ) -> Result<Option<DetectedPattern>, ResearchError> {
86        let mut service_modules = HashMap::new();
87
88        for file in &scan_result.files {
89            let path_str = file.path.to_string_lossy().to_lowercase();
90
91            // Look for service-related patterns in path components
92            let is_service_path = file.path.components().any(|c| {
93                let name = c.as_os_str().to_string_lossy().to_lowercase();
94                name.contains("service")
95            });
96
97            if is_service_path {
98                if let Some(parent) = file.path.parent() {
99                    *service_modules.entry(parent.to_path_buf()).or_insert(0) += 1;
100                }
101            }
102
103            // Look for service configuration files
104            if path_str.ends_with("service.yaml")
105                || path_str.ends_with("service.yml")
106                || path_str.ends_with("service.toml")
107                || path_str.ends_with("service.json")
108            {
109                if let Some(parent) = file.path.parent() {
110                    *service_modules.entry(parent.to_path_buf()).or_insert(0) += 1;
111                }
112            }
113        }
114
115        if service_modules.len() >= 2 {
116            let confidence = 0.65;
117            let locations: Vec<_> = service_modules.keys().cloned().collect();
118
119            return Ok(Some(DetectedPattern {
120                name: "Microservices Pattern".to_string(),
121                category: PatternCategory::Architectural,
122                confidence,
123                locations,
124                description: format!(
125                    "Microservices architecture detected with {} service modules",
126                    service_modules.len()
127                ),
128            }));
129        }
130
131        Ok(None)
132    }
133
134    /// Detects event-driven architecture pattern
135    ///
136    /// Event-driven architecture is characterized by:
137    /// - Event definitions and handlers
138    /// - Pub/sub or message broker patterns
139    /// - Event sourcing patterns
140    pub fn detect_event_driven_pattern(
141        scan_result: &ScanResult,
142    ) -> Result<Option<DetectedPattern>, ResearchError> {
143        let mut event_indicators = 0;
144        let mut event_files = Vec::new();
145
146        for file in &scan_result.files {
147            let file_name = file
148                .path
149                .file_name()
150                .and_then(|n| n.to_str())
151                .unwrap_or("")
152                .to_lowercase();
153
154            if file_name.contains("event") {
155                event_indicators += 2;
156                event_files.push(file.path.clone());
157            }
158            if file_name.contains("handler") {
159                event_indicators += 1;
160                event_files.push(file.path.clone());
161            }
162            if file_name.contains("listener") {
163                event_indicators += 1;
164                event_files.push(file.path.clone());
165            }
166            if file_name.contains("subscriber") {
167                event_indicators += 1;
168                event_files.push(file.path.clone());
169            }
170            if file_name.contains("publisher") {
171                event_indicators += 1;
172                event_files.push(file.path.clone());
173            }
174        }
175
176        if event_indicators >= 3 {
177            let confidence = (event_indicators as f32 / 10.0).min(0.95);
178            event_files.sort();
179            event_files.dedup();
180
181            return Ok(Some(DetectedPattern {
182                name: "Event-Driven Pattern".to_string(),
183                category: PatternCategory::Architectural,
184                confidence,
185                locations: event_files,
186                description: "Event-driven architecture detected with event handlers, listeners, and publishers".to_string(),
187            }));
188        }
189
190        Ok(None)
191    }
192
193    /// Detects monolithic architecture pattern
194    ///
195    /// Monolithic architecture is characterized by:
196    /// - Single entry point
197    /// - Tightly coupled modules
198    /// - No service separation
199    pub fn detect_monolithic_pattern(
200        scan_result: &ScanResult,
201    ) -> Result<Option<DetectedPattern>, ResearchError> {
202        let mut entry_points = Vec::new();
203        let mut service_count = 0;
204
205        for file in &scan_result.files {
206            let file_name = file.path.file_name().and_then(|n| n.to_str()).unwrap_or("");
207
208            // Look for entry points
209            if file_name == "main.rs"
210                || file_name == "main.py"
211                || file_name == "main.go"
212                || file_name == "main.java"
213                || file_name == "main.ts"
214                || file_name == "main.js"
215                || file_name == "index.ts"
216                || file_name == "index.js"
217            {
218                entry_points.push(file.path.clone());
219            }
220
221            // Count service modules
222            if file
223                .path
224                .to_string_lossy()
225                .to_lowercase()
226                .contains("service")
227            {
228                service_count += 1;
229            }
230        }
231
232        // Monolithic if single entry point and few services
233        if entry_points.len() == 1 && service_count < 3 {
234            let confidence = 0.6;
235
236            return Ok(Some(DetectedPattern {
237                name: "Monolithic Architecture".to_string(),
238                category: PatternCategory::Architectural,
239                confidence,
240                locations: entry_points,
241                description: "Monolithic architecture detected with single entry point and tightly coupled modules".to_string(),
242            }));
243        }
244
245        Ok(None)
246    }
247
248    /// Detects serverless architecture pattern
249    ///
250    /// Serverless architecture is characterized by:
251    /// - Function definitions (Lambda, Cloud Functions, etc.)
252    /// - Serverless configuration files
253    /// - Event-driven function triggers
254    pub fn detect_serverless_pattern(
255        scan_result: &ScanResult,
256    ) -> Result<Option<DetectedPattern>, ResearchError> {
257        let mut serverless_indicators = 0;
258        let mut serverless_files = Vec::new();
259
260        for file in &scan_result.files {
261            let file_name = file
262                .path
263                .file_name()
264                .and_then(|n| n.to_str())
265                .unwrap_or("")
266                .to_lowercase();
267
268            // Look for serverless configuration
269            if file_name == "serverless.yml"
270                || file_name == "serverless.yaml"
271                || file_name == "serverless.json"
272                || file_name == "sam.yaml"
273                || file_name == "sam.yml"
274            {
275                serverless_indicators += 3;
276                serverless_files.push(file.path.clone());
277            }
278
279            // Look for function definitions
280            if file_name.contains("lambda") || file_name.contains("function") {
281                serverless_indicators += 1;
282                serverless_files.push(file.path.clone());
283            }
284        }
285
286        if serverless_indicators >= 2 {
287            let confidence = (serverless_indicators as f32 / 6.0).min(0.9);
288            serverless_files.sort();
289            serverless_files.dedup();
290
291            return Ok(Some(DetectedPattern {
292                name: "Serverless Pattern".to_string(),
293                category: PatternCategory::Architectural,
294                confidence,
295                locations: serverless_files,
296                description: "Serverless architecture detected with function definitions and serverless configuration".to_string(),
297            }));
298        }
299
300        Ok(None)
301    }
302
303    /// Detects plugin architecture pattern
304    ///
305    /// Plugin architecture is characterized by:
306    /// - Plugin directories/modules
307    /// - Plugin interface definitions
308    /// - Plugin loader/registry
309    pub fn detect_plugin_architecture(
310        scan_result: &ScanResult,
311    ) -> Result<Option<DetectedPattern>, ResearchError> {
312        let mut plugin_indicators = 0;
313        let mut plugin_files = Vec::new();
314
315        for file in &scan_result.files {
316            let path_str = file.path.to_string_lossy().to_lowercase();
317            let file_name = file
318                .path
319                .file_name()
320                .and_then(|n| n.to_str())
321                .unwrap_or("")
322                .to_lowercase();
323
324            if path_str.contains("plugin") {
325                plugin_indicators += 1;
326                plugin_files.push(file.path.clone());
327            }
328            if file_name.contains("loader") || file_name.contains("registry") {
329                plugin_indicators += 1;
330                plugin_files.push(file.path.clone());
331            }
332        }
333
334        if plugin_indicators >= 2 {
335            let confidence = (plugin_indicators as f32 / 5.0).min(0.85);
336            plugin_files.sort();
337            plugin_files.dedup();
338
339            return Ok(Some(DetectedPattern {
340                name: "Plugin Architecture".to_string(),
341                category: PatternCategory::Architectural,
342                confidence,
343                locations: plugin_files,
344                description: "Plugin architecture detected with plugin modules and loader/registry"
345                    .to_string(),
346            }));
347        }
348
349        Ok(None)
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use crate::codebase_scanner::FileMetadata;
357    use crate::models::Language;
358    use std::path::PathBuf;
359
360    fn create_test_scan_result(files: Vec<(&str, Option<Language>)>) -> ScanResult {
361        let files = files
362            .into_iter()
363            .map(|(path, lang)| FileMetadata {
364                path: PathBuf::from(path),
365                language: lang,
366                size: 100,
367                is_test: false,
368            })
369            .collect();
370
371        ScanResult {
372            files,
373            languages: vec![],
374            frameworks: vec![],
375            source_dirs: vec![],
376            test_dirs: vec![],
377        }
378    }
379
380    #[test]
381    fn test_detect_layered_architecture() {
382        let scan_result = create_test_scan_result(vec![
383            ("src/domain/entity.rs", Some(Language::Rust)),
384            ("src/application/service.rs", Some(Language::Rust)),
385            ("src/infrastructure/repository.rs", Some(Language::Rust)),
386        ]);
387
388        let pattern =
389            ArchitecturalPatternDetector::detect_layered_architecture(&scan_result).unwrap();
390        assert!(pattern.is_some());
391        let pattern = pattern.unwrap();
392        assert_eq!(pattern.name, "Layered Architecture");
393        assert!(pattern.confidence > 0.5);
394    }
395
396    #[test]
397    fn test_detect_microservices_pattern() {
398        let scan_result = create_test_scan_result(vec![
399            ("services/user-service/main.rs", Some(Language::Rust)),
400            ("services/order-service/main.rs", Some(Language::Rust)),
401        ]);
402
403        let pattern =
404            ArchitecturalPatternDetector::detect_microservices_pattern(&scan_result).unwrap();
405        assert!(pattern.is_some());
406    }
407
408    #[test]
409    fn test_detect_event_driven_pattern() {
410        let scan_result = create_test_scan_result(vec![
411            ("src/events/user_event.rs", Some(Language::Rust)),
412            ("src/handlers/user_handler.rs", Some(Language::Rust)),
413            (
414                "src/listeners/notification_listener.rs",
415                Some(Language::Rust),
416            ),
417        ]);
418
419        let pattern =
420            ArchitecturalPatternDetector::detect_event_driven_pattern(&scan_result).unwrap();
421        assert!(pattern.is_some());
422    }
423
424    #[test]
425    fn test_detect_monolithic_pattern() {
426        let scan_result = create_test_scan_result(vec![
427            ("src/main.rs", Some(Language::Rust)),
428            ("src/lib.rs", Some(Language::Rust)),
429        ]);
430
431        let pattern =
432            ArchitecturalPatternDetector::detect_monolithic_pattern(&scan_result).unwrap();
433        assert!(pattern.is_some());
434    }
435}