-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
issue setting empty values #19
Comments
some (though not much, at the time of this writing) discussion of this on golang-nuts here. After talking with a couple of engineers at Google, I'm leaning toward option 3, and just updating the docs to recommend use of the helper methods in |
ugh. So goprotobuf's Looking at things more closely, I'm pretty sure we really only need the |
So yesterday I went through and updated all of our structs for GitHub resources to use pointers for singular fields, and was getting ready to write another "this makes me sad" commit message. Except that I realized that by doing this, it makes the output of The goprotobuf library handles this by providing a custom For anyone interested on exactly what the generated structs will look like (and more importantly why they do what they do), read the goprotobuf README |
So I've switched I've also gone ahead and merged that into my personal master branch just to see what the generated docs will look like (see here). They're certainly more verbose than what we had before, particularly because of all the new It is customary to have proto files in a separate package ending in "pb" or "proto", so we could move these to The other sad part about moving to protos that I've seen is the lack of support for Given that the library simply doesn't work as-is (see original bug report), we have to do something, and this still seems like the best approach, despite the drawbacks. /cc everyone who has contributed thus far in case anyone wants to weigh in before I move forward with this (@wlynch92, @imjasonh @sqs, @gedex, @stianeikeland, @beyang, @yml, @ktoso, @RC1140), since I'm not sure if you'll see this otherwise. |
Hi Will, When it comes to proto / no proto, I was initially leaning against using protos because as library devs it's always nice to "pull nothing in", then again when looking at the code without protobuf helpers today... "oh, that's pretty ugly". And it's so common anyway that I'd +1 just using protobuf just like you started already. As for the pattern with "the Of course I'll try to help out again as much as I can - but argh, so much travel lately! PS: As for |
I'd guess that most of the users of this library are only reading data from GitHub, not writing/updating data on GitHub. I think that any solution involving pointer fields for non-repeated values would increase the complexity of the API reader use case. What about a 4th approach, where you must be explicit about the fields to include in the server request when writing/updating data? Then reading data from GitHub would remain as-is, and writing/updating might look like this: newRepoInfo := &Repository{
Description: "", // erase description
Homepage: "https://example.com", // set new homepage
}
client.Repositories.Edit("user", "repo", newRepoInfo.WithFields("description", "homepage")) Behind the scenes, The obvious downsides to this approach are that it's ad-hoc (protobuf is a superior general solution) and non-typesafe (the fields would be passed as strings). But it would retain the library's ease of use for API readers and may be simpler overall. Just an idea...the protobuf solution would certainly work for us as well. |
It depends on where in the API it is. Most of the timestamps are returned from GitHub as JSON strings (e.g. There are a couple of other places where times are returned as ints (notably, the new rate.reset), so yes you're absolutely right there... those would be stored in proto format as int64. We could of course have some convenience methods for converting both of these (strings and ints) to |
@willnorris I can help with switching |
@gedex: hold off for now... another update coming soon. |
(continuing to update this bug with my progress, as this may be of use to others, or myself, in the future...) So I'm running into more issues with how we handle Event payloads. Today we decode GitHub's "payload" field into a In essence, using So two options I'll be exploring:
|
@willnorris This maybe irrelevant with current issue, but calling events, _, err := c.Activity.ListEventsPerformedByUser("gedex", true, nil)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for _, e := range events {
if "PushEvent" == e.Type {
ev := e.Payload()
fmt.Printf("%+v\n", ev.Commits) // will panic
}
} Using type assertion: for _, e := range events {
if "PushEvent" == e.Type {
ev := e.Payload().(*github.PushEvent)
fmt.Printf("%+v\n", ev.Commits)
}
} Maybe I used it improperly on first example? |
@gedex yeah, you're right that you still need to do a type assertion. But |
our package docs are too long yet to really need to be in their own file, but I'd like to flesh them out a bit more, particularly once #19 is resolved.
Like a51d6b4, this change makes me sad, mainly because it is a breaking change for all clients, and makes common tasks like reading data out of structs slightly more work, with no direct benefit. Notably, developers will need to make sure and check for nil values before trying to dereference these pointers. Sadly, the change is still necessary, as is more fully explained in issue #19. We can make the nil pointer checks a little easier by adding some Get* funcs like goprotobuf does. I spent a lot of time over the last few weeks exploring this change (switching fields to pointers) versus the much larger change of using protocol buffers for all GitHub data types. While the goprotobuf library is very mature and feature-rich (it's used heavily inside of Google), it's the wrong tool for this task, since we're not actually using the proto wire format. While it does address the immediate concern in #19, it makes way too many other things terribly awkward. One of the biggest drawbacks of this change is that it will make the string output from fmt.Printf("%v") next to useless, since all pointer values are displayed as their memory address. To handle that, I'll be writing a custom String() function for these structs that is heavily inspired by goprotobuf and internals from go's fmt package.
fixed in 3072d06 and 084b5991154b78abe559f04029a66d41a109cbd0. This will break all existing users of the library. Again. 😞 In the end, I decided to use simple pointers for struct fields, and then migrate over the convenience methods from goprotobuf that were helpful for us. I'll open a new bug to look into generating convenience I did really like Quinn's suggestion, but don't want to lose the type safety. We could pass a separate I'm still not completely happy with the final result, but I think it's the least bad option, and this issue has stalled other progress for far too long. If you find new problems this introduces, please open new issues for them. |
our package docs are too long yet to really need to be in their own file, but I'd like to flesh them out a bit more, particularly once google#19 is resolved.
Like a51d6b4, this change makes me sad, mainly because it is a breaking change for all clients, and makes common tasks like reading data out of structs slightly more work, with no direct benefit. Notably, developers will need to make sure and check for nil values before trying to dereference these pointers. Sadly, the change is still necessary, as is more fully explained in issue google#19. We can make the nil pointer checks a little easier by adding some Get* funcs like goprotobuf does. I spent a lot of time over the last few weeks exploring this change (switching fields to pointers) versus the much larger change of using protocol buffers for all GitHub data types. While the goprotobuf library is very mature and feature-rich (it's used heavily inside of Google), it's the wrong tool for this task, since we're not actually using the proto wire format. While it does address the immediate concern in google#19, it makes way too many other things terribly awkward. One of the biggest drawbacks of this change is that it will make the string output from fmt.Printf("%v") next to useless, since all pointer values are displayed as their memory address. To handle that, I'll be writing a custom String() function for these structs that is heavily inspired by goprotobuf and internals from go's fmt package.
Well, this certainly sucks. How about sending a patch upstream, to the JSON marshaller, so we can make the decision about omitting empty fields or not upon every marshaling? omitempty := true
json.Marshal(foo, omitempty) |
I'm not sure I understand what you're suggesting. The issue isn't that we need the flexibility to specify whether empty fields are omitted or not at the time of marshaling. The issue is that for a given (non-pointer) field with a zero value, we don't know if it's the zero value because it was simply initialized that way, or if the developer explicitly set it to that. Pointers remove that ambiguity. If anything, you would need the ability to specify whether empty fields should be omitted on a per-field basis, which is effectively what was suggested above. Given how Go's zero values work, I actually don't know of a better way to handle this, so I don't think there is really anything to be patched upstream. |
good, |
Could you elaborate on this topic ? |
Hi there, sorry for replying to an old issue. I encountered the the same problem as described in this thread. After some investigation, I think there might be a possible workaround for the problem with handling empty values from JSON:
See https://go.dev/play/p/aKDfn4HQLxM for a runnable example. Advantages:
Disadvantages:
What do you think? |
I'm concerned that this might be quite disruptive at this point with so many users of this repo, making a change like this 9 years later. It seems to me that this would be a major retrofit to users of this client library and a completely different style of usage (using the It might make sense to create a fork and try out these ideas and see how things go. |
Thank you for the kind reply! I agree with you that it's unwise to try to change this repo. I have just turned the idea into a little library called filedmask. As you suggested, I plan to try this library in real-world REST APIs and see how it will go. Thanks again! |
Excellent! Feel free to report back here and let us know how the experiment goes. Thanks. |
Depending on how our types are defined, we run into various interesting problems with setting empty values.
Option 1
This is the current behavior throughout the library.
As currently setup, we can't set empty values at all on Create or Update methods. For example, this won't work to remove the description from an existing repository:
That is actually a no-op because the empty Description field gets dropped, since it is currently defined with
omitempty
. Fortunately, there are only a handful of mutable values where the zero value is actually meaningful, most notablybool
values.Option 2
We could then instead drop the
omitempty
, but that has potentially really bad side-effects, particularly on edit methods. Take the above code sample. Withoutomitempty
on any of our fields, this would work as intended. However, it would also have the unintended side-effect of wiping out the repo homepage, [and once we add the additional fields...] making it public, and disabling issues and the wiki for the repo, since all of those fields would be passing their zero values. The solution there is to first callGet
, update the response, and then callEdit
with the updated object:If you forget to follow that flow, you're gonna have a bad time.
Option 3
The third option is to do what goprotobuf does and actually use pointers for all non-repeated fields, since that allows a clear distinction between "unset" (nil) and "empty" (non-nil, zero value). That would result in types like:
This is by far the safest approach, but does make working with the library a bit cumbersome, since creating pointers to primitive types takes a little extra work (multiplied by the number of fields you are setting). For example, the above code sample now becomes:
The
goprotobuf
library makes this a little simpler by providing helper functions for creating pointers to primitives. Using those methods would result in:Additionally, when working with these values, developers will always have to remember to dereference them where appropriate. While this is common for anyone used to working with protocol buffers in go, I'm not sure how unexpected it will be for the general community.
The text was updated successfully, but these errors were encountered: