Skip to content

Commit

Permalink
Nicer path extractor, uses query params
Browse files Browse the repository at this point in the history
  • Loading branch information
CathalMullan committed Sep 2, 2024
1 parent 634e10b commit ed18002
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 16 deletions.
144 changes: 136 additions & 8 deletions examples/hyper/src/extract/path.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,142 @@
use super::FromRequestParts;
use crate::SharedAppState;
use http::request::Parts;
use std::convert::Infallible;
use crate::{response::IntoResponse, SharedAppState};
use http::Response;
use http::{request::Parts, StatusCode};
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;

pub struct Path(pub String);
pub struct Path<T>(pub T);

impl FromRequestParts for Path {
type Rejection = Infallible;
#[derive(Debug)]
pub enum PathRejection {
MissingPathParams,
ParseError(String),
WrongNumberOfParameters { expected: usize, actual: usize },
}

impl IntoResponse for PathRejection {
fn into_response(self) -> crate::response::AppResponse {
let (status, body) = match self {
Self::MissingPathParams => (
StatusCode::INTERNAL_SERVER_ERROR,
"Missing path parameters".into(),
),
Self::ParseError(err) => (
StatusCode::BAD_REQUEST,
format!("Failed to parse path parameter: {err}"),
),
Self::WrongNumberOfParameters { expected, actual } => (
StatusCode::BAD_REQUEST,
format!("Wrong number of path parameters. Expected {expected}, got {actual}"),
),
};

Response::builder()
.status(status)
.body(body.into())
.unwrap()
}
}

pub trait FromPath: Sized {
fn from_path(map: &HashMap<String, String>) -> Result<Self, PathRejection>;
}

impl<T> FromRequestParts for Path<T>
where
T: FromPath,
{
type Rejection = PathRejection;

fn from_request_parts(
parts: &mut Parts,
_state: &SharedAppState,
) -> Result<Self, Self::Rejection> {
let params = parts
.extensions
.get::<Arc<HashMap<String, String>>>()
.ok_or(PathRejection::MissingPathParams)?;

fn from_request_parts(parts: &mut Parts, _: &SharedAppState) -> Result<Self, Self::Rejection> {
Ok(Self(parts.uri.path().to_string()))
T::from_path(params).map(Path)
}
}

impl<T> FromPath for T
where
T: FromStr,
T::Err: std::fmt::Display,
{
fn from_path(map: &HashMap<String, String>) -> Result<Self, PathRejection> {
if map.len() != 1 {
return Err(PathRejection::WrongNumberOfParameters {
expected: 1,
actual: map.len(),
});
}

let (_, value) = map.iter().next().unwrap();
T::from_str(value).map_err(|e| PathRejection::ParseError(e.to_string()))
}
}

pub trait FromPathTuple: Sized {
fn from_path_tuple(map: &HashMap<String, String>) -> Result<Self, PathRejection>;
}

macro_rules! count_idents {
() => (0);
($T:ident $(,$Rest:ident)*) => (1 + count_idents!($($Rest),*));
}

macro_rules! impl_from_path_tuple {
($($T:ident),+) => {
impl<$($T),+> FromPathTuple for ($($T,)+)
where
$($T: FromStr,)+
$($T::Err: std::fmt::Display,)+
{
fn from_path_tuple(map: &HashMap<String, String>) -> Result<Self, PathRejection> {
let expected = count_idents!($($T),+);
if map.len() != expected {
return Err(PathRejection::WrongNumberOfParameters {
expected,
actual: map.len(),
});
}

let mut values = map.values();
Ok((
$(
values.next()
.ok_or(PathRejection::MissingPathParams)?
.parse::<$T>()
.map_err(|e| PathRejection::ParseError(e.to_string()))?,
)+
))
}
}
};
}

macro_rules! all_the_tuples {
($name:ident) => {
$name!(T1, T2);
$name!(T1, T2, T3);
$name!(T1, T2, T3, T4);
$name!(T1, T2, T3, T4, T5);
$name!(T1, T2, T3, T4, T5, T6);
$name!(T1, T2, T3, T4, T5, T6, T7);
$name!(T1, T2, T3, T4, T5, T6, T7, T8);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15);
$name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16);
};
}

all_the_tuples!(impl_from_path_tuple);
10 changes: 9 additions & 1 deletion examples/hyper/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ impl AppRouter {
self
}

pub async fn handle(&self, req: AppRequest, state: SharedAppState) -> AppResponse {
pub async fn handle(&self, mut req: AppRequest, state: SharedAppState) -> AppResponse {
let method = req.method();
let path = req.uri().path();

Expand All @@ -55,6 +55,14 @@ impl AppRouter {
match router.search(&path) {
Ok(Some(search)) => {
let handler = &search.data.value;

let parameters: HashMap<String, String> = search
.parameters
.into_iter()
.map(|p| (p.key.to_string(), p.value.to_string()))
.collect();

req.extensions_mut().insert(Arc::new(parameters));
handler(req, state).await
}
_ => Response::builder()
Expand Down
3 changes: 1 addition & 2 deletions examples/hyper/src/routes/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ pub struct DeleteResponse {
pub key: String,
}

pub async fn handle_delete(state: SharedAppState, Path(path): Path) -> impl IntoResponse {
let key = path.trim_start_matches('/').to_string();
pub async fn handle_delete(state: SharedAppState, Path(key): Path<String>) -> impl IntoResponse {
state.write().await.db.remove(&key);

let response = DeleteResponse {
Expand Down
3 changes: 1 addition & 2 deletions examples/hyper/src/routes/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ pub struct GetResponse {
pub value: Option<String>,
}

pub async fn handle_get(state: SharedAppState, Path(path): Path) -> impl IntoResponse {
let key = path.trim_start_matches('/').to_string();
pub async fn handle_get(state: SharedAppState, Path(key): Path<String>) -> impl IntoResponse {
let db = &state.read().await.db;

let Some(value) = db.get(&key) else {
Expand Down
4 changes: 1 addition & 3 deletions examples/hyper/src/routes/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@ pub struct SetResponse {

pub async fn handle_set(
state: SharedAppState,
Path(path): Path,
Path(key): Path<String>,
req: AppRequest,
) -> impl IntoResponse {
let key = path.trim_start_matches('/').to_string();

let body = req.collect().await.unwrap().to_bytes();
state.write().await.db.insert(key.clone(), body);

Expand Down

0 comments on commit ed18002

Please sign in to comment.