mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss] phase 17: Track L.15 — Gin / Echo / Fiber / Chi adapters + Axum / Actix / Rocket / Warp adapters
This commit is contained in:
parent
5393fe22f2
commit
2b96c6005b
33 changed files with 3247 additions and 27 deletions
126
src/dynamic/framework/adapters/go_chi.rs
Normal file
126
src/dynamic/framework/adapters/go_chi.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
//! Chi [`super::super::FrameworkAdapter`] (Phase 17 — Track L.15).
|
||||
//!
|
||||
//! Recognises the canonical chi route declaration:
|
||||
//!
|
||||
//! ```go
|
||||
//! r := chi.NewRouter()
|
||||
//! r.Get("/users/{id}", Show)
|
||||
//! r.Post("/save", func(w http.ResponseWriter, r *http.Request) {})
|
||||
//! ```
|
||||
//!
|
||||
//! Chi uses brace placeholders (`{id}`, `{id:[0-9]+}`) and pascal-
|
||||
//! cased verb methods. Handler signature is `func(w, r)` — the
|
||||
//! request-param binder treats `w` / `r` as implicit context.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::go_routes::{
|
||||
bind_go_path_params, find_go_function, find_route_for_callee, go_formal_names,
|
||||
source_imports_chi,
|
||||
};
|
||||
|
||||
pub struct GoChiAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "go-chi";
|
||||
|
||||
impl FrameworkAdapter for GoChiAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Go
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
ast: Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
if !source_imports_chi(file_bytes) {
|
||||
return None;
|
||||
}
|
||||
let (method, path) = find_route_for_callee(ast, file_bytes, &summary.name)?;
|
||||
let request_params = find_go_function(ast, file_bytes, &summary.name)
|
||||
.map(|func| {
|
||||
let formals = go_formal_names(func, file_bytes);
|
||||
bind_go_path_params(&formals, &path)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::framework::{HttpMethod, ParamSource};
|
||||
|
||||
fn parse(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_go::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
fn summary(name: &str) -> FuncSummary {
|
||||
FuncSummary {
|
||||
name: name.into(),
|
||||
lang: "go".into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_get_with_brace_placeholder() {
|
||||
let src: &[u8] = b"package main\nimport (\"net/http\"; \"github.com/go-chi/chi/v5\")\n\
|
||||
func init() { r := chi.NewRouter(); r.Get(\"/users/{id}\", Show) }\n\
|
||||
func Show(w http.ResponseWriter, r *http.Request) {}\n";
|
||||
let tree = parse(src);
|
||||
let binding = GoChiAdapter
|
||||
.detect(&summary("Show"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.adapter, "go-chi");
|
||||
let route = binding.route.expect("route");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert_eq!(route.path, "/users/{id}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_regex_placeholder() {
|
||||
let src: &[u8] = b"package main\nimport \"github.com/go-chi/chi/v5\"\n\
|
||||
func init() { r := chi.NewRouter(); r.Get(\"/u/{id:[0-9]+}\", Show) }\n\
|
||||
func Show(w interface{}, id string) {}\n";
|
||||
let tree = parse(src);
|
||||
let binding = GoChiAdapter
|
||||
.detect(&summary("Show"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
let id = binding
|
||||
.request_params
|
||||
.iter()
|
||||
.find(|p| p.name == "id")
|
||||
.unwrap();
|
||||
assert!(matches!(id.source, ParamSource::PathSegment(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_chi_not_imported() {
|
||||
let src: &[u8] = b"package main\nfunc Show() {}\n";
|
||||
let tree = parse(src);
|
||||
assert!(GoChiAdapter
|
||||
.detect(&summary("Show"), tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
127
src/dynamic/framework/adapters/go_echo.rs
Normal file
127
src/dynamic/framework/adapters/go_echo.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
//! Echo [`super::super::FrameworkAdapter`] (Phase 17 — Track L.15).
|
||||
//!
|
||||
//! Recognises the canonical echo route declaration:
|
||||
//!
|
||||
//! ```go
|
||||
//! e := echo.New()
|
||||
//! e.GET("/users/:id", Show)
|
||||
//! e.POST("/save", func(c echo.Context) error { return nil })
|
||||
//! ```
|
||||
//!
|
||||
//! The adapter binds the route to the function whose name matches
|
||||
//! `summary.name`; the path-placeholder syntax (`:id`) shares the
|
||||
//! same vocabulary as gin / fiber.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::go_routes::{
|
||||
bind_go_path_params, find_go_function, find_route_for_callee, go_formal_names,
|
||||
source_imports_echo,
|
||||
};
|
||||
|
||||
pub struct GoEchoAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "go-echo";
|
||||
|
||||
impl FrameworkAdapter for GoEchoAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Go
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
ast: Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
if !source_imports_echo(file_bytes) {
|
||||
return None;
|
||||
}
|
||||
let (method, path) = find_route_for_callee(ast, file_bytes, &summary.name)?;
|
||||
let request_params = find_go_function(ast, file_bytes, &summary.name)
|
||||
.map(|func| {
|
||||
let formals = go_formal_names(func, file_bytes);
|
||||
bind_go_path_params(&formals, &path)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::framework::{HttpMethod, ParamSource};
|
||||
|
||||
fn parse(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_go::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
fn summary(name: &str) -> FuncSummary {
|
||||
FuncSummary {
|
||||
name: name.into(),
|
||||
lang: "go".into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_get_with_identifier_callable() {
|
||||
let src: &[u8] = b"package main\nimport \"github.com/labstack/echo/v4\"\n\
|
||||
func init() { e := echo.New(); e.GET(\"/users/:id\", Show) }\n\
|
||||
func Show(c echo.Context, id string) error { return nil }\n";
|
||||
let tree = parse(src);
|
||||
let binding = GoEchoAdapter
|
||||
.detect(&summary("Show"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.adapter, "go-echo");
|
||||
let route = binding.route.expect("route");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert_eq!(route.path, "/users/:id");
|
||||
let id = binding
|
||||
.request_params
|
||||
.iter()
|
||||
.find(|p| p.name == "id")
|
||||
.unwrap();
|
||||
assert!(matches!(id.source, ParamSource::PathSegment(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_put_verb() {
|
||||
let src: &[u8] = b"package main\nimport \"github.com/labstack/echo\"\n\
|
||||
func init() { e := echo.New(); e.PUT(\"/users/:id\", Update) }\n\
|
||||
func Update(c echo.Context, id string) error { return nil }\n";
|
||||
let tree = parse(src);
|
||||
let binding = GoEchoAdapter
|
||||
.detect(&summary("Update"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.route.unwrap().method, HttpMethod::PUT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_echo_not_imported() {
|
||||
let src: &[u8] = b"package main\nfunc Show() {}\n";
|
||||
let tree = parse(src);
|
||||
assert!(GoEchoAdapter
|
||||
.detect(&summary("Show"), tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
133
src/dynamic/framework/adapters/go_fiber.rs
Normal file
133
src/dynamic/framework/adapters/go_fiber.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
//! Fiber [`super::super::FrameworkAdapter`] (Phase 17 — Track L.15).
|
||||
//!
|
||||
//! Recognises the canonical fiber route declaration:
|
||||
//!
|
||||
//! ```go
|
||||
//! app := fiber.New()
|
||||
//! app.Get("/users/:id", Show)
|
||||
//! app.Post("/save", func(c *fiber.Ctx) error { return nil })
|
||||
//! ```
|
||||
//!
|
||||
//! Fiber uses pascal-cased verb methods (`Get`/`Post`/`Put`/...), and
|
||||
//! its path vocabulary includes `:id`, `:id?` (optional), `+name`
|
||||
//! (greedy non-empty), and `*name` (greedy match-all). All three
|
||||
//! placeholder shapes resolve via [`super::go_routes::extract_go_path_placeholders`].
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::go_routes::{
|
||||
bind_go_path_params, find_go_function, find_route_for_callee, go_formal_names,
|
||||
source_imports_fiber,
|
||||
};
|
||||
|
||||
pub struct GoFiberAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "go-fiber";
|
||||
|
||||
impl FrameworkAdapter for GoFiberAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Go
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
ast: Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
if !source_imports_fiber(file_bytes) {
|
||||
return None;
|
||||
}
|
||||
let (method, path) = find_route_for_callee(ast, file_bytes, &summary.name)?;
|
||||
let request_params = find_go_function(ast, file_bytes, &summary.name)
|
||||
.map(|func| {
|
||||
let formals = go_formal_names(func, file_bytes);
|
||||
bind_go_path_params(&formals, &path)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::framework::{HttpMethod, ParamSource};
|
||||
|
||||
fn parse(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_go::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
fn summary(name: &str) -> FuncSummary {
|
||||
FuncSummary {
|
||||
name: name.into(),
|
||||
lang: "go".into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_get_with_identifier_callable() {
|
||||
let src: &[u8] = b"package main\nimport \"github.com/gofiber/fiber/v2\"\n\
|
||||
func init() { app := fiber.New(); app.Get(\"/users/:id\", Show) }\n\
|
||||
func Show(c *fiber.Ctx, id string) error { return nil }\n";
|
||||
let tree = parse(src);
|
||||
let binding = GoFiberAdapter
|
||||
.detect(&summary("Show"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.adapter, "go-fiber");
|
||||
let route = binding.route.expect("route");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert_eq!(route.path, "/users/:id");
|
||||
let id = binding
|
||||
.request_params
|
||||
.iter()
|
||||
.find(|p| p.name == "id")
|
||||
.unwrap();
|
||||
assert!(matches!(id.source, ParamSource::PathSegment(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_greedy_plus_wildcard() {
|
||||
let src: &[u8] = b"package main\nimport \"github.com/gofiber/fiber/v2\"\n\
|
||||
func init() { app := fiber.New(); app.Get(\"/files/+rest\", Stream) }\n\
|
||||
func Stream(c *fiber.Ctx, rest string) error { return nil }\n";
|
||||
let tree = parse(src);
|
||||
let binding = GoFiberAdapter
|
||||
.detect(&summary("Stream"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
let rest = binding
|
||||
.request_params
|
||||
.iter()
|
||||
.find(|p| p.name == "rest")
|
||||
.unwrap();
|
||||
assert!(matches!(rest.source, ParamSource::PathSegment(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_fiber_not_imported() {
|
||||
let src: &[u8] = b"package main\nfunc Show() {}\n";
|
||||
let tree = parse(src);
|
||||
assert!(GoFiberAdapter
|
||||
.detect(&summary("Show"), tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
152
src/dynamic/framework/adapters/go_gin.rs
Normal file
152
src/dynamic/framework/adapters/go_gin.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
//! Gin [`super::super::FrameworkAdapter`] (Phase 17 — Track L.15).
|
||||
//!
|
||||
//! Recognises the canonical gin route declaration:
|
||||
//!
|
||||
//! ```go
|
||||
//! r := gin.Default()
|
||||
//! r.GET("/users/:id", Show)
|
||||
//! r.POST("/save", func(c *gin.Context) { /* ... */ })
|
||||
//! ```
|
||||
//!
|
||||
//! The adapter binds the route to the function whose name matches
|
||||
//! `summary.name` either via a bare identifier callable, a selector
|
||||
//! callable (`controllers.Show`), or via a func literal (closure)
|
||||
//! that this implementation accepts as a wildcard because the
|
||||
//! surrounding adapter has already narrowed to the func whose name
|
||||
//! matches the summary.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::go_routes::{
|
||||
bind_go_path_params, find_go_function, find_route_for_callee, go_formal_names,
|
||||
source_imports_gin,
|
||||
};
|
||||
|
||||
pub struct GoGinAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "go-gin";
|
||||
|
||||
impl FrameworkAdapter for GoGinAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Go
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
ast: Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
if !source_imports_gin(file_bytes) {
|
||||
return None;
|
||||
}
|
||||
let (method, path) = find_route_for_callee(ast, file_bytes, &summary.name)?;
|
||||
let request_params = find_go_function(ast, file_bytes, &summary.name)
|
||||
.map(|func| {
|
||||
let formals = go_formal_names(func, file_bytes);
|
||||
bind_go_path_params(&formals, &path)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::framework::{HttpMethod, ParamSource};
|
||||
|
||||
fn parse(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_go::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
fn summary(name: &str) -> FuncSummary {
|
||||
FuncSummary {
|
||||
name: name.into(),
|
||||
lang: "go".into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_get_with_identifier_callable() {
|
||||
let src: &[u8] = b"package main\nimport \"github.com/gin-gonic/gin\"\n\
|
||||
func init() { r := gin.Default(); r.GET(\"/users/:id\", Show) }\n\
|
||||
func Show(c *gin.Context, id string) {}\n";
|
||||
let tree = parse(src);
|
||||
let binding = GoGinAdapter
|
||||
.detect(&summary("Show"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.adapter, "go-gin");
|
||||
assert_eq!(binding.kind, EntryKind::HttpRoute);
|
||||
let route = binding.route.expect("route");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert_eq!(route.path, "/users/:id");
|
||||
let id = binding
|
||||
.request_params
|
||||
.iter()
|
||||
.find(|p| p.name == "id")
|
||||
.unwrap();
|
||||
assert!(matches!(id.source, ParamSource::PathSegment(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_post_with_closure() {
|
||||
let src: &[u8] = b"package main\nimport \"github.com/gin-gonic/gin\"\n\
|
||||
func Save(c *gin.Context) {}\n\
|
||||
func init() { r := gin.Default(); r.POST(\"/save\", Save) }\n";
|
||||
let tree = parse(src);
|
||||
let binding = GoGinAdapter
|
||||
.detect(&summary("Save"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.route.unwrap().method, HttpMethod::POST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_gin_not_imported() {
|
||||
let src: &[u8] = b"package main\nfunc Show(id string) {}\n";
|
||||
let tree = parse(src);
|
||||
assert!(GoGinAdapter
|
||||
.detect(&summary("Show"), tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_route_does_not_reference_function() {
|
||||
let src: &[u8] =
|
||||
b"package main\nimport \"github.com/gin-gonic/gin\"\nfunc init() { r := gin.Default(); r.GET(\"/users\", Show) }\nfunc Helper(x string) {}\n";
|
||||
let tree = parse(src);
|
||||
assert!(GoGinAdapter
|
||||
.detect(&summary("Helper"), tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_marker_comment() {
|
||||
let src: &[u8] =
|
||||
b"// nyx-shape: gin\npackage main\nfunc init() { r.GET(\"/x\", Show) }\nfunc Show(c interface{}) {}\n";
|
||||
let tree = parse(src);
|
||||
let binding = GoGinAdapter
|
||||
.detect(&summary("Show"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.adapter, "go-gin");
|
||||
}
|
||||
}
|
||||
456
src/dynamic/framework/adapters/go_routes.rs
Normal file
456
src/dynamic/framework/adapters/go_routes.rs
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
//! Shared Go-route adapter helpers (Phase 17 — Track L.15).
|
||||
//!
|
||||
//! The gin / echo / fiber / chi adapters all need the same handful
|
||||
//! of tree-sitter helpers: locate a `func` declaration by name,
|
||||
//! enumerate formal parameter names, walk the file looking for a
|
||||
//! `engine.GET("/path", handler)` / `router.Post("/x", handler)` call
|
||||
//! whose callable references a target function name, parse a path
|
||||
//! template into placeholder names, and bind formals to request
|
||||
//! slots. Centralising the helpers here keeps the four adapters
|
||||
//! terse and lets every framework share the same placeholder-binding
|
||||
//! semantics.
|
||||
//!
|
||||
//! Path placeholder vocabulary:
|
||||
//! - gin / echo / chi use `:id` and (chi) `{id}` interchangeably.
|
||||
//! - fiber uses `:id` and `+` / `*` greedy wildcards.
|
||||
//! [`extract_go_path_placeholders`] supports both syntaxes.
|
||||
|
||||
use crate::dynamic::framework::{HttpMethod, ParamBinding, ParamSource};
|
||||
use tree_sitter::Node;
|
||||
|
||||
/// True when `bytes` carries any of the well-known gin markers.
|
||||
pub fn source_imports_gin(bytes: &[u8]) -> bool {
|
||||
contains_any(
|
||||
bytes,
|
||||
&[
|
||||
b"github.com/gin-gonic/gin",
|
||||
b"gin.Engine",
|
||||
b"gin.Default",
|
||||
b"gin.New",
|
||||
b"// nyx-shape: gin",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// True when `bytes` carries any of the well-known echo markers.
|
||||
pub fn source_imports_echo(bytes: &[u8]) -> bool {
|
||||
contains_any(
|
||||
bytes,
|
||||
&[
|
||||
b"github.com/labstack/echo",
|
||||
b"echo.Echo",
|
||||
b"echo.New",
|
||||
b"echo.Context",
|
||||
b"// nyx-shape: echo",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// True when `bytes` carries any of the well-known fiber markers.
|
||||
pub fn source_imports_fiber(bytes: &[u8]) -> bool {
|
||||
contains_any(
|
||||
bytes,
|
||||
&[
|
||||
b"github.com/gofiber/fiber",
|
||||
b"fiber.App",
|
||||
b"fiber.New",
|
||||
b"fiber.Ctx",
|
||||
b"// nyx-shape: fiber",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// True when `bytes` carries any of the well-known chi markers.
|
||||
pub fn source_imports_chi(bytes: &[u8]) -> bool {
|
||||
contains_any(
|
||||
bytes,
|
||||
&[
|
||||
b"github.com/go-chi/chi",
|
||||
b"chi.NewRouter",
|
||||
b"chi.Mux",
|
||||
b"chi.Router",
|
||||
b"// nyx-shape: chi",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn contains_any(haystack: &[u8], needles: &[&[u8]]) -> bool {
|
||||
needles
|
||||
.iter()
|
||||
.any(|n| haystack.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
/// Find a top-level `function_declaration` or a `method_declaration`
|
||||
/// whose name equals `target`. Returns the matching node.
|
||||
pub fn find_go_function<'a>(
|
||||
root: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
) -> Option<Node<'a>> {
|
||||
let mut hit: Option<Node<'a>> = None;
|
||||
walk_go(root, bytes, target, &mut hit);
|
||||
hit
|
||||
}
|
||||
|
||||
fn walk_go<'a>(
|
||||
node: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
out: &mut Option<Node<'a>>,
|
||||
) {
|
||||
if out.is_some() {
|
||||
return;
|
||||
}
|
||||
match node.kind() {
|
||||
"function_declaration" | "method_declaration" => {
|
||||
if let Some(name) = node.child_by_field_name("name")
|
||||
&& let Ok(text) = name.utf8_text(bytes)
|
||||
&& text == target
|
||||
{
|
||||
*out = Some(node);
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let mut cur = node.walk();
|
||||
for child in node.children(&mut cur) {
|
||||
walk_go(child, bytes, target, out);
|
||||
}
|
||||
}
|
||||
|
||||
/// Read formal parameter names from a `function_declaration` /
|
||||
/// `method_declaration` / `func_literal`. Drops the receiver
|
||||
/// parameter of a method (it is not part of the request surface).
|
||||
pub fn go_formal_names(func: Node<'_>, bytes: &[u8]) -> Vec<String> {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
let Some(params) = func.child_by_field_name("parameters") else {
|
||||
return out;
|
||||
};
|
||||
let mut cur = params.walk();
|
||||
for p in params.named_children(&mut cur) {
|
||||
if p.kind() != "parameter_declaration" {
|
||||
continue;
|
||||
}
|
||||
let mut pc = p.walk();
|
||||
for c in p.named_children(&mut pc) {
|
||||
if c.kind() == "identifier" {
|
||||
if let Ok(text) = c.utf8_text(bytes) {
|
||||
out.push(text.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Extract placeholder names from a Go route path template.
|
||||
///
|
||||
/// Supports:
|
||||
/// - gin / echo / fiber `:id` style: `/u/:id` → `id`
|
||||
/// - chi `{id}` style: `/u/{id}` → `id`
|
||||
/// - fiber `+` greedy: `/files/+rest` → `rest`
|
||||
/// - fiber/chi `*` wildcard: `/files/*rest` → `rest`
|
||||
pub fn extract_go_path_placeholders(path: &str) -> Vec<String> {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
let mut push = |name: String| {
|
||||
if !name.is_empty() && !out.iter().any(|n| n == &name) {
|
||||
out.push(name);
|
||||
}
|
||||
};
|
||||
let bytes = path.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
match bytes[i] {
|
||||
b':' => {
|
||||
let start = i + 1;
|
||||
let mut j = start;
|
||||
while j < bytes.len() && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
|
||||
j += 1;
|
||||
}
|
||||
if j > start {
|
||||
push(path[start..j].to_owned());
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
b'{' => {
|
||||
if let Some(end) = bytes[i + 1..].iter().position(|&b| b == b'}') {
|
||||
let inner = &path[i + 1..i + 1 + end];
|
||||
let name = inner.split(':').next().unwrap_or(inner);
|
||||
push(name.to_owned());
|
||||
i += end + 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
b'*' | b'+' => {
|
||||
let start = i + 1;
|
||||
let mut j = start;
|
||||
while j < bytes.len() && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
|
||||
j += 1;
|
||||
}
|
||||
if j > start {
|
||||
push(path[start..j].to_owned());
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Bind formals to request slots given a Go route path template.
|
||||
///
|
||||
/// `c` / `ctx` / `w` / `r` formals become [`ParamSource::Implicit`]
|
||||
/// (the framework context object or `http.ResponseWriter` /
|
||||
/// `*http.Request` pair). Names matching the path placeholder list
|
||||
/// become [`ParamSource::PathSegment`]. Every other formal falls
|
||||
/// back to a [`ParamSource::QueryParam`] of the same name.
|
||||
pub fn bind_go_path_params(formals: &[String], path: &str) -> Vec<ParamBinding> {
|
||||
let placeholders = extract_go_path_placeholders(path);
|
||||
formals
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, name)| {
|
||||
let source = if is_implicit_formal(name) {
|
||||
ParamSource::Implicit
|
||||
} else if placeholders.iter().any(|p| p == name) {
|
||||
ParamSource::PathSegment(name.clone())
|
||||
} else {
|
||||
ParamSource::QueryParam(name.clone())
|
||||
};
|
||||
ParamBinding {
|
||||
index: idx,
|
||||
name: name.clone(),
|
||||
source,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_implicit_formal(name: &str) -> bool {
|
||||
matches!(name, "c" | "ctx" | "w" | "r" | "req" | "res" | "rw")
|
||||
}
|
||||
|
||||
/// Parse Go verb-method names: `GET`, `POST`, `PUT`, `PATCH`,
|
||||
/// `DELETE`, `HEAD`, `OPTIONS` (case-insensitive — gin uses upper,
|
||||
/// echo / chi use upper, fiber uses pascal-cased like `Get`,
|
||||
/// `Post`). Returns `None` for unrelated identifiers.
|
||||
pub fn verb_from_method(method: &str) -> Option<HttpMethod> {
|
||||
let upper = method.to_ascii_uppercase();
|
||||
match upper.as_str() {
|
||||
"GET" => Some(HttpMethod::GET),
|
||||
"POST" => Some(HttpMethod::POST),
|
||||
"PUT" => Some(HttpMethod::PUT),
|
||||
"PATCH" => Some(HttpMethod::PATCH),
|
||||
"DELETE" => Some(HttpMethod::DELETE),
|
||||
"HEAD" => Some(HttpMethod::HEAD),
|
||||
"OPTIONS" => Some(HttpMethod::OPTIONS),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Locate the `(method, path)` of a `receiver.Verb("/path", target)`
|
||||
/// call expression registered against `target` in the file. Walks
|
||||
/// every `call_expression` in `root` and inspects each one whose
|
||||
/// callee is a `selector_expression` of the shape
|
||||
/// `<receiver>.<Verb>(<string>, <callable>)`. Returns `None` when no
|
||||
/// such call references `target` directly.
|
||||
///
|
||||
/// `target` matches against:
|
||||
/// - bare identifier callee (`r.GET("/x", handler)`)
|
||||
/// - qualified callee whose last segment equals `target`
|
||||
/// (`r.GET("/x", controllers.Show)`)
|
||||
/// - method-value callee (`r.GET("/x", (&UserController{}).Show)`)
|
||||
pub fn find_route_for_callee<'a>(
|
||||
root: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
) -> Option<(HttpMethod, String)> {
|
||||
let mut hit: Option<(HttpMethod, String)> = None;
|
||||
walk_routes(root, bytes, target, &mut hit);
|
||||
hit
|
||||
}
|
||||
|
||||
fn walk_routes<'a>(
|
||||
node: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
out: &mut Option<(HttpMethod, String)>,
|
||||
) {
|
||||
if out.is_some() {
|
||||
return;
|
||||
}
|
||||
if node.kind() == "call_expression"
|
||||
&& let Some(found) = try_route_call(node, bytes, target)
|
||||
{
|
||||
*out = Some(found);
|
||||
return;
|
||||
}
|
||||
let mut cur = node.walk();
|
||||
for child in node.children(&mut cur) {
|
||||
walk_routes(child, bytes, target, out);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_route_call<'a>(
|
||||
call: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
) -> Option<(HttpMethod, String)> {
|
||||
let callee = call.child_by_field_name("function")?;
|
||||
if callee.kind() != "selector_expression" {
|
||||
return None;
|
||||
}
|
||||
let verb_node = callee.child_by_field_name("field")?.utf8_text(bytes).ok()?;
|
||||
let method = verb_from_method(verb_node)?;
|
||||
let args = call.child_by_field_name("arguments")?;
|
||||
let positional: Vec<Node<'_>> = {
|
||||
let mut cur = args.walk();
|
||||
args.named_children(&mut cur)
|
||||
.filter(|c| c.kind() != "comment")
|
||||
.collect()
|
||||
};
|
||||
if positional.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let path = go_string_literal(positional[0], bytes)?;
|
||||
if !callable_matches(positional[1], bytes, target) {
|
||||
return None;
|
||||
}
|
||||
Some((method, path))
|
||||
}
|
||||
|
||||
/// Read a Go interpreted_string_literal's content, dropping the
|
||||
/// surrounding `"` quotes. Returns `None` if `node` is not a string
|
||||
/// literal.
|
||||
pub fn go_string_literal(node: Node<'_>, bytes: &[u8]) -> Option<String> {
|
||||
if node.kind() != "interpreted_string_literal" && node.kind() != "raw_string_literal" {
|
||||
return None;
|
||||
}
|
||||
let raw = node.utf8_text(bytes).ok()?;
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let first = trimmed.as_bytes()[0];
|
||||
let last = trimmed.as_bytes()[trimmed.len() - 1];
|
||||
if (first == b'"' && last == b'"') || (first == b'`' && last == b'`') {
|
||||
Some(trimmed[1..trimmed.len() - 1].to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// True when the callable argument resolves to `target`. Accepts:
|
||||
/// - bare identifier (`Handler`)
|
||||
/// - selector chain (`controllers.Show`, `c.Show`)
|
||||
/// - func literal — wildcard (the surrounding adapter already
|
||||
/// narrowed to a Go function whose name matches the summary)
|
||||
/// - method-value calls — wildcard
|
||||
fn callable_matches(node: Node<'_>, bytes: &[u8], target: &str) -> bool {
|
||||
match node.kind() {
|
||||
"identifier" => node.utf8_text(bytes).map(|s| s == target).unwrap_or(false),
|
||||
"selector_expression" => {
|
||||
let Some(field) = node.child_by_field_name("field") else {
|
||||
return false;
|
||||
};
|
||||
field.utf8_text(bytes).map(|s| s == target).unwrap_or(false)
|
||||
}
|
||||
"func_literal" => true,
|
||||
"call_expression" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_go::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_colon_placeholders() {
|
||||
assert_eq!(extract_go_path_placeholders("/u/:id"), vec!["id"]);
|
||||
assert_eq!(
|
||||
extract_go_path_placeholders("/u/:id/posts/:slug"),
|
||||
vec!["id", "slug"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_brace_placeholders() {
|
||||
assert_eq!(extract_go_path_placeholders("/u/{id}"), vec!["id"]);
|
||||
assert_eq!(extract_go_path_placeholders("/u/{id:[0-9]+}"), vec!["id"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_fiber_wildcards() {
|
||||
assert_eq!(extract_go_path_placeholders("/files/+rest"), vec!["rest"]);
|
||||
assert_eq!(extract_go_path_placeholders("/files/*rest"), vec!["rest"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binds_known_placeholder_as_path_segment() {
|
||||
let formals = vec!["c".to_string(), "id".to_string(), "extra".to_string()];
|
||||
let bindings = bind_go_path_params(&formals, "/u/:id");
|
||||
assert!(matches!(bindings[0].source, ParamSource::Implicit));
|
||||
assert!(matches!(bindings[1].source, ParamSource::PathSegment(_)));
|
||||
assert!(matches!(bindings[2].source, ParamSource::QueryParam(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verb_recognises_pascal_case() {
|
||||
assert_eq!(verb_from_method("GET"), Some(HttpMethod::GET));
|
||||
assert_eq!(verb_from_method("Get"), Some(HttpMethod::GET));
|
||||
assert_eq!(verb_from_method("post"), Some(HttpMethod::POST));
|
||||
assert_eq!(verb_from_method("Handler"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_function_declaration() {
|
||||
let src: &[u8] = b"package main\nfunc Show(c interface{}) {}\n";
|
||||
let tree = parse(src);
|
||||
let n = find_go_function(tree.root_node(), src, "Show").unwrap();
|
||||
assert_eq!(n.kind(), "function_declaration");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_route_for_bare_identifier_callee() {
|
||||
let src: &[u8] =
|
||||
b"package main\nfunc init() { r := gin.New(); r.GET(\"/u/:id\", Show) }\nfunc Show(c interface{}) {}\n";
|
||||
let tree = parse(src);
|
||||
let (method, path) =
|
||||
find_route_for_callee(tree.root_node(), src, "Show").expect("hit");
|
||||
assert_eq!(method, HttpMethod::GET);
|
||||
assert_eq!(path, "/u/:id");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_route_for_selector_callee() {
|
||||
let src: &[u8] =
|
||||
b"package main\nfunc init() { r := chi.NewRouter(); r.Get(\"/x\", controllers.Show) }\n";
|
||||
let tree = parse(src);
|
||||
let (method, path) =
|
||||
find_route_for_callee(tree.root_node(), src, "Show").expect("hit");
|
||||
assert_eq!(method, HttpMethod::GET);
|
||||
assert_eq!(path, "/x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formal_names_skip_types() {
|
||||
let src: &[u8] = b"package main\nfunc Show(c *gin.Context, id string) {}\n";
|
||||
let tree = parse(src);
|
||||
let f = find_go_function(tree.root_node(), src, "Show").unwrap();
|
||||
let names = go_formal_names(f, src);
|
||||
assert_eq!(names, vec!["c", "id"]);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,11 @@ pub mod header_php;
|
|||
pub mod header_python;
|
||||
pub mod header_ruby;
|
||||
pub mod header_rust;
|
||||
pub mod go_chi;
|
||||
pub mod go_echo;
|
||||
pub mod go_fiber;
|
||||
pub mod go_gin;
|
||||
pub mod go_routes;
|
||||
pub mod java_deserialize;
|
||||
pub mod java_micronaut;
|
||||
pub mod java_quarkus;
|
||||
|
|
@ -63,6 +68,11 @@ pub mod ruby_marshal;
|
|||
pub mod ruby_rails;
|
||||
pub mod ruby_routes;
|
||||
pub mod ruby_sinatra;
|
||||
pub mod rust_actix;
|
||||
pub mod rust_axum;
|
||||
pub mod rust_rocket;
|
||||
pub mod rust_routes;
|
||||
pub mod rust_warp;
|
||||
pub mod xpath_java;
|
||||
pub mod xpath_js;
|
||||
pub mod xpath_php;
|
||||
|
|
@ -80,6 +90,10 @@ pub use header_php::HeaderPhpAdapter;
|
|||
pub use header_python::HeaderPythonAdapter;
|
||||
pub use header_ruby::HeaderRubyAdapter;
|
||||
pub use header_rust::HeaderRustAdapter;
|
||||
pub use go_chi::GoChiAdapter;
|
||||
pub use go_echo::GoEchoAdapter;
|
||||
pub use go_fiber::GoFiberAdapter;
|
||||
pub use go_gin::GoGinAdapter;
|
||||
pub use java_deserialize::JavaDeserializeAdapter;
|
||||
pub use java_micronaut::JavaMicronautAdapter;
|
||||
pub use java_quarkus::JavaQuarkusAdapter;
|
||||
|
|
@ -120,6 +134,10 @@ pub use ruby_hanami::RubyHanamiAdapter;
|
|||
pub use ruby_marshal::RubyMarshalAdapter;
|
||||
pub use ruby_rails::RubyRailsAdapter;
|
||||
pub use ruby_sinatra::RubySinatraAdapter;
|
||||
pub use rust_actix::RustActixAdapter;
|
||||
pub use rust_axum::RustAxumAdapter;
|
||||
pub use rust_rocket::RustRocketAdapter;
|
||||
pub use rust_warp::RustWarpAdapter;
|
||||
pub use xpath_java::XpathJavaAdapter;
|
||||
pub use xpath_js::XpathJsAdapter;
|
||||
pub use xpath_php::XpathPhpAdapter;
|
||||
|
|
|
|||
129
src/dynamic/framework/adapters/rust_actix.rs
Normal file
129
src/dynamic/framework/adapters/rust_actix.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
//! Actix-web [`super::super::FrameworkAdapter`] (Phase 17 — Track L.15).
|
||||
//!
|
||||
//! Recognises actix's `#[get("/path")]` / `#[post("/path")]`
|
||||
//! attribute macros on handler functions:
|
||||
//!
|
||||
//! ```rust
|
||||
//! #[get("/users/{id}")]
|
||||
//! async fn show(id: web::Path<String>) -> impl Responder { id }
|
||||
//! ```
|
||||
//!
|
||||
//! The adapter walks the attribute_items immediately preceding the
|
||||
//! `function_item` named `summary.name`, picks up the verb leaf
|
||||
//! (`get` / `post` / ...) and the first string-literal argument.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::rust_routes::{
|
||||
bind_rust_path_params, find_method_attribute, find_rust_function, rust_formal_names,
|
||||
source_imports_actix,
|
||||
};
|
||||
|
||||
pub struct RustActixAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "rust-actix";
|
||||
|
||||
impl FrameworkAdapter for RustActixAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Rust
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
ast: Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
if !source_imports_actix(file_bytes) {
|
||||
return None;
|
||||
}
|
||||
let func = find_rust_function(ast, file_bytes, &summary.name)?;
|
||||
let (method, path) = find_method_attribute(func, file_bytes)?;
|
||||
let formals = rust_formal_names(func, file_bytes);
|
||||
let request_params = bind_rust_path_params(&formals, &path);
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::framework::{HttpMethod, ParamSource};
|
||||
|
||||
fn parse(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_rust::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
fn summary(name: &str) -> FuncSummary {
|
||||
FuncSummary {
|
||||
name: name.into(),
|
||||
lang: "rust".into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_get_attribute() {
|
||||
let src: &[u8] = b"use actix_web::get;\n#[get(\"/u/{id}\")]\nasync fn show(id: String) -> String { id }\n";
|
||||
let tree = parse(src);
|
||||
let binding = RustActixAdapter
|
||||
.detect(&summary("show"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.adapter, "rust-actix");
|
||||
let route = binding.route.expect("route");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert_eq!(route.path, "/u/{id}");
|
||||
let id = binding
|
||||
.request_params
|
||||
.iter()
|
||||
.find(|p| p.name == "id")
|
||||
.unwrap();
|
||||
assert!(matches!(id.source, ParamSource::PathSegment(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_post_attribute() {
|
||||
let src: &[u8] = b"use actix_web::post;\n#[post(\"/save\")]\nasync fn save(body: String) -> String { body }\n";
|
||||
let tree = parse(src);
|
||||
let binding = RustActixAdapter
|
||||
.detect(&summary("save"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.route.unwrap().method, HttpMethod::POST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_actix_not_imported() {
|
||||
let src: &[u8] = b"#[get(\"/u\")]\nfn show() {}\n";
|
||||
let tree = parse(src);
|
||||
assert!(RustActixAdapter
|
||||
.detect(&summary("show"), tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_attribute_missing() {
|
||||
let src: &[u8] = b"use actix_web::App;\nfn helper(x: String) {}\n";
|
||||
let tree = parse(src);
|
||||
assert!(RustActixAdapter
|
||||
.detect(&summary("helper"), tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
132
src/dynamic/framework/adapters/rust_axum.rs
Normal file
132
src/dynamic/framework/adapters/rust_axum.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
//! Axum [`super::super::FrameworkAdapter`] (Phase 17 — Track L.15).
|
||||
//!
|
||||
//! Recognises the canonical axum route builder:
|
||||
//!
|
||||
//! ```rust
|
||||
//! let app = Router::new()
|
||||
//! .route("/users/{id}", get(show))
|
||||
//! .route("/save", post(save));
|
||||
//! ```
|
||||
//!
|
||||
//! The adapter binds the route to the function whose name matches
|
||||
//! `summary.name`. Both the lowercase `get(handler)` helper and the
|
||||
//! scoped `axum::routing::get(handler)` form are accepted.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::rust_routes::{
|
||||
bind_rust_path_params, find_axum_route, find_rust_function, rust_formal_names,
|
||||
source_imports_axum,
|
||||
};
|
||||
|
||||
pub struct RustAxumAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "rust-axum";
|
||||
|
||||
impl FrameworkAdapter for RustAxumAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Rust
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
ast: Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
if !source_imports_axum(file_bytes) {
|
||||
return None;
|
||||
}
|
||||
let (method, path) = find_axum_route(ast, file_bytes, &summary.name)?;
|
||||
let request_params = find_rust_function(ast, file_bytes, &summary.name)
|
||||
.map(|func| {
|
||||
let formals = rust_formal_names(func, file_bytes);
|
||||
bind_rust_path_params(&formals, &path)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::framework::{HttpMethod, ParamSource};
|
||||
|
||||
fn parse(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_rust::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
fn summary(name: &str) -> FuncSummary {
|
||||
FuncSummary {
|
||||
name: name.into(),
|
||||
lang: "rust".into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_get_handler() {
|
||||
let src: &[u8] = b"use axum::Router;\nfn build() -> Router { Router::new().route(\"/u/{id}\", get(show)) }\nfn show(id: String) -> String { id }\n";
|
||||
let tree = parse(src);
|
||||
let binding = RustAxumAdapter
|
||||
.detect(&summary("show"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.adapter, "rust-axum");
|
||||
let route = binding.route.expect("route");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert_eq!(route.path, "/u/{id}");
|
||||
let id = binding
|
||||
.request_params
|
||||
.iter()
|
||||
.find(|p| p.name == "id")
|
||||
.unwrap();
|
||||
assert!(matches!(id.source, ParamSource::PathSegment(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_scoped_post_handler() {
|
||||
let src: &[u8] = b"use axum::Router;\nfn build() -> Router { Router::new().route(\"/save\", axum::routing::post(save)) }\nfn save(body: String) {}\n";
|
||||
let tree = parse(src);
|
||||
let binding = RustAxumAdapter
|
||||
.detect(&summary("save"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.route.unwrap().method, HttpMethod::POST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_axum_not_imported() {
|
||||
let src: &[u8] = b"fn show() {}\n";
|
||||
let tree = parse(src);
|
||||
assert!(RustAxumAdapter
|
||||
.detect(&summary("show"), tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_route_does_not_reference_function() {
|
||||
let src: &[u8] = b"use axum::Router;\nfn build() -> Router { Router::new().route(\"/u\", get(show)) }\nfn helper() {}\n";
|
||||
let tree = parse(src);
|
||||
assert!(RustAxumAdapter
|
||||
.detect(&summary("helper"), tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
125
src/dynamic/framework/adapters/rust_rocket.rs
Normal file
125
src/dynamic/framework/adapters/rust_rocket.rs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
//! Rocket [`super::super::FrameworkAdapter`] (Phase 17 — Track L.15).
|
||||
//!
|
||||
//! Recognises rocket's `#[get("/path")]` / `#[post("/path")]`
|
||||
//! attribute macros plus the `routes![handler]` macro:
|
||||
//!
|
||||
//! ```rust
|
||||
//! #[get("/users/<id>")]
|
||||
//! fn show(id: String) -> String { id }
|
||||
//!
|
||||
//! #[launch]
|
||||
//! fn rocket() -> _ { rocket::build().mount("/", routes![show]) }
|
||||
//! ```
|
||||
//!
|
||||
//! Rocket's placeholder syntax `<id>` plus brace syntax `<id..>`
|
||||
//! resolve via [`super::rust_routes::extract_rust_path_placeholders`].
|
||||
//! The adapter shares the attribute-walk path with actix; the only
|
||||
//! difference is the source-import discriminator.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::rust_routes::{
|
||||
bind_rust_path_params, find_method_attribute, find_rust_function, rust_formal_names,
|
||||
source_imports_rocket,
|
||||
};
|
||||
|
||||
pub struct RustRocketAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "rust-rocket";
|
||||
|
||||
impl FrameworkAdapter for RustRocketAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Rust
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
ast: Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
if !source_imports_rocket(file_bytes) {
|
||||
return None;
|
||||
}
|
||||
let func = find_rust_function(ast, file_bytes, &summary.name)?;
|
||||
let (method, path) = find_method_attribute(func, file_bytes)?;
|
||||
let formals = rust_formal_names(func, file_bytes);
|
||||
let request_params = bind_rust_path_params(&formals, &path);
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::framework::{HttpMethod, ParamSource};
|
||||
|
||||
fn parse(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_rust::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
fn summary(name: &str) -> FuncSummary {
|
||||
FuncSummary {
|
||||
name: name.into(),
|
||||
lang: "rust".into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_get_with_angle_placeholder() {
|
||||
let src: &[u8] = b"use rocket::get;\n#[get(\"/u/<id>\")]\nfn show(id: String) -> String { id }\n";
|
||||
let tree = parse(src);
|
||||
let binding = RustRocketAdapter
|
||||
.detect(&summary("show"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.adapter, "rust-rocket");
|
||||
let route = binding.route.expect("route");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert_eq!(route.path, "/u/<id>");
|
||||
let id = binding
|
||||
.request_params
|
||||
.iter()
|
||||
.find(|p| p.name == "id")
|
||||
.unwrap();
|
||||
assert!(matches!(id.source, ParamSource::PathSegment(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_post_with_data_param() {
|
||||
let src: &[u8] =
|
||||
b"use rocket::post;\n#[post(\"/save\", data = \"<body>\")]\nfn save(body: String) {}\n";
|
||||
let tree = parse(src);
|
||||
let binding = RustRocketAdapter
|
||||
.detect(&summary("save"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.route.unwrap().method, HttpMethod::POST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_rocket_not_imported() {
|
||||
let src: &[u8] = b"#[get(\"/u\")]\nfn show() {}\n";
|
||||
let tree = parse(src);
|
||||
assert!(RustRocketAdapter
|
||||
.detect(&summary("show"), tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
728
src/dynamic/framework/adapters/rust_routes.rs
Normal file
728
src/dynamic/framework/adapters/rust_routes.rs
Normal file
|
|
@ -0,0 +1,728 @@
|
|||
//! Shared Rust-route adapter helpers (Phase 17 — Track L.15).
|
||||
//!
|
||||
//! The axum / actix-web / rocket / warp adapters all need the same
|
||||
//! handful of tree-sitter helpers: locate a `function_item` by name,
|
||||
//! enumerate formal parameter names, walk macro/attribute invocations
|
||||
//! (`#[get("/x")]` for actix / rocket, `Router::new().route(...)` for
|
||||
//! axum, `warp::path!(...)`for warp), extract HTTP verbs / path
|
||||
//! templates, and bind formals to request slots.
|
||||
//!
|
||||
//! Placeholder vocabulary:
|
||||
//! - axum / actix / rocket use `{id}` or `<id>`.
|
||||
//! - warp uses `warp::path!("users" / u32)` style — different
|
||||
//! paradigm; the warp adapter binds formals positionally rather
|
||||
//! than by name.
|
||||
|
||||
use crate::dynamic::framework::{HttpMethod, ParamBinding, ParamSource};
|
||||
use tree_sitter::Node;
|
||||
|
||||
/// True when `bytes` carries any of the well-known axum markers.
|
||||
pub fn source_imports_axum(bytes: &[u8]) -> bool {
|
||||
contains_any(
|
||||
bytes,
|
||||
&[
|
||||
b"use axum::",
|
||||
b"axum::Router",
|
||||
b"axum::routing",
|
||||
b"Router::new",
|
||||
b"IntoResponse",
|
||||
b"// nyx-shape: axum",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// True when `bytes` carries any of the well-known actix-web markers.
|
||||
pub fn source_imports_actix(bytes: &[u8]) -> bool {
|
||||
contains_any(
|
||||
bytes,
|
||||
&[
|
||||
b"use actix_web",
|
||||
b"actix_web::",
|
||||
b"App::new",
|
||||
b"HttpResponse",
|
||||
b"web::resource",
|
||||
b"// nyx-shape: actix",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// True when `bytes` carries any of the well-known rocket markers.
|
||||
pub fn source_imports_rocket(bytes: &[u8]) -> bool {
|
||||
contains_any(
|
||||
bytes,
|
||||
&[
|
||||
b"use rocket::",
|
||||
b"#[macro_use] extern crate rocket",
|
||||
b"rocket::routes",
|
||||
b"#[launch]",
|
||||
b"// nyx-shape: rocket",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// True when `bytes` carries any of the well-known warp markers.
|
||||
pub fn source_imports_warp(bytes: &[u8]) -> bool {
|
||||
contains_any(
|
||||
bytes,
|
||||
&[
|
||||
b"use warp::",
|
||||
b"warp::Filter",
|
||||
b"warp::path",
|
||||
b"warp::serve",
|
||||
b"// nyx-shape: warp",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn contains_any(haystack: &[u8], needles: &[&[u8]]) -> bool {
|
||||
needles
|
||||
.iter()
|
||||
.any(|n| haystack.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
/// Find a top-level `function_item` whose `name` field equals
|
||||
/// `target`. Walks the AST recursively so functions nested inside
|
||||
/// `impl` blocks are also matched.
|
||||
pub fn find_rust_function<'a>(
|
||||
root: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
) -> Option<Node<'a>> {
|
||||
let mut hit: Option<Node<'a>> = None;
|
||||
walk_rs(root, bytes, target, &mut hit);
|
||||
hit
|
||||
}
|
||||
|
||||
fn walk_rs<'a>(
|
||||
node: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
out: &mut Option<Node<'a>>,
|
||||
) {
|
||||
if out.is_some() {
|
||||
return;
|
||||
}
|
||||
if node.kind() == "function_item"
|
||||
&& let Some(name) = node.child_by_field_name("name")
|
||||
&& let Ok(text) = name.utf8_text(bytes)
|
||||
&& text == target
|
||||
{
|
||||
*out = Some(node);
|
||||
return;
|
||||
}
|
||||
let mut cur = node.walk();
|
||||
for child in node.children(&mut cur) {
|
||||
walk_rs(child, bytes, target, out);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumerate formal parameter names from a `function_item`'s
|
||||
/// `parameters` field. Skips the implicit `self` receiver and
|
||||
/// `_` patterns. Returns names in declaration order.
|
||||
pub fn rust_formal_names(func: Node<'_>, bytes: &[u8]) -> Vec<String> {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
let Some(params) = func.child_by_field_name("parameters") else {
|
||||
return out;
|
||||
};
|
||||
let mut cur = params.walk();
|
||||
for p in params.named_children(&mut cur) {
|
||||
match p.kind() {
|
||||
"self_parameter" => {}
|
||||
"parameter" => {
|
||||
if let Some(pat) = p.child_by_field_name("pattern") {
|
||||
push_pattern_name(pat, bytes, &mut out);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn push_pattern_name(pat: Node<'_>, bytes: &[u8], out: &mut Vec<String>) {
|
||||
match pat.kind() {
|
||||
"identifier" => {
|
||||
if let Ok(text) = pat.utf8_text(bytes) {
|
||||
if text != "_" {
|
||||
out.push(text.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
"mut_pattern" | "ref_pattern" => {
|
||||
let mut cur = pat.walk();
|
||||
if let Some(inner) = pat.named_children(&mut cur).next() {
|
||||
push_pattern_name(inner, bytes, out);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract placeholder names from a Rust framework route path
|
||||
/// template.
|
||||
///
|
||||
/// Supports:
|
||||
/// - axum / actix / rocket / chi-style `{id}`: `/u/{id}` → `id`
|
||||
/// - rocket `<id>` syntax: `/u/<id>` → `id`
|
||||
/// - typed rocket `<id..>` syntax: `/u/<id..>` → `id`
|
||||
pub fn extract_rust_path_placeholders(path: &str) -> Vec<String> {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
let mut push = |name: String| {
|
||||
if !name.is_empty() && !out.iter().any(|n| n == &name) {
|
||||
out.push(name);
|
||||
}
|
||||
};
|
||||
let bytes = path.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
match bytes[i] {
|
||||
b'{' => {
|
||||
if let Some(end) = bytes[i + 1..].iter().position(|&b| b == b'}') {
|
||||
let inner = &path[i + 1..i + 1 + end];
|
||||
let name = inner.split(':').next().unwrap_or(inner);
|
||||
let name = name.trim_end_matches('*').trim_end_matches('?');
|
||||
push(name.to_owned());
|
||||
i += end + 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
b'<' => {
|
||||
if let Some(end) = bytes[i + 1..].iter().position(|&b| b == b'>') {
|
||||
let inner = &path[i + 1..i + 1 + end];
|
||||
let name = inner.trim_end_matches("..");
|
||||
push(name.to_owned());
|
||||
i += end + 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Bind formals to request slots given a Rust route path template.
|
||||
///
|
||||
/// Names matching the path placeholder list become a
|
||||
/// [`ParamSource::PathSegment`]; `req` / `request` / `state` formals
|
||||
/// fall to [`ParamSource::Implicit`]; every other formal becomes a
|
||||
/// [`ParamSource::QueryParam`].
|
||||
pub fn bind_rust_path_params(formals: &[String], path: &str) -> Vec<ParamBinding> {
|
||||
let placeholders = extract_rust_path_placeholders(path);
|
||||
formals
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, name)| {
|
||||
let source = if is_implicit_formal(name) {
|
||||
ParamSource::Implicit
|
||||
} else if placeholders.iter().any(|p| p == name) {
|
||||
ParamSource::PathSegment(name.clone())
|
||||
} else {
|
||||
ParamSource::QueryParam(name.clone())
|
||||
};
|
||||
ParamBinding {
|
||||
index: idx,
|
||||
name: name.clone(),
|
||||
source,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_implicit_formal(name: &str) -> bool {
|
||||
matches!(name, "req" | "request" | "state" | "ctx" | "cx" | "headers")
|
||||
}
|
||||
|
||||
/// Parse Rust framework verb names (`get` / `post` / `put` / `patch`
|
||||
/// / `delete` / `head` / `options`). Both axum's lowercase routing
|
||||
/// helpers (`get(handler)`) and actix's `web::get()` use the same
|
||||
/// lowercase identifiers; rocket's attribute macro shape
|
||||
/// (`#[get("/x")]`) uses the same. Returns `None` for unrelated
|
||||
/// identifiers.
|
||||
pub fn verb_from_ident(ident: &str) -> Option<HttpMethod> {
|
||||
match ident.to_ascii_lowercase().as_str() {
|
||||
"get" => Some(HttpMethod::GET),
|
||||
"post" => Some(HttpMethod::POST),
|
||||
"put" => Some(HttpMethod::PUT),
|
||||
"patch" => Some(HttpMethod::PATCH),
|
||||
"delete" => Some(HttpMethod::DELETE),
|
||||
"head" => Some(HttpMethod::HEAD),
|
||||
"options" => Some(HttpMethod::OPTIONS),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the content of a Rust `string_literal` node, stripping the
|
||||
/// surrounding `"` quotes. Returns `None` if `node` is not a string
|
||||
/// literal.
|
||||
pub fn rust_string_literal(node: Node<'_>, bytes: &[u8]) -> Option<String> {
|
||||
if node.kind() != "string_literal" {
|
||||
return None;
|
||||
}
|
||||
let mut cur = node.walk();
|
||||
for c in node.named_children(&mut cur) {
|
||||
if c.kind() == "string_content" {
|
||||
return c.utf8_text(bytes).ok().map(str::to_owned);
|
||||
}
|
||||
}
|
||||
let raw = node.utf8_text(bytes).ok()?;
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.len() >= 2
|
||||
&& trimmed.starts_with('"')
|
||||
&& trimmed.ends_with('"')
|
||||
{
|
||||
Some(trimmed[1..trimmed.len() - 1].to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk every `attribute_item` immediately preceding `func` looking
|
||||
/// for a `#[get("/path")]` / `#[post(...)]` / `#[route(...)]` macro.
|
||||
/// Returns `(method, path)` on first match. Used by both actix-web
|
||||
/// (`#[get("/path")]`) and rocket (same syntax).
|
||||
pub fn find_method_attribute<'a>(
|
||||
func: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
) -> Option<(HttpMethod, String)> {
|
||||
let parent = func.parent()?;
|
||||
let mut cur = parent.walk();
|
||||
let children: Vec<Node<'_>> = parent.children(&mut cur).collect();
|
||||
let pos = children.iter().position(|c| c.id() == func.id())?;
|
||||
// Walk backwards over attribute_items immediately above the
|
||||
// function declaration.
|
||||
for child in children[..pos].iter().rev() {
|
||||
if child.kind() == "attribute_item" {
|
||||
if let Some(hit) = read_route_attribute(*child, bytes) {
|
||||
return Some(hit);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if child.is_extra() {
|
||||
continue;
|
||||
}
|
||||
// Some grammars insert `line_comment` nodes between attributes
|
||||
// and the function; tolerate them but stop on any other named
|
||||
// child.
|
||||
if matches!(child.kind(), "line_comment" | "block_comment") {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Fallback: some tree-sitter Rust grammar revisions wrap
|
||||
// attributes inside the function_item's own preamble. Walk every
|
||||
// attribute_item descendent directly under the function node and
|
||||
// try those too.
|
||||
let mut cur = func.walk();
|
||||
for c in func.children(&mut cur) {
|
||||
if c.kind() == "attribute_item" {
|
||||
if let Some(hit) = read_route_attribute(c, bytes) {
|
||||
return Some(hit);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn read_route_attribute(attr: Node<'_>, bytes: &[u8]) -> Option<(HttpMethod, String)> {
|
||||
let mut cur = attr.walk();
|
||||
let attribute = attr
|
||||
.named_children(&mut cur)
|
||||
.find(|c| c.kind() == "attribute")?;
|
||||
// The tree-sitter-rust grammar packs an attribute as
|
||||
// `<identifier|scoped_identifier> <token_tree>`. Walk the named
|
||||
// children directly rather than `child_by_field_name`, since the
|
||||
// field labels (`path` / `arguments`) are not exposed across
|
||||
// grammar versions we depend on.
|
||||
let mut ac = attribute.walk();
|
||||
let children: Vec<Node<'_>> = attribute.named_children(&mut ac).collect();
|
||||
let head = children.first()?;
|
||||
let verb_text = match head.kind() {
|
||||
"identifier" => head.utf8_text(bytes).ok()?.to_owned(),
|
||||
"scoped_identifier" => {
|
||||
let mut sc = head.walk();
|
||||
head.named_children(&mut sc)
|
||||
.filter_map(|c| {
|
||||
if c.kind() == "identifier" {
|
||||
c.utf8_text(bytes).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.last()?
|
||||
.to_owned()
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let method = verb_from_ident(&verb_text)?;
|
||||
for child in &children[1..] {
|
||||
if child.kind() == "token_tree" {
|
||||
// Recurse to find the first string_literal under the
|
||||
// token_tree (rocket also accepts `data = "<body>"` so we
|
||||
// can't restrict to the first child).
|
||||
if let Some(literal) = first_string_in(*child, bytes) {
|
||||
return Some((method, literal));
|
||||
}
|
||||
}
|
||||
if let Some(literal) = rust_string_literal(*child, bytes) {
|
||||
return Some((method, literal));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn first_string_in(node: Node<'_>, bytes: &[u8]) -> Option<String> {
|
||||
if let Some(literal) = rust_string_literal(node, bytes) {
|
||||
return Some(literal);
|
||||
}
|
||||
let mut cur = node.walk();
|
||||
for child in node.named_children(&mut cur) {
|
||||
if let Some(literal) = first_string_in(child, bytes) {
|
||||
return Some(literal);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Walk `root` looking for an axum `Router::new().route("/path",
|
||||
/// get(handler))` / `.route("/path", post(handler))` chain that
|
||||
/// registers `target` as the handler. Returns `(method, path)` on
|
||||
/// first match.
|
||||
pub fn find_axum_route<'a>(
|
||||
root: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
) -> Option<(HttpMethod, String)> {
|
||||
let mut hit: Option<(HttpMethod, String)> = None;
|
||||
walk_axum(root, bytes, target, &mut hit);
|
||||
hit
|
||||
}
|
||||
|
||||
fn walk_axum<'a>(
|
||||
node: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
out: &mut Option<(HttpMethod, String)>,
|
||||
) {
|
||||
if out.is_some() {
|
||||
return;
|
||||
}
|
||||
if node.kind() == "call_expression"
|
||||
&& let Some(found) = try_axum_route_call(node, bytes, target)
|
||||
{
|
||||
*out = Some(found);
|
||||
return;
|
||||
}
|
||||
let mut cur = node.walk();
|
||||
for child in node.children(&mut cur) {
|
||||
walk_axum(child, bytes, target, out);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_axum_route_call<'a>(
|
||||
call: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
) -> Option<(HttpMethod, String)> {
|
||||
let func = call.child_by_field_name("function")?;
|
||||
if func.kind() != "field_expression" {
|
||||
return None;
|
||||
}
|
||||
let field = func.child_by_field_name("field")?.utf8_text(bytes).ok()?;
|
||||
if field != "route" {
|
||||
return None;
|
||||
}
|
||||
let args = call.child_by_field_name("arguments")?;
|
||||
let positional: Vec<Node<'_>> = {
|
||||
let mut cur = args.walk();
|
||||
args.named_children(&mut cur)
|
||||
.filter(|c| !matches!(c.kind(), "line_comment" | "block_comment"))
|
||||
.collect()
|
||||
};
|
||||
if positional.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let path = rust_string_literal(positional[0], bytes)?;
|
||||
let (method, callable) = parse_axum_verb_wrapper(positional[1], bytes)?;
|
||||
if !axum_callable_matches(callable, bytes, target) {
|
||||
return None;
|
||||
}
|
||||
Some((method, path))
|
||||
}
|
||||
|
||||
/// Parse the `get(handler)` / `axum::routing::get(handler)` wrapper
|
||||
/// emitted by axum. Returns `(method, handler_node)` on success.
|
||||
fn parse_axum_verb_wrapper<'a>(
|
||||
node: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
) -> Option<(HttpMethod, Node<'a>)> {
|
||||
if node.kind() != "call_expression" {
|
||||
return None;
|
||||
}
|
||||
let func = node.child_by_field_name("function")?;
|
||||
let leaf = match func.kind() {
|
||||
"identifier" => func.utf8_text(bytes).ok()?,
|
||||
"scoped_identifier" => func
|
||||
.child_by_field_name("name")?
|
||||
.utf8_text(bytes)
|
||||
.ok()?,
|
||||
_ => return None,
|
||||
};
|
||||
let method = verb_from_ident(leaf)?;
|
||||
let args = node.child_by_field_name("arguments")?;
|
||||
let mut cur = args.walk();
|
||||
let handler = args
|
||||
.named_children(&mut cur)
|
||||
.find(|c| !matches!(c.kind(), "line_comment" | "block_comment"))?;
|
||||
Some((method, handler))
|
||||
}
|
||||
|
||||
fn axum_callable_matches(node: Node<'_>, bytes: &[u8], target: &str) -> bool {
|
||||
match node.kind() {
|
||||
"identifier" => node.utf8_text(bytes).map(|s| s == target).unwrap_or(false),
|
||||
"scoped_identifier" => node
|
||||
.child_by_field_name("name")
|
||||
.and_then(|n| n.utf8_text(bytes).ok())
|
||||
.map(|s| s == target)
|
||||
.unwrap_or(false),
|
||||
"field_expression" => node
|
||||
.child_by_field_name("field")
|
||||
.and_then(|n| n.utf8_text(bytes).ok())
|
||||
.map(|s| s == target)
|
||||
.unwrap_or(false),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk `root` looking for a `warp::path!("users" / u32)` macro
|
||||
/// invocation that bridges to `target` via `.map(target)` /
|
||||
/// `.and_then(target)`. Returns `(method, path)` on first match.
|
||||
/// Method defaults to `GET` because warp's verb chain is added later
|
||||
/// (`.and(warp::post())`); a future pass can refine.
|
||||
pub fn find_warp_route<'a>(
|
||||
root: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
) -> Option<(HttpMethod, String)> {
|
||||
let mut hit: Option<(HttpMethod, String)> = None;
|
||||
walk_warp(root, bytes, target, &mut hit);
|
||||
hit
|
||||
}
|
||||
|
||||
fn walk_warp<'a>(
|
||||
node: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
out: &mut Option<(HttpMethod, String)>,
|
||||
) {
|
||||
if out.is_some() {
|
||||
return;
|
||||
}
|
||||
if node.kind() == "macro_invocation"
|
||||
&& let Some(path_text) = try_warp_path_macro(node, bytes)
|
||||
{
|
||||
// Walk siblings / outer call chain for a `.map(target)` /
|
||||
// `.and_then(target)` that wires this path macro to `target`.
|
||||
let mut parent = node.parent();
|
||||
let mut verb = HttpMethod::GET;
|
||||
let mut hit_target = false;
|
||||
while let Some(p) = parent {
|
||||
match p.kind() {
|
||||
"call_expression" => {
|
||||
if let Some(func) = p.child_by_field_name("function")
|
||||
&& func.kind() == "field_expression"
|
||||
&& let Some(field) = func.child_by_field_name("field")
|
||||
&& let Ok(field_text) = field.utf8_text(bytes)
|
||||
&& matches!(field_text, "map" | "and_then" | "untuple_one")
|
||||
{
|
||||
let args = p.child_by_field_name("arguments");
|
||||
if let Some(args) = args {
|
||||
let mut cur = args.walk();
|
||||
for c in args.named_children(&mut cur) {
|
||||
if axum_callable_matches(c, bytes, target) {
|
||||
hit_target = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// Detect verb-filter calls (`warp::get()`, `warp::post()`).
|
||||
let mut cur = p.walk();
|
||||
for child in p.children(&mut cur) {
|
||||
if child.kind() == "call_expression"
|
||||
&& let Some(func) = child.child_by_field_name("function")
|
||||
&& func.kind() == "scoped_identifier"
|
||||
&& let Some(name) = func.child_by_field_name("name")
|
||||
&& let Ok(name_text) = name.utf8_text(bytes)
|
||||
&& let Some(method) = verb_from_ident(name_text)
|
||||
{
|
||||
verb = method;
|
||||
}
|
||||
}
|
||||
parent = p.parent();
|
||||
}
|
||||
if hit_target {
|
||||
*out = Some((verb, path_text));
|
||||
return;
|
||||
}
|
||||
}
|
||||
let mut cur = node.walk();
|
||||
for child in node.children(&mut cur) {
|
||||
walk_warp(child, bytes, target, out);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_warp_path_macro(invocation: Node<'_>, bytes: &[u8]) -> Option<String> {
|
||||
// Tree-sitter rust grammar surfaces the macro callee under
|
||||
// `macro` field.
|
||||
let macro_node = invocation.child_by_field_name("macro")?;
|
||||
let leaf = match macro_node.kind() {
|
||||
"identifier" => macro_node.utf8_text(bytes).ok()?,
|
||||
"scoped_identifier" => macro_node
|
||||
.child_by_field_name("name")?
|
||||
.utf8_text(bytes)
|
||||
.ok()?,
|
||||
_ => return None,
|
||||
};
|
||||
if leaf != "path" {
|
||||
return None;
|
||||
}
|
||||
// Reconstruct the path template from the macro's token tree.
|
||||
let mut cur = invocation.walk();
|
||||
let token_tree = invocation
|
||||
.named_children(&mut cur)
|
||||
.find(|c| c.kind() == "token_tree")?;
|
||||
let mut path = String::from("/");
|
||||
let mut first = true;
|
||||
let mut tc = token_tree.walk();
|
||||
for token in token_tree.named_children(&mut tc) {
|
||||
match token.kind() {
|
||||
"string_literal" => {
|
||||
let literal = rust_string_literal(token, bytes)?;
|
||||
if !first {
|
||||
path.push('/');
|
||||
}
|
||||
path.push_str(&literal);
|
||||
first = false;
|
||||
}
|
||||
"primitive_type" | "type_identifier" | "identifier" => {
|
||||
if !first {
|
||||
path.push('/');
|
||||
}
|
||||
if let Ok(text) = token.utf8_text(bytes) {
|
||||
path.push_str(&format!("{{{}}}", text));
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if first {
|
||||
return None;
|
||||
}
|
||||
Some(path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_rust::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_brace_placeholders() {
|
||||
assert_eq!(extract_rust_path_placeholders("/u/{id}"), vec!["id"]);
|
||||
assert_eq!(
|
||||
extract_rust_path_placeholders("/u/{id}/posts/{slug}"),
|
||||
vec!["id", "slug"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_rocket_angle_placeholders() {
|
||||
assert_eq!(extract_rust_path_placeholders("/u/<id>"), vec!["id"]);
|
||||
assert_eq!(extract_rust_path_placeholders("/u/<rest..>"), vec!["rest"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_axum_route_get() {
|
||||
let src: &[u8] = b"use axum::Router;\nfn build() -> Router { Router::new().route(\"/u/{id}\", get(show)) }\nfn show() {}\n";
|
||||
let tree = parse(src);
|
||||
let (method, path) =
|
||||
find_axum_route(tree.root_node(), src, "show").expect("hit");
|
||||
assert_eq!(method, HttpMethod::GET);
|
||||
assert_eq!(path, "/u/{id}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_axum_route_with_scoped_verb() {
|
||||
let src: &[u8] = b"use axum::Router;\nfn build() -> Router { Router::new().route(\"/x\", axum::routing::post(save)) }\nfn save() {}\n";
|
||||
let tree = parse(src);
|
||||
let (method, path) =
|
||||
find_axum_route(tree.root_node(), src, "save").expect("hit");
|
||||
assert_eq!(method, HttpMethod::POST);
|
||||
assert_eq!(path, "/x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_actix_get_attribute() {
|
||||
let src: &[u8] = b"#[get(\"/u/{id}\")]\nfn show(id: String) -> String { id }\n";
|
||||
let tree = parse(src);
|
||||
let func = find_rust_function(tree.root_node(), src, "show").unwrap();
|
||||
let (method, path) = find_method_attribute(func, src).expect("hit");
|
||||
assert_eq!(method, HttpMethod::GET);
|
||||
assert_eq!(path, "/u/{id}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_rocket_post_attribute() {
|
||||
let src: &[u8] =
|
||||
b"#[post(\"/save\", data = \"<body>\")]\nfn save(body: String) {}\n";
|
||||
let tree = parse(src);
|
||||
let func = find_rust_function(tree.root_node(), src, "save").unwrap();
|
||||
let (method, path) = find_method_attribute(func, src).expect("hit");
|
||||
assert_eq!(method, HttpMethod::POST);
|
||||
assert_eq!(path, "/save");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binds_known_placeholder_as_path_segment() {
|
||||
let formals = vec!["id".to_string(), "extra".to_string()];
|
||||
let bindings = bind_rust_path_params(&formals, "/u/{id}");
|
||||
assert!(matches!(bindings[0].source, ParamSource::PathSegment(_)));
|
||||
assert!(matches!(bindings[1].source, ParamSource::QueryParam(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binds_implicit_request_as_implicit() {
|
||||
let formals = vec!["req".to_string(), "request".to_string(), "state".to_string()];
|
||||
let bindings = bind_rust_path_params(&formals, "/x");
|
||||
for b in &bindings {
|
||||
assert!(matches!(b.source, ParamSource::Implicit));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verb_recognises_get_post() {
|
||||
assert_eq!(verb_from_ident("get"), Some(HttpMethod::GET));
|
||||
assert_eq!(verb_from_ident("POST"), Some(HttpMethod::POST));
|
||||
assert_eq!(verb_from_ident("handler"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_warp_path_macro_with_map_target() {
|
||||
let src: &[u8] = b"use warp::Filter;\nfn build() { let r = warp::path!(\"users\" / u32).map(show); }\nfn show(id: u32) -> String { String::new() }\n";
|
||||
let tree = parse(src);
|
||||
let (_method, path) =
|
||||
find_warp_route(tree.root_node(), src, "show").expect("hit");
|
||||
assert!(path.contains("users"));
|
||||
}
|
||||
}
|
||||
128
src/dynamic/framework/adapters/rust_warp.rs
Normal file
128
src/dynamic/framework/adapters/rust_warp.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
//! Warp [`super::super::FrameworkAdapter`] (Phase 17 — Track L.15).
|
||||
//!
|
||||
//! Recognises warp's `warp::path!(...)` macro chained with `.map(...)`
|
||||
//! or `.and_then(...)` to bridge into a handler function:
|
||||
//!
|
||||
//! ```rust
|
||||
//! let r = warp::path!("users" / u32)
|
||||
//! .and(warp::get())
|
||||
//! .map(show);
|
||||
//! ```
|
||||
//!
|
||||
//! Warp's path DSL embeds typed segments as positional placeholders;
|
||||
//! the adapter reconstructs a brace-style path template
|
||||
//! (`/users/{u32}`) and binds formals positionally via the per-arg
|
||||
//! name in the handler's signature.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::rust_routes::{
|
||||
bind_rust_path_params, find_rust_function, find_warp_route, rust_formal_names,
|
||||
source_imports_warp,
|
||||
};
|
||||
|
||||
pub struct RustWarpAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "rust-warp";
|
||||
|
||||
impl FrameworkAdapter for RustWarpAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Rust
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
ast: Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
if !source_imports_warp(file_bytes) {
|
||||
return None;
|
||||
}
|
||||
let (method, path) = find_warp_route(ast, file_bytes, &summary.name)?;
|
||||
let request_params = find_rust_function(ast, file_bytes, &summary.name)
|
||||
.map(|func| {
|
||||
let formals = rust_formal_names(func, file_bytes);
|
||||
bind_rust_path_params(&formals, &path)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::framework::HttpMethod;
|
||||
|
||||
fn parse(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_rust::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
fn summary(name: &str) -> FuncSummary {
|
||||
FuncSummary {
|
||||
name: name.into(),
|
||||
lang: "rust".into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_path_macro_with_map_target() {
|
||||
let src: &[u8] = b"use warp::Filter;\nfn build() { let r = warp::path!(\"users\" / u32).map(show); }\nfn show(id: u32) -> String { String::new() }\n";
|
||||
let tree = parse(src);
|
||||
let binding = RustWarpAdapter
|
||||
.detect(&summary("show"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.adapter, "rust-warp");
|
||||
let route = binding.route.expect("route");
|
||||
assert!(route.path.contains("users"));
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_path_macro_with_and_then_target() {
|
||||
let src: &[u8] = b"use warp::Filter;\nfn build() { let r = warp::path!(\"x\").and_then(handle); }\nasync fn handle() -> Result<&'static str, warp::Rejection> { Ok(\"ok\") }\n";
|
||||
let tree = parse(src);
|
||||
let binding = RustWarpAdapter
|
||||
.detect(&summary("handle"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert!(binding.route.unwrap().path.contains("x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_warp_not_imported() {
|
||||
let src: &[u8] = b"fn show() {}\n";
|
||||
let tree = parse(src);
|
||||
assert!(RustWarpAdapter
|
||||
.detect(&summary("show"), tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_no_path_macro() {
|
||||
let src: &[u8] = b"use warp::Filter;\nfn show() {}\n";
|
||||
let tree = parse(src);
|
||||
assert!(RustWarpAdapter
|
||||
.detect(&summary("show"), tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue