mod csharp;
mod elixir;
mod generic;
mod go;
mod helpers;
mod java;
mod js_ts;
mod php;
mod python;
mod ruby;
mod rust_lang;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::graph::GraphQuery;
use helpers::{detect_from_docstring, language_from_file, Lang};
use csharp::detect_csharp_route;
use elixir::detect_elixir_route;
use generic::detect_generic_route;
use go::detect_go_route;
use java::detect_java_route;
use js_ts::detect_js_ts_route;
use php::detect_php_route;
use python::detect_python_route;
use ruby::detect_ruby_route;
use rust_lang::detect_rust_route;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Route {
pub method: String,
pub path: String,
pub handler_id: String,
pub file: String,
pub framework: String,
}
pub fn detect_routes(gq: &GraphQuery) -> Result<Vec<Route>> {
let rows = gq.raw_query(
"MATCH (s:Symbol) WHERE s.kind IN ['Function', 'Method'] \
RETURN s.id, s.name, s.kind, s.file, s.docstring",
)?;
let mut routes = Vec::new();
for row in &rows {
let id = &row[0];
let name = &row[1];
let _kind = &row[2];
let file = &row[3];
let docstring = row.get(4).map(|s| s.as_str()).unwrap_or("");
if let Some(route) = detect_route_from_symbol(id, name, file, docstring) {
routes.push(route);
}
}
routes.sort_by(|a, b| a.file.cmp(&b.file).then(a.path.cmp(&b.path)));
Ok(routes)
}
fn detect_route_from_symbol(id: &str, name: &str, file: &str, docstring: &str) -> Option<Route> {
let name_lower = name.to_lowercase();
let doc_lower = docstring.to_lowercase();
let lang = language_from_file(file);
if let Some(route) = detect_from_docstring(id, name, file, &doc_lower) {
return Some(route);
}
match lang {
Lang::Python => detect_python_route(id, name, &name_lower, file, &doc_lower),
Lang::JavaScript | Lang::TypeScript => {
detect_js_ts_route(id, name, &name_lower, file, &doc_lower)
}
Lang::Go => detect_go_route(id, name, &name_lower, file, &doc_lower),
Lang::Java => detect_java_route(id, name, &name_lower, file, &doc_lower),
Lang::Rust => detect_rust_route(id, name, &name_lower, file, &doc_lower),
Lang::Ruby => detect_ruby_route(id, name, &name_lower, file, &doc_lower),
Lang::Php => detect_php_route(id, name, &name_lower, file, &doc_lower),
Lang::CSharp => detect_csharp_route(id, name, &name_lower, file, &doc_lower),
Lang::Elixir => detect_elixir_route(id, name, &name_lower, file, &doc_lower),
Lang::Other => detect_generic_route(id, name, &name_lower, file, &doc_lower),
}
}
pub fn format_routes(routes: &[Route]) -> String {
if routes.is_empty() {
return "No HTTP routes detected.".to_string();
}
let mut out = format!("Detected {} HTTP route(s):\n\n", routes.len());
let mut current_file = "";
for route in routes {
if route.file != current_file {
current_file = &route.file;
out.push_str(&format!(" {}:\n", current_file));
}
out.push_str(&format!(
" {:>7} {:30} [{:15}] [{}]\n",
route.method, route.path, route.framework, route.handler_id
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use helpers::{camel_to_path, extract_path_from_text};
#[test]
fn test_python_get_prefix() {
let route = detect_route_from_symbol("views.py::get_users", "get_users", "views.py", "");
assert!(route.is_some());
let r = route.unwrap();
assert_eq!(r.method, "GET");
assert_eq!(r.path, "/users");
}
#[test]
fn test_python_post_prefix() {
let route = detect_route_from_symbol("views.py::post_order", "post_order", "views.py", "");
assert!(route.is_some());
let r = route.unwrap();
assert_eq!(r.method, "POST");
assert_eq!(r.path, "/order");
}
#[test]
fn test_python_handler_suffix() {
let route =
detect_route_from_symbol("views.py::user_handler", "user_handler", "views.py", "");
assert!(route.is_some());
let r = route.unwrap();
assert_eq!(r.path, "/user");
}
#[test]
fn test_go_handler_suffix() {
let route = detect_route_from_symbol("api.go::UsersHandler", "UsersHandler", "api.go", "");
assert!(route.is_some());
let r = route.unwrap();
assert!(r.path.contains("users"));
}
#[test]
fn test_go_serve_http() {
let route = detect_route_from_symbol(
"server.go::MyHandler::ServeHTTP",
"ServeHTTP",
"server.go",
"",
);
assert!(route.is_some());
}
#[test]
fn test_js_handler() {
let route =
detect_route_from_symbol("api/users.ts::handler", "handler", "api/users.ts", "");
assert!(route.is_some());
}
#[test]
fn test_docstring_route() {
let route = detect_route_from_symbol(
"app.py::list_items",
"list_items",
"app.py",
"GET /api/items endpoint",
);
assert!(route.is_some());
let r = route.unwrap();
assert_eq!(r.method, "GET");
assert_eq!(r.path, "/api/items");
}
#[test]
fn test_java_controller_file() {
let route = detect_route_from_symbol(
"UserController.java::UserController::getUsers",
"getUsers",
"com/example/controller/UserController.java",
"",
);
assert!(route.is_some());
let r = route.unwrap();
assert_eq!(r.method, "GET");
}
#[test]
fn test_no_false_positive_regular_function() {
let route =
detect_route_from_symbol("utils.py::format_string", "format_string", "utils.py", "");
assert!(route.is_none());
}
#[test]
fn test_extract_path_from_text() {
assert_eq!(
extract_path_from_text("route \"/api/users\""),
Some("/api/users".to_string())
);
assert_eq!(
extract_path_from_text("GET /api/items endpoint"),
Some("/api/items".to_string())
);
}
#[test]
fn test_camel_to_path() {
assert_eq!(camel_to_path("users"), "users");
assert_eq!(camel_to_path("user_profile"), "user/profile");
}
}