carllerche / tower-web

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

Multiple response types with status and generics #202

Open 0xpr03 opened 5 years ago

0xpr03 commented 5 years ago

I'll try my best to summarize this, because apparently it's not as obvious as I thought, to see where the use case and problem lies.

Problem: It's hard to write something that returns multiple types of Responses (structs) + different status codes without manually defining a multitude of #[web(either)] or turning everything into Strings. Moreover the #[web(either)] approach doesn't work with setting the status code.

Use case: You have multiple REST apis that return something, depending on the outcome. One might have for example something like this in pseudo-code:

#[post("/foo/baz")]
#[content_type("application/json")]
fn playback_start(&self, body: MyStruct) -> Failure<Foo> {
    match do_something(&self.state, &body.id) {
        Some(v) => {v.xy()?;
             SuccessStruct {
                http_status_code: 200, // or leave it out because 200 should be default
                body: ...
            }
         },
        None => ErrorStruct {
                 http_status_code: 409,
                 body: ..
       }
    }
}

So depending on the outcome of the operation we return a different Struct, that has to be serialized and send back, and, we want to set the http status code differently.

Problem Details: Setting the http-status code can be done directly with one Struct as documented in the docs. But setting it from multiple Structs in one return gives headaches. For this one example one might come up with an enum like so:

#[derive(Response)]
#[web(either)]
enum MyResponse {
    Success(A),
    Fail(B),
}

#[derive(Response)]
struct SuccessStruct {
    #[web(status)]
    custom_status: u16,
    body: String,
}

#[derive(Response)]
struct ErrorStruct {
    #[web(status)]
    custom_status: u16,
    body: String,
}

But it doesn't work for three reasons: A) We can't define Response more than once, so MyResponse would have to handle the status already. B) We can't make this generic due to #190. So we can't make this enum generic to allow multiple success and for example one error type. => We end up with having to do an enum for every possible state. C) Even if A would work, this doesn't scale for more different types due to B.

We also can't just return a http::response::Response<T> due to T not being allowed as generic and the enums serialization striking back again, so we can't use an enum approach. Also we still would have to create a Response via the builder.

Things to keep in mind: We don't want to get the enum itself serialized ( Succes { .. } is not a desired json return). We probably still want to allow Failure<MyResult> because there is a difference in specifically designed returns and blatantly internal server errors. (When doing a try over a result that's supposed to work.)

Current workaround: One can currently work around by returning Failure<Response<String>> and adding some helpers to build a response and serialize their structs. Example of actual code in use