ricecoder_research/
architectural_patterns.rs1use crate::codebase_scanner::ScanResult;
4use crate::models::{DetectedPattern, PatternCategory};
5use crate::ResearchError;
6use std::collections::HashMap;
7
8pub struct ArchitecturalPatternDetector;
10
11impl ArchitecturalPatternDetector {
12 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 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 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 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 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 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 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 if file
223 .path
224 .to_string_lossy()
225 .to_lowercase()
226 .contains("service")
227 {
228 service_count += 1;
229 }
230 }
231
232 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 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 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 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 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}