blob: b73c123c8f9644e021dd11b68585b43972997e30 [file] [log] [blame]
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001package descriptor
2
3import (
4 "fmt"
5 "path"
6 "path/filepath"
7 "strings"
8
9 "github.com/golang/glog"
10 descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor"
11 plugin "github.com/golang/protobuf/protoc-gen-go/plugin"
12 "google.golang.org/genproto/googleapis/api/annotations"
13)
14
15// Registry is a registry of information extracted from plugin.CodeGeneratorRequest.
16type Registry struct {
17 // msgs is a mapping from fully-qualified message name to descriptor
18 msgs map[string]*Message
19
20 // enums is a mapping from fully-qualified enum name to descriptor
21 enums map[string]*Enum
22
23 // files is a mapping from file path to descriptor
24 files map[string]*File
25
26 // prefix is a prefix to be inserted to golang package paths generated from proto package names.
27 prefix string
28
29 // 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.
30 importPath string
31
32 // pkgMap is a user-specified mapping from file path to proto package.
33 pkgMap map[string]string
34
35 // pkgAliases is a mapping from package aliases to package paths in go which are already taken.
36 pkgAliases map[string]string
37
38 // allowDeleteBody permits http delete methods to have a body
39 allowDeleteBody bool
40
41 // externalHttpRules is a mapping from fully qualified service method names to additional HttpRules applicable besides the ones found in annotations.
42 externalHTTPRules map[string][]*annotations.HttpRule
43
44 // allowMerge generation one swagger file out of multiple protos
45 allowMerge bool
46
47 // mergeFileName target swagger file name after merge
48 mergeFileName string
49
50 // allowRepeatedFieldsInBody permits repeated field in body field path of `google.api.http` annotation option
51 allowRepeatedFieldsInBody bool
52
53 // includePackageInTags controls whether the package name defined in the `package` directive
54 // in the proto file can be prepended to the gRPC service name in the `Tags` field of every operation.
55 includePackageInTags bool
56
57 // repeatedPathParamSeparator specifies how path parameter repeated fields are separated
58 repeatedPathParamSeparator repeatedFieldSeparator
59
60 // useJSONNamesForFields if true json tag name is used for generating fields in swagger definitions,
61 // otherwise the original proto name is used. It's helpful for synchronizing the swagger definition
62 // with grpc-gateway response, if it uses json tags for marshaling.
63 useJSONNamesForFields bool
64
65 // useFQNForSwaggerName if true swagger names will use the full qualified name (FQN) from proto definition,
66 // and generate a dot-separated swagger name concatenating all elements from the proto FQN.
67 // If false, the default behavior is to concat the last 2 elements of the FQN if they are unique, otherwise concat
68 // all the elements of the FQN without any separator
69 useFQNForSwaggerName bool
70
71 // allowColonFinalSegments determines whether colons are permitted
72 // in the final segment of a path.
73 allowColonFinalSegments bool
Arjun E K57a7fcb2020-01-30 06:44:45 +000074
75 // useGoTemplate determines whether you want to use GO templates
76 // in your protofile comments
77 useGoTemplate bool
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -070078}
79
80type repeatedFieldSeparator struct {
81 name string
82 sep rune
83}
84
85// NewRegistry returns a new Registry.
86func NewRegistry() *Registry {
87 return &Registry{
88 msgs: make(map[string]*Message),
89 enums: make(map[string]*Enum),
90 files: make(map[string]*File),
91 pkgMap: make(map[string]string),
92 pkgAliases: make(map[string]string),
93 externalHTTPRules: make(map[string][]*annotations.HttpRule),
94 repeatedPathParamSeparator: repeatedFieldSeparator{
95 name: "csv",
96 sep: ',',
97 },
98 }
99}
100
101// Load loads definitions of services, methods, messages, enumerations and fields from "req".
102func (r *Registry) Load(req *plugin.CodeGeneratorRequest) error {
103 for _, file := range req.GetProtoFile() {
104 r.loadFile(file)
105 }
106
107 var targetPkg string
108 for _, name := range req.FileToGenerate {
109 target := r.files[name]
110 if target == nil {
111 return fmt.Errorf("no such file: %s", name)
112 }
113 name := r.packageIdentityName(target.FileDescriptorProto)
114 if targetPkg == "" {
115 targetPkg = name
116 } else {
117 if targetPkg != name {
118 return fmt.Errorf("inconsistent package names: %s %s", targetPkg, name)
119 }
120 }
121
122 if err := r.loadServices(target); err != nil {
123 return err
124 }
125 }
126 return nil
127}
128
129// loadFile loads messages, enumerations and fields from "file".
130// It does not loads services and methods in "file". You need to call
131// loadServices after loadFiles is called for all files to load services and methods.
132func (r *Registry) loadFile(file *descriptor.FileDescriptorProto) {
133 pkg := GoPackage{
134 Path: r.goPackagePath(file),
135 Name: r.defaultGoPackageName(file),
136 }
137 if err := r.ReserveGoPackageAlias(pkg.Name, pkg.Path); err != nil {
138 for i := 0; ; i++ {
139 alias := fmt.Sprintf("%s_%d", pkg.Name, i)
140 if err := r.ReserveGoPackageAlias(alias, pkg.Path); err == nil {
141 pkg.Alias = alias
142 break
143 }
144 }
145 }
146 f := &File{
147 FileDescriptorProto: file,
148 GoPkg: pkg,
149 }
150
151 r.files[file.GetName()] = f
152 r.registerMsg(f, nil, file.GetMessageType())
153 r.registerEnum(f, nil, file.GetEnumType())
154}
155
156func (r *Registry) registerMsg(file *File, outerPath []string, msgs []*descriptor.DescriptorProto) {
157 for i, md := range msgs {
158 m := &Message{
159 File: file,
160 Outers: outerPath,
161 DescriptorProto: md,
162 Index: i,
163 }
164 for _, fd := range md.GetField() {
165 m.Fields = append(m.Fields, &Field{
166 Message: m,
167 FieldDescriptorProto: fd,
168 })
169 }
170 file.Messages = append(file.Messages, m)
171 r.msgs[m.FQMN()] = m
172 glog.V(1).Infof("register name: %s", m.FQMN())
173
174 var outers []string
175 outers = append(outers, outerPath...)
176 outers = append(outers, m.GetName())
177 r.registerMsg(file, outers, m.GetNestedType())
178 r.registerEnum(file, outers, m.GetEnumType())
179 }
180}
181
182func (r *Registry) registerEnum(file *File, outerPath []string, enums []*descriptor.EnumDescriptorProto) {
183 for i, ed := range enums {
184 e := &Enum{
185 File: file,
186 Outers: outerPath,
187 EnumDescriptorProto: ed,
188 Index: i,
189 }
190 file.Enums = append(file.Enums, e)
191 r.enums[e.FQEN()] = e
192 glog.V(1).Infof("register enum name: %s", e.FQEN())
193 }
194}
195
196// LookupMsg looks up a message type by "name".
197// It tries to resolve "name" from "location" if "name" is a relative message name.
198func (r *Registry) LookupMsg(location, name string) (*Message, error) {
199 glog.V(1).Infof("lookup %s from %s", name, location)
200 if strings.HasPrefix(name, ".") {
201 m, ok := r.msgs[name]
202 if !ok {
203 return nil, fmt.Errorf("no message found: %s", name)
204 }
205 return m, nil
206 }
207
208 if !strings.HasPrefix(location, ".") {
209 location = fmt.Sprintf(".%s", location)
210 }
211 components := strings.Split(location, ".")
212 for len(components) > 0 {
213 fqmn := strings.Join(append(components, name), ".")
214 if m, ok := r.msgs[fqmn]; ok {
215 return m, nil
216 }
217 components = components[:len(components)-1]
218 }
219 return nil, fmt.Errorf("no message found: %s", name)
220}
221
222// LookupEnum looks up a enum type by "name".
223// It tries to resolve "name" from "location" if "name" is a relative enum name.
224func (r *Registry) LookupEnum(location, name string) (*Enum, error) {
225 glog.V(1).Infof("lookup enum %s from %s", name, location)
226 if strings.HasPrefix(name, ".") {
227 e, ok := r.enums[name]
228 if !ok {
229 return nil, fmt.Errorf("no enum found: %s", name)
230 }
231 return e, nil
232 }
233
234 if !strings.HasPrefix(location, ".") {
235 location = fmt.Sprintf(".%s", location)
236 }
237 components := strings.Split(location, ".")
238 for len(components) > 0 {
239 fqen := strings.Join(append(components, name), ".")
240 if e, ok := r.enums[fqen]; ok {
241 return e, nil
242 }
243 components = components[:len(components)-1]
244 }
245 return nil, fmt.Errorf("no enum found: %s", name)
246}
247
248// LookupFile looks up a file by name.
249func (r *Registry) LookupFile(name string) (*File, error) {
250 f, ok := r.files[name]
251 if !ok {
252 return nil, fmt.Errorf("no such file given: %s", name)
253 }
254 return f, nil
255}
256
257// LookupExternalHTTPRules looks up external http rules by fully qualified service method name
258func (r *Registry) LookupExternalHTTPRules(qualifiedMethodName string) []*annotations.HttpRule {
259 return r.externalHTTPRules[qualifiedMethodName]
260}
261
262// AddExternalHTTPRule adds an external http rule for the given fully qualified service method name
263func (r *Registry) AddExternalHTTPRule(qualifiedMethodName string, rule *annotations.HttpRule) {
264 r.externalHTTPRules[qualifiedMethodName] = append(r.externalHTTPRules[qualifiedMethodName], rule)
265}
266
267// AddPkgMap adds a mapping from a .proto file to proto package name.
268func (r *Registry) AddPkgMap(file, protoPkg string) {
269 r.pkgMap[file] = protoPkg
270}
271
272// SetPrefix registers the prefix to be added to go package paths generated from proto package names.
273func (r *Registry) SetPrefix(prefix string) {
274 r.prefix = prefix
275}
276
277// SetImportPath registers the importPath which is used as the package if no
278// input files declare go_package. If it contains slashes, everything up to the
279// rightmost slash is ignored.
280func (r *Registry) SetImportPath(importPath string) {
281 r.importPath = importPath
282}
283
284// ReserveGoPackageAlias reserves the unique alias of go package.
285// If succeeded, the alias will be never used for other packages in generated go files.
286// If failed, the alias is already taken by another package, so you need to use another
287// alias for the package in your go files.
288func (r *Registry) ReserveGoPackageAlias(alias, pkgpath string) error {
289 if taken, ok := r.pkgAliases[alias]; ok {
290 if taken == pkgpath {
291 return nil
292 }
293 return fmt.Errorf("package name %s is already taken. Use another alias", alias)
294 }
295 r.pkgAliases[alias] = pkgpath
296 return nil
297}
298
299// goPackagePath returns the go package path which go files generated from "f" should have.
300// It respects the mapping registered by AddPkgMap if exists. Or use go_package as import path
301// if it includes a slash, Otherwide, it generates a path from the file name of "f".
302func (r *Registry) goPackagePath(f *descriptor.FileDescriptorProto) string {
303 name := f.GetName()
304 if pkg, ok := r.pkgMap[name]; ok {
305 return path.Join(r.prefix, pkg)
306 }
307
308 gopkg := f.Options.GetGoPackage()
309 idx := strings.LastIndex(gopkg, "/")
310 if idx >= 0 {
311 if sc := strings.LastIndex(gopkg, ";"); sc > 0 {
312 gopkg = gopkg[:sc+1-1]
313 }
314 return gopkg
315 }
316
317 return path.Join(r.prefix, path.Dir(name))
318}
319
320// GetAllFQMNs returns a list of all FQMNs
321func (r *Registry) GetAllFQMNs() []string {
322 var keys []string
323 for k := range r.msgs {
324 keys = append(keys, k)
325 }
326 return keys
327}
328
329// GetAllFQENs returns a list of all FQENs
330func (r *Registry) GetAllFQENs() []string {
331 var keys []string
332 for k := range r.enums {
333 keys = append(keys, k)
334 }
335 return keys
336}
337
338// SetAllowDeleteBody controls whether http delete methods may have a
339// body or fail loading if encountered.
340func (r *Registry) SetAllowDeleteBody(allow bool) {
341 r.allowDeleteBody = allow
342}
343
344// SetAllowMerge controls whether generation one swagger file out of multiple protos
345func (r *Registry) SetAllowMerge(allow bool) {
346 r.allowMerge = allow
347}
348
349// IsAllowMerge whether generation one swagger file out of multiple protos
350func (r *Registry) IsAllowMerge() bool {
351 return r.allowMerge
352}
353
354// SetMergeFileName controls the target swagger file name out of multiple protos
355func (r *Registry) SetMergeFileName(mergeFileName string) {
356 r.mergeFileName = mergeFileName
357}
358
359// SetAllowRepeatedFieldsInBody controls whether repeated field can be used
360// in `body` and `response_body` (`google.api.http` annotation option) field path or not
361func (r *Registry) SetAllowRepeatedFieldsInBody(allow bool) {
362 r.allowRepeatedFieldsInBody = allow
363}
364
365// IsAllowRepeatedFieldsInBody checks if repeated field can be used
366// in `body` and `response_body` (`google.api.http` annotation option) field path or not
367func (r *Registry) IsAllowRepeatedFieldsInBody() bool {
368 return r.allowRepeatedFieldsInBody
369}
370
371// SetIncludePackageInTags controls whether the package name defined in the `package` directive
372// in the proto file can be prepended to the gRPC service name in the `Tags` field of every operation.
373func (r *Registry) SetIncludePackageInTags(allow bool) {
374 r.includePackageInTags = allow
375}
376
377// IsIncludePackageInTags checks whether the package name defined in the `package` directive
378// in the proto file can be prepended to the gRPC service name in the `Tags` field of every operation.
379func (r *Registry) IsIncludePackageInTags() bool {
380 return r.includePackageInTags
381}
382
383// GetRepeatedPathParamSeparator returns a rune spcifying how
384// path parameter repeated fields are separated.
385func (r *Registry) GetRepeatedPathParamSeparator() rune {
386 return r.repeatedPathParamSeparator.sep
387}
388
389// GetRepeatedPathParamSeparatorName returns the name path parameter repeated
390// fields repeatedFieldSeparator. I.e. 'csv', 'pipe', 'ssv' or 'tsv'
391func (r *Registry) GetRepeatedPathParamSeparatorName() string {
392 return r.repeatedPathParamSeparator.name
393}
394
395// SetRepeatedPathParamSeparator sets how path parameter repeated fields are
396// separated. Allowed names are 'csv', 'pipe', 'ssv' and 'tsv'.
397func (r *Registry) SetRepeatedPathParamSeparator(name string) error {
398 var sep rune
399 switch name {
400 case "csv":
401 sep = ','
402 case "pipes":
403 sep = '|'
404 case "ssv":
405 sep = ' '
406 case "tsv":
407 sep = '\t'
408 default:
409 return fmt.Errorf("unknown repeated path parameter separator: %s", name)
410 }
411 r.repeatedPathParamSeparator = repeatedFieldSeparator{
412 name: name,
413 sep: sep,
414 }
415 return nil
416}
417
418// SetUseJSONNamesForFields sets useJSONNamesForFields
419func (r *Registry) SetUseJSONNamesForFields(use bool) {
420 r.useJSONNamesForFields = use
421}
422
423// GetUseJSONNamesForFields returns useJSONNamesForFields
424func (r *Registry) GetUseJSONNamesForFields() bool {
425 return r.useJSONNamesForFields
426}
427
428// SetUseFQNForSwaggerName sets useFQNForSwaggerName
429func (r *Registry) SetUseFQNForSwaggerName(use bool) {
430 r.useFQNForSwaggerName = use
431}
432
433// GetAllowColonFinalSegments returns allowColonFinalSegments
434func (r *Registry) GetAllowColonFinalSegments() bool {
435 return r.allowColonFinalSegments
436}
437
438// SetAllowColonFinalSegments sets allowColonFinalSegments
439func (r *Registry) SetAllowColonFinalSegments(use bool) {
440 r.allowColonFinalSegments = use
441}
442
443// GetUseFQNForSwaggerName returns useFQNForSwaggerName
444func (r *Registry) GetUseFQNForSwaggerName() bool {
445 return r.useFQNForSwaggerName
446}
447
448// GetMergeFileName return the target merge swagger file name
449func (r *Registry) GetMergeFileName() string {
450 return r.mergeFileName
451}
452
Arjun E K57a7fcb2020-01-30 06:44:45 +0000453// SetUseGoTemplate sets useGoTemplate
454func (r *Registry) SetUseGoTemplate(use bool) {
455 r.useGoTemplate = use
456}
457
458// GetUseGoTemplate returns useGoTemplate
459func (r *Registry) GetUseGoTemplate() bool {
460 return r.useGoTemplate
461}
462
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700463// sanitizePackageName replaces unallowed character in package name
464// with allowed character.
465func sanitizePackageName(pkgName string) string {
466 pkgName = strings.Replace(pkgName, ".", "_", -1)
467 pkgName = strings.Replace(pkgName, "-", "_", -1)
468 return pkgName
469}
470
471// defaultGoPackageName returns the default go package name to be used for go files generated from "f".
472// You might need to use an unique alias for the package when you import it. Use ReserveGoPackageAlias to get a unique alias.
473func (r *Registry) defaultGoPackageName(f *descriptor.FileDescriptorProto) string {
474 name := r.packageIdentityName(f)
475 return sanitizePackageName(name)
476}
477
478// packageIdentityName returns the identity of packages.
479// protoc-gen-grpc-gateway rejects CodeGenerationRequests which contains more than one packages
480// as protoc-gen-go does.
481func (r *Registry) packageIdentityName(f *descriptor.FileDescriptorProto) string {
482 if f.Options != nil && f.Options.GoPackage != nil {
483 gopkg := f.Options.GetGoPackage()
484 idx := strings.LastIndex(gopkg, "/")
485 if idx < 0 {
486 gopkg = gopkg[idx+1:]
487 }
488
489 gopkg = gopkg[idx+1:]
490 // package name is overrided with the string after the
491 // ';' character
492 sc := strings.IndexByte(gopkg, ';')
493 if sc < 0 {
494 return sanitizePackageName(gopkg)
495
496 }
497 return sanitizePackageName(gopkg[sc+1:])
498 }
499 if p := r.importPath; len(p) != 0 {
500 if i := strings.LastIndex(p, "/"); i >= 0 {
501 p = p[i+1:]
502 }
503 return p
504 }
505
506 if f.Package == nil {
507 base := filepath.Base(f.GetName())
508 ext := filepath.Ext(base)
509 return strings.TrimSuffix(base, ext)
510 }
511 return f.GetPackage()
512}