1mod csharp;
2mod elixir;
3mod generic;
4mod go;
5mod helpers;
6mod java;
7mod js_ts;
8mod php;
9mod python;
10mod ruby;
11mod rust_lang;
12
13use anyhow::Result;
14use serde::{Deserialize, Serialize};
15
16use crate::graph::GraphQuery;
17
18use helpers::{detect_from_docstring, language_from_file, Lang};
19
20use csharp::detect_csharp_route;
21use elixir::detect_elixir_route;
22use generic::detect_generic_route;
23use go::detect_go_route;
24use java::detect_java_route;
25use js_ts::detect_js_ts_route;
26use php::detect_php_route;
27use python::detect_python_route;
28use ruby::detect_ruby_route;
29use rust_lang::detect_rust_route;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Route {
34 pub method: String,
36 pub path: String,
38 pub handler_id: String,
40 pub file: String,
42 pub framework: String,
44}
45
46pub fn detect_routes(gq: &GraphQuery) -> Result<Vec<Route>> {
52 let rows = gq.raw_query(
53 "MATCH (s:Symbol) WHERE s.kind IN ['Function', 'Method'] \
54 RETURN s.id, s.name, s.kind, s.file, s.docstring",
55 )?;
56
57 let mut routes = Vec::new();
58
59 for row in &rows {
60 let id = &row[0];
61 let name = &row[1];
62 let _kind = &row[2];
63 let file = &row[3];
64 let docstring = row.get(4).map(|s| s.as_str()).unwrap_or("");
65
66 if let Some(route) = detect_route_from_symbol(id, name, file, docstring) {
67 routes.push(route);
68 }
69 }
70
71 routes.sort_by(|a, b| a.file.cmp(&b.file).then(a.path.cmp(&b.path)));
73
74 Ok(routes)
75}
76
77fn detect_route_from_symbol(id: &str, name: &str, file: &str, docstring: &str) -> Option<Route> {
79 let name_lower = name.to_lowercase();
80 let doc_lower = docstring.to_lowercase();
81
82 let lang = language_from_file(file);
84
85 if let Some(route) = detect_from_docstring(id, name, file, &doc_lower) {
88 return Some(route);
89 }
90
91 match lang {
93 Lang::Python => detect_python_route(id, name, &name_lower, file, &doc_lower),
94 Lang::JavaScript | Lang::TypeScript => {
95 detect_js_ts_route(id, name, &name_lower, file, &doc_lower)
96 }
97 Lang::Go => detect_go_route(id, name, &name_lower, file, &doc_lower),
98 Lang::Java => detect_java_route(id, name, &name_lower, file, &doc_lower),
99 Lang::Rust => detect_rust_route(id, name, &name_lower, file, &doc_lower),
100 Lang::Ruby => detect_ruby_route(id, name, &name_lower, file, &doc_lower),
101 Lang::Php => detect_php_route(id, name, &name_lower, file, &doc_lower),
102 Lang::CSharp => detect_csharp_route(id, name, &name_lower, file, &doc_lower),
103 Lang::Elixir => detect_elixir_route(id, name, &name_lower, file, &doc_lower),
104 Lang::Other => detect_generic_route(id, name, &name_lower, file, &doc_lower),
105 }
106}
107
108pub fn format_routes(routes: &[Route]) -> String {
110 if routes.is_empty() {
111 return "No HTTP routes detected.".to_string();
112 }
113
114 let mut out = format!("Detected {} HTTP route(s):\n\n", routes.len());
115
116 let mut current_file = "";
117 for route in routes {
118 if route.file != current_file {
119 current_file = &route.file;
120 out.push_str(&format!(" {}:\n", current_file));
121 }
122 out.push_str(&format!(
123 " {:>7} {:30} [{:15}] [{}]\n",
124 route.method, route.path, route.framework, route.handler_id
125 ));
126 }
127
128 out
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use helpers::{camel_to_path, extract_path_from_text};
135
136 #[test]
137 fn test_python_get_prefix() {
138 let route = detect_route_from_symbol("views.py::get_users", "get_users", "views.py", "");
139 assert!(route.is_some());
140 let r = route.unwrap();
141 assert_eq!(r.method, "GET");
142 assert_eq!(r.path, "/users");
143 }
144
145 #[test]
146 fn test_python_post_prefix() {
147 let route = detect_route_from_symbol("views.py::post_order", "post_order", "views.py", "");
148 assert!(route.is_some());
149 let r = route.unwrap();
150 assert_eq!(r.method, "POST");
151 assert_eq!(r.path, "/order");
152 }
153
154 #[test]
155 fn test_python_handler_suffix() {
156 let route =
157 detect_route_from_symbol("views.py::user_handler", "user_handler", "views.py", "");
158 assert!(route.is_some());
159 let r = route.unwrap();
160 assert_eq!(r.path, "/user");
161 }
162
163 #[test]
164 fn test_go_handler_suffix() {
165 let route = detect_route_from_symbol("api.go::UsersHandler", "UsersHandler", "api.go", "");
166 assert!(route.is_some());
167 let r = route.unwrap();
168 assert!(r.path.contains("users"));
169 }
170
171 #[test]
172 fn test_go_serve_http() {
173 let route = detect_route_from_symbol(
174 "server.go::MyHandler::ServeHTTP",
175 "ServeHTTP",
176 "server.go",
177 "",
178 );
179 assert!(route.is_some());
180 }
181
182 #[test]
183 fn test_js_handler() {
184 let route =
185 detect_route_from_symbol("api/users.ts::handler", "handler", "api/users.ts", "");
186 assert!(route.is_some());
187 }
188
189 #[test]
190 fn test_docstring_route() {
191 let route = detect_route_from_symbol(
192 "app.py::list_items",
193 "list_items",
194 "app.py",
195 "GET /api/items endpoint",
196 );
197 assert!(route.is_some());
198 let r = route.unwrap();
199 assert_eq!(r.method, "GET");
200 assert_eq!(r.path, "/api/items");
201 }
202
203 #[test]
204 fn test_java_controller_file() {
205 let route = detect_route_from_symbol(
206 "UserController.java::UserController::getUsers",
207 "getUsers",
208 "com/example/controller/UserController.java",
209 "",
210 );
211 assert!(route.is_some());
212 let r = route.unwrap();
213 assert_eq!(r.method, "GET");
214 }
215
216 #[test]
217 fn test_no_false_positive_regular_function() {
218 let route =
219 detect_route_from_symbol("utils.py::format_string", "format_string", "utils.py", "");
220 assert!(route.is_none());
221 }
222
223 #[test]
224 fn test_extract_path_from_text() {
225 assert_eq!(
226 extract_path_from_text("route \"/api/users\""),
227 Some("/api/users".to_string())
228 );
229 assert_eq!(
230 extract_path_from_text("GET /api/items endpoint"),
231 Some("/api/items".to_string())
232 );
233 }
234
235 #[test]
236 fn test_camel_to_path() {
237 assert_eq!(camel_to_path("users"), "users");
238 assert_eq!(camel_to_path("user_profile"), "user/profile");
239 }
240}