package descriptor import ( "fmt" "path" "path/filepath" "strings" "github.com/golang/glog" descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" plugin "github.com/golang/protobuf/protoc-gen-go/plugin" "google.golang.org/genproto/googleapis/api/annotations" ) // Registry is a registry of information extracted from plugin.CodeGeneratorRequest. type Registry struct { // msgs is a mapping from fully-qualified message name to descriptor msgs map[string]*Message // enums is a mapping from fully-qualified enum name to descriptor enums map[string]*Enum // files is a mapping from file path to descriptor files map[string]*File // prefix is a prefix to be inserted to golang package paths generated from proto package names. prefix string // importPath is used as the package if no input files declare go_package. If it contains slashes, everything up to the rightmost slash is ignored. importPath string // pkgMap is a user-specified mapping from file path to proto package. pkgMap map[string]string // pkgAliases is a mapping from package aliases to package paths in go which are already taken. pkgAliases map[string]string // allowDeleteBody permits http delete methods to have a body allowDeleteBody bool // externalHttpRules is a mapping from fully qualified service method names to additional HttpRules applicable besides the ones found in annotations. externalHTTPRules map[string][]*annotations.HttpRule // allowMerge generation one swagger file out of multiple protos allowMerge bool // mergeFileName target swagger file name after merge mergeFileName string // allowRepeatedFieldsInBody permits repeated field in body field path of `google.api.http` annotation option allowRepeatedFieldsInBody bool // includePackageInTags controls whether the package name defined in the `package` directive // in the proto file can be prepended to the gRPC service name in the `Tags` field of every operation. includePackageInTags bool // repeatedPathParamSeparator specifies how path parameter repeated fields are separated repeatedPathParamSeparator repeatedFieldSeparator // useJSONNamesForFields if true json tag name is used for generating fields in swagger definitions, // otherwise the original proto name is used. It's helpful for synchronizing the swagger definition // with grpc-gateway response, if it uses json tags for marshaling. useJSONNamesForFields bool // useFQNForSwaggerName if true swagger names will use the full qualified name (FQN) from proto definition, // and generate a dot-separated swagger name concatenating all elements from the proto FQN. // If false, the default behavior is to concat the last 2 elements of the FQN if they are unique, otherwise concat // all the elements of the FQN without any separator useFQNForSwaggerName bool // allowColonFinalSegments determines whether colons are permitted // in the final segment of a path. allowColonFinalSegments bool } type repeatedFieldSeparator struct { name string sep rune } // NewRegistry returns a new Registry. func NewRegistry() *Registry { return &Registry{ msgs: make(map[string]*Message), enums: make(map[string]*Enum), files: make(map[string]*File), pkgMap: make(map[string]string), pkgAliases: make(map[string]string), externalHTTPRules: make(map[string][]*annotations.HttpRule), repeatedPathParamSeparator: repeatedFieldSeparator{ name: "csv", sep: ',', }, } } // Load loads definitions of services, methods, messages, enumerations and fields from "req". func (r *Registry) Load(req *plugin.CodeGeneratorRequest) error { for _, file := range req.GetProtoFile() { r.loadFile(file) } var targetPkg string for _, name := range req.FileToGenerate { target := r.files[name] if target == nil { return fmt.Errorf("no such file: %s", name) } name := r.packageIdentityName(target.FileDescriptorProto) if targetPkg == "" { targetPkg = name } else { if targetPkg != name { return fmt.Errorf("inconsistent package names: %s %s", targetPkg, name) } } if err := r.loadServices(target); err != nil { return err } } return nil } // loadFile loads messages, enumerations and fields from "file". // It does not loads services and methods in "file". You need to call // loadServices after loadFiles is called for all files to load services and methods. func (r *Registry) loadFile(file *descriptor.FileDescriptorProto) { pkg := GoPackage{ Path: r.goPackagePath(file), Name: r.defaultGoPackageName(file), } if err := r.ReserveGoPackageAlias(pkg.Name, pkg.Path); err != nil { for i := 0; ; i++ { alias := fmt.Sprintf("%s_%d", pkg.Name, i) if err := r.ReserveGoPackageAlias(alias, pkg.Path); err == nil { pkg.Alias = alias break } } } f := &File{ FileDescriptorProto: file, GoPkg: pkg, } r.files[file.GetName()] = f r.registerMsg(f, nil, file.GetMessageType()) r.registerEnum(f, nil, file.GetEnumType()) } func (r *Registry) registerMsg(file *File, outerPath []string, msgs []*descriptor.DescriptorProto) { for i, md := range msgs { m := &Message{ File: file, Outers: outerPath, DescriptorProto: md, Index: i, } for _, fd := range md.GetField() { m.Fields = append(m.Fields, &Field{ Message: m, FieldDescriptorProto: fd, }) } file.Messages = append(file.Messages, m) r.msgs[m.FQMN()] = m glog.V(1).Infof("register name: %s", m.FQMN()) var outers []string outers = append(outers, outerPath...) outers = append(outers, m.GetName()) r.registerMsg(file, outers, m.GetNestedType()) r.registerEnum(file, outers, m.GetEnumType()) } } func (r *Registry) registerEnum(file *File, outerPath []string, enums []*descriptor.EnumDescriptorProto) { for i, ed := range enums { e := &Enum{ File: file, Outers: outerPath, EnumDescriptorProto: ed, Index: i, } file.Enums = append(file.Enums, e) r.enums[e.FQEN()] = e glog.V(1).Infof("register enum name: %s", e.FQEN()) } } // LookupMsg looks up a message type by "name". // It tries to resolve "name" from "location" if "name" is a relative message name. func (r *Registry) LookupMsg(location, name string) (*Message, error) { glog.V(1).Infof("lookup %s from %s", name, location) if strings.HasPrefix(name, ".") { m, ok := r.msgs[name] if !ok { return nil, fmt.Errorf("no message found: %s", name) } return m, nil } if !strings.HasPrefix(location, ".") { location = fmt.Sprintf(".%s", location) } components := strings.Split(location, ".") for len(components) > 0 { fqmn := strings.Join(append(components, name), ".") if m, ok := r.msgs[fqmn]; ok { return m, nil } components = components[:len(components)-1] } return nil, fmt.Errorf("no message found: %s", name) } // LookupEnum looks up a enum type by "name". // It tries to resolve "name" from "location" if "name" is a relative enum name. func (r *Registry) LookupEnum(location, name string) (*Enum, error) { glog.V(1).Infof("lookup enum %s from %s", name, location) if strings.HasPrefix(name, ".") { e, ok := r.enums[name] if !ok { return nil, fmt.Errorf("no enum found: %s", name) } return e, nil } if !strings.HasPrefix(location, ".") { location = fmt.Sprintf(".%s", location) } components := strings.Split(location, ".") for len(components) > 0 { fqen := strings.Join(append(components, name), ".") if e, ok := r.enums[fqen]; ok { return e, nil } components = components[:len(components)-1] } return nil, fmt.Errorf("no enum found: %s", name) } // LookupFile looks up a file by name. func (r *Registry) LookupFile(name string) (*File, error) { f, ok := r.files[name] if !ok { return nil, fmt.Errorf("no such file given: %s", name) } return f, nil } // LookupExternalHTTPRules looks up external http rules by fully qualified service method name func (r *Registry) LookupExternalHTTPRules(qualifiedMethodName string) []*annotations.HttpRule { return r.externalHTTPRules[qualifiedMethodName] } // AddExternalHTTPRule adds an external http rule for the given fully qualified service method name func (r *Registry) AddExternalHTTPRule(qualifiedMethodName string, rule *annotations.HttpRule) { r.externalHTTPRules[qualifiedMethodName] = append(r.externalHTTPRules[qualifiedMethodName], rule) } // AddPkgMap adds a mapping from a .proto file to proto package name. func (r *Registry) AddPkgMap(file, protoPkg string) { r.pkgMap[file] = protoPkg } // SetPrefix registers the prefix to be added to go package paths generated from proto package names. func (r *Registry) SetPrefix(prefix string) { r.prefix = prefix } // SetImportPath registers the importPath which is used as the package if no // input files declare go_package. If it contains slashes, everything up to the // rightmost slash is ignored. func (r *Registry) SetImportPath(importPath string) { r.importPath = importPath } // ReserveGoPackageAlias reserves the unique alias of go package. // If succeeded, the alias will be never used for other packages in generated go files. // If failed, the alias is already taken by another package, so you need to use another // alias for the package in your go files. func (r *Registry) ReserveGoPackageAlias(alias, pkgpath string) error { if taken, ok := r.pkgAliases[alias]; ok { if taken == pkgpath { return nil } return fmt.Errorf("package name %s is already taken. Use another alias", alias) } r.pkgAliases[alias] = pkgpath return nil } // goPackagePath returns the go package path which go files generated from "f" should have. // It respects the mapping registered by AddPkgMap if exists. Or use go_package as import path // if it includes a slash, Otherwide, it generates a path from the file name of "f". func (r *Registry) goPackagePath(f *descriptor.FileDescriptorProto) string { name := f.GetName() if pkg, ok := r.pkgMap[name]; ok { return path.Join(r.prefix, pkg) } gopkg := f.Options.GetGoPackage() idx := strings.LastIndex(gopkg, "/") if idx >= 0 { if sc := strings.LastIndex(gopkg, ";"); sc > 0 { gopkg = gopkg[:sc+1-1] } return gopkg } return path.Join(r.prefix, path.Dir(name)) } // GetAllFQMNs returns a list of all FQMNs func (r *Registry) GetAllFQMNs() []string { var keys []string for k := range r.msgs { keys = append(keys, k) } return keys } // GetAllFQENs returns a list of all FQENs func (r *Registry) GetAllFQENs() []string { var keys []string for k := range r.enums { keys = append(keys, k) } return keys } // SetAllowDeleteBody controls whether http delete methods may have a // body or fail loading if encountered. func (r *Registry) SetAllowDeleteBody(allow bool) { r.allowDeleteBody = allow } // SetAllowMerge controls whether generation one swagger file out of multiple protos func (r *Registry) SetAllowMerge(allow bool) { r.allowMerge = allow } // IsAllowMerge whether generation one swagger file out of multiple protos func (r *Registry) IsAllowMerge() bool { return r.allowMerge } // SetMergeFileName controls the target swagger file name out of multiple protos func (r *Registry) SetMergeFileName(mergeFileName string) { r.mergeFileName = mergeFileName } // SetAllowRepeatedFieldsInBody controls whether repeated field can be used // in `body` and `response_body` (`google.api.http` annotation option) field path or not func (r *Registry) SetAllowRepeatedFieldsInBody(allow bool) { r.allowRepeatedFieldsInBody = allow } // IsAllowRepeatedFieldsInBody checks if repeated field can be used // in `body` and `response_body` (`google.api.http` annotation option) field path or not func (r *Registry) IsAllowRepeatedFieldsInBody() bool { return r.allowRepeatedFieldsInBody } // SetIncludePackageInTags controls whether the package name defined in the `package` directive // in the proto file can be prepended to the gRPC service name in the `Tags` field of every operation. func (r *Registry) SetIncludePackageInTags(allow bool) { r.includePackageInTags = allow } // IsIncludePackageInTags checks whether the package name defined in the `package` directive // in the proto file can be prepended to the gRPC service name in the `Tags` field of every operation. func (r *Registry) IsIncludePackageInTags() bool { return r.includePackageInTags } // GetRepeatedPathParamSeparator returns a rune spcifying how // path parameter repeated fields are separated. func (r *Registry) GetRepeatedPathParamSeparator() rune { return r.repeatedPathParamSeparator.sep } // GetRepeatedPathParamSeparatorName returns the name path parameter repeated // fields repeatedFieldSeparator. I.e. 'csv', 'pipe', 'ssv' or 'tsv' func (r *Registry) GetRepeatedPathParamSeparatorName() string { return r.repeatedPathParamSeparator.name } // SetRepeatedPathParamSeparator sets how path parameter repeated fields are // separated. Allowed names are 'csv', 'pipe', 'ssv' and 'tsv'. func (r *Registry) SetRepeatedPathParamSeparator(name string) error { var sep rune switch name { case "csv": sep = ',' case "pipes": sep = '|' case "ssv": sep = ' ' case "tsv": sep = '\t' default: return fmt.Errorf("unknown repeated path parameter separator: %s", name) } r.repeatedPathParamSeparator = repeatedFieldSeparator{ name: name, sep: sep, } return nil } // SetUseJSONNamesForFields sets useJSONNamesForFields func (r *Registry) SetUseJSONNamesForFields(use bool) { r.useJSONNamesForFields = use } // GetUseJSONNamesForFields returns useJSONNamesForFields func (r *Registry) GetUseJSONNamesForFields() bool { return r.useJSONNamesForFields } // SetUseFQNForSwaggerName sets useFQNForSwaggerName func (r *Registry) SetUseFQNForSwaggerName(use bool) { r.useFQNForSwaggerName = use } // GetAllowColonFinalSegments returns allowColonFinalSegments func (r *Registry) GetAllowColonFinalSegments() bool { return r.allowColonFinalSegments } // SetAllowColonFinalSegments sets allowColonFinalSegments func (r *Registry) SetAllowColonFinalSegments(use bool) { r.allowColonFinalSegments = use } // GetUseFQNForSwaggerName returns useFQNForSwaggerName func (r *Registry) GetUseFQNForSwaggerName() bool { return r.useFQNForSwaggerName } // GetMergeFileName return the target merge swagger file name func (r *Registry) GetMergeFileName() string { return r.mergeFileName } // sanitizePackageName replaces unallowed character in package name // with allowed character. func sanitizePackageName(pkgName string) string { pkgName = strings.Replace(pkgName, ".", "_", -1) pkgName = strings.Replace(pkgName, "-", "_", -1) return pkgName } // defaultGoPackageName returns the default go package name to be used for go files generated from "f". // You might need to use an unique alias for the package when you import it. Use ReserveGoPackageAlias to get a unique alias. func (r *Registry) defaultGoPackageName(f *descriptor.FileDescriptorProto) string { name := r.packageIdentityName(f) return sanitizePackageName(name) } // packageIdentityName returns the identity of packages. // protoc-gen-grpc-gateway rejects CodeGenerationRequests which contains more than one packages // as protoc-gen-go does. func (r *Registry) packageIdentityName(f *descriptor.FileDescriptorProto) string { if f.Options != nil && f.Options.GoPackage != nil { gopkg := f.Options.GetGoPackage() idx := strings.LastIndex(gopkg, "/") if idx < 0 { gopkg = gopkg[idx+1:] } gopkg = gopkg[idx+1:] // package name is overrided with the string after the // ';' character sc := strings.IndexByte(gopkg, ';') if sc < 0 { return sanitizePackageName(gopkg) } return sanitizePackageName(gopkg[sc+1:]) } if p := r.importPath; len(p) != 0 { if i := strings.LastIndex(p, "/"); i >= 0 { p = p[i+1:] } return p } if f.Package == nil { base := filepath.Base(f.GetName()) ext := filepath.Ext(base) return strings.TrimSuffix(base, ext) } return f.GetPackage() }