1package 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)
13
14// Registry is a registry of information extracted from plugin.CodeGeneratorRequest.
15type Registry struct {
16	// msgs is a mapping from fully-qualified message name to descriptor
17	msgs map[string]*Message
18
19	// enums is a mapping from fully-qualified enum name to descriptor
20	enums map[string]*Enum
21
22	// files is a mapping from file path to descriptor
23	files map[string]*File
24
25	// prefix is a prefix to be inserted to golang package paths generated from proto package names.
26	prefix string
27
28	// pkgMap is a user-specified mapping from file path to proto package.
29	pkgMap map[string]string
30
31	// pkgAliases is a mapping from package aliases to package paths in go which are already taken.
32	pkgAliases map[string]string
33
34	// allowDeleteBody permits http delete methods to have a body
35	allowDeleteBody bool
36}
37
38// NewRegistry returns a new Registry.
39func NewRegistry() *Registry {
40	return &Registry{
41		msgs:       make(map[string]*Message),
42		enums:      make(map[string]*Enum),
43		files:      make(map[string]*File),
44		pkgMap:     make(map[string]string),
45		pkgAliases: make(map[string]string),
46	}
47}
48
49// Load loads definitions of services, methods, messages, enumerations and fields from "req".
50func (r *Registry) Load(req *plugin.CodeGeneratorRequest) error {
51	for _, file := range req.GetProtoFile() {
52		r.loadFile(file)
53	}
54
55	var targetPkg string
56	for _, name := range req.FileToGenerate {
57		target := r.files[name]
58		if target == nil {
59			return fmt.Errorf("no such file: %s", name)
60		}
61		name := packageIdentityName(target.FileDescriptorProto)
62		if targetPkg == "" {
63			targetPkg = name
64		} else {
65			if targetPkg != name {
66				return fmt.Errorf("inconsistent package names: %s %s", targetPkg, name)
67			}
68		}
69
70		if err := r.loadServices(target); err != nil {
71			return err
72		}
73	}
74	return nil
75}
76
77// loadFile loads messages, enumerations and fields from "file".
78// It does not loads services and methods in "file".  You need to call
79// loadServices after loadFiles is called for all files to load services and methods.
80func (r *Registry) loadFile(file *descriptor.FileDescriptorProto) {
81	pkg := GoPackage{
82		Path: r.goPackagePath(file),
83		Name: defaultGoPackageName(file),
84	}
85	if err := r.ReserveGoPackageAlias(pkg.Name, pkg.Path); err != nil {
86		for i := 0; ; i++ {
87			alias := fmt.Sprintf("%s_%d", pkg.Name, i)
88			if err := r.ReserveGoPackageAlias(alias, pkg.Path); err == nil {
89				pkg.Alias = alias
90				break
91			}
92		}
93	}
94	f := &File{
95		FileDescriptorProto: file,
96		GoPkg:               pkg,
97	}
98
99	r.files[file.GetName()] = f
100	r.registerMsg(f, nil, file.GetMessageType())
101	r.registerEnum(f, nil, file.GetEnumType())
102}
103
104func (r *Registry) registerMsg(file *File, outerPath []string, msgs []*descriptor.DescriptorProto) {
105	for i, md := range msgs {
106		m := &Message{
107			File:            file,
108			Outers:          outerPath,
109			DescriptorProto: md,
110			Index:           i,
111		}
112		for _, fd := range md.GetField() {
113			m.Fields = append(m.Fields, &Field{
114				Message:              m,
115				FieldDescriptorProto: fd,
116			})
117		}
118		file.Messages = append(file.Messages, m)
119		r.msgs[m.FQMN()] = m
120		glog.V(1).Infof("register name: %s", m.FQMN())
121
122		var outers []string
123		outers = append(outers, outerPath...)
124		outers = append(outers, m.GetName())
125		r.registerMsg(file, outers, m.GetNestedType())
126		r.registerEnum(file, outers, m.GetEnumType())
127	}
128}
129
130func (r *Registry) registerEnum(file *File, outerPath []string, enums []*descriptor.EnumDescriptorProto) {
131	for i, ed := range enums {
132		e := &Enum{
133			File:                file,
134			Outers:              outerPath,
135			EnumDescriptorProto: ed,
136			Index:               i,
137		}
138		file.Enums = append(file.Enums, e)
139		r.enums[e.FQEN()] = e
140		glog.V(1).Infof("register enum name: %s", e.FQEN())
141	}
142}
143
144// LookupMsg looks up a message type by "name".
145// It tries to resolve "name" from "location" if "name" is a relative message name.
146func (r *Registry) LookupMsg(location, name string) (*Message, error) {
147	glog.V(1).Infof("lookup %s from %s", name, location)
148	if strings.HasPrefix(name, ".") {
149		m, ok := r.msgs[name]
150		if !ok {
151			return nil, fmt.Errorf("no message found: %s", name)
152		}
153		return m, nil
154	}
155
156	if !strings.HasPrefix(location, ".") {
157		location = fmt.Sprintf(".%s", location)
158	}
159	components := strings.Split(location, ".")
160	for len(components) > 0 {
161		fqmn := strings.Join(append(components, name), ".")
162		if m, ok := r.msgs[fqmn]; ok {
163			return m, nil
164		}
165		components = components[:len(components)-1]
166	}
167	return nil, fmt.Errorf("no message found: %s", name)
168}
169
170// LookupEnum looks up a enum type by "name".
171// It tries to resolve "name" from "location" if "name" is a relative enum name.
172func (r *Registry) LookupEnum(location, name string) (*Enum, error) {
173	glog.V(1).Infof("lookup enum %s from %s", name, location)
174	if strings.HasPrefix(name, ".") {
175		e, ok := r.enums[name]
176		if !ok {
177			return nil, fmt.Errorf("no enum found: %s", name)
178		}
179		return e, nil
180	}
181
182	if !strings.HasPrefix(location, ".") {
183		location = fmt.Sprintf(".%s", location)
184	}
185	components := strings.Split(location, ".")
186	for len(components) > 0 {
187		fqen := strings.Join(append(components, name), ".")
188		if e, ok := r.enums[fqen]; ok {
189			return e, nil
190		}
191		components = components[:len(components)-1]
192	}
193	return nil, fmt.Errorf("no enum found: %s", name)
194}
195
196// LookupFile looks up a file by name.
197func (r *Registry) LookupFile(name string) (*File, error) {
198	f, ok := r.files[name]
199	if !ok {
200		return nil, fmt.Errorf("no such file given: %s", name)
201	}
202	return f, nil
203}
204
205// AddPkgMap adds a mapping from a .proto file to proto package name.
206func (r *Registry) AddPkgMap(file, protoPkg string) {
207	r.pkgMap[file] = protoPkg
208}
209
210// SetPrefix registeres the perfix to be added to go package paths generated from proto package names.
211func (r *Registry) SetPrefix(prefix string) {
212	r.prefix = prefix
213}
214
215// ReserveGoPackageAlias reserves the unique alias of go package.
216// If succeeded, the alias will be never used for other packages in generated go files.
217// If failed, the alias is already taken by another package, so you need to use another
218// alias for the package in your go files.
219func (r *Registry) ReserveGoPackageAlias(alias, pkgpath string) error {
220	if taken, ok := r.pkgAliases[alias]; ok {
221		if taken == pkgpath {
222			return nil
223		}
224		return fmt.Errorf("package name %s is already taken. Use another alias", alias)
225	}
226	r.pkgAliases[alias] = pkgpath
227	return nil
228}
229
230// goPackagePath returns the go package path which go files generated from "f" should have.
231// It respects the mapping registered by AddPkgMap if exists. Or use go_package as import path
232// if it includes a slash,  Otherwide, it generates a path from the file name of "f".
233func (r *Registry) goPackagePath(f *descriptor.FileDescriptorProto) string {
234	name := f.GetName()
235	if pkg, ok := r.pkgMap[name]; ok {
236		return path.Join(r.prefix, pkg)
237	}
238
239	gopkg := f.Options.GetGoPackage()
240	idx := strings.LastIndex(gopkg, "/")
241	if idx >= 0 {
242		return gopkg
243	}
244
245	return path.Join(r.prefix, path.Dir(name))
246}
247
248// GetAllFQMNs returns a list of all FQMNs
249func (r *Registry) GetAllFQMNs() []string {
250	var keys []string
251	for k := range r.msgs {
252		keys = append(keys, k)
253	}
254	return keys
255}
256
257// GetAllFQENs returns a list of all FQENs
258func (r *Registry) GetAllFQENs() []string {
259	var keys []string
260	for k := range r.enums {
261		keys = append(keys, k)
262	}
263	return keys
264}
265
266// SetAllowDeleteBody controls whether http delete methods may have a
267// body or fail loading if encountered.
268func (r *Registry) SetAllowDeleteBody(allow bool) {
269	r.allowDeleteBody = allow
270}
271
272// defaultGoPackageName returns the default go package name to be used for go files generated from "f".
273// You might need to use an unique alias for the package when you import it.  Use ReserveGoPackageAlias to get a unique alias.
274func defaultGoPackageName(f *descriptor.FileDescriptorProto) string {
275	name := packageIdentityName(f)
276	return strings.Replace(name, ".", "_", -1)
277}
278
279// packageIdentityName returns the identity of packages.
280// protoc-gen-grpc-gateway rejects CodeGenerationRequests which contains more than one packages
281// as protoc-gen-go does.
282func packageIdentityName(f *descriptor.FileDescriptorProto) string {
283	if f.Options != nil && f.Options.GoPackage != nil {
284		gopkg := f.Options.GetGoPackage()
285		idx := strings.LastIndex(gopkg, "/")
286		if idx < 0 {
287			return gopkg
288		}
289
290		return gopkg[idx+1:]
291	}
292
293	if f.Package == nil {
294		base := filepath.Base(f.GetName())
295		ext := filepath.Ext(base)
296		return strings.TrimSuffix(base, ext)
297	}
298	return f.GetPackage()
299}
300