Skip to content

Commit

Permalink
feat: Registry merging gogo and protov2 file descriptors (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
amaury1093 authored Feb 8, 2023
1 parent f0ad06d commit e7c6f1d
Show file tree
Hide file tree
Showing 62 changed files with 12,371 additions and 11,889 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Improvements

- [#37](https://github.com/cosmos/gogoproto/pull/37) Add `MergedFileDescriptors` and `MergedRegistry` to retrieve a registry with merged file descriptors from both gogo and protoregistry.

### Bug Fixes

- [#34](https://github.com/cosmos/gogoproto/pull/34) Allow empty package name, as per gogo original behavior. Fix regression introduced in v1.4.4
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ go 1.19

require (
github.com/golang/protobuf v1.5.2
github.com/google/go-cmp v0.5.9
golang.org/x/exp v0.0.0-20230131160201-f062dba9d201
google.golang.org/grpc v1.52.3
google.golang.org/protobuf v1.28.1
)

require (
Expand All @@ -14,5 +17,4 @@ require (
golang.org/x/text v0.6.0 // indirect
golang.org/x/tools v0.5.0 // indirect
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 h1:BEABXpNXLEz0WxtA+6CQIz2xkg80e+1zrhWyMcq8VzE=
golang.org/x/exp v0.0.0-20230131160201-f062dba9d201/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
Expand Down
127 changes: 127 additions & 0 deletions proto/merge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package proto

import (
"bytes"
"compress/gzip"
"fmt"
"io"
"strings"

"github.com/google/go-cmp/cmp"
"golang.org/x/exp/slices"
protov2 "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/descriptorpb"
)

// MergedFileDescriptors returns a single FileDescriptorSet containing all the
// file descriptors registered with both gogoproto and protoregistry. When
// merging it also performs the following checks:
// - check that all file descriptors' import paths are correct (i.e. match
// their fully-qualified package name).
// - check that if both gogo and protoregistry import the same file descriptor,
// that these are identical under proto.Equal.
func MergedFileDescriptors() (*descriptorpb.FileDescriptorSet, error) {
fds := &descriptorpb.FileDescriptorSet{}

// load gogo proto file descriptors
gogoFds := AllFileDescriptors()
gogoFdsMap := map[string]*descriptorpb.FileDescriptorProto{}
for _, compressedBz := range gogoFds {
rdr, err := gzip.NewReader(bytes.NewReader(compressedBz))
if err != nil {
return nil, err
}

bz, err := io.ReadAll(rdr)
if err != nil {
return nil, err
}

fd := &descriptorpb.FileDescriptorProto{}
err = protov2.Unmarshal(bz, fd)
if err != nil {
return nil, err
}

err = CheckImportPath(*fd.Name, *fd.Package)
if err != nil {
return nil, err
}

fds.File = append(fds.File, fd)
gogoFdsMap[*fd.Name] = fd
}

// load any protoregistry file descriptors not in gogo
var (
checkImportErr []string
diffErr error
)
protoregistry.GlobalFiles.RangeFiles(func(fileDescriptor protoreflect.FileDescriptor) bool {
fd := protodesc.ToFileDescriptorProto(fileDescriptor)
if fd.Name != nil && fd.Package != nil {
if err := CheckImportPath(*fd.Name, *fd.Package); err != nil {
checkImportErr = append(checkImportErr, err.Error())
}
}

gogoFd, found := gogoFdsMap[fileDescriptor.Path()]
// If we already loaded gogo's file descriptor, compare that the 2
// are strictly equal.
if found {
if !protov2.Equal(gogoFd, fd) {
diff := cmp.Diff(fd, gogoFd, protocmp.Transform())
diffErr = fmt.Errorf("got different file descriptors for %s; %s", *fd.Name, diff)
return false
}
} else {
fds.File = append(fds.File, protodesc.ToFileDescriptorProto(fileDescriptor))
}

return true
})
if len(checkImportErr) > 0 {
return nil, fmt.Errorf("got %d file descriptor import path errors:\n%s", len(checkImportErr), strings.Join(checkImportErr, "\n"))
}
if diffErr != nil {
return nil, diffErr
}

slices.SortFunc(fds.File, func(x, y *descriptorpb.FileDescriptorProto) bool {
return *x.Name < *y.Name
})

return fds, nil
}

// MergedRegistry returns a *protoregistry.Files that acts as a single registry
// which contains all the file descriptors registered with both gogoproto and
// protoregistry.
func MergedRegistry() (*protoregistry.Files, error) {
fds, err := MergedFileDescriptors()
if err != nil {
return nil, err
}

return protodesc.NewFiles(fds)
}

// CheckImportPath checks that the import path of the given file descriptor
// matches its fully qualified package name.
//
// Example:
// Proto file "google/protobuf/descriptor.proto" should be imported
// from OS path ./google/protobuf/descriptor.proto, relatively to a protoc
// path folder (-I flag).
func CheckImportPath(fdName, fdPackage string) error {
expectedPrefix := strings.ReplaceAll(fdPackage, ".", "/") + "/"
if !strings.HasPrefix(fdName, expectedPrefix) {
return fmt.Errorf("file name %s does not start with expected %s; please make sure your folder structure matches the proto files fully-qualified names", fdName, expectedPrefix)
}

return nil
}
45 changes: 17 additions & 28 deletions protobuf/Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
VERSION=3.9.1
URL="https://raw.githubusercontent.com/protocolbuffers/protobuf/v${VERSION}/src/google/protobuf"
# This is the version shipped with google.golang.org/protobuf v1.28.1
VERSION=3.15.3
URL=https://raw.githubusercontent.com/protocolbuffers/protobuf/v${VERSION}/src/google/protobuf

regenerate:

Expand All @@ -18,45 +19,33 @@ regenerate:
google/protobuf/field_mask.proto \
google/protobuf/source_context.proto

mv ../types/google/protobuf/*.pb.go ../types/ || true
rmdir ../types/google/protobuf || true
rmdir ../types/google || true
mv google.golang.org/protobuf/types/known/*/*.pb.go ../types/ || true
rm -rf ../types/google.golang.org || true

# Fix package names, because in gogo they are all in `types`.
cd ../types && sed -i.bak 's/package anypb/package types/' any.pb.go && rm any.pb.go.bak
cd ../types && sed -i.bak 's/package apipb/package types/' api.pb.go && rm api.pb.go.bak
cd ../types && sed -i.bak 's/package durationpb/package types/' duration.pb.go && rm duration.pb.go.bak
cd ../types && sed -i.bak 's/package emptypb/package types/' empty.pb.go && rm empty.pb.go.bak
cd ../types && sed -i.bak 's/package fieldmaskpb/package types/' field_mask.pb.go && rm field_mask.pb.go.bak
cd ../types && sed -i.bak 's/package sourcecontextpb/package types/' source_context.pb.go && rm source_context.pb.go.bak
cd ../types && sed -i.bak 's/package structpb/package types/' struct.pb.go && rm struct.pb.go.bak
cd ../types && sed -i.bak 's/package timestamppb/package types/' timestamp.pb.go && rm timestamp.pb.go.bak
cd ../types && sed -i.bak 's/package typepb/package types/' type.pb.go && rm type.pb.go.bak
cd ../types && sed -i.bak 's/package wrapperspb/package types/' wrappers.pb.go && rm wrappers.pb.go.bak

update:

(cd ./google/protobuf && rm descriptor.proto; wget ${URL}/descriptor.proto)
# gogoprotobuf requires users to import gogo.proto which imports descriptor.proto
# The descriptor.proto is only compatible with proto3 just because of the reserved keyword.
# We remove it to stay compatible with previous versions of protoc before proto3
gogoreplace 'reserved 38;' '//reserved 38;' ./google/protobuf/descriptor.proto
gogoreplace 'reserved 8;' '//reserved 8;' ./google/protobuf/descriptor.proto
gogoreplace 'reserved 9;' '//reserved 9;' ./google/protobuf/descriptor.proto
gogoreplace 'reserved 4;' '//reserved 4;' ./google/protobuf/descriptor.proto
gogoreplace 'reserved 5;' '//reserved 5;' ./google/protobuf/descriptor.proto
gogoreplace 'option go_package = "github.com/golang/protobuf/protoc-gen-go/descriptor;descriptor";' 'option go_package = "descriptor";' ./google/protobuf/descriptor.proto

(cd ./google/protobuf/compiler && rm plugin.proto; wget ${URL}/compiler/plugin.proto)
gogoreplace 'option go_package = "github.com/golang/protobuf/protoc-gen-go/plugin;plugin_go";' 'option go_package = "plugin_go";' ./google/protobuf/compiler/plugin.proto

(cd ./google/protobuf && rm any.proto; wget ${URL}/any.proto)
gogoreplace 'go_package = "github.com/golang/protobuf/ptypes/any";' 'go_package = "types";' ./google/protobuf/any.proto
(cd ./google/protobuf && rm empty.proto; wget ${URL}/empty.proto)
gogoreplace 'go_package = "github.com/golang/protobuf/ptypes/empty";' 'go_package = "types";' ./google/protobuf/empty.proto
(cd ./google/protobuf && rm timestamp.proto; wget ${URL}/timestamp.proto)
gogoreplace 'go_package = "github.com/golang/protobuf/ptypes/timestamp";' 'go_package = "types";' ./google/protobuf/timestamp.proto
(cd ./google/protobuf && rm duration.proto; wget ${URL}/duration.proto)
gogoreplace 'go_package = "github.com/golang/protobuf/ptypes/duration";' 'go_package = "types";' ./google/protobuf/duration.proto
(cd ./google/protobuf && rm struct.proto; wget ${URL}/struct.proto)
gogoreplace 'go_package = "github.com/golang/protobuf/ptypes/struct;structpb";' 'go_package = "types";' ./google/protobuf/struct.proto
(cd ./google/protobuf && rm wrappers.proto; wget ${URL}/wrappers.proto)
gogoreplace 'go_package = "github.com/golang/protobuf/ptypes/wrappers";' 'go_package = "types";' ./google/protobuf/wrappers.proto
(cd ./google/protobuf && rm field_mask.proto; wget ${URL}/field_mask.proto)
gogoreplace 'option go_package = "google.golang.org/genproto/protobuf/field_mask;field_mask";' 'option go_package = "types";' ./google/protobuf/field_mask.proto
(cd ./google/protobuf && rm api.proto; wget ${URL}/api.proto)
gogoreplace 'option go_package = "google.golang.org/genproto/protobuf/api;api";' 'option go_package = "types";' ./google/protobuf/api.proto
(cd ./google/protobuf && rm type.proto; wget ${URL}/type.proto)
gogoreplace 'option go_package = "google.golang.org/genproto/protobuf/ptype;ptype";' 'option go_package = "types";' ./google/protobuf/type.proto
(cd ./google/protobuf && rm source_context.proto; wget ${URL}/source_context.proto)
gogoreplace 'option go_package = "google.golang.org/genproto/protobuf/source_context;source_context";' 'option go_package = "types";' ./google/protobuf/source_context.proto


9 changes: 6 additions & 3 deletions protobuf/google/protobuf/any.proto
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ syntax = "proto3";
package google.protobuf;

option csharp_namespace = "Google.Protobuf.WellKnownTypes";
option go_package = "types";
option go_package = "google.golang.org/protobuf/types/known/anypb";
option java_package = "com.google.protobuf";
option java_outer_classname = "AnyProto";
option java_multiple_files = true;
Expand Down Expand Up @@ -77,10 +77,13 @@ option objc_class_prefix = "GPB";
// Example 4: Pack and unpack a message in Go
//
// foo := &pb.Foo{...}
// any, err := ptypes.MarshalAny(foo)
// any, err := anypb.New(foo)
// if err != nil {
// ...
// }
// ...
// foo := &pb.Foo{}
// if err := ptypes.UnmarshalAny(any, foo); err != nil {
// if err := any.UnmarshalTo(foo); err != nil {
// ...
// }
//
Expand Down
6 changes: 2 additions & 4 deletions protobuf/google/protobuf/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ option java_package = "com.google.protobuf";
option java_outer_classname = "ApiProto";
option java_multiple_files = true;
option objc_class_prefix = "GPB";
option go_package = "types";
option go_package = "google.golang.org/protobuf/types/known/apipb";

// Api is a light-weight descriptor for an API Interface.
//
Expand All @@ -52,7 +52,6 @@ option go_package = "types";
// this message itself. See https://cloud.google.com/apis/design/glossary for
// detailed terminology.
message Api {

// The fully qualified name of this interface, including package name
// followed by the interface's simple name.
string name = 1;
Expand Down Expand Up @@ -99,7 +98,6 @@ message Api {

// Method represents a method of an API interface.
message Method {

// The simple name of this method.
string name = 1;

Expand Down Expand Up @@ -169,7 +167,7 @@ message Method {
// The mixin construct implies that all methods in `AccessControl` are
// also declared with same name and request/response types in
// `Storage`. A documentation generator or annotation processor will
// see the effective `Storage.GetAcl` method after inherting
// see the effective `Storage.GetAcl` method after inheriting
// documentation and annotations as follows:
//
// service Storage {
Expand Down
17 changes: 16 additions & 1 deletion protobuf/google/protobuf/compiler/plugin.proto
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ package google.protobuf.compiler;
option java_package = "com.google.protobuf.compiler";
option java_outer_classname = "PluginProtos";

option go_package = "plugin_go";
option go_package = "google.golang.org/protobuf/types/pluginpb";

import "google/protobuf/descriptor.proto";

Expand Down Expand Up @@ -107,6 +107,16 @@ message CodeGeneratorResponse {
// exiting with a non-zero status code.
optional string error = 1;

// A bitmask of supported features that the code generator supports.
// This is a bitwise "or" of values from the Feature enum.
optional uint64 supported_features = 2;

// Sync with code_generator.h.
enum Feature {
FEATURE_NONE = 0;
FEATURE_PROTO3_OPTIONAL = 1;
}

// Represents a single generated file.
message File {
// The file name, relative to the output directory. The name must not
Expand Down Expand Up @@ -163,6 +173,11 @@ message CodeGeneratorResponse {

// The file contents.
optional string content = 15;

// Information describing the file content being inserted. If an insertion
// point is used, this information will be appropriately offset and inserted
// into the code generation metadata for the generated files.
optional GeneratedCodeInfo generated_code_info = 16;
}
repeated File file = 15;
}
Loading

0 comments on commit e7c6f1d

Please sign in to comment.