use crate::analysis::rust::macro_analyzer::{MacroAnalyzer, SummerMacro};
use crate::protocol::types::{LocationResponse, PositionResponse, RangeResponse};
use lsp_types::Url;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use walkdir::WalkDir;
pub struct RouteScanner {
macro_analyzer: MacroAnalyzer,
}
impl RouteScanner {
pub fn new() -> Self {
Self {
macro_analyzer: MacroAnalyzer::new(),
}
}
pub fn scan_routes(&self, project_path: &Path) -> Result<Vec<RouteInfoResponse>, ScanError> {
let mut routes = Vec::new();
let src_path = project_path.join("src");
if !src_path.exists() {
return Err(ScanError::InvalidProject(
"src directory not found".to_string(),
));
}
for entry in WalkDir::new(&src_path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
{
let file_path = entry.path();
let content = match fs::read_to_string(file_path) {
Ok(content) => content,
Err(e) => {
tracing::warn!("Failed to read file {:?}: {}", file_path, e);
continue;
}
};
let file_url = match Url::from_file_path(file_path) {
Ok(url) => url,
Err(_) => {
tracing::warn!("Failed to convert path to URL: {:?}", file_path);
continue;
}
};
let rust_doc = match self.macro_analyzer.parse(file_url.clone(), content) {
Ok(doc) => doc,
Err(e) => {
tracing::warn!("Failed to parse file {:?}: {}", file_path, e);
continue;
}
};
let rust_doc = match self.macro_analyzer.extract_macros(rust_doc) {
Ok(doc) => doc,
Err(e) => {
tracing::warn!("Failed to extract macros from {:?}: {}", file_path, e);
continue;
}
};
for summer_macro in &rust_doc.macros {
if let SummerMacro::Route(route_macro) = summer_macro {
for method in &route_macro.methods {
routes.push(RouteInfoResponse {
method: method.as_str().to_string(),
path: route_macro.path.clone(),
handler: route_macro.handler_name.clone(),
is_openapi: route_macro.is_openapi,
location: LocationResponse {
uri: file_url.to_string(),
range: RangeResponse {
start: PositionResponse {
line: route_macro.range.start.line,
character: route_macro.range.start.character,
},
end: PositionResponse {
line: route_macro.range.end.line,
character: route_macro.range.end.character,
},
},
},
});
}
}
}
}
Ok(routes)
}
}
impl Default for RouteScanner {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteInfoResponse {
pub method: String,
pub path: String,
pub handler: String,
#[serde(rename = "isOpenapi")]
pub is_openapi: bool,
pub location: LocationResponse,
}
#[derive(Debug, Deserialize)]
pub struct RoutesRequest {
#[serde(rename = "appPath")]
pub app_path: String,
}
#[derive(Debug, Serialize)]
pub struct RoutesResponse {
pub routes: Vec<RouteInfoResponse>,
}
#[derive(Debug, thiserror::Error)]
pub enum ScanError {
#[error("Failed to read file: {0}")]
FileRead(#[from] std::io::Error),
#[error("Invalid project structure: {0}")]
InvalidProject(String),
#[error("No routes found")]
NoRoutes,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_route_scanner_new() {
let _scanner = RouteScanner::new();
}
#[test]
fn test_route_scanner_default() {
let _scanner = RouteScanner::default();
}
}
use lsp_types::Location;
use std::collections::HashMap;
pub struct RouteNavigator {
}
impl RouteNavigator {
pub fn new() -> Self {
Self {}
}
pub fn find_handler_location(&self, _route_path: &str) -> Option<Location> {
None
}
}
impl Default for RouteNavigator {
fn default() -> Self {
Self::new()
}
}
pub struct RouteIndex {
routes: HashMap<String, Route>,
}
impl RouteIndex {
pub fn new() -> Self {
Self {
routes: HashMap::new(),
}
}
pub fn add_route(&mut self, route: Route) {
let key = format!("{} {}", route.method.as_str(), route.path);
self.routes.insert(key, route);
}
pub fn find_route(&self, method: HttpMethod, path: &str) -> Option<&Route> {
let key = format!("{} {}", method.as_str(), path);
self.routes.get(&key)
}
pub fn all_routes(&self) -> Vec<&Route> {
self.routes.values().collect()
}
}
impl Default for RouteIndex {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HttpMethod {
GET,
POST,
PUT,
DELETE,
PATCH,
HEAD,
OPTIONS,
}
impl HttpMethod {
pub fn as_str(&self) -> &'static str {
match self {
HttpMethod::GET => "GET",
HttpMethod::POST => "POST",
HttpMethod::PUT => "PUT",
HttpMethod::DELETE => "DELETE",
HttpMethod::PATCH => "PATCH",
HttpMethod::HEAD => "HEAD",
HttpMethod::OPTIONS => "OPTIONS",
}
}
}
#[derive(Debug, Clone)]
pub struct Route {
pub method: HttpMethod,
pub path: String,
pub handler: String,
pub location: Location,
}