blob: 51d2c764e497165e41aa7776c2d2bb484a9cfd33 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package protogenutil provides support for protoc plugin development with the
// appproto and protogen packages.
package protogenutil
import (
"context"
"fmt"
"path"
"sort"
"strings"
)
import (
"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/pluginpb"
)
import (
"github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/app"
"github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/app/appproto"
)
// NewHandler returns a new appproto.Handler for the protogen.Plugin function.
func NewHandler(f func(*protogen.Plugin) error, options ...HandlerOption) appproto.Handler {
handlerOptions := newHandlerOptions()
for _, option := range options {
option(handlerOptions)
}
return appproto.HandlerFunc(
func(
ctx context.Context,
container app.EnvStderrContainer,
responseWriter appproto.ResponseBuilder,
request *pluginpb.CodeGeneratorRequest,
) error {
plugin, err := protogen.Options{
ParamFunc: handlerOptions.optionHandler,
}.New(request)
if err != nil {
return err
}
if err := f(plugin); err != nil {
plugin.Error(err)
}
response := plugin.Response()
for _, file := range response.File {
if err := responseWriter.AddFile(file); err != nil {
return err
}
}
// plugin.proto specifies that only non-empty errors are considered errors.
// This is also consistent with protoc's behavior.
// Ref: https://github.com/protocolbuffers/protobuf/blob/069f989b483e63005f87ab309de130677718bbec/src/google/protobuf/compiler/plugin.proto#L100-L108.
if response.GetError() != "" {
responseWriter.AddError(response.GetError())
}
responseWriter.SetFeatureProto3Optional()
return nil
},
)
}
// NewFileHandler returns a newHandler for the protogen file function.
//
// This will invoke f with every file marked for generation.
func NewFileHandler(f func(*protogen.Plugin, []*protogen.File) error, options ...HandlerOption) appproto.Handler {
return NewHandler(
func(plugin *protogen.Plugin) error {
generateFiles := make([]*protogen.File, 0, len(plugin.Files))
for _, file := range plugin.Files {
if file.Generate {
generateFiles = append(generateFiles, file)
}
}
sort.Slice(
generateFiles,
func(i int, j int) bool {
return generateFiles[i].Proto.GetName() < generateFiles[j].Proto.GetName()
},
)
return f(plugin, generateFiles)
},
options...,
)
}
// NewPerFileHandler returns a newHandler for the protogen per-file function.
//
// This will invoke f for every file marked for generation.
func NewPerFileHandler(f func(*protogen.Plugin, *protogen.File) error, options ...HandlerOption) appproto.Handler {
return NewFileHandler(
func(plugin *protogen.Plugin, files []*protogen.File) error {
for _, file := range files {
if err := f(plugin, file); err != nil {
return err
}
}
return nil
},
options...,
)
}
// NewGoPackageHandler returns a newHandler for the protogen package function.
//
// This validates that all files marked for generation that would be generated to
// the same directory also have the same go package and go import path.
//
// This will invoke f with every file marked for generation.
func NewGoPackageHandler(f func(*protogen.Plugin, []*GoPackageFileSet) error, options ...HandlerOption) appproto.Handler {
return NewHandler(
func(plugin *protogen.Plugin) error {
generatedDirToGoPackageFileSet := make(map[string]*GoPackageFileSet)
for _, file := range plugin.Files {
if file.Generate {
generatedDir := path.Dir(file.GeneratedFilenamePrefix)
goPackageFileSet, ok := generatedDirToGoPackageFileSet[generatedDir]
if !ok {
generatedDirToGoPackageFileSet[generatedDir] = &GoPackageFileSet{
GeneratedDir: generatedDir,
GoImportPath: file.GoImportPath,
GoPackageName: file.GoPackageName,
ProtoPackage: file.Proto.GetPackage(),
Files: []*protogen.File{file},
}
} else {
if goPackageFileSet.GoImportPath != file.GoImportPath {
return fmt.Errorf(
"mismatched go import paths for generated directory %q: %q %q",
generatedDir,
string(goPackageFileSet.GoImportPath),
string(file.GoImportPath),
)
}
if goPackageFileSet.GoPackageName != file.GoPackageName {
return fmt.Errorf(
"mismatched go package names for generated directory %q: %q %q",
generatedDir,
string(goPackageFileSet.GoPackageName),
string(file.GoPackageName),
)
}
if goPackageFileSet.ProtoPackage != file.Proto.GetPackage() {
return fmt.Errorf(
"mismatched proto package names for generated directory %q: %q %q",
generatedDir,
goPackageFileSet.ProtoPackage,
file.Proto.GetPackage(),
)
}
goPackageFileSet.Files = append(goPackageFileSet.Files, file)
}
}
}
goPackageFileSets := make([]*GoPackageFileSet, 0, len(generatedDirToGoPackageFileSet))
for _, goPackageFileSet := range generatedDirToGoPackageFileSet {
goPackageFileSets = append(goPackageFileSets, goPackageFileSet)
}
sort.Slice(
goPackageFileSets,
func(i int, j int) bool {
return goPackageFileSets[i].ProtoPackage < goPackageFileSets[j].ProtoPackage
},
)
return f(plugin, goPackageFileSets)
},
options...,
)
}
// NewPerGoPackageHandler returns a newHandler for the protogen per-package function.
//
// This validates that all files marked for generation that would be generated to
// the same directory also have the same go package and go import path.
//
// This will invoke f for every file marked for generation.
func NewPerGoPackageHandler(f func(*protogen.Plugin, *GoPackageFileSet) error, options ...HandlerOption) appproto.Handler {
return NewGoPackageHandler(
func(plugin *protogen.Plugin, goPackageFileSets []*GoPackageFileSet) error {
for _, goPackageFileSet := range goPackageFileSets {
if err := f(plugin, goPackageFileSet); err != nil {
return err
}
}
return nil
},
options...,
)
}
// HandlerOption is an option for a new Handler.
type HandlerOption func(*handlerOptions)
// HandlerWithOptionHandler returns a new HandlerOption that sets the given param function.
//
// This parses options given on the command line.
func HandlerWithOptionHandler(optionHandler func(string, string) error) HandlerOption {
return func(handlerOptions *handlerOptions) {
handlerOptions.optionHandler = optionHandler
}
}
// GoPackageFileSet are files within a single Go package.
type GoPackageFileSet struct {
// The directory the golang/protobuf files would be generated to.
GeneratedDir string
// The Go import path the golang/protobuf files would be generated to.
GoImportPath protogen.GoImportPath
// The Go package name the golang/protobuf files would be generated to.
GoPackageName protogen.GoPackageName
// ProtoPackage is the proto package for all files.
ProtoPackage string
// The files within this package that are marked for generate.
Files []*protogen.File
}
// Services returns all the services in this Go package sorted by Go name.
func (g *GoPackageFileSet) Services() []*protogen.Service {
var services []*protogen.Service
for _, file := range g.Files {
services = append(services, file.Services...)
}
sort.Slice(
services,
func(i int, j int) bool {
return services[i].GoName < services[j].GoName
},
)
return services
}
// NamedHelper is a helper to deal with named golang plugins.
//
// Named plugins should be named in the form protoc-gen-go-foobar, where the plugin
// name is consiered to be "foobar". The plugin name must be lowercase.
type NamedHelper interface {
// NewGoPackageName gets the helper GoPackageName for the pluginName.
NewGoPackageName(
baseGoPackageName protogen.GoPackageName,
pluginName string,
) protogen.GoPackageName
// NewGoImportPath gets the helper GoImportPath for the pluginName.
NewGoImportPath(
file *protogen.File,
pluginName string,
) (protogen.GoImportPath, error)
// NewPackageGoImportPath gets the helper GoImportPath for the pluginName.
NewPackageGoImportPath(
goPackageFileSet *GoPackageFileSet,
pluginName string,
) (protogen.GoImportPath, error)
// NewGlobalImportPath gets the helper GoImportPath for the pluginName.
NewGlobalGoImportPath(
pluginName string,
) (protogen.GoImportPath, error)
// NewGeneratedFile returns a new individual GeneratedFile for a named plugin.
//
// This should be used for named plugins that have a 1-1 mapping between Protobuf files
// and generated files.
//
// This also prints the file header and package.
NewGeneratedFile(
plugin *protogen.Plugin,
file *protogen.File,
pluginName string,
) (*protogen.GeneratedFile, error)
// NewPackageGeneratedFile returns a new individual GeneratedFile for a named plugin.
//
// This should be used for named plugins that have a 1-1 mapping between Protobuf files
// and generated files. The generated file name will not overlap with the base name
// of any .proto file in the package.
//
// This also prints the file header and package.
NewPackageGeneratedFile(
plugin *protogen.Plugin,
goPackageFileSet *GoPackageFileSet,
pluginName string,
) (*protogen.GeneratedFile, error)
// NewGlobalGeneratedFile returns a new global GeneratedFile for a named plugin.
//
// This also prints the file header and package.
NewGlobalGeneratedFile(
plugin *protogen.Plugin,
pluginName string,
) (*protogen.GeneratedFile, error)
}
// NewNamedFileHandler returns a new file handler for a named plugin.
func NewNamedFileHandler(f func(NamedHelper, *protogen.Plugin, []*protogen.File) error) appproto.Handler {
namedHelper := newNamedHelper()
return NewFileHandler(
func(plugin *protogen.Plugin, files []*protogen.File) error {
return f(namedHelper, plugin, files)
},
HandlerWithOptionHandler(
namedHelper.handleOption,
),
)
}
// NewNamedPerFileHandler returns a new per-file handler for a named plugin.
func NewNamedPerFileHandler(f func(NamedHelper, *protogen.Plugin, *protogen.File) error) appproto.Handler {
namedHelper := newNamedHelper()
return NewPerFileHandler(
func(plugin *protogen.Plugin, file *protogen.File) error {
return f(namedHelper, plugin, file)
},
HandlerWithOptionHandler(
namedHelper.handleOption,
),
)
}
// NewNamedGoPackageHandler returns a new go package handler for a named plugin.
func NewNamedGoPackageHandler(f func(NamedHelper, *protogen.Plugin, []*GoPackageFileSet) error) appproto.Handler {
namedHelper := newNamedHelper()
return NewGoPackageHandler(
func(plugin *protogen.Plugin, goPackageFileSets []*GoPackageFileSet) error {
return f(namedHelper, plugin, goPackageFileSets)
},
HandlerWithOptionHandler(
namedHelper.handleOption,
),
)
}
// NewNamedPerGoPackageHandler returns a new per-go-package handler for a named plugin.
func NewNamedPerGoPackageHandler(f func(NamedHelper, *protogen.Plugin, *GoPackageFileSet) error) appproto.Handler {
namedHelper := newNamedHelper()
return NewPerGoPackageHandler(
func(plugin *protogen.Plugin, goPackageFileSet *GoPackageFileSet) error {
return f(namedHelper, plugin, goPackageFileSet)
},
HandlerWithOptionHandler(
namedHelper.handleOption,
),
)
}
// ValidateMethodUnary validates that the method is unary.
func ValidateMethodUnary(method *protogen.Method) error {
if method.Desc.IsStreamingClient() || method.Desc.IsStreamingServer() {
return fmt.Errorf("plugin does not allow streaming methods: %v", method.GoName)
}
return nil
}
// ValidateFieldNotOneof validates that the field is not a oneof.
func ValidateFieldNotOneof(field *protogen.Field) error {
if oneof := field.Oneof; oneof != nil && !oneof.Desc.IsSynthetic() {
return fmt.Errorf("plugin does not allow oneofs for request fields: %v", field.GoName)
}
return nil
}
// ValidateFieldNotMap validates that the field is not a map.
func ValidateFieldNotMap(field *protogen.Field) error {
if field.Desc.IsMap() {
return fmt.Errorf("plugin does not allow maps for request fields: %v", field.GoName)
}
return nil
}
// GetUnexportGoName returns a new unexported type for the go name.
//
// This makes the first character lowercase.
// If the goName is empty, this returns empty.
func GetUnexportGoName(goName string) string {
if goName == "" {
return ""
}
return strings.ToLower(goName[:1]) + goName[1:]
}
// GetRequestAndResponseParameterStrings gets the parameters for the given request and response fields.
func GetRequestAndResponseParameterStrings(
generatedFile *protogen.GeneratedFile,
requestFields []*protogen.Field,
responseFields []*protogen.Field,
) (requestParameterStrings []string, responseParameterStrings []string, _ error) {
requestParameterStrings = make([]string, len(requestFields))
responseParameterStrings = make([]string, len(responseFields))
fieldNames := make(map[string]struct{})
for i, field := range requestFields {
if err := ValidateFieldNotOneof(field); err != nil {
return nil, nil, err
}
fieldGoType, err := GetFieldGoType(generatedFile, field)
if err != nil {
return nil, nil, err
}
fieldName := GetUnexportGoName(field.GoName)
fieldNames[fieldName] = struct{}{}
requestParameterStrings[i] = fieldName + ` ` + fieldGoType
}
for i, field := range responseFields {
if err := ValidateFieldNotOneof(field); err != nil {
return nil, nil, err
}
fieldGoType, err := GetFieldGoType(generatedFile, field)
if err != nil {
return nil, nil, err
}
fieldName := GetUnexportGoName(field.GoName)
for {
if _, ok := fieldNames[fieldName]; !ok {
break
}
fieldName = fieldName + "Response"
}
fieldNames[fieldName] = struct{}{}
responseParameterStrings[i] = fieldName + ` ` + fieldGoType
}
return requestParameterStrings, responseParameterStrings, nil
}
// GetParameterErrorReturnString gets the return string for an error for a method.
func GetParameterErrorReturnString(
generatedFile *protogen.GeneratedFile,
fields []*protogen.Field,
errorVarName string,
) (string, error) {
varStrings := make([]string, len(fields)+1)
for i, field := range fields {
if err := ValidateFieldNotOneof(field); err != nil {
return "", err
}
fieldGoZeroValue, err := GetFieldGoZeroValue(generatedFile, field)
if err != nil {
return "", err
}
varStrings[i] = fieldGoZeroValue
}
varStrings[len(varStrings)-1] = errorVarName
return "return " + strings.Join(varStrings, ", "), nil
}
// GetFieldGoType returns the Go type used for a field.
//
// Adapted from https://github.com/protocolbuffers/protobuf-go/blob/81d297c66c9b1e0606eee19a9ee718dcf149276d/cmd/protoc-gen-go/internal_gengo/main.go#L640
// See https://github.com/protocolbuffers/protobuf-go/blob/81d297c66c9b1e0606eee19a9ee718dcf149276d/LICENSE for the license.
func GetFieldGoType(
generatedFile *protogen.GeneratedFile,
field *protogen.Field,
) (string, error) {
if field.Desc.IsWeak() {
return "struct{}", nil
}
var goType string
pointer := field.Desc.HasPresence()
switch field.Desc.Kind() {
case protoreflect.BoolKind:
goType = "bool"
case protoreflect.EnumKind:
goType = generatedFile.QualifiedGoIdent(field.Enum.GoIdent)
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
goType = "int32"
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
goType = "uint32"
case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
goType = "int64"
case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
goType = "uint64"
case protoreflect.FloatKind:
goType = "float32"
case protoreflect.DoubleKind:
goType = "float64"
case protoreflect.StringKind:
goType = "string"
case protoreflect.BytesKind:
goType = "[]byte"
pointer = false // rely on nullability of slices for presence
case protoreflect.MessageKind, protoreflect.GroupKind:
goType = "*" + generatedFile.QualifiedGoIdent(field.Message.GoIdent)
pointer = false // pointer captured as part of the type
default:
return "", fmt.Errorf("unknown Kind: %T", field.Desc.Kind())
}
switch {
case field.Desc.IsList():
return "[]" + goType, nil
case field.Desc.IsMap():
keyType, err := GetFieldGoType(generatedFile, field.Message.Fields[0])
if err != nil {
return "", err
}
valType, err := GetFieldGoType(generatedFile, field.Message.Fields[1])
if err != nil {
return "", err
}
return fmt.Sprintf("map[%v]%v", keyType, valType), nil
}
if pointer {
goType = "*" + goType
}
return goType, nil
}
// GetFieldGoZeroValue returns the go zero value for a field.
func GetFieldGoZeroValue(
generatedFile *protogen.GeneratedFile,
field *protogen.Field,
) (string, error) {
if field.Desc.IsWeak() {
return "struct{}", nil
}
if field.Desc.HasPresence() {
return "nil", nil
}
if field.Desc.IsList() {
return "nil", nil
}
if field.Desc.IsMap() {
return "nil", nil
}
switch field.Desc.Kind() {
case protoreflect.BoolKind:
return "false", nil
case protoreflect.EnumKind:
return generatedFile.QualifiedGoIdent(field.Enum.GoIdent) + "(0)", nil
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
return "0", nil
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
return "0", nil
case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
return "0", nil
case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
return "0", nil
case protoreflect.FloatKind:
return "0", nil
case protoreflect.DoubleKind:
return "0", nil
case protoreflect.StringKind:
return `""`, nil
case protoreflect.BytesKind:
return "nil", nil
case protoreflect.MessageKind, protoreflect.GroupKind:
return "nil", nil
default:
return "", fmt.Errorf("unknown Kind: %T", field.Desc.Kind())
}
}
type handlerOptions struct {
optionHandler func(string, string) error
}
func newHandlerOptions() *handlerOptions {
return &handlerOptions{}
}