use super::{
first_string_literal, keyword_arg, make_route_id, parse_methods_list, split_decorator,
RouteEdge, RouteNode,
};
use crate::code_tree::models::FunctionInfo;
const FRAMEWORK: &str = "flask";
const METHOD_SHORTCUTS: &[&str] = &["get", "post", "put", "delete", "patch", "options", "head"];
pub(super) fn detect(functions: &[FunctionInfo]) -> (Vec<RouteNode>, Vec<RouteEdge>) {
let mut nodes = Vec::new();
let mut edges = Vec::new();
for fn_info in functions {
for raw in &fn_info.decorators {
let Some((head, args)) = split_decorator(raw) else {
continue;
};
let suffix = head.rsplit('.').next().unwrap_or(head).to_ascii_lowercase();
if is_fastapi_holder(head) {
continue;
}
if suffix == "route" {
if let Some(path) = first_string_literal(args) {
let methods = keyword_arg(args, "methods")
.map(parse_methods_list)
.unwrap_or_else(|| vec!["ANY".to_string()]);
for method in methods {
emit(&mut nodes, &mut edges, fn_info, &path, &method);
}
}
continue;
}
if METHOD_SHORTCUTS.contains(&suffix.as_str()) {
if let Some(path) = first_string_literal(args) {
emit(
&mut nodes,
&mut edges,
fn_info,
&path,
&suffix.to_ascii_uppercase(),
);
}
}
}
}
(nodes, edges)
}
fn emit(
nodes: &mut Vec<RouteNode>,
edges: &mut Vec<RouteEdge>,
fn_info: &FunctionInfo,
path: &str,
method: &str,
) {
let id = make_route_id(FRAMEWORK, method, path);
nodes.push(RouteNode {
id: id.clone(),
name: path.to_string(),
path: path.to_string(),
method: method.to_string(),
framework: FRAMEWORK.to_string(),
file_path: fn_info.file_path.clone(),
line_number: fn_info.line_number,
});
edges.push(RouteEdge {
route_id: id,
function_qname: fn_info.qualified_name.clone(),
});
}
fn is_fastapi_holder(head: &str) -> bool {
let holder = head.rsplit('.').nth(1).unwrap_or("").to_ascii_lowercase();
holder == "router" || holder == "api_router"
}