From b7a8f85b08d6c976856d9ef982748badf90e31f3 Mon Sep 17 00:00:00 2001 From: Edward Rudd Date: Thu, 30 Mar 2023 11:25:44 -0400 Subject: [PATCH] add custom path matcher - this allows a custom function to - match multiple path segments - extract whatever it wants from the path - advance the consumed path to use path()/end() after --- src/filters/path.rs | 44 ++++++++++++++++++++++++++++++ tests/path.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/src/filters/path.rs b/src/filters/path.rs index 29676a729..543ade7e1 100644 --- a/src/filters/path.rs +++ b/src/filters/path.rs @@ -274,6 +274,50 @@ pub fn param( }) } +/// A custom path matching filter. +/// +/// This will call a function with the remaining path segment and allow the function +/// to extract whatever it wishes and return the number of characters to advance int he path +/// to continue standard path() filters. +/// +/// The function can reject with any warp error. To pass on to other routes +/// reject with warp::rejcct::not_found(). +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// +/// let route = warp::path::custom(|tail: &str| { +/// Ok(( +/// 10, +/// ("Something Good".to_string(),) +/// )) +/// }) +/// .map(|data: String| { +/// format!("You found /{}", data) +/// }); +/// ``` +pub fn custom(fun: F) -> impl Filter + Copy +where + F: FnOnce(&str) -> Result<(usize, T), Rejection> + Copy + 'static, + T: Tuple + Send + 'static, +{ + filter_fn(move |route| { + let remaining = route.path(); + let result = fun(remaining); + match result { + Ok((skip, extracted)) => { + if skip > 0 { + route.set_unmatched_path(skip); + } + future::ok(extracted) + } + Err(err) => future::err(err), + } + }) +} + /// Extract the unmatched tail of the path. /// /// This will return a `Tail`, which allows access to the rest of the path diff --git a/tests/path.rs b/tests/path.rs index 4f9302036..ee5d9c20c 100644 --- a/tests/path.rs +++ b/tests/path.rs @@ -3,6 +3,7 @@ extern crate warp; use futures_util::future; +use warp::path::Tail; use warp::Filter; #[tokio::test] @@ -59,6 +60,71 @@ async fn param() { ); } +#[tokio::test] +async fn custom() { + let _ = pretty_env_logger::try_init(); + + // extracting path segment and advancing + let simple = warp::path::custom(|remaining| { + if let Some(pos) = remaining.rfind('/') { + let ret = &remaining[0..pos]; + Ok((ret.len(), (ret.to_string(),))) + } else { + Err(warp::reject::not_found()) + } + }) + .and(warp::path::tail()) + .map(|m, t: Tail| (m, t.as_str().to_string())); + + let req = warp::test::request().path("/one/two/three"); + assert_eq!( + req.filter(&simple).await.unwrap(), + ("one/two".to_string(), "three".to_string()) + ); + + // no extracting + let no_extract = warp::path::custom(|remaining| { + if remaining.ends_with(".bmp") { + Ok((0, ())) + } else { + Err(warp::reject::not_found()) + } + }) + .and(warp::path::tail()) + .map(|t: Tail| (t.as_str().to_string())); + + let req = warp::test::request().path("/one/two/three.bmp"); + assert_eq!( + req.filter(&no_extract).await.unwrap(), + ("one/two/three.bmp".to_string()) + ); + + let req = warp::test::request().path("/one/two/three.png"); + assert!( + !req.matches(&no_extract).await, + "custom() doesn't match .png" + ); + + // prefixed and postfixed path() matching + let mixed = warp::path::path("prefix") + .and(warp::path::custom(|remaining| { + if let Some(pos) = remaining.rfind('/') { + let ret = &remaining[0..pos]; + Ok((ret.len(), (ret.to_string(),))) + } else { + Err(warp::reject::not_found()) + } + })) + .and(warp::path::path("postfix")) + .and(warp::path::end()); + + let req = warp::test::request().path("/prefix/middle/area/postfix"); + assert_eq!( + req.filter(&mixed).await.unwrap(), + ("middle/area".to_string()) + ); +} + #[tokio::test] async fn end() { let _ = pretty_env_logger::try_init();