1use std::collections::HashMap;
11use std::sync::{Arc, Mutex};
12
13use apcore_toolkit::{
14 deduplicate_ids, filter_modules, infer_annotations_from_method, ScannedModule,
15};
16use async_trait::async_trait;
17
18use crate::errors::AxumApcoreError;
19use crate::scanner::AxumScanner;
20
21#[derive(Debug, Clone)]
23pub struct RouteMetadata {
24 pub method: String,
26 pub path: String,
28 pub handler_name: String,
30 pub description: String,
32 pub tags: Vec<String>,
34 pub input_schema: serde_json::Value,
36 pub output_schema: serde_json::Value,
38 pub documentation: Option<String>,
40}
41
42static ROUTE_REGISTRY: std::sync::LazyLock<Arc<Mutex<Vec<RouteMetadata>>>> =
47 std::sync::LazyLock::new(|| Arc::new(Mutex::new(Vec::new())));
48
49pub fn register_route(metadata: RouteMetadata) {
51 let mut registry = ROUTE_REGISTRY.lock().expect("route registry lock poisoned");
52 registry.push(metadata);
53}
54
55pub fn clear_routes() {
57 let mut registry = ROUTE_REGISTRY.lock().expect("route registry lock poisoned");
58 registry.clear();
59}
60
61pub fn get_registered_routes() -> Vec<RouteMetadata> {
63 let registry = ROUTE_REGISTRY.lock().expect("route registry lock poisoned");
64 registry.clone()
65}
66
67pub struct NativeAxumScanner;
69
70impl NativeAxumScanner {
71 pub fn new() -> Self {
72 Self
73 }
74
75 pub fn scan_routes(
77 &self,
78 routes: &[RouteMetadata],
79 include: Option<&str>,
80 exclude: Option<&str>,
81 ) -> Result<Vec<ScannedModule>, AxumApcoreError> {
82 let modules: Vec<ScannedModule> = routes
83 .iter()
84 .map(|meta| self.metadata_to_module(meta))
85 .collect();
86
87 let modules = deduplicate_ids(modules);
88 let modules = filter_modules(&modules, include, exclude)?;
89 Ok(modules)
90 }
91
92 fn metadata_to_module(&self, meta: &RouteMetadata) -> ScannedModule {
94 let tag = if meta.tags.is_empty() {
95 extract_tag_from_path(&meta.path)
96 } else {
97 meta.tags[0].clone()
98 };
99
100 let module_id = format!(
101 "{}.{}.{}",
102 tag,
103 meta.handler_name,
104 meta.method.to_lowercase()
105 );
106
107 let annotations = infer_annotations_from_method(&meta.method);
108
109 let mut metadata_map = HashMap::new();
110 metadata_map.insert(
111 "http_method".into(),
112 serde_json::Value::String(meta.method.clone()),
113 );
114 metadata_map.insert(
115 "url_path".into(),
116 serde_json::Value::String(meta.path.clone()),
117 );
118
119 ScannedModule {
120 module_id,
121 description: meta.description.clone(),
122 input_schema: meta.input_schema.clone(),
123 output_schema: meta.output_schema.clone(),
124 tags: meta.tags.clone(),
125 target: format!("axum::{}", meta.handler_name),
126 version: "1.0.0".into(),
127 annotations: Some(annotations),
128 documentation: meta.documentation.clone(),
129 examples: vec![],
130 metadata: metadata_map,
131 warnings: vec![],
132 }
133 }
134}
135
136impl Default for NativeAxumScanner {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142#[async_trait]
143impl AxumScanner for NativeAxumScanner {
144 async fn scan(
145 &self,
146 _app: &axum::Router,
147 include: Option<&str>,
148 exclude: Option<&str>,
149 ) -> Result<Vec<ScannedModule>, AxumApcoreError> {
150 let routes = get_registered_routes();
151
152 let modules: Vec<ScannedModule> = routes
153 .iter()
154 .map(|meta| self.metadata_to_module(meta))
155 .collect();
156
157 let modules = deduplicate_ids(modules);
158 let modules = filter_modules(&modules, include, exclude)?;
159
160 Ok(modules)
161 }
162
163 fn source_name(&self) -> &str {
164 "native"
165 }
166}
167
168fn extract_tag_from_path(path: &str) -> String {
172 path.split('/')
173 .find(|s| !s.is_empty() && !s.starts_with(':') && *s != "api" && !s.starts_with('v'))
174 .unwrap_or("default")
175 .to_string()
176}
177
178#[macro_export]
197macro_rules! ap_handler {
198 (
199 method: $method:expr,
200 path: $path:expr,
201 handler: $handler:ident,
202 description: $desc:expr,
203 tags: [$($tag:expr),* $(,)?],
204 input_schema: $input:expr,
205 output_schema: $output:expr $(,)?
206 ) => {
207 $crate::scanner::native::register_route($crate::scanner::native::RouteMetadata {
208 method: $method.to_string(),
209 path: $path.to_string(),
210 handler_name: stringify!($handler).to_string(),
211 description: $desc.to_string(),
212 tags: vec![$($tag.to_string()),*],
213 input_schema: $input,
214 output_schema: $output,
215 documentation: None,
216 });
217 };
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use serde_json::json;
224
225 fn sample_route() -> RouteMetadata {
226 RouteMetadata {
227 method: "GET".into(),
228 path: "/api/users/:id".into(),
229 handler_name: "get_user".into(),
230 description: "Get a user by ID".into(),
231 tags: vec!["users".into()],
232 input_schema: json!({"type": "object", "properties": {"id": {"type": "string"}}}),
233 output_schema: json!({"type": "object", "properties": {"name": {"type": "string"}}}),
234 documentation: None,
235 }
236 }
237
238 #[test]
239 fn test_metadata_to_module() {
240 let scanner = NativeAxumScanner::new();
241 let meta = sample_route();
242 let module = scanner.metadata_to_module(&meta);
243
244 assert_eq!(module.module_id, "users.get_user.get");
245 assert_eq!(module.description, "Get a user by ID");
246 assert_eq!(module.target, "axum::get_user");
247 assert!(module.annotations.as_ref().unwrap().readonly);
248 assert!(module.annotations.as_ref().unwrap().cacheable);
249 }
250
251 #[test]
252 fn test_metadata_to_module_no_tags() {
253 let scanner = NativeAxumScanner::new();
254 let meta = RouteMetadata {
255 method: "POST".into(),
256 path: "/api/tasks".into(),
257 handler_name: "create_task".into(),
258 description: "Create a task".into(),
259 tags: vec![],
260 input_schema: json!({}),
261 output_schema: json!({}),
262 documentation: None,
263 };
264 let module = scanner.metadata_to_module(&meta);
265 assert_eq!(module.module_id, "tasks.create_task.post");
266 }
267
268 #[test]
269 fn test_extract_tag_from_path() {
270 assert_eq!(extract_tag_from_path("/api/users/:id"), "users");
271 assert_eq!(extract_tag_from_path("/api/v1/tasks"), "tasks");
272 assert_eq!(extract_tag_from_path("/health"), "health");
273 assert_eq!(extract_tag_from_path("/"), "default");
274 }
275
276 #[test]
277 fn test_scan_routes_with_multiple() {
278 let scanner = NativeAxumScanner::new();
279 let routes = vec![
280 sample_route(),
281 RouteMetadata {
282 method: "DELETE".into(),
283 path: "/api/users/:id".into(),
284 handler_name: "delete_user".into(),
285 description: "Delete a user".into(),
286 tags: vec!["users".into()],
287 input_schema: json!({}),
288 output_schema: json!({}),
289 documentation: None,
290 },
291 ];
292 let modules = scanner.scan_routes(&routes, None, None).unwrap();
293
294 assert_eq!(modules.len(), 2);
295 assert_eq!(modules[0].module_id, "users.get_user.get");
296 assert_eq!(modules[1].module_id, "users.delete_user.delete");
297
298 assert!(modules[0].annotations.as_ref().unwrap().readonly);
300 assert!(modules[1].annotations.as_ref().unwrap().destructive);
301 }
302
303 #[test]
304 fn test_scan_routes_with_include_filter() {
305 let scanner = NativeAxumScanner::new();
306 let routes = vec![
307 sample_route(),
308 RouteMetadata {
309 method: "GET".into(),
310 path: "/api/tasks".into(),
311 handler_name: "list_tasks".into(),
312 description: "List tasks".into(),
313 tags: vec!["tasks".into()],
314 input_schema: json!({}),
315 output_schema: json!({}),
316 documentation: None,
317 },
318 ];
319 let modules = scanner.scan_routes(&routes, Some("tasks"), None).unwrap();
320
321 assert_eq!(modules.len(), 1);
322 assert_eq!(modules[0].module_id, "tasks.list_tasks.get");
323 }
324
325 #[test]
326 fn test_scan_routes_empty() {
327 let scanner = NativeAxumScanner::new();
328 let modules = scanner.scan_routes(&[], None, None).unwrap();
329 assert!(modules.is_empty());
330 }
331
332 #[test]
333 fn test_metadata_http_method_in_metadata() {
334 let scanner = NativeAxumScanner::new();
335 let meta = sample_route();
336 let module = scanner.metadata_to_module(&meta);
337 assert_eq!(module.metadata.get("http_method").unwrap(), "GET");
338 assert_eq!(module.metadata.get("url_path").unwrap(), "/api/users/:id");
339 }
340}