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

Fix auto generated fieldmask case in patch requests #1049

Merged
merged 12 commits into from
Sep 27, 2019
Merged

Fix auto generated fieldmask case in patch requests #1049

merged 12 commits into from
Sep 27, 2019

Conversation

william-plano-oxb
Copy link
Contributor

Fix up tests to check expected behaviour
Fix up documentation to match new behaviour

Fix up tests to check expected behaviour
Fix up documentation to match new behaviour
func (s *_ABitOfEverythingServer) PatchWithFieldMaskInBody(ctx context.Context, request *examples.UpdateV2Request) (*empty.Empty, error) {
// low-effort attempt to modify the field mask to only include paths for the ABE struct. Since this is only for the
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This method could be removed entirely and the rest endpoint annotation in the proto file added an additional binding to the UpdateV2 method. But it's perhaps useful for documentation to have it separate. 🤷‍♂

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it'd be nicer to remove this and show that it's possible to use an additional binding instead. Would you mind changing it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure thing, I was very much on-the-fence about whether to keep it.

Copy link
Collaborator

@johanbrandhorst johanbrandhorst 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 a great PR - thanks for all the comments.

Something I'm curious about, what happens if someone uses non-standard casing in their protobuf field names. Will using camelCase for your fields produce camelCased field masks?

func (s *_ABitOfEverythingServer) PatchWithFieldMaskInBody(ctx context.Context, request *examples.UpdateV2Request) (*empty.Empty, error) {
// low-effort attempt to modify the field mask to only include paths for the ABE struct. Since this is only for the
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it'd be nicer to remove this and show that it's possible to use an additional binding instead. Would you mind changing it?

@johanbrandhorst
Copy link
Collaborator

The bazel failure is real btw: https://circleci.com/gh/grpc-ecosystem/grpc-gateway/4409. We'll need to run something to re-generate the bazel files I think (@achew22, @drigz?).

@william-plano-oxb
Copy link
Contributor Author

This is a great PR - thanks for all the comments.

Thanks 😄

Something I'm curious about, what happens if someone uses non-standard casing in their protobuf field names. Will using camelCase for your fields produce camelCased field masks?

The intention is that the field masks are taken from the json keys in the request as-is. So as long as the user passes camelCase names matching the protobuf field names in the request, it should result in camelCase field masks too. That said, this seems like a reasonable thing to add a test for.

@william-plano-oxb
Copy link
Contributor Author

The bazel failure is real btw: https://circleci.com/gh/grpc-ecosystem/grpc-gateway/4409. We'll need to run something to re-generate the bazel files I think (@achew22, @drigz?).

I did have a look at that, but I've not used Bazel before and I wasn't sure what it was telling me (though there is a command in the error output that I'll try running locally and see if it makes any changes).
Oddly the diff that it seems to be complaining about doesn't look like a diff at all, maybe it's a line ending thing?

@drigz
Copy link
Contributor

drigz commented Sep 25, 2019

Oddly the diff that it seems to be complaining about doesn't look like a diff at all, maybe it's a line ending thing?

You should be able to run the command or manually apply the diff, eg:

patch -p0 << EOF
--- examples/server/BUILD.bazel	1970-01-01 00:00:00.000000000 +0000
+++ examples/server/BUILD.bazel	1970-01-01 00:00:00.000000000 +0000
@@ -20,6 +20,7 @@
         "//examples/proto/sub2:go_default_library",
         "@com_github_golang_glog//:go_default_library",
         "@com_github_golang_protobuf//proto:go_default_library",
+        "@com_github_golang_protobuf//protoc-gen-go/generator:go_default_library_gen",
         "@com_github_rogpeppe_fastuuid//:go_default_library",
         "@go_googleapis//google/rpc:errdetails_go_proto",
         "@io_bazel_rules_go//proto/wkt:duration_go_proto",
--- runtime/BUILD.bazel	1970-01-01 00:00:00.000000000 +0000
+++ runtime/BUILD.bazel	1970-01-01 00:00:00.000000000 +0000
@@ -29,7 +29,6 @@
         "//utilities:go_default_library",
         "@com_github_golang_protobuf//jsonpb:go_default_library_gen",
         "@com_github_golang_protobuf//proto:go_default_library",
-        "@com_github_golang_protobuf//protoc-gen-go/generator:go_default_library_gen",
         "@go_googleapis//google/api:httpbody_go_proto",
         "@io_bazel_rules_go//proto/wkt:any_go_proto",
         "@io_bazel_rules_go//proto/wkt:duration_go_proto",
EOF

The diff reflects the fact the changes in the import "github.com/golang/protobuf/protoc-gen-go/generator" lines.

@johanbrandhorst
Copy link
Collaborator

Something I'm curious about, what happens if someone uses non-standard casing in their protobuf field names. Will using camelCase for your fields produce camelCased field masks?

The intention is that the field masks are taken from the json keys in the request as-is. So as long as the user passes camelCase names matching the protobuf field names in the request, it should result in camelCase field masks too. That said, this seems like a reasonable thing to add a test for.

Hmm, I wonder how this will interact with the JSON marshaler OrigName setting, and if we need to do something about it. A proto field's JSON representation can be field_name or fieldName depending on the marshaller setting. Could we add a test for the latter being transformed correctly as well?

@william-plano-oxb
Copy link
Contributor Author

The diff reflects the fact the changes in the import "github.com/golang/protobuf/protoc-gen-go/generator" lines

🤦‍♂ Of course, it's 2 diffs. I was just reading it wrong.

@william-plano-oxb
Copy link
Contributor Author

Could we add a test for the latter being transformed correctly as well?

Will do, fingers crossed it "just works"™️

@william-plano-oxb
Copy link
Contributor Author

Could we add a test for the latter being transformed correctly as well?

Will do, fingers crossed it "just works"™️

Unfortunately, if somewhat predictably, it doesn't work very well. In the simple cases it works just fine, however there a few edge cases that my testing has picked up where JSON's camelCasing and protoc-gen-go/generator's camelCasing disagree, for a concrete example "foo_Bar" becomes "fooBar" with the former, but remains "foo_Bar" with the latter.

The more serious issue is however that if you specify [json_name] options in the proto file, then all bets are off since you can completely rename things rather than just changing the case.

I'll have to have a bit of a think about how to sort this out.

@johanbrandhorst
Copy link
Collaborator

Unfortunately, if somewhat predictably, it doesn't work very well. In the simple cases it works just fine, however there a few edge cases that my testing has picked up where JSON's camelCasing and protoc-gen-go/generator's camelCasing disagree, for a concrete example "foo_Bar" becomes "fooBar" with the former, but remains "foo_Bar" with the latter.

The more serious issue is however that if you specify [json_name] options in the proto file, then all bets are off since you can completely rename things rather than just changing the case.

I'll have to have a bit of a think about how to sort this out.

I don't think we're going to be able to support all the cases, and that's okay. It's possible for users to turn off the FieldMask implementation, and for the vast majority of cases it should work. I think this fix is better than what we have at the moment as it should be closer to the source of truth (the proto field name). I'm OK with these shortcomings as long as we document them, of course.

@codecov-io
Copy link

codecov-io commented Sep 26, 2019

Codecov Report

Merging #1049 into master will increase coverage by 0.1%.
The diff coverage is 10%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1049      +/-   ##
==========================================
+ Coverage   53.86%   53.97%   +0.11%     
==========================================
  Files          40       41       +1     
  Lines        4127     4126       -1     
==========================================
+ Hits         2223     2227       +4     
+ Misses       1661     1657       -4     
+ Partials      243      242       -1
Impacted Files Coverage Δ
protoc-gen-grpc-gateway/gengateway/template.go 80.8% <ø> (+15.15%) ⬆️
examples/server/a_bit_of_everything.go 0% <0%> (ø) ⬆️
examples/server/non_standard_names.go 0% <0%> (ø)
examples/server/main.go 0% <0%> (ø) ⬆️
runtime/fieldmask.go 94.11% <100%> (-2.32%) ⬇️
examples/server/fieldmask_helper.go 92% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6527435...84008e0. Read the comment docs.

@william-plano-oxb
Copy link
Contributor Author

Okay, sorry, I went down a bit of a rabbit hole trying to support the json names. I have left that out of this PR for now.
I do have something that works, but it's not very clean. Shall I check it in as a separate PR on my fork?

Copy link
Collaborator

@johanbrandhorst johanbrandhorst left a comment

Choose a reason for hiding this comment

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

Could we add another section to the _docs/patch.md documenting the supported json <-> proto field name mappings? Just something like "If you define custom JSON names, you're SOL with regards to the automatic PATCH feature".

@johanbrandhorst
Copy link
Collaborator

Okay, sorry, I went down a bit of a rabbit hole trying to support the json names. I have left that out of this PR for now.
I do have something that works, but it's not very clean. Shall I check it in as a separate PR on my fork?

Hm, no, go ahead and check it into this one. I'm curious now :). This is already one of the best PRs I've reviewed so the anticipation is great!

@william-plano-oxb
Copy link
Contributor Author

Hm, no, go ahead and check it into this one. I'm curious now :). This is already one of the best PRs I've reviewed so the anticipation is great!

I'm a bit reluctant to mess this PR up if we decide not to take the change though. I guess we could always revert it with a force push to my branch, not sure if that would break the world though.

@johanbrandhorst
Copy link
Collaborator

Hm, no, go ahead and check it into this one. I'm curious now :). This is already one of the best PRs I've reviewed so the anticipation is great!

I'm a bit reluctant to mess this PR up if we decide not to take the change though. I guess we could always revert it with a force push to my branch, not sure if that would break the world though.

Alright fine, lets leave it out for now and open a separate PR.

@william-plano-oxb
Copy link
Contributor Author

Alright fine, lets leave it out for now and open a separate PR.

Okay, all synced up with master now, and I've created https://github.com/william-plano-oxb/grpc-gateway/pull/1 on my fork with my attempt at getting the json names supported.

@johanbrandhorst
Copy link
Collaborator

Could we add another section to the _docs/patch.md documenting the supported json <-> proto field name mappings? Just something like "If you define custom JSON names, you're SOL with regards to the automatic PATCH feature".

Cool, as I said in the other PR, it LGTM, but we can still merge it separately. I'd be happy to merge this after ^ has been addressed.

@william-plano-oxb
Copy link
Contributor Author

Hmm, as I was fixing up the documentation for this I've noticed something that didn't seem right: The second case where we say

  • The FieldMask is exposed to the REST request (as in the PatchWithFieldMaskInBody example). For this case, a check is made as to whether the FieldMask is nil/empty prior to populating with the request body

Isn't true at all, we just don't do anything for this case. I've checked the code before I started poking at it, and all we did then was the case translation, it didn't take anything from the Body of the message. And I have confirmed this by modifying the TestABEPatchBody test to add an extra field on the originalValues that isn't in the input values, in the case of nil/empty field mask we're just falling through to the full Update/PUT semantics.

The question is; what do we want to do here? Shall I just fix up the docs, or modify the code to support this case properly? (And if the latter, should that be in another separate PR?)

@william-plano-oxb
Copy link
Contributor Author

william-plano-oxb commented Sep 27, 2019

In fact, I'm not sure how we would know what the resource is called in that case. Maybe we can just say the request body can only have 2 fields, and one of them must be the field_mask so the resource is necessarily the other one?

@johanbrandhorst
Copy link
Collaborator

johanbrandhorst commented Sep 27, 2019

In fact, I'm not sure how we would know what the resource is called in that case. Maybe we can just say the request body can only have 2 fields, and one of them must be the field_mask so the resource is necessarily the other one?

Yeah I think a prerequisite of using this feature is to design your PATCH requests following the Google API design convention (https://cloud.google.com/apis/design/standard_methods#update). I don't particularly care about the fieldmask-in-body request since then you can also just not use this feature, so we can remove it I think.

@william-plano-oxb
Copy link
Contributor Author

following the Google API design convention

  • The request message field containing the resource must map to the request body.

I don't think I realised it was this strict, seems fair to exclude this case then. I'll fix up the docs while I'm there.

Add benchmark for patching ABE
for i := 0; i < b.N; i++ {
testABEPatch(b)
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hm, this benchmark actually fires requests at a real server? The cost of the fieldmask function is surely going to be lost in the variance of I/O cost? Could we make the benchmark run the transformation directly or is this due to the circular dependency problem?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you're right, let me have a go at aiming this more directly at the function in question.

william-plano-oxb and others added 3 commits September 27, 2019 13:29
Co-Authored-By: Johan Brandhorst <johan.brandhorst@gmail.com>
and just measure the FieldMaskFromRequestBody method
Fix typos (again)

Co-Authored-By: Johan Brandhorst <johan.brandhorst@gmail.com>
Copy link
Collaborator

@johanbrandhorst johanbrandhorst left a comment

Choose a reason for hiding this comment

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

Fantastic, thanks for this brilliant PR and for being such a great contributor! I'm beyond impressed. I hope we can work together again in the future :).

@johanbrandhorst johanbrandhorst merged commit a8afee1 into grpc-ecosystem:master Sep 27, 2019
@william-plano-oxb
Copy link
Contributor Author

Thank you for being so responsive to the contribution. It's a real pleasure to help out with a great project like this.


// CamelCaseFieldMask updates the given FieldMask by converting all of its paths to CamelCase, using the same heuristic
// that's used for naming protobuf fields in Go.
func CamelCaseFieldMask(mask *field_mask.FieldMask) {

Choose a reason for hiding this comment

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

This func being removed, and subsequently released as a patch release (v1.11.3), broke semantic versioning guarantees (our code no longer builds simply by updating from v1.11.2 to v1.11.3). Could you please avoid doing this in the future?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Terribly sorry @RJPercival, you're right about this breaking users using a generator from version v1.11.2 and a runtime library with version v1.11.3. However, we do always encourage our users to ensure their generator versions and runtime library versions match up, so this shouldn't be a problem for users that can upgrade both at the same time. Sorry for the inconvenience caused.

Choose a reason for hiding this comment

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

I don't think this can be guaranteed with Go Modules. You may import a dependency that used an older version of the generator and Go Modules will happily use a newer runtime library, so long as it's at the same major version, since that's supposed to mean it's backwards compatible.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This can (and should) be guaranteed by go modules. Define a tool dependency for the generator, and run go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway before invoking protoc.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree that it's not ideal though, and I apologize for the inconvenience caused.

Choose a reason for hiding this comment

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

I don't think that guarantees what you think it guarantees. We already define a tool dependency, and have grpc-gateway listed in our go.mod file with a pre-v1.11.3 version number. However, according to the version selection rules of Go Modules, anyone can import our module and require a newer v1 version of grpc-gateway in their go.mod file and it'll use that newer version (because it's supposed to be backwards-compatible), resulting in a build error. They then have to figure out that v1.11.3 of grpc-gateway broke them and pin to an earlier version.

Thanks for the apology. I don't mean to labour the point, but I just want to make it clear that violating semantic versioning rules will break your users and so we'd very much appreciate you avoiding it, thanks! 🙂

adasari pushed a commit to adasari/grpc-gateway that referenced this pull request Apr 9, 2020
)

* Fix fieldmask case
Fix up tests to check expected behaviour
Fix up documentation to match new behaviour

* Remove PatchWithFieldMaskInBody
Replaced with an additional binding to UpdateV2

* Regenerated code

* `bazel run :gazelle`

* Added tests for proto file with non snake_case field names

* tabs -> spaces

* Update patch documentation
Add benchmark for patching ABE

* Apply suggestions from code review

Co-Authored-By: Johan Brandhorst <johan.brandhorst@gmail.com>

* Move benchmark to fieldmask_test
and just measure the FieldMaskFromRequestBody method

* Apply suggestions from code review

Fix typos (again)

Co-Authored-By: Johan Brandhorst <johan.brandhorst@gmail.com>

* Add more representative benchmark
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants