David Taylor
David Taylor

Reputation: 31

Is there a simpler way to create a custom Filter method to invoke a Warp handler?

Instead of writing something like:

let hello = get()
    .and(path!(String))
    .and_then(|string| handlers::hello(string).map_err(warp::reject::custom))
    .boxed()

I'd like to be able to write:

let hello = get()
    .and(path!(String))
    .handle(handlers::hello)

where handle does the and_then -> map_err -> boxed thing.

Right now I use a set of traits for each arity handler. This is the 2-arity handler:

pub trait Handle2<A, B, Fut, R, F>
where
    R: Reject,
    F: Fn(A, B) -> Fut,
    Fut: Future<Output = Result<String, R>>,
{
    fn handle(self, f: F) -> BoxedFilter<(String,)>;
}

impl<A, B, Fut, R, F, T, Futr> Handle2<A, B, Fut, R, F> for T
where
    R: Reject,
    F: Fn(A, B) -> Fut + Clone + Sync + Send + 'static,
    Fut: Future<Output = Result<String, R>> + Send,
    Futr: Future<Output = Result<(A, B), Rejection>> + Send,
    T: Filter<Extract = (A, B), Error = Rejection, Future = Futr> + Sync + Send + 'static,
{
    fn handle(self, f: F) -> BoxedFilter<(String,)> {
        self.and_then(move |a, b| f(a, b).map_err(warp::reject::custom))
            .boxed()
    }
}

It's awesome that you can do this in Rust, but is there a simpler way to achieve this?

Upvotes: 2

Views: 1977

Answers (1)

Nicolas Marshall
Nicolas Marshall

Reputation: 4444

There is.

With a route like this one declared in your main():

let routes = get().and(path("hello")).and_then(routes::getRoot);

You can use the async syntax sugar to auto-transform a simple handler into a future (a 0.3/std one). That makes it implement all the necessary traits to use it as a warp::Filter:

// routes/mod.rs

pub async fn getRoot() -> Result<impl warp::Reply, warp::Rejection> {
    Ok("Hello world !")
}

Pretty simple so far ! Now the one thing you wanted that's still missing is proper error handling with warp::reject::custom.

First, let's make our handler return an error:

// routes/mod.rs

pub async fn getRoot() -> Result<impl warp::Reply, warp::Rejection> {
    let _parsed_url = Url::parse(&"https://whydoesn.it/work?").map_err(ServiceError::from)?;
    Ok("Hello world !")
}

Notice the map_err(ServiceError::from) and the return type warp::Rejection ? We need to turn our specific error into a custom error enum (that's ServiceError, we'll define it soon), and then provide a way to auto-convert a ServiceError into a warp::Rejection.

Let's do that by creating a way to handle errors from all route handler functions:

// ./errors.rs

/// An internal error enum for representing all the possible failure states
#[derive(thiserror::Error, Debug)]
pub enum ServiceError {
    #[error("unacceptable !")]
    SomeSpecificWayOfFailing,
    #[error(transparent)]
    Other(#[from] Box<dyn std::error::Error + Sync + Send>), // catchAll error type
}
impl warp::reject::Reject for ServiceError {}
impl From<ServiceError> for warp::reject::Rejection {
    fn from(e: ServiceError) -> Self {
        warp::reject::custom(e)
    }
}

In our case the error example that I gave returns an url::ParseError. Let's add a way to map it to a ServiceError:

// ./errors.rs

impl From<url::ParseError> for ServiceError {
    fn from(e: url::ParseError) -> Self {
        ServiceError::Other(e.into()) // you can refine that and turn it into a more specific enum variant of ServiceError here
    }
}

You should be all set. Now adding a new route/handler should be as easy as adding an async function (plus adding impl ServiceError::from for any new error type that you want to handle in the handler function)

Upvotes: 2

Related Questions