use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use apcore_toolkit::{
deduplicate_ids, filter_modules, infer_annotations_from_method, ScannedModule,
};
use async_trait::async_trait;
use crate::errors::AxumApcoreError;
use crate::scanner::AxumScanner;
#[derive(Debug, Clone)]
pub struct RouteMetadata {
pub method: String,
pub path: String,
pub handler_name: String,
pub description: String,
pub tags: Vec<String>,
pub input_schema: serde_json::Value,
pub output_schema: serde_json::Value,
pub documentation: Option<String>,
}
static ROUTE_REGISTRY: std::sync::LazyLock<Arc<Mutex<Vec<RouteMetadata>>>> =
std::sync::LazyLock::new(|| Arc::new(Mutex::new(Vec::new())));
pub fn register_route(metadata: RouteMetadata) {
let mut registry = ROUTE_REGISTRY.lock().expect("route registry lock poisoned");
registry.push(metadata);
}
pub fn clear_routes() {
let mut registry = ROUTE_REGISTRY.lock().expect("route registry lock poisoned");
registry.clear();
}
pub fn get_registered_routes() -> Vec<RouteMetadata> {
let registry = ROUTE_REGISTRY.lock().expect("route registry lock poisoned");
registry.clone()
}
pub struct NativeAxumScanner;
impl NativeAxumScanner {
pub fn new() -> Self {
Self
}
pub fn scan_routes(
&self,
routes: &[RouteMetadata],
include: Option<&str>,
exclude: Option<&str>,
) -> Result<Vec<ScannedModule>, AxumApcoreError> {
let modules: Vec<ScannedModule> = routes
.iter()
.map(|meta| self.metadata_to_module(meta))
.collect();
let modules = deduplicate_ids(modules);
let modules = filter_modules(&modules, include, exclude)?;
Ok(modules)
}
fn metadata_to_module(&self, meta: &RouteMetadata) -> ScannedModule {
let tag = if meta.tags.is_empty() {
extract_tag_from_path(&meta.path)
} else {
meta.tags[0].clone()
};
let module_id = format!(
"{}.{}.{}",
tag,
meta.handler_name,
meta.method.to_lowercase()
);
let annotations = infer_annotations_from_method(&meta.method);
let mut metadata_map = HashMap::new();
metadata_map.insert(
"http_method".into(),
serde_json::Value::String(meta.method.clone()),
);
metadata_map.insert(
"url_path".into(),
serde_json::Value::String(meta.path.clone()),
);
ScannedModule {
module_id,
description: meta.description.clone(),
input_schema: meta.input_schema.clone(),
output_schema: meta.output_schema.clone(),
tags: meta.tags.clone(),
target: format!("axum::{}", meta.handler_name),
version: "1.0.0".into(),
annotations: Some(annotations),
documentation: meta.documentation.clone(),
examples: vec![],
metadata: metadata_map,
warnings: vec![],
}
}
}
impl Default for NativeAxumScanner {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl AxumScanner for NativeAxumScanner {
async fn scan(
&self,
_app: &axum::Router,
include: Option<&str>,
exclude: Option<&str>,
) -> Result<Vec<ScannedModule>, AxumApcoreError> {
let routes = get_registered_routes();
let modules: Vec<ScannedModule> = routes
.iter()
.map(|meta| self.metadata_to_module(meta))
.collect();
let modules = deduplicate_ids(modules);
let modules = filter_modules(&modules, include, exclude)?;
Ok(modules)
}
fn source_name(&self) -> &str {
"native"
}
}
fn extract_tag_from_path(path: &str) -> String {
path.split('/')
.find(|s| !s.is_empty() && !s.starts_with(':') && *s != "api" && !s.starts_with('v'))
.unwrap_or("default")
.to_string()
}
#[macro_export]
macro_rules! ap_handler {
(
method: $method:expr,
path: $path:expr,
handler: $handler:ident,
description: $desc:expr,
tags: [$($tag:expr),* $(,)?],
input_schema: $input:expr,
output_schema: $output:expr $(,)?
) => {
$crate::scanner::native::register_route($crate::scanner::native::RouteMetadata {
method: $method.to_string(),
path: $path.to_string(),
handler_name: stringify!($handler).to_string(),
description: $desc.to_string(),
tags: vec![$($tag.to_string()),*],
input_schema: $input,
output_schema: $output,
documentation: None,
});
};
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn sample_route() -> RouteMetadata {
RouteMetadata {
method: "GET".into(),
path: "/api/users/:id".into(),
handler_name: "get_user".into(),
description: "Get a user by ID".into(),
tags: vec!["users".into()],
input_schema: json!({"type": "object", "properties": {"id": {"type": "string"}}}),
output_schema: json!({"type": "object", "properties": {"name": {"type": "string"}}}),
documentation: None,
}
}
#[test]
fn test_metadata_to_module() {
let scanner = NativeAxumScanner::new();
let meta = sample_route();
let module = scanner.metadata_to_module(&meta);
assert_eq!(module.module_id, "users.get_user.get");
assert_eq!(module.description, "Get a user by ID");
assert_eq!(module.target, "axum::get_user");
assert!(module.annotations.as_ref().unwrap().readonly);
assert!(module.annotations.as_ref().unwrap().cacheable);
}
#[test]
fn test_metadata_to_module_no_tags() {
let scanner = NativeAxumScanner::new();
let meta = RouteMetadata {
method: "POST".into(),
path: "/api/tasks".into(),
handler_name: "create_task".into(),
description: "Create a task".into(),
tags: vec![],
input_schema: json!({}),
output_schema: json!({}),
documentation: None,
};
let module = scanner.metadata_to_module(&meta);
assert_eq!(module.module_id, "tasks.create_task.post");
}
#[test]
fn test_extract_tag_from_path() {
assert_eq!(extract_tag_from_path("/api/users/:id"), "users");
assert_eq!(extract_tag_from_path("/api/v1/tasks"), "tasks");
assert_eq!(extract_tag_from_path("/health"), "health");
assert_eq!(extract_tag_from_path("/"), "default");
}
#[test]
fn test_scan_routes_with_multiple() {
let scanner = NativeAxumScanner::new();
let routes = vec![
sample_route(),
RouteMetadata {
method: "DELETE".into(),
path: "/api/users/:id".into(),
handler_name: "delete_user".into(),
description: "Delete a user".into(),
tags: vec!["users".into()],
input_schema: json!({}),
output_schema: json!({}),
documentation: None,
},
];
let modules = scanner.scan_routes(&routes, None, None).unwrap();
assert_eq!(modules.len(), 2);
assert_eq!(modules[0].module_id, "users.get_user.get");
assert_eq!(modules[1].module_id, "users.delete_user.delete");
assert!(modules[0].annotations.as_ref().unwrap().readonly);
assert!(modules[1].annotations.as_ref().unwrap().destructive);
}
#[test]
fn test_scan_routes_with_include_filter() {
let scanner = NativeAxumScanner::new();
let routes = vec![
sample_route(),
RouteMetadata {
method: "GET".into(),
path: "/api/tasks".into(),
handler_name: "list_tasks".into(),
description: "List tasks".into(),
tags: vec!["tasks".into()],
input_schema: json!({}),
output_schema: json!({}),
documentation: None,
},
];
let modules = scanner.scan_routes(&routes, Some("tasks"), None).unwrap();
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].module_id, "tasks.list_tasks.get");
}
#[test]
fn test_scan_routes_empty() {
let scanner = NativeAxumScanner::new();
let modules = scanner.scan_routes(&[], None, None).unwrap();
assert!(modules.is_empty());
}
#[test]
fn test_metadata_http_method_in_metadata() {
let scanner = NativeAxumScanner::new();
let meta = sample_route();
let module = scanner.metadata_to_module(&meta);
assert_eq!(module.metadata.get("http_method").unwrap(), "GET");
assert_eq!(module.metadata.get("url_path").unwrap(), "/api/users/:id");
}
}