-
-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rama web router #423
Rama web router #423
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
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 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. | ||
|
||
pub struct Router<State> { | ||
routes: MatchitRouter<HashMap<Method, Arc<BoxService<State, Request, Response, Infallible>>>>, | ||
not_found: Arc<BoxService<State, Request, Response, Infallible>>, | ||
} | ||
|
||
impl<State> std::fmt::Debug for Router<State> { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
f.debug_struct("Router").finish() | ||
} | ||
} | ||
|
||
impl<State> Clone for Router<State> { | ||
fn clone(&self) -> Self { | ||
Self { | ||
routes: self.routes.clone(), | ||
not_found: self.not_found.clone(), | ||
} | ||
} | ||
} | ||
|
||
/// default trait | ||
impl<State> Default for Router<State> | ||
where | ||
State: Clone + Send + Sync + 'static, | ||
{ | ||
fn default() -> Self { | ||
Self::new() | ||
} | ||
} | ||
|
||
impl<State> Router<State> | ||
where | ||
State: Clone + Send + Sync + 'static, | ||
{ | ||
/// create a new web router | ||
pub(crate) fn new() -> Self { | ||
Self { | ||
routes: MatchitRouter::new(), | ||
not_found: Arc::new( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this can probably be made optional so you don't need to assign it if not set, and instead just fallback to NOT_FOUND anyway but on the spot |
||
service_fn(|| async { Ok(StatusCode::NOT_FOUND.into_response()) }).boxed(), | ||
), | ||
} | ||
} | ||
|
||
pub fn route<I, T>(mut self, method: Method, path: &str, service: I) -> Self | ||
where | ||
I: IntoEndpointService<State, T>, | ||
{ | ||
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<I, T>(self, path: &str, service: I) -> Self | ||
where | ||
I: IntoEndpointService<State, T>, | ||
{ | ||
self.route(Method::GET, path, service) | ||
} | ||
|
||
pub fn post<I, T>(self, path: &str, service: I) -> Self | ||
where | ||
I: IntoEndpointService<State, T>, | ||
{ | ||
self.route(Method::POST, path, service) | ||
} | ||
|
||
pub fn put<I, T>(self, path: &str, service: I) -> Self | ||
where | ||
I: IntoEndpointService<State, T>, | ||
{ | ||
self.route(Method::PUT, path, service) | ||
} | ||
|
||
pub fn delete<I, T>(self, path: &str, service: I) -> Self | ||
where | ||
I: IntoEndpointService<State, T>, | ||
{ | ||
self.route(Method::DELETE, path, service) | ||
} | ||
|
||
pub fn patch<I, T>(self, path: &str, service: I) -> Self | ||
where | ||
I: IntoEndpointService<State, T>, | ||
{ | ||
self.route(Method::PATCH, path, service) | ||
} | ||
|
||
pub fn head<I, T>(self, path: &str, service: I) -> Self | ||
where | ||
I: IntoEndpointService<State, T>, | ||
{ | ||
self.route(Method::HEAD, path, service) | ||
} | ||
|
||
pub fn options<I, T>(self, path: &str, service: I) -> Self | ||
where | ||
I: IntoEndpointService<State, T>, | ||
{ | ||
self.route(Method::OPTIONS, path, service) | ||
} | ||
|
||
pub fn trace<I, T>(self, path: &str, service: I) -> Self | ||
where | ||
I: IntoEndpointService<State, T>, | ||
{ | ||
self.route(Method::TRACE, path, service) | ||
} | ||
|
||
/// use the given service in case no match could be found. | ||
pub fn not_found<I, T>(mut self, service: I) -> Self | ||
where | ||
I: IntoEndpointService<State, T>, | ||
{ | ||
self.not_found = Arc::new(service.into_endpoint_service().boxed()); | ||
self | ||
} | ||
} | ||
|
||
impl<State> Service<State, Request> for Router<State> | ||
where | ||
State: Clone + Send + Sync + 'static, | ||
{ | ||
type Response = Response; | ||
type Error = Infallible; | ||
|
||
async fn serve( | ||
&self, | ||
mut ctx: Context<State>, | ||
req: Request<>, | ||
) -> Result<Self::Response, Self::Error> { | ||
let uri_string = req.uri().to_string(); | ||
match &self.routes.at(uri_string.as_str()) { | ||
Ok(matched) => { | ||
let params: HashMap<String, String> = matched.params.clone().iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will need to be collected as https://docs.rs/rama/latest/rama/http/matcher/struct.UriParams.html to be compatible with the existing matcher object. Feel free to expand the logic of UriParams if you miss something. E.g. might make code cleaner here if you implement There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also in general there should be no reason to double allocate.
Probably that first clone isn't needed |
||
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 | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also need to be able to nest and merge btw, so I think this approach is a bit too simplistic