1use apcore::module::ModuleAnnotations;
8use async_trait::async_trait;
9use regex::Regex;
10
11use crate::types::ScannedModule;
12
13#[async_trait]
39pub trait Scanner<App: Send + Sync = ()> {
40 async fn scan(&self, app: &App) -> Vec<ScannedModule>;
45
46 fn source_name(&self) -> &str;
48}
49
50pub fn filter_modules(
57 modules: &[ScannedModule],
58 include: Option<&str>,
59 exclude: Option<&str>,
60) -> Result<Vec<ScannedModule>, regex::Error> {
61 let mut result: Vec<ScannedModule> = modules.to_vec();
62
63 if let Some(pattern) = include {
64 let re = Regex::new(pattern)?;
65 result.retain(|m| re.is_match(&m.module_id));
66 }
67
68 if let Some(pattern) = exclude {
69 let re = Regex::new(pattern)?;
70 result.retain(|m| !re.is_match(&m.module_id));
71 }
72
73 Ok(result)
74}
75
76pub fn deduplicate_ids(modules: Vec<ScannedModule>) -> Vec<ScannedModule> {
80 let mut seen: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
81 let mut result: Vec<ScannedModule> = Vec::with_capacity(modules.len());
82
83 for mut module in modules {
84 let mid = module.module_id.clone();
85 let count = seen.entry(mid.clone()).or_insert(0);
86 *count += 1;
87
88 if *count > 1 {
89 let new_id = format!("{}_{}", mid, count);
90 module.warnings.push(format!(
91 "Module ID renamed from '{}' to '{}' to avoid collision",
92 mid, new_id
93 ));
94 module.module_id = new_id;
95 }
96
97 result.push(module);
98 }
99
100 result
101}
102
103pub fn infer_annotations_from_method(method: &str) -> ModuleAnnotations {
111 match method.to_uppercase().as_str() {
112 "GET" => ModuleAnnotations {
113 readonly: true,
114 cacheable: true,
115 ..Default::default()
116 },
117 "DELETE" => ModuleAnnotations {
118 destructive: true,
119 ..Default::default()
120 },
121 "PUT" => ModuleAnnotations {
122 idempotent: true,
123 ..Default::default()
124 },
125 _ => ModuleAnnotations::default(),
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use serde_json::json;
133
134 fn make_module(id: &str) -> ScannedModule {
135 ScannedModule::new(
136 id.into(),
137 "test".into(),
138 json!({}),
139 json!({}),
140 vec![],
141 "app:func".into(),
142 )
143 }
144
145 #[test]
146 fn test_filter_modules_include() {
147 let modules = vec![
148 make_module("users.get"),
149 make_module("users.create"),
150 make_module("tasks.list"),
151 ];
152 let filtered = filter_modules(&modules, Some("users"), None).unwrap();
153 assert_eq!(filtered.len(), 2);
154 assert!(filtered.iter().all(|m| m.module_id.starts_with("users")));
155 }
156
157 #[test]
158 fn test_filter_modules_exclude() {
159 let modules = vec![
160 make_module("users.get"),
161 make_module("users.create"),
162 make_module("tasks.list"),
163 ];
164 let filtered = filter_modules(&modules, None, Some("users")).unwrap();
165 assert_eq!(filtered.len(), 1);
166 assert_eq!(filtered[0].module_id, "tasks.list");
167 }
168
169 #[test]
170 fn test_filter_modules_both() {
171 let modules = vec![
172 make_module("users.get"),
173 make_module("users.admin.create"),
174 make_module("tasks.list"),
175 ];
176 let filtered = filter_modules(&modules, Some("users"), Some("admin")).unwrap();
177 assert_eq!(filtered.len(), 1);
178 assert_eq!(filtered[0].module_id, "users.get");
179 }
180
181 #[test]
182 fn test_deduplicate_ids_no_duplicates() {
183 let modules = vec![make_module("a"), make_module("b")];
184 let result = deduplicate_ids(modules);
185 assert_eq!(result[0].module_id, "a");
186 assert_eq!(result[1].module_id, "b");
187 assert!(result[0].warnings.is_empty());
188 }
189
190 #[test]
191 fn test_deduplicate_ids_with_duplicates() {
192 let modules = vec![make_module("a"), make_module("a"), make_module("a")];
193 let result = deduplicate_ids(modules);
194 assert_eq!(result[0].module_id, "a");
195 assert_eq!(result[1].module_id, "a_2");
196 assert_eq!(result[2].module_id, "a_3");
197 assert!(result[1].warnings[0].contains("renamed"));
198 }
199
200 #[test]
201 fn test_infer_annotations_get() {
202 let ann = infer_annotations_from_method("GET");
203 assert!(ann.readonly);
204 assert!(ann.cacheable);
205 assert!(!ann.destructive);
206 }
207
208 #[test]
209 fn test_infer_annotations_delete() {
210 let ann = infer_annotations_from_method("DELETE");
211 assert!(ann.destructive);
212 assert!(!ann.readonly);
213 }
214
215 #[test]
216 fn test_infer_annotations_put() {
217 let ann = infer_annotations_from_method("PUT");
218 assert!(ann.idempotent);
219 assert!(!ann.readonly);
220 }
221
222 #[test]
223 fn test_infer_annotations_post() {
224 let ann = infer_annotations_from_method("POST");
225 assert!(!ann.readonly);
226 assert!(!ann.destructive);
227 assert!(!ann.idempotent);
228 }
229
230 #[test]
231 fn test_infer_annotations_case_insensitive() {
232 let ann = infer_annotations_from_method("get");
233 assert!(ann.readonly);
234 }
235
236 #[test]
237 fn test_filter_modules_no_filters() {
238 let modules = vec![make_module("users.get"), make_module("tasks.list")];
239 let filtered = filter_modules(&modules, None, None).unwrap();
240 assert_eq!(filtered.len(), 2);
241 }
242
243 #[test]
244 fn test_filter_modules_include_matches_none() {
245 let modules = vec![make_module("users.get"), make_module("tasks.list")];
246 let filtered = filter_modules(&modules, Some("^zzz$"), None).unwrap();
247 assert!(filtered.is_empty());
248 }
249
250 #[test]
251 fn test_filter_modules_exclude_matches_all() {
252 let modules = vec![make_module("users.get"), make_module("users.create")];
253 let filtered = filter_modules(&modules, None, Some("users")).unwrap();
254 assert!(filtered.is_empty());
255 }
256
257 #[test]
258 fn test_filter_modules_invalid_include_regex() {
259 let modules = vec![make_module("a")];
260 let result = filter_modules(&modules, Some("[invalid"), None);
261 assert!(result.is_err());
262 }
263
264 #[test]
265 fn test_filter_modules_invalid_exclude_regex() {
266 let modules = vec![make_module("a")];
267 let result = filter_modules(&modules, None, Some("[invalid"));
268 assert!(result.is_err());
269 }
270
271 #[test]
272 fn test_deduplicate_ids_empty_list() {
273 let result = deduplicate_ids(vec![]);
274 assert!(result.is_empty());
275 }
276
277 #[test]
278 fn test_deduplicate_ids_original_unchanged() {
279 let original = vec![make_module("a"), make_module("a")];
280 let cloned = original.clone();
281 let result = deduplicate_ids(original);
282
283 assert_eq!(cloned[0].module_id, "a");
286 assert_eq!(cloned[1].module_id, "a");
287 assert!(cloned[0].warnings.is_empty());
288 assert!(cloned[1].warnings.is_empty());
289
290 assert_eq!(result[1].module_id, "a_2");
292 }
293
294 #[test]
295 fn test_deduplicate_ids_mixed() {
296 let modules = vec![
297 make_module("a"),
298 make_module("b"),
299 make_module("a"),
300 make_module("c"),
301 make_module("b"),
302 ];
303 let result = deduplicate_ids(modules);
304 assert_eq!(result[0].module_id, "a");
305 assert_eq!(result[1].module_id, "b");
306 assert_eq!(result[2].module_id, "a_2");
307 assert_eq!(result[3].module_id, "c");
308 assert_eq!(result[4].module_id, "b_2");
309 }
310
311 #[test]
312 fn test_deduplicate_warnings_first_no_warning() {
313 let modules = vec![make_module("x"), make_module("x")];
314 let result = deduplicate_ids(modules);
315 assert!(
316 result[0].warnings.is_empty(),
317 "First occurrence should have no warning"
318 );
319 assert!(
320 !result[1].warnings.is_empty(),
321 "Duplicate should have a warning"
322 );
323 }
324
325 #[test]
326 fn test_deduplicate_warnings_preserved() {
327 let mut m = make_module("dup");
328 m.warnings.push("existing warning".into());
329 let modules = vec![make_module("dup"), m];
330 let result = deduplicate_ids(modules);
331
332 assert_eq!(result[1].warnings.len(), 2);
335 assert_eq!(result[1].warnings[0], "existing warning");
336 assert!(result[1].warnings[1].contains("renamed"));
337 }
338
339 #[test]
340 fn test_infer_annotations_patch() {
341 let ann = infer_annotations_from_method("PATCH");
342 assert!(!ann.readonly);
343 assert!(!ann.destructive);
344 assert!(!ann.idempotent);
345 assert!(!ann.cacheable);
346 }
347}