LukeMathWalker / zero-to-production

Code for "Zero To Production In Rust", a book on API development using Rust.
https://www.zero2prod.com
Apache License 2.0
5.86k stars 519 forks source link

The book seems to infer `NewSubscriber` type from try_into in Chapter 7 - yet this does not work rust 1.7.2 #227

Closed dan-cooke closed 1 year ago

dan-cooke commented 1 year ago

The following line

https://github.com/LukeMathWalker/zero-to-production/blob/root-chapter-07-part2/src/routes/subscriptions.rs#L43

// ---
pub async fn subscribe(body: web::Json<SubscribeBody>, pool: web::Data<PgPool>) -> HttpResponse {
    let new_subscriber = match body.0.try_into() {
        Ok(subscriber) => subscriber,
        Err(_) => return HttpResponse::BadRequest().finish(),
    };
 // ---
}

This code will not compile, and I will get

Diagnostics:
1. the trait bound `(): std::convert::From<subscriptions::SubscribeBody>` is not satisfied
   the following other types implement trait `std::convert::From<T>`:
     <(usize, usize) as std::convert::From<toml::tokens::Span>>
     <(T,) as std::convert::From<[T; 1]>>
     <(T, T) as std::convert::From<[T; 2]>>
     <(T, T, T) as std::convert::From<[T; 3]>>
     <(T, T, T, T) as std::convert::From<[T; 4]>>
     <(T, T, T, T, T) as std::convert::From<[T; 5]>>
     <(T, T, T, T, T, T) as std::convert::From<[T; 6]>>
     <(T, T, T, T, T, T, T) as std::convert::From<[T; 7]>>
   and 5 others
   required for `subscriptions::SubscribeBody` to implement `Into<()>`
   required for `()` to implement `TryFrom<subscriptions::SubscribeBody>`
   required for `subscriptions::SubscribeBody` to implement `TryInto<()>` [E0277]

Which is very odd - it seems to be trying to convert into the Unit type ().

If we break down the match block a little bit and do the following

    let test = body.0.try_into();

We get the following error

Diagnostics:
1. type inside `async` block must be known in this context
   cannot infer type for type parameter `U` [E0698]

Question

I am quite new to rust, and I have not been following updates for long - has something changed since the book was published with type inference? It seems we can no longer infer the type of NewSubscriber here and we must explicitly do

    let new_subscriber: NewSubscriber = match body.0.try_into() {
        Ok(subscriber) => subscriber,
        Err(_) => return HttpResponse::BadRequest().finish(),
    };

_which honestly makes sense to me, the way its done in the book seems like magic to me__

rj-jones commented 1 year ago

The compiler is trying to tell you that the type you are trying to convert to doesn't implement a into or a try_into/try_from fn.

Working example

#[tracing::instrument(
    name = "Adding a new subscriber",
    skip(form, pool),
    fields(
        email = %form.email,
        name = %form.name
    )
)]
pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> HttpResponse {
    let new_subscriber: NewSubscriber = match form.0.try_into() {
        Ok(form) => form,
        Err(_) => return HttpResponse::BadRequest().finish(),
    };
    match insert_subscriber(&pool, &new_subscriber).await {
        Ok(_) => HttpResponse::Ok().finish(),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

NewSubscriber (see how it has a TryFrom impl)

use crate::domain::SubscriberEmail;
use crate::domain::SubscriberName;
use crate::routes::FormData;

pub struct NewSubscriber {
    pub email: SubscriberEmail,
    pub name: SubscriberName,
}

impl TryFrom<FormData> for NewSubscriber {
    type Error = String;

    fn try_from(value: FormData) -> Result<Self, Self::Error> {
        let name = SubscriberName::parse(value.name)?;
        let email = SubscriberEmail::parse(value.email)?;
        Ok(Self { email, name })
    }
}

Just make sure your SubscribeBody has a TryFrom or TryInto implemented for it. And I think it's a good idea to always have the type marked for into/try_into lines.

dan-cooke commented 1 year ago

@rj-jones that is not the case unfortunately I do infact have a TryFrom implementation for that specific struct.

you can see in my post that I am calling try_into and it works, when I explicitly annotate the variable with NewSubscriber - so I don't think the issue is a missing trait definition.

Here is my TryFrom impl

use crate::routes::SubscribeBody;

use super::subscriber_email::SubscriberEmail;

#[derive(Debug)]
pub struct NewSubscriber {
    pub email: SubscriberEmail,
}

impl TryFrom<SubscribeBody> for NewSubscriber {
    type Error = String;

    fn try_from(value: SubscribeBody) -> Result<NewSubscriber, Self::Error> {
        let email = SubscriberEmail::parse(value.email)?;
        Ok(Self { email })
    }
}

And I can succesfully do this

    let new_subscriber = NewSubscriber::try_from(body.0);

This works perfectly fine, so try_from is definetly implemented correctly.

The issue is the type inference

The compiler thinks that I am trying to try_into the type () because it is unable to infer NewSubsciber without an explicit definition

My question is...

Why can the book can get away with no explicit type hint (let new_subscriber = match...) ...

wheras in rust 1.7.1 I cannot - I must do let new_subscriber: NewSubscriber = match. I suspect there has been a change in the rust toolchain

dan-cooke commented 1 year ago

@rj-jones wow okay I have just figured out why this is happening

The difference is this line

---    fn try_from(value: SubscribeBody) -> Result<NewSubscriber, Self::Error> {
+++    fn try_from(value: SubscribeBody) -> Result<Self, Self::Error> {

I think this is happening because technically -> Result<NewSusbcriber... is not the correct signature for satisfying the TryFrom trait

TIL: Self !== TheType in this context

Wow also this is a real edge case the compier seems to be unable to catch... it throws an error if I try to return any other type, but it sees Self and NewSubscriber as equivalent - even though, it does not satisfy the trait def

Weird

rj-jones commented 1 year ago

Good catch! I've been staring at this for some time now and that was the only difference I could see. But that shouldn't be causing the issue 🤔...interesting. Since you mentioned you were new to Rust, it is good practice to use Self in impl's like that. Also, I agree with you about doing let new_subscriber: NewSubscriber = match , I think this is a good idea for clarity.