-
Notifications
You must be signed in to change notification settings - Fork 12
feat: use packages when looking up services and message types. #70
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: use packages when looking up services and message types. #70
Conversation
342db47 to
e001efa
Compare
ermul
left a comment
There was a problem hiding this 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 haven't made it through the whole diff, but will finish after I have some lunch.
| let (method_name, service_part) = if method_name.contains(':') { | ||
| method_name.split_once(':').unwrap_or((method_name, "")) | ||
| } else { | ||
| (method_name, "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
This isn't part of your diff, but I believe the if/else is redundant here:
let (method_name, service_part) = method_name.split_once(':').unwrap_or((method_name, ""))There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, looks like it, I can update it later
| }) | ||
| .unwrap_or_else(|| (name, None)) | ||
| } else { | ||
| // otherwise it's a relative name, so if it contains dots, this means embedded types, not packages? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the ??
I poked around prost-build & the protocolbuffers/protobuf source and this does appear to be consistent. Examples
| /// Converts a relative protobuf type name to a fully qualified one by appending a `.<package>.` prefix, | ||
| /// or if the package is empty, just a `.` prefix. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: It reads a little funny as appending a prefix, maybe:
/// Converts a relative protobuf type name to a fully qualified one by prepending `.<package>.`,
/// or if the package is empty, just `.`.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea, I'll update late tonight. Thanks!
|
|
||
| let expected_message_type = content_type.attributes.get("message"); | ||
|
|
||
| // TODO: what if both the request and response have the same type but different matching rules? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm understanding correctly, the request and response having the same type would imply that message_type receives the same value w/o considering the matching rules.
It's not clear to me in what context this function is invoked. This function takes the expected and actual payloads as input, but is also trying to figure out whether to match against the request or response message of the service method. This leads me to believe that the expected_request may be coming from a contract that is being verified against the provider. Unfortunately this also doesn't seem correct, since the contract would include whether it's the request or response. Could you help me understand what I'm missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this part is tricky. The only place this function is invoked (after my changes) is in the compare_contents flow.
This is different from gRPC testing flow that we're used to.
It's invoked when you do a message provider verification, or apparently you can also verify a grpc interaction too without using gRPC testing flow (which I haven't used myself and have a pretty vague understanding of ) - https://github.com/pactflow/pact-protobuf-plugin?tab=readme-ov-file#testing-a-grpc-service-method-interaction-without-a-grpc-server
Here plugin receives CompareContents request.
This request would contain:
- expected and actual message and matching rules
- contentType string with message type as an attribute
- pact configs with
messageorservicefield, but never both - If it has
messagethis means we're verifying a message pact, if it's aservice, we're comparing either the request being sent to the server or the response received from the server. - so now we know we're comparing the server interaction
- we know the service descriptor and we know the message type we've received too, but we don't know if we're verifying a request message or a response message, it's not specified explicitly
- hence the logic to compare the type, like if the message is smth like GetFeatureRequest, and it matches the request type in the server, we know we're comparing the request. But if the service method is defined as
rpc UpdatePoint (Point) returns (Point) {}, we wouldn't know which one it is we're checking. - a proper fix would be to include extra data in the contentType attributes, something like
application/protobuf;message=.routeguide.Point;flow=[Request|Response], or find a different way to pass data from the consumer
At least that's my understanding so far, but I haven't been able to test this flow properly - but I haven't really changed it much either, apart from using package names there too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, this would not be used by many people. Most would be using a gRPC server for their service calls. However, I know of one company that uses pure Protobufs and not gRPC. But I think it is ok to leave this for now until we have confirmation that it is needed by someone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the detailed explanation!
|
Lots of respect! This is a mammoth effort. |
|
|
||
| let expected_message_type = content_type.attributes.get("message"); | ||
|
|
||
| // TODO: what if both the request and response have the same type but different matching rules? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, this would not be used by many people. Most would be using a gRPC server for their service calls. However, I know of one company that uses pure Protobufs and not gRPC. But I think it is ok to leave this for now until we have confirmation that it is needed by someone.
|
I'm happy to merge this. Let me know when you are ready (I saw some comments about some small changes). |
ermul
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM Thanks for taking this on! My comments are mainly just questions and commentary.
Have you happened to look into whether it's possible to leverage the machinery from libprotobuf or some rust implementation to help w/ resolving references? - Maybe the full AST has some useful methods? 🤷
- This has a high-level Rust API for libprotobuf: https://github.com/MaterializeInc/rust-protobuf-native
- Rust-native protobuf compiler: https://github.com/stepancheg/rust-protobuf
| // current name resolution is dumb - just splits package and message name by a dot | ||
| // but if you have .package.Message.NestedMessage.NestedMessageDeeperLevel this whole structure breaks down | ||
| // because we'll be looking for packages with the name `.package.Message.NestedMessage` | ||
| // while here the package is `.package` and then we need to find message called `Message` and go over it's nested types. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also a little concerned about how we will handle resolving relative references when there are nested packages & messages. This looks non-trivial from the outside after skimming through reference resolution in the protobuf language specification.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not, that's why it's something to look into at a later time. I think the next step would be nested types with fully qualified names, and I don't think we should tackle relative names any time soon - at least currently all the descriptors I've seen had fully-qualified type names filled in, I wonder if this can be adjusted by protoc flags? Because the docs on descriptor types mention a possibility of relative names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if there's a way to confirm that descriptors we get from protoc will always have fully-qualified type names filled in. If they do, this makes our life so much easier, this means all the heavy-lifting of resolving relative references and all we have to do is to figure out if the dot-separated token refers to a package or a type, which is annoying but not too hard to do
| /// Type name format is the same as in `type_name` field in field descriptor | ||
| /// or the `input_type`/`output_type` fields in method descriptor. | ||
| /// | ||
| /// If type name contains a dot ('.') it is a fully qualified name, so it is split into package name and message name; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be "If the type name is prefixed with a dot it is a fully qualified name"?
As I understand it, we're not handling all the possibilities for partially qualified, and unqualified references yet. - Which I think this is fine, because I expect that this already covers the vast majority of wild protos. Please correct me if I'm wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's correct, it's about starting with a dot. The comment is inaccurate, let me send another PR to address this PR comments :)
Nope, and it's a good idea to look into this, thanks! Will def start there if/when I get to resolving nested types. |
This is a really good shout, we should table this as a new issue and anyone is then free to pick it up |
This PR changes how message and service types are looked up in file descriptors.
Previously in most of the places the lookup was simply by the message or service name. This was leading to issues where a wrong proto message was selected for decoding the request or response if multiple messages had the same name, causing test failures.
Now the package is taken into account everywhere and lookup by name function is gone.
This PR introduces new
find_...functions that replacefind_by_namefunctions.New functions take a message or a service name as an input still, but attempt to parse it if the name is fully qualified.
The rules are:
This does not support embedded types yet (these lookup functions haven't supported them before either). Embedded types are partially supported in decode_message, and embedded enums are supported to some extent too, but not searching for message types, input/output messages or service descriptors.
I will send a separate PR to support embedded fields at some point in the future (unless someone beats me to it - it's relatively straightforward, all lookups are encapsulated in a couple of functions in the
utilsfile)With new behavior, we're using fully qualified name in several places in the pact file now:
serviceormessagefield - previously it was just the message or a service name, now it includes the package. This is used to share data between the consumer and provider pacts.messageattribute, this is also sharing data between different plugin workflows (message consumer to message provider or protobuf (non-grpc) service consumer to provider)Because of pact file change, the plugin is backwards compatible (with a caveat for message provider), but not forwards compatible:
servicefield without package and will fallback to the old behavior, searching by name and ignoring package - e.g.RouteGuide/GetFeatureservicewhich old plugin won't be able to parse - e.g..routeguide.RouteGuide/GetFeaturecontentTypedepending on whether the pact is generated by the older plugin or the newer one, like here - if verifying older pact, must bemessage=Feature; if verifying newer pact, must bemessage=.routeguide.Feature.As a side note, it would be good to add some kind of a backwards compatibility test script, which would install an existing version of the plugin, generate some pacts, build and install a new version of the plugin and verify those pacts with the new plugin version. I can look into this too, but I won't have bandwidth in the near future.