How can Rust Rocket deduce return types wrapped in Results/Options?

Viewed 266

This is probably not a high-quality question as I am new to Rust.

I'm working with Rocket. Rocket handlers can return "A value of any type that implements the Responder trait". However, I notice that I can wrap something that implements Responder in any number of Results or Options, and things appear to work. For example:

#[post("/add_data")]
fn add_data() -> std::result::Result<Option<Response<'static>>, Status> {
    Ok(Some(Response::new()))
}

Surely there is no code in Rocket to handle all these different combinations of return variants, but with what I know about Rust so far, I didn't expect this to 'just work'. Can someone explain what's happening or how this works?

3 Answers

Look at the documentation for Responder's Provided Implementations. The relevant ones are

Option<T>

If the Option is Some, the wrapped responder is used to respond to the client. Otherwise, an Err with status 404 Not Found is returned and a warning is printed to the console.

Result<T, E> where E: Debug

If the Result is Ok, the wrapped responder is used to respond to the client. Otherwise, an Err with status 500 Internal Server Error is returned and the error is printed to the console using the Debug implementation.

Result<T, E> where E: Debug + Responder

If the Result is Ok, the wrapped Ok responder is used to respond to the client. If the Result is Err, the wrapped Err responder is used to respond to the client.

Though it's imprecise and you can see fuller signatures below:

impl<'r, R: Responder<'r>> Responder<'r> for Option<R>

impl<'r, R: Responder<'r>, E: Debug> Responder<'r> for Result<R, E>

impl<'r, R: Responder<'r>, E: Responder<'r> + Debug> Responder<'r> for Result<R, E>

So

  1. Response<'static> implements Responder<'static>. Therefore

  2. Option<Response<'static>> implements Responder<'static>. Therefore

  3. Result<Option<Response<'static>>, Status> implements Responder<'static>.

Look at the docs for the Responder trait at the bottom you can see all types that implement the Responder trait.

// If self is Some, responds with the wrapped Responder. Otherwise prints
// a warning message and returns an Err of Status::NotFound.
impl<'r, R: Responder<'r>> Responder<'r> for Option<R>

// If self is Ok, responds with the wrapped Responder. Otherwise prints
// an error message with the Err value returns an Err of Status::InternalServerError.
impl<'r, R: Responder<'r>, E: Debug> Responder<'r> for Result<R, E>

impl<'r> Responder<'r> for Response<'r>

There's no special sauce in the macro that handles all these cases, it's that there's a blanket implementation of Responder that works on Options and Results that contain responders.

The answer lies more in how the types work. You can write generic type signatures in Rust that work with any types:

// A concrete type `A`.
struct A;

// In defining the type `Single`, the first use of `A` is not preceded by `<A>`.
// Therefore, `Single` is a concrete type, and `A` is defined as above.
struct Single(A);
//            ^ Here is `Single`s first use of the type `A`.

// Here, `<T>` precedes the first use of `T`, so `SingleGen` is a generic type.
// Because the type parameter `T` is generic, it could be anything, including
// the concrete type `A` defined at the top.
struct SingleGen<T>(T);

fn main() {
    // `Single` is concrete and explicitly takes `A`.
    let _s = Single(A);
    
    // Create a variable `_char` of type `SingleGen<char>`
    // and give it the value `SingleGen('a')`.
    // Here, `SingleGen` has a type parameter explicitly specified.
    let _char: SingleGen<char> = SingleGen('a');

    // `SingleGen` can also have a type parameter implicitly specified:
    let _t    = SingleGen(A); // Uses `A` defined at the top.
    let _i32  = SingleGen(6); // Uses `i32`.
    let _char = SingleGen('a'); // Uses `char`.
}

Generics traditionally use a capital letter, like R and E in the Rocket code. The Rust compiler figures out if the types being used are ok at compile time for whatever type(s) are passed based on the type signatures. Although different than Rust types, Typescript also has quite similar functionality and has some decent documentation that explains the concept. And the Responder bit is just the magic of traits.

Related