From 955b468839ee91ddacebee63eefaebab3e7a2b85 Mon Sep 17 00:00:00 2001 From: nagxsan Date: Sun, 16 Feb 2025 17:42:33 +0530 Subject: [PATCH 1/2] add trie struct to router state --- rama-http/src/service/web/mod.rs | 4 ++ rama-http/src/service/web/router.rs | 106 ++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 rama-http/src/service/web/router.rs diff --git a/rama-http/src/service/web/mod.rs b/rama-http/src/service/web/mod.rs index 7c39386ba..58680c7ba 100644 --- a/rama-http/src/service/web/mod.rs +++ b/rama-http/src/service/web/mod.rs @@ -11,3 +11,7 @@ pub use endpoint::{extract, EndpointServiceFn, IntoEndpointService}; pub mod k8s; #[doc(inline)] pub use k8s::{k8s_health, k8s_health_builder}; + +mod router; +#[doc(inline)] +pub use router::{Router}; diff --git a/rama-http/src/service/web/router.rs b/rama-http/src/service/web/router.rs new file mode 100644 index 000000000..ebd5f363a --- /dev/null +++ b/rama-http/src/service/web/router.rs @@ -0,0 +1,106 @@ +use super::{endpoint::Endpoint, IntoEndpointService, WebService}; +use crate::{ + matcher::{HttpMatcher, UriParams}, + service::fs::ServeDir, + Body, IntoResponse, Request, Response, StatusCode, Uri, +}; +use rama_core::{ + context::Extensions, + matcher::Matcher, + service::{service_fn, BoxService, Service}, + Context, +}; +use std::{convert::Infallible, fmt, future::Future, marker::PhantomData, sync::Arc}; +use std::collections::HashMap; +use http::Method; + +/// A basic web router that can be used to serve HTTP requests based on path matching. +/// It will also provide extraction of path parameters and wildcards out of the box so +/// you can define your paths accordingly. + +#[derive(Debug)] +enum RouterError { + NotFound, + MethodNotAllowed, + InternalServerError, +} + +impl std::fmt::Display for RouterError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for RouterError {} + +struct TrieNode { + children: HashMap>, + param_child: Option>>, + wildcard_child: Option>>, + param_name: Option, + handlers: HashMap, Response, RouterError>>> +} + +impl TrieNode { + fn new() -> Self { + Self { + children: HashMap::new(), + param_child: None, + wildcard_child: None, + handlers: HashMap::new(), + param_name: None, + } + } +} + +impl Clone for TrieNode { + fn clone(&self) -> Self { + Self { + children: self.children.clone(), + param_child: self.param_child.as_ref().map(|child| child.clone()), + wildcard_child: self.wildcard_child.as_ref().map(|child| child.clone()), + handlers: self.handlers.clone(), + param_name: self.param_name.clone(), + } + } +} + +pub struct Router { + routes: TrieNode +} + +impl std::fmt::Debug for Router { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Router").finish() + } +} + +impl Clone for Router { + fn clone(&self) -> Self { + Self { + routes: self.routes.clone(), + } + } +} + +/// default trait +impl Default for Router +where + State: Clone + Send + Sync + 'static, +{ + fn default() -> Self { + Self::new() + } +} + +impl Router +where + State: Clone + Send + Sync + 'static, +{ + /// create a new web router + pub(crate) fn new() -> Self { + Self { + routes: TrieNode::new(), + } + } +} From eef8ebd786c1f7865157738e912fe2e415bdcd41 Mon Sep 17 00:00:00 2001 From: nagxsan Date: Mon, 17 Feb 2025 09:13:29 +0530 Subject: [PATCH 2/2] replace trie with matchit implementation --- rama-http/Cargo.toml | 1 + rama-http/src/service/web/router.rs | 193 +++++++++++++++++++--------- 2 files changed, 131 insertions(+), 63 deletions(-) diff --git a/rama-http/Cargo.toml b/rama-http/Cargo.toml index ac596aa22..47dfa0dfd 100644 --- a/rama-http/Cargo.toml +++ b/rama-http/Cargo.toml @@ -59,6 +59,7 @@ tokio = { workspace = true, features = ["macros", "fs", "io-std"] } tokio-util = { workspace = true, features = ["io"] } tracing = { workspace = true } uuid = { workspace = true, features = ["v4"] } +matchit = "0.8.6" [dev-dependencies] brotli = { workspace = true } diff --git a/rama-http/src/service/web/router.rs b/rama-http/src/service/web/router.rs index ebd5f363a..47508aeb1 100644 --- a/rama-http/src/service/web/router.rs +++ b/rama-http/src/service/web/router.rs @@ -1,72 +1,20 @@ -use super::{endpoint::Endpoint, IntoEndpointService, WebService}; -use crate::{ - matcher::{HttpMatcher, UriParams}, - service::fs::ServeDir, - Body, IntoResponse, Request, Response, StatusCode, Uri, -}; -use rama_core::{ - context::Extensions, - matcher::Matcher, - service::{service_fn, BoxService, Service}, - Context, -}; -use std::{convert::Infallible, fmt, future::Future, marker::PhantomData, sync::Arc}; +use super::{IntoEndpointService}; +use crate::{Request, Response, StatusCode}; +use rama_core::{service::{BoxService, Service, service_fn}, Context}; +use std::{convert::Infallible, sync::Arc}; use std::collections::HashMap; -use http::Method; +use http::{Method}; + +use matchit::Router as MatchitRouter; +use rama_http_types::IntoResponse; /// A basic web router that can be used to serve HTTP requests based on path matching. /// It will also provide extraction of path parameters and wildcards out of the box so /// you can define your paths accordingly. -#[derive(Debug)] -enum RouterError { - NotFound, - MethodNotAllowed, - InternalServerError, -} - -impl std::fmt::Display for RouterError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} - -impl std::error::Error for RouterError {} - -struct TrieNode { - children: HashMap>, - param_child: Option>>, - wildcard_child: Option>>, - param_name: Option, - handlers: HashMap, Response, RouterError>>> -} - -impl TrieNode { - fn new() -> Self { - Self { - children: HashMap::new(), - param_child: None, - wildcard_child: None, - handlers: HashMap::new(), - param_name: None, - } - } -} - -impl Clone for TrieNode { - fn clone(&self) -> Self { - Self { - children: self.children.clone(), - param_child: self.param_child.as_ref().map(|child| child.clone()), - wildcard_child: self.wildcard_child.as_ref().map(|child| child.clone()), - handlers: self.handlers.clone(), - param_name: self.param_name.clone(), - } - } -} - pub struct Router { - routes: TrieNode + routes: MatchitRouter>>>, + not_found: Arc>, } impl std::fmt::Debug for Router { @@ -79,6 +27,7 @@ impl Clone for Router { fn clone(&self) -> Self { Self { routes: self.routes.clone(), + not_found: self.not_found.clone(), } } } @@ -100,7 +49,125 @@ where /// create a new web router pub(crate) fn new() -> Self { Self { - routes: TrieNode::new(), + routes: MatchitRouter::new(), + not_found: Arc::new( + service_fn(|| async { Ok(StatusCode::NOT_FOUND.into_response()) }).boxed(), + ), + } + } + + pub fn route(mut self, method: Method, path: &str, service: I) -> Self + where + I: IntoEndpointService, + { + let boxed_service = Arc::new(BoxService::new(service.into_endpoint_service())); + match self.routes.insert(path.to_string(), HashMap::new()) { + Ok(_) => { + if let Some(entry) = self.routes.at_mut(path).ok() { + entry.value.insert(method, boxed_service); + } + }, + Err(_err) => { + if let Some(existing) = self.routes.at_mut(path).ok() { + existing.value.insert(method, boxed_service); + } + } + }; + self + } + + pub fn get(self, path: &str, service: I) -> Self + where + I: IntoEndpointService, + { + self.route(Method::GET, path, service) + } + + pub fn post(self, path: &str, service: I) -> Self + where + I: IntoEndpointService, + { + self.route(Method::POST, path, service) + } + + pub fn put(self, path: &str, service: I) -> Self + where + I: IntoEndpointService, + { + self.route(Method::PUT, path, service) + } + + pub fn delete(self, path: &str, service: I) -> Self + where + I: IntoEndpointService, + { + self.route(Method::DELETE, path, service) + } + + pub fn patch(self, path: &str, service: I) -> Self + where + I: IntoEndpointService, + { + self.route(Method::PATCH, path, service) + } + + pub fn head(self, path: &str, service: I) -> Self + where + I: IntoEndpointService, + { + self.route(Method::HEAD, path, service) + } + + pub fn options(self, path: &str, service: I) -> Self + where + I: IntoEndpointService, + { + self.route(Method::OPTIONS, path, service) + } + + pub fn trace(self, path: &str, service: I) -> Self + where + I: IntoEndpointService, + { + self.route(Method::TRACE, path, service) + } + + /// use the given service in case no match could be found. + pub fn not_found(mut self, service: I) -> Self + where + I: IntoEndpointService, + { + self.not_found = Arc::new(service.into_endpoint_service().boxed()); + self + } +} + +impl Service for Router +where + State: Clone + Send + Sync + 'static, +{ + type Response = Response; + type Error = Infallible; + + async fn serve( + &self, + mut ctx: Context, + req: Request<>, + ) -> Result { + let uri_string = req.uri().to_string(); + match &self.routes.at(uri_string.as_str()) { + Ok(matched) => { + let params: HashMap = matched.params.clone().iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(); + ctx.insert(params); + if let Some(service) = matched.value.get(&req.method()) { + service.boxed().serve(ctx, req).await + } else { + self.not_found.serve(ctx, req).await + } + }, + Err(_err) => { + self.not_found.serve(ctx, req).await + } } } }