From 5bd986f199edc542b0bdd7fa99e36c379e99828f Mon Sep 17 00:00:00 2001 From: itowlson Date: Tue, 29 Apr 2025 15:16:19 +1200 Subject: [PATCH] Routing modes Signed-off-by: itowlson --- src/http/router.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/src/http/router.rs b/src/http/router.rs index 8f43033..6383d02 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -154,9 +154,42 @@ pub type Params = Captures<'static, 'static>; /// # fn handle_single_segment(req: Request, params: Params) -> anyhow::Result { todo!() } /// # fn handle_exact(req: Request, params: Params) -> anyhow::Result { todo!() } /// ``` +/// +/// Route based on the trailing segment of a Spin wildcard route, instead of on the full path +/// +/// ```no_run +/// // spin.toml +/// // +/// // [[trigger.http]] +/// // route = "/shop/..." +/// +/// // component +/// # use spin_sdk::http::{IntoResponse, Params, Request, Response, Router}; +/// fn handle_route(req: Request) -> Response { +/// let mut router = Router::suffix(); +/// router.any("/users/*", handle_users); +/// router.any("/products/*", handle_products); +/// router.handle(req) +/// } +/// +/// // '/shop/users/1' is routed to `handle_users` +/// // '/shop/products/1' is routed to `handle_products` +/// # fn handle_users(req: Request, params: Params) -> anyhow::Result { todo!() } +/// # fn handle_products(req: Request, params: Params) -> anyhow::Result { todo!() } +/// ``` pub struct Router { methods_map: HashMap>>, any_methods: MethodRouter>, + route_on: RouteOn, +} + +/// Describes what part of the path the Router will route on. +enum RouteOn { + /// The router will route on the full path. + FullPath, + /// The router expects the component to be handling a route with a trailing wildcard + /// (e.g. `route = /shop/...`), and will route on the trailing segment. + Suffix, } impl Default for Router { @@ -203,7 +236,16 @@ impl Router { Err(e) => return e.into_response(), }; let method = request.method.clone(); - let path = &request.path(); + let path = match self.route_on { + RouteOn::FullPath => request.path(), + RouteOn::Suffix => match trailing_suffix(&request) { + Some(path) => path, + None => { + eprintln!("Internal error: Router configured with suffix routing but trigger route has no trailing wildcard"); + return responses::internal_server_error(); + } + }, + }; let RouteMatch { params, handler } = self.find(path, method); handler.handle(request, params).await } @@ -516,11 +558,22 @@ impl Router { self.add_async(path, Method::Options, handler) } - /// Construct a new Router. + /// Construct a new Router that matches on the full path. pub fn new() -> Self { Router { methods_map: HashMap::default(), any_methods: MethodRouter::new(), + route_on: RouteOn::FullPath, + } + } + + /// Construct a new Router that matches on the trailing wildcard + /// component of the route. + pub fn suffix() -> Self { + Router { + methods_map: HashMap::default(), + any_methods: MethodRouter::new(), + route_on: RouteOn::Suffix, } } } @@ -533,6 +586,11 @@ async fn method_not_allowed(_req: Request, _params: Params) -> Response { responses::method_not_allowed() } +fn trailing_suffix(req: &Request) -> Option<&str> { + req.header("spin-path-info") + .and_then(|path_info| path_info.as_str()) +} + /// A macro to help with constructing a Router from a stream of tokens. #[macro_export] macro_rules! http_router { @@ -579,6 +637,12 @@ mod tests { Request::new(method, path) } + fn make_wildcard_request(method: Method, path: &str, trailing: &str) -> Request { + let mut req = Request::new(method, path); + req.set_header("spin-path-info", trailing); + req + } + fn echo_param(_req: Request, params: Params) -> Response { match params.get("x") { Some(path) => Response::new(200, path), @@ -666,6 +730,16 @@ mod tests { assert_eq!(res.body, "foo".to_owned().into_bytes()); } + #[test] + fn test_spin_trailing_wildcard() { + let mut router = Router::suffix(); + router.get("/:x/*", echo_param); + + let req = make_wildcard_request(Method::Get, "/base/baser/foo/bar", "/foo/bar"); + let res = router.handle(req); + assert_eq!(res.body, "foo".to_owned().into_bytes()); + } + #[test] fn test_router_display() { let mut router = Router::default();