axum icon indicating copy to clipboard operation
axum copied to clipboard

Idea: Split FromRequest trait into three (like the Fn traits)

Open jplatte opened this issue 3 years ago • 8 comments

FromRequest with a shared-reference parameter would mostly be useful for middleware or other places where you have a request that you want to pull some bits out of while knowing you're not going to modify it for the final handler. FromRequestMut would be used for all but the last extractor and FromRequestOnce would be used for the last one.

I think this could even obviate the need for the RequestParts type.

I'll mark as hard because I think it requires changes to all of the crates, including both macro_rules! and proc-macro code.

jplatte avatar Jun 23 '22 13:06 jplatte

I think this sounds very promising. I'm gonna explore it!

davidpdrsn avatar Jun 23 '22 13:06 davidpdrsn

I give this a quick shut but ran into some issues

trait IntoResponse {}
impl IntoResponse for () {}

struct Request<B>(B);

trait FromRequestOnce<B>: Sized {
    type Rejection: IntoResponse;

    fn from_request_once(req: Request<B>) -> Result<Self, Self::Rejection>;
}

trait FromRequestMut<B>: FromRequestOnce<B> + Sized {
    fn from_request_mut(req: &mut Request<B>) -> Result<Self, Self::Rejection>;
}

// I think this impl would be nice, otherwise you have to manually implement both traits all the
// time
impl<B, T> FromRequestOnce<B> for T
where
    T: FromRequestMut<B>,
{
    // what type should this be? `Rejection` is defined in `FromRequestOnce` which is the trait
    // we're implementing, but we're delegating to `FromRequestMut` which doesn't have it...
    type Rejection = T::Rejection;

    fn from_request_once(mut req: Request<B>) -> Result<Self, Self::Rejection> {
        T::from_request_mut(&mut req)
    }
}

// error[E0275]: overflow evaluating the requirement `<() as FromRequestOnce<B>>::Rejection == _`
impl<B> FromRequestMut<B> for () {
    fn from_request_mut(req: &mut Request<B>) -> Result<Self, Self::Rejection> {
        todo!()
    }
}

davidpdrsn avatar Jun 23 '22 15:06 davidpdrsn

Hm, I wonder whether the trait hierarchy is even needed, maybe the blanket impls are enough. In that case I think the solution would be to just have type Rejection on all of the traits.

jplatte avatar Jun 23 '22 15:06 jplatte

Yeah that was my first though as well but that leads to conflicting impls:

trait FromRequestOnce<B> {
    type Rejection: IntoResponse;

    fn from_request_once(req: Request<B>) -> Result<Self, Self::Rejection>;
}

trait FromRequestMut<B>: Sized {
    type Rejection: IntoResponse;

    fn from_request_mut(req: &mut Request<B>) -> Result<Self, Self::Rejection>;
}

impl<B, T> FromRequestOnce<B> for T
where
    T: FromRequestMut<B>,
{
    type Rejection = T::Rejection;

    fn from_request_once(mut req: Request<B>) -> Result<Self, Self::Rejection> {
        T::from_request_mut(&mut req)
    }
}

// error[E0119]: conflicting implementations of trait `FromRequestOnce<_>` for type `Bytes`
impl<B> FromRequestOnce<B> for Bytes {
    fn from_request_once(mut req: Request<B>) -> Result<Self, Self::Rejection> {
        todo!()
    }
}

davidpdrsn avatar Jun 23 '22 19:06 davidpdrsn

Something that does work is

trait FromRequestOnce<B>: Sized {
    type Rejection: IntoResponse;

    fn from_request_once(req: Request<B>) -> Result<Self, Self::Rejection>;
}

// this works because there is no blanket impl
// but it requires users to manually write a `FromRequestOnce` impl that
// delegates to `FromRequestMut`
// maybe thats okay..?
trait FromRequestMut<B>: FromRequestOnce<B> + Sized {
    fn from_request_mut(req: &mut Request<B>) -> Result<Self, Self::Rejection>;
}

impl<B> FromRequestOnce<B> for Bytes {
    type Rejection = ();

    fn from_request_once(req: Request<B>) -> Result<Self, <Self as FromRequestOnce<B>>::Rejection> {
        todo!()
    }
}

impl<B> FromRequestMut<B> for () {
    fn from_request_mut(req: &mut Request<B>) -> Result<Self, Self::Rejection> {
        todo!()
    }
}

// could we provide a macro to generate this?
impl<B> FromRequestOnce<B> for () {
    type Rejection = ();

    fn from_request_once(mut req: Request<B>) -> Result<Self, Self::Rejection> {
        <Self as FromRequestMut<B>>::from_request_mut(&mut req)
    }
}

davidpdrsn avatar Jun 25 '22 12:06 davidpdrsn

Maybes something like this

struct RequestParts<R, B>(B, PhantomData<R>);

struct Mut;
struct Owned;

impl<R, B> RequestParts<R, B> {
    // fn headers()
    // fn extensions()
    // ...
}

// only `RequestParts<Owned, _>` allows you to take the body
impl<B> RequestParts<Owned, B> {
    // fn take_body()
}

// `R` is either `Mut` or `Owned`
trait FromRequest<R, B>: Sized {
    type Rejection: IntoResponse;

    fn from_request(req: &mut RequestParts<R, B>) -> Result<Self, Self::Rejection>;
}

// this impl applies to any `R`, i.e. both `Mut` and `Owned`
impl<R, B> FromRequest<R, B> for () {
    type Rejection = ();

    fn from_request(req: &mut RequestParts<R, B>) -> Result<Self, Self::Rejection> {
        todo!()
    }
}

// this only works for `Owned`
impl<B> FromRequest<Owned, B> for Bytes {
    type Rejection = ();

    fn from_request(req: &mut RequestParts<Owned, B>) -> Result<Self, Self::Rejection> {
        todo!()
    }
}

// `Handler` impls

trait Handler<T, B> {}

impl<B, F> Handler<(), B> for F where F: Fn() {}

impl<B, F, T1> Handler<(T1,), B> for F
where
    F: Fn(T1),
    T1: FromRequest<Owned, B>,
{
}

impl<B, F, T1, T2> Handler<(T1, T2), B> for F
where
    F: Fn(T1, T2),
    T1: FromRequest<Mut, B>,
    T2: FromRequest<Owned, B>,
{
}

davidpdrsn avatar Jun 26 '22 09:06 davidpdrsn

I took stab at implement that in https://github.com/tokio-rs/axum/pull/1121. Looking very promising!

davidpdrsn avatar Jun 26 '22 11:06 davidpdrsn

Regarding the conflicting impls, this only happens when the trait has a generic parameter. Without that, it works. I guess it's allowed for downstream crates to implement FromRequestMut<LocalType> for Bytes and that's why there is a conflict?

jplatte avatar Jul 19 '22 19:07 jplatte