Skip to content

Commit

Permalink
add custom path matcher
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
urkle committed Jan 19, 2024
1 parent 7b07043 commit b7a8f85
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 0 deletions.
44 changes: 44 additions & 0 deletions src/filters/path.rs
Expand Up @@ -274,6 +274,50 @@ pub fn param<T: FromStr + Send + 'static>(
})
}

/// 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<F, T>(fun: F) -> impl Filter<Extract = T, Error = Rejection> + 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
Expand Down
66 changes: 66 additions & 0 deletions tests/path.rs
Expand Up @@ -3,6 +3,7 @@
extern crate warp;

use futures_util::future;
use warp::path::Tail;
use warp::Filter;

#[tokio::test]
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit b7a8f85

Please sign in to comment.