1use std::collections::HashMap;
15use std::path::Path;
16
17use crate::annotate::{AnnotationType, Suggestion, SuggestionSource};
18
19static PATH_DOMAIN_MAPPINGS: &[(&str, &str)] = &[
21 ("auth", "authentication"),
22 ("authentication", "authentication"),
23 ("login", "authentication"),
24 ("user", "users"),
25 ("users", "users"),
26 ("account", "users"),
27 ("billing", "billing"),
28 ("payment", "billing"),
29 ("payments", "billing"),
30 ("stripe", "billing"),
31 ("api", "api"),
32 ("rest", "api"),
33 ("graphql", "api"),
34 ("grpc", "api"),
35 ("db", "database"),
36 ("database", "database"),
37 ("models", "database"),
38 ("repository", "database"),
39 ("repositories", "database"),
40 ("cache", "caching"),
41 ("redis", "caching"),
42 ("queue", "messaging"),
43 ("messaging", "messaging"),
44 ("events", "messaging"),
45 ("pubsub", "messaging"),
46 ("util", "utilities"),
47 ("utils", "utilities"),
48 ("helpers", "utilities"),
49 ("common", "utilities"),
50 ("shared", "utilities"),
51 ("test", "testing"),
52 ("tests", "testing"),
53 ("spec", "testing"),
54 ("__tests__", "testing"),
55 ("e2e", "testing"),
56 ("integration", "testing"),
57 ("config", "configuration"),
58 ("settings", "configuration"),
59 ("middleware", "middleware"),
60 ("interceptor", "middleware"),
61 ("handlers", "handlers"),
62 ("controllers", "handlers"),
63 ("services", "services"),
64 ("domain", "domain"),
65 ("core", "core"),
66 ("lib", "library"),
67 ("pkg", "library"),
68 ("internal", "internal"),
69 ("vendor", "vendor"),
70 ("third_party", "vendor"),
71 ("security", "security"),
72 ("crypto", "security"),
73 ("notifications", "notifications"),
74 ("email", "notifications"),
75 ("sms", "notifications"),
76 ("analytics", "analytics"),
77 ("metrics", "analytics"),
78 ("monitoring", "monitoring"),
79 ("logging", "monitoring"),
80 ("admin", "administration"),
81 ("dashboard", "administration"),
82];
83
84static PATH_LAYER_MAPPINGS: &[(&str, &str)] = &[
86 ("handlers", "handler"),
87 ("controllers", "handler"),
88 ("api", "handler"),
89 ("routes", "handler"),
90 ("endpoints", "handler"),
91 ("services", "service"),
92 ("usecases", "service"),
93 ("application", "service"),
94 ("repository", "repository"),
95 ("repositories", "repository"),
96 ("dao", "repository"),
97 ("db", "repository"),
98 ("models", "model"),
99 ("entities", "model"),
100 ("domain", "model"),
101 ("middleware", "middleware"),
102 ("interceptors", "middleware"),
103 ("filters", "middleware"),
104 ("guards", "middleware"),
105 ("utils", "utility"),
106 ("helpers", "utility"),
107 ("lib", "utility"),
108 ("common", "utility"),
109 ("config", "config"),
110 ("settings", "config"),
111];
112
113pub struct PathHeuristics {
116 custom_domain_mappings: HashMap<String, String>,
118}
119
120impl PathHeuristics {
121 pub fn new() -> Self {
123 Self {
124 custom_domain_mappings: HashMap::new(),
125 }
126 }
127
128 pub fn with_domain_mappings(mut self, mappings: HashMap<String, String>) -> Self {
130 self.custom_domain_mappings = mappings;
131 self
132 }
133
134 pub fn suggest(&self, file_path: &str, target: &str, line: usize) -> Vec<Suggestion> {
136 let mut suggestions = Vec::new();
137 let path = Path::new(file_path);
138
139 for component in path.components() {
141 let comp_str = component.as_os_str().to_string_lossy().to_lowercase();
142
143 if let Some(domain) = self.custom_domain_mappings.get(&comp_str) {
145 suggestions.push(
146 Suggestion::domain(target, line, domain, SuggestionSource::Heuristic)
147 .with_confidence(0.8),
148 );
149 }
150
151 for (pattern, domain) in PATH_DOMAIN_MAPPINGS.iter() {
153 if comp_str == *pattern || comp_str.contains(pattern) {
154 suggestions.push(
155 Suggestion::domain(target, line, *domain, SuggestionSource::Heuristic)
156 .with_confidence(0.7),
157 );
158 break; }
160 }
161
162 for (pattern, layer) in PATH_LAYER_MAPPINGS.iter() {
164 if comp_str == *pattern || comp_str.contains(pattern) {
165 suggestions.push(
166 Suggestion::layer(target, line, *layer, SuggestionSource::Heuristic)
167 .with_confidence(0.6),
168 );
169 break; }
171 }
172 }
173
174 let mut seen_domains = std::collections::HashSet::new();
176 let mut seen_layers = std::collections::HashSet::new();
177
178 suggestions.retain(|s| match s.annotation_type {
179 AnnotationType::Domain => seen_domains.insert(s.value.clone()),
180 AnnotationType::Layer => seen_layers.insert(s.value.clone()),
181 _ => true,
182 });
183
184 suggestions
185 }
186
187 pub fn infer_module_name(&self, file_path: &str) -> Option<String> {
191 let path = Path::new(file_path);
192
193 let file_stem = path.file_stem()?.to_string_lossy();
194
195 let generic_names = ["index", "mod", "main", "lib", "__init__", "init"];
197 if generic_names.contains(&file_stem.as_ref()) {
198 let parent = path.parent()?.file_name()?.to_string_lossy();
200 return Some(humanize_name(&parent));
201 }
202
203 Some(humanize_name(&file_stem))
204 }
205
206 pub fn is_test_path(&self, file_path: &str) -> bool {
208 let path_lower = file_path.to_lowercase();
209 let test_indicators = [
210 "/test/",
211 "/tests/",
212 "/__tests__/",
213 "/spec/",
214 "/e2e/",
215 ".test.",
216 ".spec.",
217 "_test.",
218 "_spec.",
219 ];
220
221 test_indicators.iter().any(|ind| path_lower.contains(ind))
222 }
223}
224
225impl Default for PathHeuristics {
226 fn default() -> Self {
227 Self::new()
228 }
229}
230
231fn humanize_name(name: &str) -> String {
233 let mut words = Vec::new();
235 let mut current = String::new();
236
237 for (i, c) in name.chars().enumerate() {
238 if c == '_' || c == '-' {
239 if !current.is_empty() {
240 words.push(current);
241 current = String::new();
242 }
243 } else if c.is_uppercase() && i > 0 {
244 if !current.is_empty() {
245 words.push(current);
246 current = String::new();
247 }
248 current.push(c);
249 } else {
250 current.push(c);
251 }
252 }
253
254 if !current.is_empty() {
255 words.push(current);
256 }
257
258 words
260 .iter()
261 .enumerate()
262 .map(|(i, w)| {
263 if i == 0 {
264 let mut chars = w.chars();
265 match chars.next() {
266 None => String::new(),
267 Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
268 }
269 } else {
270 w.to_lowercase()
271 }
272 })
273 .collect::<Vec<_>>()
274 .join(" ")
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_suggest_domain_from_path() {
283 let heuristics = PathHeuristics::new();
284
285 let suggestions = heuristics.suggest("src/auth/service.ts", "AuthService", 10);
286 let has_auth_domain = suggestions
287 .iter()
288 .any(|s| s.annotation_type == AnnotationType::Domain && s.value == "authentication");
289 assert!(has_auth_domain);
290
291 let suggestions = heuristics.suggest("src/billing/payments.ts", "ProcessPayment", 10);
292 let has_billing_domain = suggestions
293 .iter()
294 .any(|s| s.annotation_type == AnnotationType::Domain && s.value == "billing");
295 assert!(has_billing_domain);
296 }
297
298 #[test]
299 fn test_suggest_layer_from_path() {
300 let heuristics = PathHeuristics::new();
301
302 let suggestions = heuristics.suggest("src/handlers/user.ts", "UserHandler", 10);
303 let has_handler_layer = suggestions
304 .iter()
305 .any(|s| s.annotation_type == AnnotationType::Layer && s.value == "handler");
306 assert!(has_handler_layer);
307
308 let suggestions = heuristics.suggest("src/services/auth.ts", "AuthService", 10);
309 let has_service_layer = suggestions
310 .iter()
311 .any(|s| s.annotation_type == AnnotationType::Layer && s.value == "service");
312 assert!(has_service_layer);
313 }
314
315 #[test]
316 fn test_infer_module_name() {
317 let heuristics = PathHeuristics::new();
318
319 assert_eq!(
320 heuristics.infer_module_name("src/auth/session_service.ts"),
321 Some("Session service".to_string())
322 );
323
324 assert_eq!(
325 heuristics.infer_module_name("src/auth/index.ts"),
326 Some("Auth".to_string())
327 );
328 }
329
330 #[test]
331 fn test_is_test_path() {
332 let heuristics = PathHeuristics::new();
333
334 assert!(heuristics.is_test_path("src/__tests__/auth.test.ts"));
335 assert!(heuristics.is_test_path("tests/integration/user.spec.ts"));
336 assert!(!heuristics.is_test_path("src/services/auth.ts"));
337 }
338
339 #[test]
340 fn test_humanize_name() {
341 assert_eq!(humanize_name("user_service"), "User service");
342 assert_eq!(humanize_name("UserService"), "User service");
343 assert_eq!(humanize_name("auth-handler"), "Auth handler");
344 }
345
346 #[test]
347 fn test_custom_domain_mappings() {
348 let mut mappings = HashMap::new();
349 mappings.insert("checkout".to_string(), "commerce".to_string());
350
351 let heuristics = PathHeuristics::new().with_domain_mappings(mappings);
352 let suggestions = heuristics.suggest("src/checkout/cart.ts", "Cart", 10);
353
354 let has_commerce = suggestions
355 .iter()
356 .any(|s| s.annotation_type == AnnotationType::Domain && s.value == "commerce");
357 assert!(has_commerce);
358 }
359}