1use apcore::module::ModuleAnnotations;
8use async_trait::async_trait;
9use regex::Regex;
10
11use crate::types::ScannedModule;
12
13#[async_trait]
61pub trait BaseScanner<App: Send + Sync = ()> {
62 async fn scan(&self, app: &App) -> Vec<ScannedModule>;
67
68 fn source_name(&self) -> &str;
70}
71
72pub fn filter_modules(
94 modules: &[ScannedModule],
95 include: Option<&str>,
96 exclude: Option<&str>,
97) -> Result<Vec<ScannedModule>, regex::Error> {
98 let mut result: Vec<ScannedModule> = modules.to_vec();
99
100 if let Some(pattern) = include {
101 let re = Regex::new(pattern)?;
102 result.retain(|m| re.is_match(&m.module_id));
103 }
104
105 if let Some(pattern) = exclude {
106 let re = Regex::new(pattern)?;
107 result.retain(|m| !re.is_match(&m.module_id));
108 }
109
110 Ok(result)
111}
112
113pub fn deduplicate_ids(modules: Vec<ScannedModule>) -> Vec<ScannedModule> {
117 let original_ids: std::collections::HashSet<String> =
121 modules.iter().map(|m| m.module_id.clone()).collect();
122 let mut occurrence_count: std::collections::HashMap<String, usize> =
123 std::collections::HashMap::new();
124 let mut assigned: std::collections::HashSet<String> = std::collections::HashSet::new();
125 let mut result: Vec<ScannedModule> = Vec::with_capacity(modules.len());
126
127 for mut module in modules {
128 let mid = module.module_id.clone();
129 let count = occurrence_count.entry(mid.clone()).or_insert(0);
130 *count += 1;
131
132 if *count == 1 {
133 assigned.insert(mid.clone());
134 } else {
135 let mut suffix = *count;
138 let mut new_id = format!("{}_{}", mid, suffix);
139 while assigned.contains(&new_id) || original_ids.contains(&new_id) {
140 suffix += 1;
141 new_id = format!("{}_{}", mid, suffix);
142 }
143 assigned.insert(new_id.clone());
144 module.warnings.push(format!(
145 "Module ID renamed from '{}' to '{}' to avoid collision",
146 mid, new_id
147 ));
148 module.module_id = new_id;
149 }
150
151 result.push(module);
152 }
153
154 result
155}
156
157pub fn infer_annotations_from_method(method: &str) -> ModuleAnnotations {
168 match method.to_uppercase().as_str() {
169 "GET" => ModuleAnnotations {
170 readonly: true,
171 cacheable: true,
172 ..Default::default()
173 },
174 "HEAD" | "OPTIONS" => ModuleAnnotations {
175 readonly: true,
176 ..Default::default()
177 },
178 "DELETE" => ModuleAnnotations {
179 destructive: true,
180 ..Default::default()
181 },
182 "PUT" => ModuleAnnotations {
183 idempotent: true,
184 ..Default::default()
185 },
186 _ => ModuleAnnotations::default(),
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use serde_json::json;
194
195 fn make_module(id: &str) -> ScannedModule {
196 ScannedModule::new(
197 id.into(),
198 "test".into(),
199 json!({}),
200 json!({}),
201 vec![],
202 "app:func".into(),
203 )
204 }
205
206 #[test]
207 fn test_filter_modules_include() {
208 let modules = vec![
209 make_module("users.get"),
210 make_module("users.create"),
211 make_module("tasks.list"),
212 ];
213 let filtered = filter_modules(&modules, Some("users"), None).unwrap();
214 assert_eq!(filtered.len(), 2);
215 assert!(filtered.iter().all(|m| m.module_id.starts_with("users")));
216 }
217
218 #[test]
219 fn test_filter_modules_exclude() {
220 let modules = vec![
221 make_module("users.get"),
222 make_module("users.create"),
223 make_module("tasks.list"),
224 ];
225 let filtered = filter_modules(&modules, None, Some("users")).unwrap();
226 assert_eq!(filtered.len(), 1);
227 assert_eq!(filtered[0].module_id, "tasks.list");
228 }
229
230 #[test]
231 fn test_filter_modules_both() {
232 let modules = vec![
233 make_module("users.get"),
234 make_module("users.admin.create"),
235 make_module("tasks.list"),
236 ];
237 let filtered = filter_modules(&modules, Some("users"), Some("admin")).unwrap();
238 assert_eq!(filtered.len(), 1);
239 assert_eq!(filtered[0].module_id, "users.get");
240 }
241
242 #[test]
243 fn test_deduplicate_ids_no_duplicates() {
244 let modules = vec![make_module("a"), make_module("b")];
245 let result = deduplicate_ids(modules);
246 assert_eq!(result[0].module_id, "a");
247 assert_eq!(result[1].module_id, "b");
248 assert!(result[0].warnings.is_empty());
249 }
250
251 #[test]
252 fn test_deduplicate_ids_with_duplicates() {
253 let modules = vec![make_module("a"), make_module("a"), make_module("a")];
254 let result = deduplicate_ids(modules);
255 assert_eq!(result[0].module_id, "a");
256 assert_eq!(result[1].module_id, "a_2");
257 assert_eq!(result[2].module_id, "a_3");
258 assert!(result[1].warnings[0].contains("renamed"));
259 }
260
261 #[test]
262 fn test_infer_annotations_get() {
263 let ann = infer_annotations_from_method("GET");
264 assert!(ann.readonly);
265 assert!(ann.cacheable);
266 assert!(!ann.destructive);
267 }
268
269 #[test]
270 fn test_infer_annotations_delete() {
271 let ann = infer_annotations_from_method("DELETE");
272 assert!(ann.destructive);
273 assert!(!ann.readonly);
274 }
275
276 #[test]
277 fn test_infer_annotations_put() {
278 let ann = infer_annotations_from_method("PUT");
279 assert!(ann.idempotent);
280 assert!(!ann.readonly);
281 }
282
283 #[test]
284 fn test_infer_annotations_post() {
285 let ann = infer_annotations_from_method("POST");
286 assert!(!ann.readonly);
287 assert!(!ann.destructive);
288 assert!(!ann.idempotent);
289 }
290
291 #[test]
292 fn test_infer_annotations_case_insensitive() {
293 let ann = infer_annotations_from_method("get");
294 assert!(ann.readonly);
295 }
296
297 #[test]
298 fn test_filter_modules_no_filters() {
299 let modules = vec![make_module("users.get"), make_module("tasks.list")];
300 let filtered = filter_modules(&modules, None, None).unwrap();
301 assert_eq!(filtered.len(), 2);
302 }
303
304 #[test]
305 fn test_filter_modules_include_matches_none() {
306 let modules = vec![make_module("users.get"), make_module("tasks.list")];
307 let filtered = filter_modules(&modules, Some("^zzz$"), None).unwrap();
308 assert!(filtered.is_empty());
309 }
310
311 #[test]
312 fn test_filter_modules_exclude_matches_all() {
313 let modules = vec![make_module("users.get"), make_module("users.create")];
314 let filtered = filter_modules(&modules, None, Some("users")).unwrap();
315 assert!(filtered.is_empty());
316 }
317
318 #[test]
319 fn test_filter_modules_invalid_include_regex() {
320 let modules = vec![make_module("a")];
321 let result = filter_modules(&modules, Some("[invalid"), None);
322 assert!(result.is_err());
323 }
324
325 #[test]
326 fn test_filter_modules_invalid_exclude_regex() {
327 let modules = vec![make_module("a")];
328 let result = filter_modules(&modules, None, Some("[invalid"));
329 assert!(result.is_err());
330 }
331
332 #[test]
333 fn test_deduplicate_ids_empty_list() {
334 let result = deduplicate_ids(vec![]);
335 assert!(result.is_empty());
336 }
337
338 #[test]
339 fn test_deduplicate_ids_original_unchanged() {
340 let original = vec![make_module("a"), make_module("a")];
341 let cloned = original.clone();
342 let result = deduplicate_ids(original);
343
344 assert_eq!(cloned[0].module_id, "a");
347 assert_eq!(cloned[1].module_id, "a");
348 assert!(cloned[0].warnings.is_empty());
349 assert!(cloned[1].warnings.is_empty());
350
351 assert_eq!(result[1].module_id, "a_2");
353 }
354
355 #[test]
356 fn test_deduplicate_ids_mixed() {
357 let modules = vec![
358 make_module("a"),
359 make_module("b"),
360 make_module("a"),
361 make_module("c"),
362 make_module("b"),
363 ];
364 let result = deduplicate_ids(modules);
365 assert_eq!(result[0].module_id, "a");
366 assert_eq!(result[1].module_id, "b");
367 assert_eq!(result[2].module_id, "a_2");
368 assert_eq!(result[3].module_id, "c");
369 assert_eq!(result[4].module_id, "b_2");
370 }
371
372 #[test]
373 fn test_deduplicate_warnings_first_no_warning() {
374 let modules = vec![make_module("x"), make_module("x")];
375 let result = deduplicate_ids(modules);
376 assert!(
377 result[0].warnings.is_empty(),
378 "First occurrence should have no warning"
379 );
380 assert!(
381 !result[1].warnings.is_empty(),
382 "Duplicate should have a warning"
383 );
384 }
385
386 #[test]
387 fn test_deduplicate_warnings_preserved() {
388 let mut m = make_module("dup");
389 m.warnings.push("existing warning".into());
390 let modules = vec![make_module("dup"), m];
391 let result = deduplicate_ids(modules);
392
393 assert_eq!(result[1].warnings.len(), 2);
396 assert_eq!(result[1].warnings[0], "existing warning");
397 assert!(result[1].warnings[1].contains("renamed"));
398 }
399
400 #[test]
401 fn test_infer_annotations_patch() {
402 let ann = infer_annotations_from_method("PATCH");
403 assert!(!ann.readonly);
404 assert!(!ann.destructive);
405 assert!(!ann.idempotent);
406 assert!(!ann.cacheable);
407 }
408
409 #[test]
410 fn test_infer_annotations_head() {
411 let ann = infer_annotations_from_method("HEAD");
412 assert!(ann.readonly, "HEAD should be readonly");
413 assert!(!ann.cacheable, "HEAD should not be cacheable (no body)");
414 assert!(!ann.destructive);
415 assert!(!ann.idempotent);
416 }
417
418 #[test]
419 fn test_infer_annotations_options() {
420 let ann = infer_annotations_from_method("OPTIONS");
421 assert!(ann.readonly, "OPTIONS should be readonly");
422 assert!(!ann.destructive);
423 assert!(!ann.idempotent);
424 }
425
426 #[test]
427 fn test_infer_annotations_head_case_insensitive() {
428 let ann = infer_annotations_from_method("head");
429 assert!(ann.readonly);
430 }
431
432 #[test]
433 fn test_deduplicate_ids_no_collision_with_preexisting_suffixed_id() {
434 let modules = vec![make_module("a"), make_module("a"), make_module("a_2")];
437 let result = deduplicate_ids(modules);
438 assert_eq!(result[0].module_id, "a", "first 'a' keeps its ID");
439 assert_eq!(
440 result[1].module_id, "a_3",
441 "second 'a' skips 'a_2' (pre-existing) and picks 'a_3'"
442 );
443 assert_eq!(result[2].module_id, "a_2", "original 'a_2' keeps its ID");
444 let ids: std::collections::HashSet<_> = result.iter().map(|m| &m.module_id).collect();
446 assert_eq!(ids.len(), 3, "all three IDs must be distinct");
447 }
448}