Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add IntoRequest and IntoStreamingRequest traits #66

Merged
merged 32 commits into from
Oct 29, 2019
Merged

feat: Add IntoRequest and IntoStreamingRequest traits #66

merged 32 commits into from
Oct 29, 2019

Conversation

alce
Copy link
Collaborator

@alce alce commented Oct 10, 2019

Motivation

This PR implements IntoRequest and IntoStreamingRequest traits, as outlined in #34. At the moment, both unary and streaming requests work but interop crate needs to be updated for this PR to be complete. I'll wait for feedback and implement what is remaining if it's decided we should move forward with this.

Solution

Implementation is almost exactly what is discussed in #34.

use futures_core::Stream;

/// A marker trait that should be implemented for all input messages.
pub trait RequestMessage {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this marker trait since there is only ever one generic?

Copy link
Collaborator Author

@alce alce Oct 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, it is not strictly necessary and a previous implementation did not have it. I decided to include it because:

  • It sort of helps us constrain the other trait implementations. It just feels weird for IntoRequest or IntoStreamingRequest impl to exist for any type that is not an input to a rpc method.
  • I am toying around with an implementation that would let us call the streaming methods with iterators and having this bound sort of helps. Not sure if this will work of if it even makes sense though.
  • It makes code generation extremely simple, basically 1 line per input type.
  • Triggered by the breakage in tonic-interop, I initially thought that it was going to be necessary to expose this marker for users to implement in certain scenarios, but that seems not to be necessary. I found the reason why interop is broken and has nothing to do with this.

None of these reasons hold much water though. In the end, however, this should be an internal implementation detail and as long as we are not changing the public api willy-nilly we should be able to iterate on the actual implementation until we find the optimal one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, those points make sense, my real motivation for removing it is that it's not actually required and just simplifies things. Happy either way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My 2¢ is that since the Request type itself is unconstrained, this shouldn't be added; if we are going to constrain the Message type on IntoRequest, we should also constrain Request, and I'd consider adding that in a separate branch.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I think it's potentially very surprising to generate implementations of a doc(hidden) trait in a user's codebase — I'd find it weird to run cargo doc on my project and see implementations of a trait that doesn't appear to exist.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this trait as suggested. IntoRequest for T is generated now instead, please let me know what you think.

@alce alce marked this pull request as ready for review October 15, 2019 01:24
@alce
Copy link
Collaborator Author

alce commented Oct 15, 2019

I've fixed the issue with interop. This implementation seems to work well with different .proto setups.

The only way I found to make it work without the RequestMessage trait is to generate IntoRequest implementations for all input types. I could not find a way to make a blanket implementation for T without the bound.

I'm happy to make that change if you prefer, it's easy.

@LucioFranco
Copy link
Member

@hawkw what do you think about having a RequestMessage trait to enforce the type that can be intoed?

@LucioFranco
Copy link
Member

and @alce sorry for the delay on this I just wanna see what eliza has to say since she came up with the original spec other wise this looks good!

tonic-build/src/client.rs Outdated Show resolved Hide resolved
tonic-build/src/client.rs Outdated Show resolved Hide resolved
tonic-build/src/client.rs Outdated Show resolved Hide resolved
use futures_core::Stream;

/// A marker trait that should be implemented for all input messages.
pub trait RequestMessage {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My 2¢ is that since the Request type itself is unconstrained, this shouldn't be added; if we are going to constrain the Message type on IntoRequest, we should also constrain Request, and I'd consider adding that in a separate branch.

use futures_core::Stream;

/// A marker trait that should be implemented for all input messages.
pub trait RequestMessage {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I think it's potentially very surprising to generate implementations of a doc(hidden) trait in a user's codebase — I'd find it weird to run cargo doc on my project and see implementations of a trait that doesn't appear to exist.

@alce alce requested a review from LucioFranco October 18, 2019 23:38
@LucioFranco
Copy link
Member

@alce can you resolve the conflicts? I think CI may not be running because of that?

@alce
Copy link
Collaborator Author

alce commented Oct 19, 2019

Goofed. Fixing...

tonic/src/request.rs Outdated Show resolved Hide resolved
@alce
Copy link
Collaborator Author

alce commented Oct 23, 2019

I am having second thoughts about this PR.

On the one hand, as Carl and Eliza pointed out, it makes writing client calls a lot nicer. Every time I write client code I moan about having to write Request::new() explicitly.

On the other hand, it just doesn't feel entirely right to me. I can't really put my finger on it but it makes the API sort of asymmetrical. Even if we were to implement something similar for Response::new() (which btw doesn't really bother me much).

Maybe if we had to call .into_request() and .into_streaming_request() explicitly it wouldn't feel so weird?.

let message = HelloRequest{name: "hello".into()}
let response = client.say_hello(message.into_request())

Thoughts?

@LucioFranco
Copy link
Member

@alce I kinda agree with you, that actually might be better. What do you think @hawkw @carllerche ?

@hawkw
Copy link
Contributor

hawkw commented Oct 23, 2019

Maybe if we had to call .into_request() and .into_streaming_request() explicitly it wouldn't feel so weird?.

let message = HelloRequest{name: "hello".into()}
let response = client.say_hello(message.into_request())

Thoughts?

I don't really see much of a difference between making users write out into_request explicitly, and making them write Request::new? If we're going to require users to call the into_request method, I think it would be better to just close this PR and continue with the current state of things.

With that said, I don't think we should close this PR. I'd rather merge the code as-is, and call into_request implicitly. While having to call Request::new is not a major issue, not having to does make the new user experience and basic examples much simpler, which I think is worth something. If the concern is that

[...] it makes the API sort of asymmetrical.

I think we should just make the same change on the server side and add an IntoResponse trait as well.

Just my two cents.

@alce
Copy link
Collaborator Author

alce commented Oct 23, 2019

The comment about asymmetry was not so much about request vs response. The weirdness I get is more about client vs server.

This client code

let point = Point { lat: 1, lon: 2 };
let response = client.get_feature(point).await?;

and this server's signature

async fn get_feature(&self, request: Request<Point>) -> Result<Response<...>;

are surprising, particularly when the client's function signature is not easily available, as is the case when following the default getting started.

But then, as I was writing this, I realized that:

  • As things are now, you kinda need to hunt around for the generated code to get anything going beyond hello world anyway, in which case people will see the sneaky IntoRequest in the client's signature.
  • My gripe is, and always has been, with how deeply hidden the generated code is.

@hawkw
Copy link
Contributor

hawkw commented Oct 23, 2019

  • As things are now, you kinda need to hunt around for the generated code to get anything going beyond hello world anyway, in which case people will see the sneaky IntoRequest in the client's signature.

  • My gripe is, and always has been, with how deeply hidden the generated code is.

@alce do you think we can replace the generated IntoRequest impls with blanket impls, like the ones @carllerche proposed in #34 (comment)? Then, the impl IntoRequest for ... would be in tonic's docs, rather than in the generated code.

@alce
Copy link
Collaborator Author

alce commented Oct 24, 2019

I couldn't figure out how to do it without a bound. That's one of the reasons why I had the RequestMessage marker trait before, which was akin to Carl's Message trait in the comment.

I will give it another go tomorrow to see if I can find another way.

@LucioFranco
Copy link
Member

I'm still not convinced that this indirection is worth it honestly. I think the Request type is a good line between too verbose and being just explicit enough.

@alce
Copy link
Collaborator Author

alce commented Oct 24, 2019

@LucioFranco I think I've found a more natural way to do it, I'll push the changes later today.

With regards to the so-called asymmetry I speak of, I think I'm splitting hairs and considering only my own workflow and bias. In a larger, more realistic context, this is almost irrelevant.

@alce
Copy link
Collaborator Author

alce commented Oct 24, 2019

Here's a cleaner implementation that combines approaches discussed before. It introduces only one new trait and does not generate any code.

Copy link
Contributor

@hawkw hawkw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great. I like that the trait implementations are no longer generated; I think this will be much less surprising for users. Left a couple of minor suggestions.

tonic/src/request.rs Outdated Show resolved Hide resolved
tonic-build/src/client.rs Outdated Show resolved Hide resolved
tonic/src/request.rs Outdated Show resolved Hide resolved
@alce alce requested a review from LucioFranco October 26, 2019 15:46
fn into_streaming_request(self) -> Request<Self::Stream>;
}

/// Conversion into a unary `Request`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a bit more docs here to explain how this works? I am thinking maybe an example that shows constructing a request and passing it and one with the message explaining the trait?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Docs are slightly better but I'm not sure they are clear enough.

Copy link
Member

@LucioFranco LucioFranco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonderful! Thank you @alce for fighting through this! <3

/// use tonic::Request;
///
/// client.get_feature(Point {});
/// client.get_feature(Request::new(Point {}));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍

@LucioFranco LucioFranco changed the title IntoRequest and IntoStreamingRequest traits feat: Add IntoRequest and IntoStreamingRequest traits Oct 29, 2019
@LucioFranco LucioFranco merged commit 4bb087b into hyperium:master Oct 29, 2019
rabbitinspace pushed a commit to satelit-project/tonic that referenced this pull request Jan 1, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants