use crate::code_tree::models::{ConstantInfo, FunctionInfo};
mod django;
mod fastapi;
mod flask;
#[derive(Debug)]
pub struct RouteNode {
pub id: String,
pub name: String,
pub path: String,
pub method: String,
pub framework: String,
pub file_path: String,
pub line_number: u32,
}
#[derive(Debug)]
pub struct RouteEdge {
pub route_id: String,
pub function_qname: String,
}
pub fn build_routes(
functions: &[FunctionInfo],
constants: &[ConstantInfo],
) -> (Vec<RouteNode>, Vec<RouteEdge>) {
let mut nodes = Vec::new();
let mut edges = Vec::new();
for (det_nodes, det_edges) in [
flask::detect(functions),
fastapi::detect(functions),
django::detect(constants, functions),
] {
nodes.extend(det_nodes);
edges.extend(det_edges);
}
(nodes, edges)
}
pub(super) fn split_decorator(raw: &str) -> Option<(&str, &str)> {
let open = raw.find('(')?;
let close = raw.rfind(')')?;
if close < open {
return None;
}
Some((raw[..open].trim(), &raw[open + 1..close]))
}
pub(super) fn first_string_literal(args: &str) -> Option<String> {
let bytes = args.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b' ' || b == b'\t' {
i += 1;
continue;
}
if b == b'\'' || b == b'"' {
let quote = b;
let start = i + 1;
let mut j = start;
while j < bytes.len() && bytes[j] != quote {
if bytes[j] == b'\\' && j + 1 < bytes.len() {
j += 2;
continue;
}
j += 1;
}
if j < bytes.len() {
return Some(std::str::from_utf8(&bytes[start..j]).ok()?.to_string());
}
return None;
}
return None;
}
None
}
pub(super) fn keyword_arg<'a>(args: &'a str, key: &str) -> Option<&'a str> {
let pat = format!("{key}=");
let mut start = 0;
while let Some(rel) = args[start..].find(&pat) {
let abs = start + rel;
let preceding_ok = abs == 0 || {
let b = args.as_bytes()[abs - 1];
b == b' ' || b == b','
};
if !preceding_ok {
start = abs + 1;
continue;
}
let after = abs + pat.len();
let bytes = args.as_bytes();
let mut depth = 0i32;
let mut in_quote: Option<u8> = None;
let mut j = after;
while j < bytes.len() {
let c = bytes[j];
if let Some(q) = in_quote {
if c == b'\\' && j + 1 < bytes.len() {
j += 2;
continue;
}
if c == q {
in_quote = None;
}
} else {
match c {
b'\'' | b'"' => in_quote = Some(c),
b'[' | b'(' | b'{' => depth += 1,
b']' | b')' | b'}' => depth -= 1,
b',' if depth == 0 => break,
_ => {}
}
}
j += 1;
}
return Some(args[after..j].trim());
}
None
}
pub(super) fn parse_methods_list(raw: &str) -> Vec<String> {
let trimmed = raw.trim();
let inner = trimmed
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.or_else(|| trimmed.strip_prefix('(').and_then(|s| s.strip_suffix(')')))
.unwrap_or(trimmed);
let mut out = Vec::new();
for piece in inner.split(',') {
let p = piece.trim().trim_matches(['\'', '"']);
if !p.is_empty() {
out.push(p.to_ascii_uppercase());
}
}
out
}
pub(super) fn make_route_id(framework: &str, method: &str, path: &str) -> String {
format!("{framework}::{method}::{path}")
}
#[cfg(test)]
mod tests {
use super::parse_methods_list;
#[test]
fn list_form() {
assert_eq!(parse_methods_list("['GET', 'POST']"), vec!["GET", "POST"]);
assert_eq!(
parse_methods_list(r#"["GET", "POST"]"#),
vec!["GET", "POST"]
);
}
#[test]
fn tuple_form() {
assert_eq!(
parse_methods_list(r#"("GET", "POST")"#),
vec!["GET", "POST"]
);
assert_eq!(parse_methods_list("('get', 'post')"), vec!["GET", "POST"]);
}
#[test]
fn bare_string() {
assert_eq!(parse_methods_list("'GET'"), vec!["GET"]);
}
#[test]
fn lowercase_uppercased() {
assert_eq!(parse_methods_list("['get', 'post']"), vec!["GET", "POST"]);
}
}