diff --git a/src/docgen/detectors/rust.py b/src/docgen/detectors/rust.py new file mode 100644 index 0000000..79e8bdd --- /dev/null +++ b/src/docgen/detectors/rust.py @@ -0,0 +1,91 @@ +"""Rust endpoint detector for Actix-web.""" + +import re +from pathlib import Path +from typing import Optional +from docgen.models import Endpoint, HTTPMethod +from docgen.detectors.base import BaseDetector + + +class RustDetector(BaseDetector): + """Detector for Rust web frameworks.""" + + extensions = [".rs"] + framework_name = "rust" + + ACTIX_PATTERN = re.compile( + r'(?:route|service)\.\(("|\')(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)("|\')\s*\.?\s*(to|handler)', + re.MULTILINE, + ) + + ACTIX_WEB_PATTERN = re.compile( + r'(?:App::new\(\)|scope|service)\.route\s*\(\s*"([^"]+)"\s*,\s*([a-zA-Z_][a-zA-Z0-9_]*)', + re.MULTILINE, + ) + + ACTIX_MACRO_PATTERN = re.compile( + r'#\[route\s*\(\s*"([^"]+)"\s*,\s*method\s*=\s*(?:HttpMethod::)?(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)', + re.MULTILINE, + ) + + HTTP_METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"} + + METHOD_MAP = { + "GET": HTTPMethod.GET, + "POST": HTTPMethod.POST, + "PUT": HTTPMethod.PUT, + "PATCH": HTTPMethod.PATCH, + "DELETE": HTTPMethod.DELETE, + "OPTIONS": HTTPMethod.OPTIONS, + "HEAD": HTTPMethod.HEAD, + } + + def detect_endpoints(self, file_path: Path) -> list[Endpoint]: + """Detect endpoints in a Rust file.""" + content = file_path.read_text() + endpoints = [] + + if self._detect_actix(content): + endpoints.extend(self._detect_actix_endpoints(content, file_path)) + + return endpoints + + def _detect_actix(self, content: str) -> bool: + """Check if file uses Actix-web.""" + indicators = [ + 'actix_web', + 'actix-web', + 'actix::', + 'HttpResponse', + 'web::Resource', + ] + return any(indicator in content for indicator in indicators) + + def _detect_actix_endpoints(self, content: str, file_path: Path) -> list[Endpoint]: + """Detect Actix-web endpoints.""" + endpoints = [] + + for match in self.ACTIX_WEB_PATTERN.finditer(content): + path, handler = match.groups() + endpoint = Endpoint( + path=path, + method=HTTPMethod.GET, + summary=f"GET {path}", + description=f"Handler: {handler}", + file_path=str(file_path), + line_number=content[:match.start()].count("\n") + 1, + ) + endpoints.append(endpoint) + + for match in self.ACTIX_MACRO_PATTERN.finditer(content): + path, method = match.groups() + endpoint = Endpoint( + path=path, + method=self.METHOD_MAP.get(method, HTTPMethod.GET), + summary=f"{method} {path}", + file_path=str(file_path), + line_number=content[:match.start()].count("\n") + 1, + ) + endpoints.append(endpoint) + + return endpoints