carllerche / tower-web

A fast, boilerplate free, web framework for Rust
MIT License
980 stars 51 forks source link

Extract chaining and futures #174

Open lnicola opened 5 years ago

lnicola commented 5 years ago

Since Extract seems to be the way to handle stuff like authentication, consider an application with two implementations of it:

Implementing Extract for DbConnection is more or less fine: you can stash a database pool in a Config struct, retrieve it from the Context, and request a new connection.

The User implementation, however, requires a database connection. You can get one with <DbConnection as Extract<B>>::extract(context), however there are some disadvantages:

Is there a better solution to this? Maybe middlewares?

carllerche commented 5 years ago

Well, there are a few things to unpack here.

ExtractFuture isn't a real Future, so using the combinators doesn't work

This is true and I think it is a mistake. Future versions of tower-web may be able to get rid of ExtractFuture in favor of a real future.

it's a bit hard to discover (you also need to call poll and extract)

I agree, we probably should work on making the API more discoverable. The extract context is passed around, so maybe that value could have an extract::Context::extract(&self) function. That might make it more discoverable. You would then do:

impl<B: BufStream> Extract<B> for User {
    type Future = Box<Future<Item = Self, Error = extract::Error>>;

    fn extract(context: &Context) -> Self::Future {
        Box::new(context.extract::<DbConnection>().and_then(|conn| {
            // ...
        }))
    }
}

This still isn't great... Part of the problem is Extract conflates extracting from the request head and extracting from the body. I wonder if that can be split up. So, there would be an Extract trait and an ExtractBody trait.

Another option would be to use a macro strategy as well...

#[web(Extract)]
impl User {
    fn extract(x_auth_token: String, db: DbConnection) -> impl Future<Item = User> {
        db.find_user(x_auth_token)
    }
}

if a route wants both a DbConnection and a User, you end up connecting to the database twice.

Definitely tricky, but there are options. extract::Context could be used to cache extracted values on a per-request basis. Do you have suggestions for specific strategies?

carllerche commented 5 years ago

I had another thought. I wonder if it would be possible to make this work somehow:

#[web(extract(cached))]
impl DbConnection {
    fn extract(config: &Config) -> impl Future<Item = Self> {
        DbConnection::connect(config)
    }
}

#[web(extract)]
impl User {
    fn extract(x_auth_token: String, db: &DbConnection) -> impl Future<Item = User> {
        db.find_user(x_auth_token)
    }
}

I'm not 100% sure what the exact attributes should be and how to make it work...