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

global options for enums and ID field names #22

Closed
wants to merge 5 commits into from

Conversation

Green7
Copy link

@Green7 Green7 commented Dec 5, 2020

Two global options for .proto files

  1. option (go.enum_prefix_all) = true;

When set to true, values in enum types do not have a prefix with the type name.
For example if we have in .proto:

enum TestEnum { VALUE_A = 0; }

we will get in go: VALUE_A instead of TestEnum_VALUE_A

This option works like goproto_enum_prefix_all with gogoprotobuf Extensions.

  1. option (go.fix_id_field_names) = true

If this option is set, ID field names are converted to uppercase. So in go we have for example fields: ID, CustomerID instead of Id, CustomerId.
This option works like customnameid.go from https://github.com/containerd/containerd/tree/master/cmd/protoc-gen-gogoctrd

Copy link
Contributor

@ydnar ydnar left a comment

Choose a reason for hiding this comment

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

Thanks! Reviewing on a phone while watching my kid, so here goes:

  • This should be two separate PRs for each feature.
  • See comments above for specific feedback.

patch/go.proto Outdated
Comment on lines 52 to 57

extend google.protobuf.FileOptions {
optional bool enum_prefix_all = 7001;

optional bool fix_id_field_names = 7002;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Stylistically, protopatch uses option messages per extended thing. It doesn't currently have a concept of a global or file-level options, but I imagine they'd be like go.file = ....

patch/options.go Outdated
@@ -26,3 +27,19 @@ func fieldOptions(f *protogen.Field) *gopb.Options {
func oneofOptions(o *protogen.Oneof) *gopb.Options {
return proto.GetExtension(o.Desc.Options(), gopb.E_Oneof).(*gopb.Options)
}

func getBoolExtension(pb proto.Message, extension protoreflect.ExtensionType, ifnotset bool) bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

Negative assertions with Boolean fields should be prefixed or marked as such. This helper should be inverted.

patch/options.go Outdated
return getBoolExtension(f.Proto.Options, gopb.E_EnumPrefixAll, false)
}

func fileFixIDFieldNamesOption(f *protogen.File) bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

While I agree that Id should be ID, it shouldn't be special cased.

My thought for this was to have a default, possibly extensible, list of initialisms like ID, HTTP, MIME, etc. Then couple this with message or file-level options to fix them automatically.

Copy link
Author

@Green7 Green7 Dec 6, 2020

Choose a reason for hiding this comment

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

@ydnar Thank you for the quick review.
I improved a few things:

  1. Global options for file. Example:
option (go.file) = {
    disable_enum_prefixes: true,
    fix_initialisms: true
 };
  1. I added a function to check field names and a list of commonInitialisms. This code is from golang linter.

  2. I added the ability to set custom initialisms list:

option (go.file) = {
    fix_initialisms: true,
    initialisms: ["ID", "WHATEVER"]    
 };

What do you think about these changes?

Copy link
Contributor

@ydnar ydnar left a comment

Choose a reason for hiding this comment

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

Thanks for the updates—this is getting closer. A few

patch/go.proto Outdated
Comment on lines 56 to 57
extend google.protobuf.OneofOptions {
optional Options oneof = 7001;
Copy link
Contributor

Choose a reason for hiding this comment

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

Trailing whitespace here. Can you run clang-format on the proto files? It should be automatic if you use VS Code and have the recommended extensions installed.

Copy link
Author

Choose a reason for hiding this comment

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

Which extension do you mean? I use VC code and vscode-proto3 extension. And it formats it differently, the above lines are replaced by:
extend google.protobuf.MessageOptions { optional Options message = 7001; }
(everything is in one line)

patch/go.proto Outdated
Comment on lines 33 to 46
// File-level options
message FileOptions {
// The disable_enum_prefixes option disables adding a prefix with a type name for an enum value.
optional bool disable_enum_prefixes = 1;

// The fix_initialisms option changes message field names to the golang naming convention.
// So for example the field named in proto file "id" would be named "ID" in go.
optional bool fix_initialisms = 2;

// The initialisms option contains the list of initialisms used if the fix_initialisms option is enabled.
// Initialisms should be in uppercase. This list may be empty, in this case the default golint initialisms
// list will be used.
repeated string initialisms = 3;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I’d imagined this as a nested set of options, so one could do something like this:

option (go.initialisms) = 'ID';
option (go.initialisms) = 'URL';
option (go.values) = {
    prefix: ''
};

I pushed a spike design here: https://github.com/alta/protopatch/blob/ydnar/file-options-design/patch/go.proto

Copy link
Contributor

Choose a reason for hiding this comment

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

Additionally, it would not have a default list of initialisms from golint, but explicitly work off the initialisms you specify in the file.

Copy link
Author

@Green7 Green7 Dec 7, 2020

Choose a reason for hiding this comment

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

I’d imagined this as a nested set of options, so one could do something like this:

It is possible now. You can write:

option (go.file) = {
    fix_initialisms: true,
    initialisms: ["ID", "URL"]    
};

or:

option (go.file).fix_initialisms = true;
option (go.file).initialisms = "ID";
option (go.file).initialisms = "URL";

Additionally, it would not have a default list of initialisms from golint, but explicitly work off the initialisms you specify in the file.

Do you want to drop the default initialism list entirely?
I think the default initialisms list is a good idea. Why? Because by enabling one option, we get a file compatible with go lint. This is hard to do by adding initialisms one by one: go lint has about 40 initialisms ...
And if you don't want to use the default list, just enter your own: then the default list is omitted. This is how the current version works.

Copy link
Contributor

Choose a reason for hiding this comment

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

The default initialisms list, while convenient, isn’t exhaustive. A single proto file will have a handful of these initialisms, and I think it’s better to have each file define what exactly it wants rather than depend on a predefined list of English-centric initialisms.

I think we should favor clarity over brevity.

Note in my branch I skipped go.file in favor of go.messages, go.enums, etc. to illustrate where each change would apply, to all messages, fields, enums, values, and oneofs.

The one issue with this is that having a top-level FileOptions extension initialisms means that this isn’t possible:

option (go.initialisms) = ["ID", "URL"];

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess we could nest our own FileOptions as a single extension to FileOptions, with messages, enums, fields, values, oneofs, so this would be possible:

option (go.file).values = {
    prefix: ''
}

Copy link
Author

Choose a reason for hiding this comment

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

The default initialisms list, while convenient, isn’t exhaustive. A single proto file will have a handful of these initialisms, and I think it’s better to have each file define what exactly it wants rather than depend on a predefined list of English-centric initialisms.

I think we should favor clarity over brevity.

Ok, but what if we have many proto files in the project? I have about 30 proto files in my small project. So in each file I have to define a list of the same initialisms. It is not very convenient to maintenance. How to solve it? Global option for all patched files?

Copy link
Contributor

Choose a reason for hiding this comment

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

How many initialisms does each proto file use?

Copy link
Author

Choose a reason for hiding this comment

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

In my case, not so much, probably max 3.

Copy link
Author

Choose a reason for hiding this comment

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

@ydnar Can you look on last commit ? Is it closer to your idea?

Copy link
Contributor

@ydnar ydnar left a comment

Choose a reason for hiding this comment

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

Getting there.

patch/patcher.go Outdated
@@ -178,7 +194,15 @@ func (p *Patcher) scanField(f *protogen.Field) {
if newName == "" && o != nil && (p.isRenamed(m.GoIdent) || p.isRenamed(o.GoIdent)) {
// Implicitly rename this oneof field because its parent(s) were renamed.
newName = f.GoName
} else {
if len(p.fileInitialisms) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thinking this should run on all names, even if renamed, so it can fix nested oneof names.

)

// lintName returns a different name if it should be different.
func lintName(name string, commonInitialisms map[string]bool) (should string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
func lintName(name string, commonInitialisms map[string]bool) (should string) {
func lintName(name string, initialisms map[string]bool) (should string) {

return string(runes)
}

// commonInitialisms is a set of common initialisms.
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be removed.

patch/patcher.go Outdated
@@ -622,3 +646,19 @@ func typeString(obj types.Object) string {
}
return obj.Type().String()
}

func newIDFieldName(fieldName string) string {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be removed.

patch/patcher.go Outdated
Comment on lines 49 to 51
fileFieldOptions *gopb.Options
fileValuesOptions *gopb.Options
fileInitialisms map[string]bool
Copy link
Contributor

Choose a reason for hiding this comment

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

These are by file, but stored here globally. One file can clobber another file’s data.

I’d prefer to see these fetched on-demand at the point of use, perhaps as a fallback if a given thing doesn’t have more specific options, it can look at the file for global options.

Copy link
Author

Choose a reason for hiding this comment

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

Please check the latest version. I have a problem with this last fix. I did fileFieldOptions,fileValuesOptions and fileInitialisms global because is hard to feetch this data at the point of use. In functions like scanEnum we don't have access to protogen.File. We would have to pass protogen.File (or fileFieldOptions etc). to the functions: scanEnum, scanMessage, scanExtension and then to scanEnumValue, scanField, etc.
Do you have a better idea?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it’s possible to walk up to the parent file from the protoreflect.Descriptor interface’s ParentFile method.

I’m not sure what the ideal interface (or helper interface is) to get it, but I think it should first examine the thing (say a protogen.Field) for options, and then look at its Desc.ParentFile for a global option.

This was referenced Dec 25, 2020
@ydnar
Copy link
Contributor

ydnar commented Jan 4, 2021

Closing in favor of #37.

@ydnar ydnar closed this Jan 4, 2021
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.

2 participants