blob: 1b9e14d545ff635cba5ddfb2dd01f66a0d9c50fc [file] [log] [blame]
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001package genswagger
2
3import (
Arjun E K57a7fcb2020-01-30 06:44:45 +00004 "bytes"
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07005 "encoding/json"
6 "fmt"
Arjun E K57a7fcb2020-01-30 06:44:45 +00007 "io/ioutil"
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07008 "os"
9 "reflect"
10 "regexp"
11 "sort"
12 "strconv"
13 "strings"
14 "sync"
Arjun E K57a7fcb2020-01-30 06:44:45 +000015 "text/template"
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -070016
17 "github.com/golang/glog"
18 "github.com/golang/protobuf/jsonpb"
19 "github.com/golang/protobuf/proto"
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -070020 pbdescriptor "github.com/golang/protobuf/protoc-gen-go/descriptor"
21 gogen "github.com/golang/protobuf/protoc-gen-go/generator"
Arjun E K57a7fcb2020-01-30 06:44:45 +000022 structpb "github.com/golang/protobuf/ptypes/struct"
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -070023 "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway/descriptor"
24 swagger_options "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options"
25)
26
27var wktSchemas = map[string]schemaCore{
28 ".google.protobuf.Timestamp": schemaCore{
29 Type: "string",
30 Format: "date-time",
31 },
32 ".google.protobuf.Duration": schemaCore{
33 Type: "string",
34 },
35 ".google.protobuf.StringValue": schemaCore{
36 Type: "string",
37 },
38 ".google.protobuf.BytesValue": schemaCore{
39 Type: "string",
40 Format: "byte",
41 },
42 ".google.protobuf.Int32Value": schemaCore{
43 Type: "integer",
44 Format: "int32",
45 },
46 ".google.protobuf.UInt32Value": schemaCore{
47 Type: "integer",
48 Format: "int64",
49 },
50 ".google.protobuf.Int64Value": schemaCore{
51 Type: "string",
52 Format: "int64",
53 },
54 ".google.protobuf.UInt64Value": schemaCore{
55 Type: "string",
56 Format: "uint64",
57 },
58 ".google.protobuf.FloatValue": schemaCore{
59 Type: "number",
60 Format: "float",
61 },
62 ".google.protobuf.DoubleValue": schemaCore{
63 Type: "number",
64 Format: "double",
65 },
66 ".google.protobuf.BoolValue": schemaCore{
67 Type: "boolean",
68 Format: "boolean",
69 },
70 ".google.protobuf.Empty": schemaCore{},
71 ".google.protobuf.Struct": schemaCore{
72 Type: "object",
73 },
74 ".google.protobuf.Value": schemaCore{
75 Type: "object",
76 },
77 ".google.protobuf.ListValue": schemaCore{
78 Type: "array",
79 Items: (*swaggerItemsObject)(&schemaCore{
80 Type: "object",
81 }),
82 },
83 ".google.protobuf.NullValue": schemaCore{
84 Type: "string",
85 },
86}
87
88func listEnumNames(enum *descriptor.Enum) (names []string) {
89 for _, value := range enum.GetValue() {
90 names = append(names, value.GetName())
91 }
92 return names
93}
94
95func getEnumDefault(enum *descriptor.Enum) string {
96 for _, value := range enum.GetValue() {
97 if value.GetNumber() == 0 {
98 return value.GetName()
99 }
100 }
101 return ""
102}
103
104// messageToQueryParameters converts a message to a list of swagger query parameters.
105func messageToQueryParameters(message *descriptor.Message, reg *descriptor.Registry, pathParams []descriptor.Parameter) (params []swaggerParameterObject, err error) {
106 for _, field := range message.Fields {
107 p, err := queryParams(message, field, "", reg, pathParams)
108 if err != nil {
109 return nil, err
110 }
111 params = append(params, p...)
112 }
113 return params, nil
114}
115
116// queryParams converts a field to a list of swagger query parameters recursively.
117func queryParams(message *descriptor.Message, field *descriptor.Field, prefix string, reg *descriptor.Registry, pathParams []descriptor.Parameter) (params []swaggerParameterObject, err error) {
118 // make sure the parameter is not already listed as a path parameter
119 for _, pathParam := range pathParams {
120 if pathParam.Target == field {
121 return nil, nil
122 }
123 }
124 schema := schemaOfField(field, reg, nil)
125 fieldType := field.GetTypeName()
126 if message.File != nil {
127 comments := fieldProtoComments(reg, message, field)
Arjun E K57a7fcb2020-01-30 06:44:45 +0000128 if err := updateSwaggerDataFromComments(reg, &schema, message, comments, false); err != nil {
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700129 return nil, err
130 }
131 }
132
133 isEnum := field.GetType() == pbdescriptor.FieldDescriptorProto_TYPE_ENUM
134 items := schema.Items
135 if schema.Type != "" || isEnum {
136 if schema.Type == "object" {
137 return nil, nil // TODO: currently, mapping object in query parameter is not supported
138 }
139 if items != nil && (items.Type == "" || items.Type == "object") && !isEnum {
140 return nil, nil // TODO: currently, mapping object in query parameter is not supported
141 }
142 desc := schema.Description
143 if schema.Title != "" { // merge title because title of parameter object will be ignored
144 desc = strings.TrimSpace(schema.Title + ". " + schema.Description)
145 }
146
147 // verify if the field is required
148 required := false
149 for _, fieldName := range schema.Required {
150 if fieldName == field.GetName() {
151 required = true
152 break
153 }
154 }
155
156 param := swaggerParameterObject{
157 Description: desc,
158 In: "query",
159 Default: schema.Default,
160 Type: schema.Type,
161 Items: schema.Items,
162 Format: schema.Format,
163 Required: required,
164 }
165 if param.Type == "array" {
166 param.CollectionFormat = "multi"
167 }
168
169 if reg.GetUseJSONNamesForFields() {
170 param.Name = prefix + field.GetJsonName()
171 } else {
172 param.Name = prefix + field.GetName()
173 }
174
175 if isEnum {
176 enum, err := reg.LookupEnum("", fieldType)
177 if err != nil {
178 return nil, fmt.Errorf("unknown enum type %s", fieldType)
179 }
180 if items != nil { // array
181 param.Items = &swaggerItemsObject{
182 Type: "string",
183 Enum: listEnumNames(enum),
184 }
185 } else {
186 param.Type = "string"
187 param.Enum = listEnumNames(enum)
188 param.Default = getEnumDefault(enum)
189 }
190 valueComments := enumValueProtoComments(reg, enum)
191 if valueComments != "" {
192 param.Description = strings.TrimLeft(param.Description+"\n\n "+valueComments, "\n")
193 }
194 }
195 return []swaggerParameterObject{param}, nil
196 }
197
198 // nested type, recurse
199 msg, err := reg.LookupMsg("", fieldType)
200 if err != nil {
201 return nil, fmt.Errorf("unknown message type %s", fieldType)
202 }
203 for _, nestedField := range msg.Fields {
204 p, err := queryParams(msg, nestedField, prefix+field.GetName()+".", reg, pathParams)
205 if err != nil {
206 return nil, err
207 }
208 params = append(params, p...)
209 }
210 return params, nil
211}
212
213// findServicesMessagesAndEnumerations discovers all messages and enums defined in the RPC methods of the service.
214func findServicesMessagesAndEnumerations(s []*descriptor.Service, reg *descriptor.Registry, m messageMap, ms messageMap, e enumMap, refs refMap) {
215 for _, svc := range s {
216 for _, meth := range svc.Methods {
217 // Request may be fully included in query
218 if _, ok := refs[fmt.Sprintf("#/definitions/%s", fullyQualifiedNameToSwaggerName(meth.RequestType.FQMN(), reg))]; ok {
219 if !skipRenderingRef(meth.RequestType.FQMN()) {
220 m[fullyQualifiedNameToSwaggerName(meth.RequestType.FQMN(), reg)] = meth.RequestType
221 }
222 }
223 findNestedMessagesAndEnumerations(meth.RequestType, reg, m, e)
224
225 if !skipRenderingRef(meth.ResponseType.FQMN()) {
226 m[fullyQualifiedNameToSwaggerName(meth.ResponseType.FQMN(), reg)] = meth.ResponseType
227 if meth.GetServerStreaming() {
228 runtimeStreamError := fullyQualifiedNameToSwaggerName(".grpc.gateway.runtime.StreamError", reg)
229 glog.V(1).Infof("StreamError FQMN: %s", runtimeStreamError)
230 streamError, err := reg.LookupMsg(".grpc.gateway.runtime", "StreamError")
231 if err == nil {
232 glog.V(1).Infof("StreamError: %v", streamError)
233 m[runtimeStreamError] = streamError
234 findNestedMessagesAndEnumerations(streamError, reg, m, e)
235 } else {
236 //just in case there is an error looking up StreamError
237 glog.Error(err)
238 }
239 ms[fullyQualifiedNameToSwaggerName(meth.ResponseType.FQMN(), reg)] = meth.ResponseType
240 }
241 }
242 findNestedMessagesAndEnumerations(meth.ResponseType, reg, m, e)
243 }
244 }
245}
246
247// findNestedMessagesAndEnumerations those can be generated by the services.
248func findNestedMessagesAndEnumerations(message *descriptor.Message, reg *descriptor.Registry, m messageMap, e enumMap) {
249 // Iterate over all the fields that
250 for _, t := range message.Fields {
251 fieldType := t.GetTypeName()
252 // If the type is an empty string then it is a proto primitive
253 if fieldType != "" {
254 if _, ok := m[fieldType]; !ok {
255 msg, err := reg.LookupMsg("", fieldType)
256 if err != nil {
257 enum, err := reg.LookupEnum("", fieldType)
258 if err != nil {
259 panic(err)
260 }
261 e[fieldType] = enum
262 continue
263 }
264 m[fieldType] = msg
265 findNestedMessagesAndEnumerations(msg, reg, m, e)
266 }
267 }
268 }
269}
270
271func skipRenderingRef(refName string) bool {
272 _, ok := wktSchemas[refName]
273 return ok
274}
275
276func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, reg *descriptor.Registry, customRefs refMap) {
277 for name, msg := range messages {
278 if skipRenderingRef(name) {
279 continue
280 }
281
282 if opt := msg.GetOptions(); opt != nil && opt.MapEntry != nil && *opt.MapEntry {
283 continue
284 }
285 schema := swaggerSchemaObject{
286 schemaCore: schemaCore{
287 Type: "object",
288 },
289 }
290 msgComments := protoComments(reg, msg.File, msg.Outers, "MessageType", int32(msg.Index))
Arjun E K57a7fcb2020-01-30 06:44:45 +0000291 if err := updateSwaggerDataFromComments(reg, &schema, msg, msgComments, false); err != nil {
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700292 panic(err)
293 }
294 opts, err := extractSchemaOptionFromMessageDescriptor(msg.DescriptorProto)
295 if err != nil {
296 panic(err)
297 }
298 if opts != nil {
Arjun E K57a7fcb2020-01-30 06:44:45 +0000299 protoSchema := swaggerSchemaFromProtoSchema(opts, reg, customRefs, msg)
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700300
301 // Warning: Make sure not to overwrite any fields already set on the schema type.
302 schema.ExternalDocs = protoSchema.ExternalDocs
303 schema.ReadOnly = protoSchema.ReadOnly
304 schema.MultipleOf = protoSchema.MultipleOf
305 schema.Maximum = protoSchema.Maximum
306 schema.ExclusiveMaximum = protoSchema.ExclusiveMaximum
307 schema.Minimum = protoSchema.Minimum
308 schema.ExclusiveMinimum = protoSchema.ExclusiveMinimum
309 schema.MaxLength = protoSchema.MaxLength
310 schema.MinLength = protoSchema.MinLength
311 schema.Pattern = protoSchema.Pattern
312 schema.Default = protoSchema.Default
313 schema.MaxItems = protoSchema.MaxItems
314 schema.MinItems = protoSchema.MinItems
315 schema.UniqueItems = protoSchema.UniqueItems
316 schema.MaxProperties = protoSchema.MaxProperties
317 schema.MinProperties = protoSchema.MinProperties
318 schema.Required = protoSchema.Required
319 if protoSchema.schemaCore.Type != "" || protoSchema.schemaCore.Ref != "" {
320 schema.schemaCore = protoSchema.schemaCore
321 }
322 if protoSchema.Title != "" {
323 schema.Title = protoSchema.Title
324 }
325 if protoSchema.Description != "" {
326 schema.Description = protoSchema.Description
327 }
328 if protoSchema.Example != nil {
329 schema.Example = protoSchema.Example
330 }
331 }
332
333 for _, f := range msg.Fields {
334 fieldValue := schemaOfField(f, reg, customRefs)
335 comments := fieldProtoComments(reg, msg, f)
Arjun E K57a7fcb2020-01-30 06:44:45 +0000336 if err := updateSwaggerDataFromComments(reg, &fieldValue, f, comments, false); err != nil {
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700337 panic(err)
338 }
339
340 kv := keyVal{Value: fieldValue}
341 if reg.GetUseJSONNamesForFields() {
342 kv.Key = f.GetJsonName()
343 } else {
344 kv.Key = f.GetName()
345 }
346 if schema.Properties == nil {
347 schema.Properties = &swaggerSchemaObjectProperties{}
348 }
349 *schema.Properties = append(*schema.Properties, kv)
350 }
351 d[fullyQualifiedNameToSwaggerName(msg.FQMN(), reg)] = schema
352 }
353}
354
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700355// schemaOfField returns a swagger Schema Object for a protobuf field.
356func schemaOfField(f *descriptor.Field, reg *descriptor.Registry, refs refMap) swaggerSchemaObject {
357 const (
358 singular = 0
359 array = 1
360 object = 2
361 )
362 var (
363 core schemaCore
364 aggregate int
365 )
366
367 fd := f.FieldDescriptorProto
368 if m, err := reg.LookupMsg("", f.GetTypeName()); err == nil {
369 if opt := m.GetOptions(); opt != nil && opt.MapEntry != nil && *opt.MapEntry {
370 fd = m.GetField()[1]
371 aggregate = object
372 }
373 }
374 if fd.GetLabel() == pbdescriptor.FieldDescriptorProto_LABEL_REPEATED {
375 aggregate = array
376 }
377
378 var props *swaggerSchemaObjectProperties
379
380 switch ft := fd.GetType(); ft {
381 case pbdescriptor.FieldDescriptorProto_TYPE_ENUM, pbdescriptor.FieldDescriptorProto_TYPE_MESSAGE, pbdescriptor.FieldDescriptorProto_TYPE_GROUP:
382 if wktSchema, ok := wktSchemas[fd.GetTypeName()]; ok {
383 core = wktSchema
384
385 if fd.GetTypeName() == ".google.protobuf.Empty" {
386 props = &swaggerSchemaObjectProperties{}
387 }
388 } else {
389 core = schemaCore{
390 Ref: "#/definitions/" + fullyQualifiedNameToSwaggerName(fd.GetTypeName(), reg),
391 }
392 if refs != nil {
393 refs[fd.GetTypeName()] = struct{}{}
394
395 }
396 }
397 default:
398 ftype, format, ok := primitiveSchema(ft)
399 if ok {
400 core = schemaCore{Type: ftype, Format: format}
401 } else {
402 core = schemaCore{Type: ft.String(), Format: "UNKNOWN"}
403 }
404 }
405
406 ret := swaggerSchemaObject{}
407
408 switch aggregate {
409 case array:
410 ret = swaggerSchemaObject{
411 schemaCore: schemaCore{
412 Type: "array",
413 Items: (*swaggerItemsObject)(&core),
414 },
415 }
416 case object:
417 ret = swaggerSchemaObject{
418 schemaCore: schemaCore{
419 Type: "object",
420 },
421 AdditionalProperties: &swaggerSchemaObject{Properties: props, schemaCore: core},
422 }
423 default:
424 ret = swaggerSchemaObject{
425 schemaCore: core,
426 Properties: props,
427 }
428 }
429
430 if j, err := extractJSONSchemaFromFieldDescriptor(fd); err == nil {
Arjun E K57a7fcb2020-01-30 06:44:45 +0000431 updateSwaggerObjectFromJSONSchema(&ret, j, reg, f)
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700432 }
433
434 return ret
435}
436
437// primitiveSchema returns a pair of "Type" and "Format" in JSON Schema for
438// the given primitive field type.
439// The last return parameter is true iff the field type is actually primitive.
440func primitiveSchema(t pbdescriptor.FieldDescriptorProto_Type) (ftype, format string, ok bool) {
441 switch t {
442 case pbdescriptor.FieldDescriptorProto_TYPE_DOUBLE:
443 return "number", "double", true
444 case pbdescriptor.FieldDescriptorProto_TYPE_FLOAT:
445 return "number", "float", true
446 case pbdescriptor.FieldDescriptorProto_TYPE_INT64:
447 return "string", "int64", true
448 case pbdescriptor.FieldDescriptorProto_TYPE_UINT64:
449 // 64bit integer types are marshaled as string in the default JSONPb marshaler.
450 // TODO(yugui) Add an option to declare 64bit integers as int64.
451 //
452 // NOTE: uint64 is not a predefined format of integer type in Swagger spec.
453 // So we cannot expect that uint64 is commonly supported by swagger processor.
454 return "string", "uint64", true
455 case pbdescriptor.FieldDescriptorProto_TYPE_INT32:
456 return "integer", "int32", true
457 case pbdescriptor.FieldDescriptorProto_TYPE_FIXED64:
458 // Ditto.
459 return "string", "uint64", true
460 case pbdescriptor.FieldDescriptorProto_TYPE_FIXED32:
461 // Ditto.
462 return "integer", "int64", true
463 case pbdescriptor.FieldDescriptorProto_TYPE_BOOL:
464 return "boolean", "boolean", true
465 case pbdescriptor.FieldDescriptorProto_TYPE_STRING:
466 // NOTE: in swagger specifition, format should be empty on string type
467 return "string", "", true
468 case pbdescriptor.FieldDescriptorProto_TYPE_BYTES:
469 return "string", "byte", true
470 case pbdescriptor.FieldDescriptorProto_TYPE_UINT32:
471 // Ditto.
472 return "integer", "int64", true
473 case pbdescriptor.FieldDescriptorProto_TYPE_SFIXED32:
474 return "integer", "int32", true
475 case pbdescriptor.FieldDescriptorProto_TYPE_SFIXED64:
476 return "string", "int64", true
477 case pbdescriptor.FieldDescriptorProto_TYPE_SINT32:
478 return "integer", "int32", true
479 case pbdescriptor.FieldDescriptorProto_TYPE_SINT64:
480 return "string", "int64", true
481 default:
482 return "", "", false
483 }
484}
485
486// renderEnumerationsAsDefinition inserts enums into the definitions object.
487func renderEnumerationsAsDefinition(enums enumMap, d swaggerDefinitionsObject, reg *descriptor.Registry) {
488 for _, enum := range enums {
489 enumComments := protoComments(reg, enum.File, enum.Outers, "EnumType", int32(enum.Index))
490
491 // it may be necessary to sort the result of the GetValue function.
492 enumNames := listEnumNames(enum)
493 defaultValue := getEnumDefault(enum)
494 valueComments := enumValueProtoComments(reg, enum)
495 if valueComments != "" {
496 enumComments = strings.TrimLeft(enumComments+"\n\n "+valueComments, "\n")
497 }
498 enumSchemaObject := swaggerSchemaObject{
499 schemaCore: schemaCore{
500 Type: "string",
501 Enum: enumNames,
502 Default: defaultValue,
503 },
504 }
Arjun E K57a7fcb2020-01-30 06:44:45 +0000505 if err := updateSwaggerDataFromComments(reg, &enumSchemaObject, enum, enumComments, false); err != nil {
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700506 panic(err)
507 }
508
509 d[fullyQualifiedNameToSwaggerName(enum.FQEN(), reg)] = enumSchemaObject
510 }
511}
512
513// Take in a FQMN or FQEN and return a swagger safe version of the FQMN
514func fullyQualifiedNameToSwaggerName(fqn string, reg *descriptor.Registry) string {
515 registriesSeenMutex.Lock()
516 defer registriesSeenMutex.Unlock()
517 if mapping, present := registriesSeen[reg]; present {
518 return mapping[fqn]
519 }
520 mapping := resolveFullyQualifiedNameToSwaggerNames(append(reg.GetAllFQMNs(), reg.GetAllFQENs()...), reg.GetUseFQNForSwaggerName())
521 registriesSeen[reg] = mapping
522 return mapping[fqn]
523}
524
525// registriesSeen is used to memoise calls to resolveFullyQualifiedNameToSwaggerNames so
526// we don't repeat it unnecessarily, since it can take some time.
527var registriesSeen = map[*descriptor.Registry]map[string]string{}
528var registriesSeenMutex sync.Mutex
529
530// Take the names of every proto and "uniq-ify" them. The idea is to produce a
531// set of names that meet a couple of conditions. They must be stable, they
532// must be unique, and they must be shorter than the FQN.
533//
534// This likely could be made better. This will always generate the same names
535// but may not always produce optimal names. This is a reasonably close
536// approximation of what they should look like in most cases.
537func resolveFullyQualifiedNameToSwaggerNames(messages []string, useFQNForSwaggerName bool) map[string]string {
538 packagesByDepth := make(map[int][][]string)
539 uniqueNames := make(map[string]string)
540
541 hierarchy := func(pkg string) []string {
542 return strings.Split(pkg, ".")
543 }
544
545 for _, p := range messages {
546 h := hierarchy(p)
547 for depth := range h {
548 if _, ok := packagesByDepth[depth]; !ok {
549 packagesByDepth[depth] = make([][]string, 0)
550 }
551 packagesByDepth[depth] = append(packagesByDepth[depth], h[len(h)-depth:])
552 }
553 }
554
555 count := func(list [][]string, item []string) int {
556 i := 0
557 for _, element := range list {
558 if reflect.DeepEqual(element, item) {
559 i++
560 }
561 }
562 return i
563 }
564
565 for _, p := range messages {
566 if useFQNForSwaggerName {
567 // strip leading dot from proto fqn
568 uniqueNames[p] = p[1:]
569 } else {
570 h := hierarchy(p)
571 for depth := 0; depth < len(h); depth++ {
572 if count(packagesByDepth[depth], h[len(h)-depth:]) == 1 {
573 uniqueNames[p] = strings.Join(h[len(h)-depth-1:], "")
574 break
575 }
576 if depth == len(h)-1 {
577 uniqueNames[p] = strings.Join(h, "")
578 }
579 }
580 }
581 }
582 return uniqueNames
583}
584
585var canRegexp = regexp.MustCompile("{([a-zA-Z][a-zA-Z0-9_.]*).*}")
586
587// Swagger expects paths of the form /path/{string_value} but grpc-gateway paths are expected to be of the form /path/{string_value=strprefix/*}. This should reformat it correctly.
Arjun E K57a7fcb2020-01-30 06:44:45 +0000588func templateToSwaggerPath(path string, reg *descriptor.Registry, fields []*descriptor.Field) string {
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700589 // It seems like the right thing to do here is to just use
590 // strings.Split(path, "/") but that breaks badly when you hit a url like
591 // /{my_field=prefix/*}/ and end up with 2 sections representing my_field.
592 // Instead do the right thing and write a small pushdown (counter) automata
593 // for it.
594 var parts []string
595 depth := 0
596 buffer := ""
597 jsonBuffer := ""
598 for _, char := range path {
599 switch char {
600 case '{':
601 // Push on the stack
602 depth++
603 buffer += string(char)
604 jsonBuffer = ""
605 jsonBuffer += string(char)
606 break
607 case '}':
608 if depth == 0 {
609 panic("Encountered } without matching { before it.")
610 }
611 // Pop from the stack
612 depth--
613 buffer += string(char)
614 if reg.GetUseJSONNamesForFields() &&
615 len(jsonBuffer) > 1 {
616 jsonSnakeCaseName := string(jsonBuffer[1:])
Arjun E K57a7fcb2020-01-30 06:44:45 +0000617 jsonCamelCaseName := string(lowerCamelCase(jsonSnakeCaseName, fields))
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700618 prev := string(buffer[:len(buffer)-len(jsonSnakeCaseName)-2])
619 buffer = strings.Join([]string{prev, "{", jsonCamelCaseName, "}"}, "")
620 jsonBuffer = ""
621 }
622 case '/':
623 if depth == 0 {
624 parts = append(parts, buffer)
625 buffer = ""
626 // Since the stack was empty when we hit the '/' we are done with this
627 // section.
628 continue
629 }
630 buffer += string(char)
631 jsonBuffer += string(char)
632 default:
633 buffer += string(char)
634 jsonBuffer += string(char)
635 break
636 }
637 }
638
639 // Now append the last element to parts
640 parts = append(parts, buffer)
641
642 // Parts is now an array of segments of the path. Interestingly, since the
643 // syntax for this subsection CAN be handled by a regexp since it has no
644 // memory.
645 for index, part := range parts {
646 // If part is a resource name such as "parent", "name", "user.name", the format info must be retained.
647 prefix := canRegexp.ReplaceAllString(part, "$1")
648 if isResourceName(prefix) {
649 continue
650 }
651 parts[index] = canRegexp.ReplaceAllString(part, "{$1}")
652 }
653
654 return strings.Join(parts, "/")
655}
656
657func isResourceName(prefix string) bool {
658 words := strings.Split(prefix, ".")
659 l := len(words)
660 field := words[l-1]
661 words = strings.Split(field, ":")
662 field = words[0]
663 return field == "parent" || field == "name"
664}
665
666func renderServices(services []*descriptor.Service, paths swaggerPathsObject, reg *descriptor.Registry, requestResponseRefs, customRefs refMap) error {
667 // Correctness of svcIdx and methIdx depends on 'services' containing the services in the same order as the 'file.Service' array.
668 for svcIdx, svc := range services {
669 for methIdx, meth := range svc.Methods {
670 for bIdx, b := range meth.Bindings {
671 // Iterate over all the swagger parameters
672 parameters := swaggerParametersObject{}
673 for _, parameter := range b.PathParams {
674
675 var paramType, paramFormat, desc, collectionFormat, defaultValue string
676 var enumNames []string
677 var items *swaggerItemsObject
678 var minItems *int
679 switch pt := parameter.Target.GetType(); pt {
680 case pbdescriptor.FieldDescriptorProto_TYPE_GROUP, pbdescriptor.FieldDescriptorProto_TYPE_MESSAGE:
681 if descriptor.IsWellKnownType(parameter.Target.GetTypeName()) {
682 if parameter.IsRepeated() {
683 return fmt.Errorf("only primitive and enum types are allowed in repeated path parameters")
684 }
685 schema := schemaOfField(parameter.Target, reg, customRefs)
686 paramType = schema.Type
687 paramFormat = schema.Format
688 desc = schema.Description
689 defaultValue = schema.Default
690 } else {
691 return fmt.Errorf("only primitive and well-known types are allowed in path parameters")
692 }
693 case pbdescriptor.FieldDescriptorProto_TYPE_ENUM:
694 paramType = "string"
695 paramFormat = ""
696 enum, err := reg.LookupEnum("", parameter.Target.GetTypeName())
697 if err != nil {
698 return err
699 }
700 enumNames = listEnumNames(enum)
701 schema := schemaOfField(parameter.Target, reg, customRefs)
702 desc = schema.Description
703 defaultValue = schema.Default
704 default:
705 var ok bool
706 paramType, paramFormat, ok = primitiveSchema(pt)
707 if !ok {
708 return fmt.Errorf("unknown field type %v", pt)
709 }
710
711 schema := schemaOfField(parameter.Target, reg, customRefs)
712 desc = schema.Description
713 defaultValue = schema.Default
714 }
715
716 if parameter.IsRepeated() {
717 core := schemaCore{Type: paramType, Format: paramFormat}
718 if parameter.IsEnum() {
719 var s []string
720 core.Enum = enumNames
721 enumNames = s
722 }
723 items = (*swaggerItemsObject)(&core)
724 paramType = "array"
725 paramFormat = ""
726 collectionFormat = reg.GetRepeatedPathParamSeparatorName()
727 minItems = new(int)
728 *minItems = 1
729 }
730
731 if desc == "" {
732 desc = fieldProtoComments(reg, parameter.Target.Message, parameter.Target)
733 }
734 parameterString := parameter.String()
735 if reg.GetUseJSONNamesForFields() {
Arjun E K57a7fcb2020-01-30 06:44:45 +0000736 parameterString = lowerCamelCase(parameterString, meth.RequestType.Fields)
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700737 }
738 parameters = append(parameters, swaggerParameterObject{
739 Name: parameterString,
740 Description: desc,
741 In: "path",
742 Required: true,
743 Default: defaultValue,
744 // Parameters in gRPC-Gateway can only be strings?
745 Type: paramType,
746 Format: paramFormat,
747 Enum: enumNames,
748 Items: items,
749 CollectionFormat: collectionFormat,
750 MinItems: minItems,
751 })
752 }
753 // Now check if there is a body parameter
754 if b.Body != nil {
755 var schema swaggerSchemaObject
756 desc := ""
757
758 if len(b.Body.FieldPath) == 0 {
759 schema = swaggerSchemaObject{
760 schemaCore: schemaCore{},
761 }
762
763 wknSchemaCore, isWkn := wktSchemas[meth.RequestType.FQMN()]
764 if !isWkn {
765 schema.Ref = fmt.Sprintf("#/definitions/%s", fullyQualifiedNameToSwaggerName(meth.RequestType.FQMN(), reg))
766 } else {
767 schema.schemaCore = wknSchemaCore
768
769 // Special workaround for Empty: it's well-known type but wknSchemas only returns schema.schemaCore; but we need to set schema.Properties which is a level higher.
770 if meth.RequestType.FQMN() == ".google.protobuf.Empty" {
771 schema.Properties = &swaggerSchemaObjectProperties{}
772 }
773 }
774 } else {
775 lastField := b.Body.FieldPath[len(b.Body.FieldPath)-1]
776 schema = schemaOfField(lastField.Target, reg, customRefs)
777 if schema.Description != "" {
778 desc = schema.Description
779 } else {
780 desc = fieldProtoComments(reg, lastField.Target.Message, lastField.Target)
781 }
782 }
783
784 if meth.GetClientStreaming() {
785 desc += " (streaming inputs)"
786 }
787 parameters = append(parameters, swaggerParameterObject{
788 Name: "body",
789 Description: desc,
790 In: "body",
791 Required: true,
792 Schema: &schema,
793 })
794 } else if b.HTTPMethod == "GET" || b.HTTPMethod == "DELETE" {
795 // add the parameters to the query string
796 queryParams, err := messageToQueryParameters(meth.RequestType, reg, b.PathParams)
797 if err != nil {
798 return err
799 }
800 parameters = append(parameters, queryParams...)
801 }
802
Arjun E K57a7fcb2020-01-30 06:44:45 +0000803 pathItemObject, ok := paths[templateToSwaggerPath(b.PathTmpl.Template, reg, meth.RequestType.Fields)]
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700804 if !ok {
805 pathItemObject = swaggerPathItemObject{}
806 }
807
808 methProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.ServiceDescriptorProto)(nil)), "Method")
809 desc := "A successful response."
810 var responseSchema swaggerSchemaObject
811
812 if b.ResponseBody == nil || len(b.ResponseBody.FieldPath) == 0 {
813 responseSchema = swaggerSchemaObject{
814 schemaCore: schemaCore{},
815 }
816
817 // Don't link to a full definition for
818 // empty; it's overly verbose.
819 // schema.Properties{} renders it as
820 // well, without a definition
821 wknSchemaCore, isWkn := wktSchemas[meth.ResponseType.FQMN()]
822 if !isWkn {
823 responseSchema.Ref = fmt.Sprintf("#/definitions/%s", fullyQualifiedNameToSwaggerName(meth.ResponseType.FQMN(), reg))
824 } else {
825 responseSchema.schemaCore = wknSchemaCore
826
827 // Special workaround for Empty: it's well-known type but wknSchemas only returns schema.schemaCore; but we need to set schema.Properties which is a level higher.
828 if meth.ResponseType.FQMN() == ".google.protobuf.Empty" {
829 responseSchema.Properties = &swaggerSchemaObjectProperties{}
830 }
831 }
832 } else {
833 // This is resolving the value of response_body in the google.api.HttpRule
834 lastField := b.ResponseBody.FieldPath[len(b.ResponseBody.FieldPath)-1]
835 responseSchema = schemaOfField(lastField.Target, reg, customRefs)
836 if responseSchema.Description != "" {
837 desc = responseSchema.Description
838 } else {
839 desc = fieldProtoComments(reg, lastField.Target.Message, lastField.Target)
840 }
841 }
842 if meth.GetServerStreaming() {
843 desc += "(streaming responses)"
Arjun E K57a7fcb2020-01-30 06:44:45 +0000844 responseSchema.Type = "object"
845 responseSchema.Title = fmt.Sprintf("Stream result of %s", fullyQualifiedNameToSwaggerName(meth.ResponseType.FQMN(), reg))
846 responseSchema.Properties = &swaggerSchemaObjectProperties{
847 keyVal{
848 Key: "result",
849 Value: swaggerSchemaObject{
850 schemaCore: schemaCore{
851 Ref: responseSchema.Ref,
852 },
853 },
854 },
855 keyVal{
856 Key: "error",
857 Value: swaggerSchemaObject{
858 schemaCore: schemaCore{
859 Ref: fmt.Sprintf("#/definitions/%s", fullyQualifiedNameToSwaggerName(".grpc.gateway.runtime.StreamError", reg))},
860 },
861 },
862 }
863 responseSchema.Ref = ""
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700864 }
865
866 tag := svc.GetName()
867 if pkg := svc.File.GetPackage(); pkg != "" && reg.IsIncludePackageInTags() {
868 tag = pkg + "." + tag
869 }
870
871 operationObject := &swaggerOperationObject{
872 Tags: []string{tag},
873 Parameters: parameters,
874 Responses: swaggerResponsesObject{
875 "200": swaggerResponseObject{
876 Description: desc,
877 Schema: responseSchema,
878 },
879 },
880 }
881 if bIdx == 0 {
882 operationObject.OperationID = fmt.Sprintf("%s", meth.GetName())
883 } else {
884 // OperationID must be unique in an OpenAPI v2 definition.
885 operationObject.OperationID = fmt.Sprintf("%s%d", meth.GetName(), bIdx+1)
886 }
887
888 // Fill reference map with referenced request messages
889 for _, param := range operationObject.Parameters {
890 if param.Schema != nil && param.Schema.Ref != "" {
891 requestResponseRefs[param.Schema.Ref] = struct{}{}
892 }
893 }
894
Arjun E K57a7fcb2020-01-30 06:44:45 +0000895 methComments := protoComments(reg, svc.File, nil, "Service", int32(svcIdx), methProtoPath, int32(methIdx))
896 if err := updateSwaggerDataFromComments(reg, operationObject, meth, methComments, false); err != nil {
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700897 panic(err)
898 }
899
900 opts, err := extractOperationOptionFromMethodDescriptor(meth.MethodDescriptorProto)
901 if opts != nil {
902 if err != nil {
903 panic(err)
904 }
Arjun E K57a7fcb2020-01-30 06:44:45 +0000905 operationObject.ExternalDocs = protoExternalDocumentationToSwaggerExternalDocumentation(opts.ExternalDocs, reg, meth)
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700906 // TODO(ivucica): this would be better supported by looking whether the method is deprecated in the proto file
907 operationObject.Deprecated = opts.Deprecated
908
909 if opts.Summary != "" {
910 operationObject.Summary = opts.Summary
911 }
912 if opts.Description != "" {
913 operationObject.Description = opts.Description
914 }
915 if len(opts.Tags) > 0 {
916 operationObject.Tags = make([]string, len(opts.Tags))
917 copy(operationObject.Tags, opts.Tags)
918 }
Arjun E K57a7fcb2020-01-30 06:44:45 +0000919 if opts.OperationId != "" {
920 operationObject.OperationID = opts.OperationId
921 }
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700922 if opts.Security != nil {
923 newSecurity := []swaggerSecurityRequirementObject{}
924 if operationObject.Security != nil {
925 newSecurity = *operationObject.Security
926 }
927 for _, secReq := range opts.Security {
928 newSecReq := swaggerSecurityRequirementObject{}
929 for secReqKey, secReqValue := range secReq.SecurityRequirement {
930 if secReqValue == nil {
931 continue
932 }
933
934 newSecReqValue := make([]string, len(secReqValue.Scope))
935 copy(newSecReqValue, secReqValue.Scope)
936 newSecReq[secReqKey] = newSecReqValue
937 }
938
939 if len(newSecReq) > 0 {
940 newSecurity = append(newSecurity, newSecReq)
941 }
942 }
943 operationObject.Security = &newSecurity
944 }
945 if opts.Responses != nil {
946 for name, resp := range opts.Responses {
947 respObj := swaggerResponseObject{
948 Description: resp.Description,
Arjun E K57a7fcb2020-01-30 06:44:45 +0000949 Schema: swaggerSchemaFromProtoSchema(resp.Schema, reg, customRefs, meth),
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700950 }
951 if resp.Extensions != nil {
952 exts, err := processExtensions(resp.Extensions)
953 if err != nil {
954 return err
955 }
956 respObj.extensions = exts
957 }
958 operationObject.Responses[name] = respObj
959 }
960 }
961
962 if opts.Extensions != nil {
963 exts, err := processExtensions(opts.Extensions)
964 if err != nil {
965 return err
966 }
967 operationObject.extensions = exts
968 }
969
970 // TODO(ivucica): add remaining fields of operation object
971 }
972
973 switch b.HTTPMethod {
974 case "DELETE":
975 pathItemObject.Delete = operationObject
976 break
977 case "GET":
978 pathItemObject.Get = operationObject
979 break
980 case "POST":
981 pathItemObject.Post = operationObject
982 break
983 case "PUT":
984 pathItemObject.Put = operationObject
985 break
986 case "PATCH":
987 pathItemObject.Patch = operationObject
988 break
989 }
Arjun E K57a7fcb2020-01-30 06:44:45 +0000990 paths[templateToSwaggerPath(b.PathTmpl.Template, reg, meth.RequestType.Fields)] = pathItemObject
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -0700991 }
992 }
993 }
994
995 // Success! return nil on the error object
996 return nil
997}
998
999// This function is called with a param which contains the entire definition of a method.
1000func applyTemplate(p param) (*swaggerObject, error) {
1001 // Create the basic template object. This is the object that everything is
1002 // defined off of.
1003 s := swaggerObject{
1004 // Swagger 2.0 is the version of this document
Arjun E K57a7fcb2020-01-30 06:44:45 +00001005 Swagger: "2.0",
1006 Consumes: []string{"application/json"},
1007 Produces: []string{"application/json"},
1008 Paths: make(swaggerPathsObject),
1009 Definitions: make(swaggerDefinitionsObject),
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001010 Info: swaggerInfoObject{
1011 Title: *p.File.Name,
1012 Version: "version not set",
1013 },
1014 }
1015
1016 // Loops through all the services and their exposed GET/POST/PUT/DELETE definitions
1017 // and create entries for all of them.
1018 // Also adds custom user specified references to second map.
1019 requestResponseRefs, customRefs := refMap{}, refMap{}
1020 if err := renderServices(p.Services, s.Paths, p.reg, requestResponseRefs, customRefs); err != nil {
1021 panic(err)
1022 }
1023
1024 // Find all the service's messages and enumerations that are defined (recursively)
1025 // and write request, response and other custom (but referenced) types out as definition objects.
1026 m := messageMap{}
1027 ms := messageMap{}
1028 e := enumMap{}
1029 findServicesMessagesAndEnumerations(p.Services, p.reg, m, ms, e, requestResponseRefs)
1030 renderMessagesAsDefinition(m, s.Definitions, p.reg, customRefs)
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001031 renderEnumerationsAsDefinition(e, s.Definitions, p.reg)
1032
1033 // File itself might have some comments and metadata.
1034 packageProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "Package")
1035 packageComments := protoComments(p.reg, p.File, nil, "Package", packageProtoPath)
Arjun E K57a7fcb2020-01-30 06:44:45 +00001036 if err := updateSwaggerDataFromComments(p.reg, &s, p, packageComments, true); err != nil {
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001037 panic(err)
1038 }
1039
1040 // There may be additional options in the swagger option in the proto.
1041 spb, err := extractSwaggerOptionFromFileDescriptor(p.FileDescriptorProto)
1042 if err != nil {
1043 panic(err)
1044 }
1045 if spb != nil {
1046 if spb.Swagger != "" {
1047 s.Swagger = spb.Swagger
1048 }
1049 if spb.Info != nil {
1050 if spb.Info.Title != "" {
1051 s.Info.Title = spb.Info.Title
1052 }
1053 if spb.Info.Description != "" {
1054 s.Info.Description = spb.Info.Description
1055 }
1056 if spb.Info.TermsOfService != "" {
1057 s.Info.TermsOfService = spb.Info.TermsOfService
1058 }
1059 if spb.Info.Version != "" {
1060 s.Info.Version = spb.Info.Version
1061 }
1062 if spb.Info.Contact != nil {
1063 if s.Info.Contact == nil {
1064 s.Info.Contact = &swaggerContactObject{}
1065 }
1066 if spb.Info.Contact.Name != "" {
1067 s.Info.Contact.Name = spb.Info.Contact.Name
1068 }
1069 if spb.Info.Contact.Url != "" {
1070 s.Info.Contact.URL = spb.Info.Contact.Url
1071 }
1072 if spb.Info.Contact.Email != "" {
1073 s.Info.Contact.Email = spb.Info.Contact.Email
1074 }
1075 }
1076 if spb.Info.License != nil {
1077 if s.Info.License == nil {
1078 s.Info.License = &swaggerLicenseObject{}
1079 }
1080 if spb.Info.License.Name != "" {
1081 s.Info.License.Name = spb.Info.License.Name
1082 }
1083 if spb.Info.License.Url != "" {
1084 s.Info.License.URL = spb.Info.License.Url
1085 }
1086 }
1087 if spb.Info.Extensions != nil {
1088 exts, err := processExtensions(spb.Info.Extensions)
1089 if err != nil {
1090 return nil, err
1091 }
1092 s.Info.extensions = exts
1093 }
1094 }
1095 if spb.Host != "" {
1096 s.Host = spb.Host
1097 }
1098 if spb.BasePath != "" {
1099 s.BasePath = spb.BasePath
1100 }
1101 if len(spb.Schemes) > 0 {
1102 s.Schemes = make([]string, len(spb.Schemes))
1103 for i, scheme := range spb.Schemes {
1104 s.Schemes[i] = strings.ToLower(scheme.String())
1105 }
1106 }
1107 if len(spb.Consumes) > 0 {
1108 s.Consumes = make([]string, len(spb.Consumes))
1109 copy(s.Consumes, spb.Consumes)
1110 }
1111 if len(spb.Produces) > 0 {
1112 s.Produces = make([]string, len(spb.Produces))
1113 copy(s.Produces, spb.Produces)
1114 }
1115 if spb.SecurityDefinitions != nil && spb.SecurityDefinitions.Security != nil {
1116 if s.SecurityDefinitions == nil {
1117 s.SecurityDefinitions = swaggerSecurityDefinitionsObject{}
1118 }
1119 for secDefKey, secDefValue := range spb.SecurityDefinitions.Security {
1120 var newSecDefValue swaggerSecuritySchemeObject
1121 if oldSecDefValue, ok := s.SecurityDefinitions[secDefKey]; !ok {
1122 newSecDefValue = swaggerSecuritySchemeObject{}
1123 } else {
1124 newSecDefValue = oldSecDefValue
1125 }
1126 if secDefValue.Type != swagger_options.SecurityScheme_TYPE_INVALID {
1127 switch secDefValue.Type {
1128 case swagger_options.SecurityScheme_TYPE_BASIC:
1129 newSecDefValue.Type = "basic"
1130 case swagger_options.SecurityScheme_TYPE_API_KEY:
1131 newSecDefValue.Type = "apiKey"
1132 case swagger_options.SecurityScheme_TYPE_OAUTH2:
1133 newSecDefValue.Type = "oauth2"
1134 }
1135 }
1136 if secDefValue.Description != "" {
1137 newSecDefValue.Description = secDefValue.Description
1138 }
1139 if secDefValue.Name != "" {
1140 newSecDefValue.Name = secDefValue.Name
1141 }
1142 if secDefValue.In != swagger_options.SecurityScheme_IN_INVALID {
1143 switch secDefValue.In {
1144 case swagger_options.SecurityScheme_IN_QUERY:
1145 newSecDefValue.In = "query"
1146 case swagger_options.SecurityScheme_IN_HEADER:
1147 newSecDefValue.In = "header"
1148 }
1149 }
1150 if secDefValue.Flow != swagger_options.SecurityScheme_FLOW_INVALID {
1151 switch secDefValue.Flow {
1152 case swagger_options.SecurityScheme_FLOW_IMPLICIT:
1153 newSecDefValue.Flow = "implicit"
1154 case swagger_options.SecurityScheme_FLOW_PASSWORD:
1155 newSecDefValue.Flow = "password"
1156 case swagger_options.SecurityScheme_FLOW_APPLICATION:
1157 newSecDefValue.Flow = "application"
1158 case swagger_options.SecurityScheme_FLOW_ACCESS_CODE:
1159 newSecDefValue.Flow = "accessCode"
1160 }
1161 }
1162 if secDefValue.AuthorizationUrl != "" {
1163 newSecDefValue.AuthorizationURL = secDefValue.AuthorizationUrl
1164 }
1165 if secDefValue.TokenUrl != "" {
1166 newSecDefValue.TokenURL = secDefValue.TokenUrl
1167 }
1168 if secDefValue.Scopes != nil {
1169 if newSecDefValue.Scopes == nil {
1170 newSecDefValue.Scopes = swaggerScopesObject{}
1171 }
1172 for scopeKey, scopeDesc := range secDefValue.Scopes.Scope {
1173 newSecDefValue.Scopes[scopeKey] = scopeDesc
1174 }
1175 }
1176 if secDefValue.Extensions != nil {
1177 exts, err := processExtensions(secDefValue.Extensions)
1178 if err != nil {
1179 return nil, err
1180 }
1181 newSecDefValue.extensions = exts
1182 }
1183 s.SecurityDefinitions[secDefKey] = newSecDefValue
1184 }
1185 }
1186 if spb.Security != nil {
1187 newSecurity := []swaggerSecurityRequirementObject{}
1188 if s.Security == nil {
1189 newSecurity = []swaggerSecurityRequirementObject{}
1190 } else {
1191 newSecurity = s.Security
1192 }
1193 for _, secReq := range spb.Security {
1194 newSecReq := swaggerSecurityRequirementObject{}
1195 for secReqKey, secReqValue := range secReq.SecurityRequirement {
1196 newSecReqValue := make([]string, len(secReqValue.Scope))
1197 copy(newSecReqValue, secReqValue.Scope)
1198 newSecReq[secReqKey] = newSecReqValue
1199 }
1200 newSecurity = append(newSecurity, newSecReq)
1201 }
1202 s.Security = newSecurity
1203 }
Arjun E K57a7fcb2020-01-30 06:44:45 +00001204 s.ExternalDocs = protoExternalDocumentationToSwaggerExternalDocumentation(spb.ExternalDocs, p.reg, spb)
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001205 // Populate all Paths with Responses set at top level,
1206 // preferring Responses already set over those at the top level.
1207 if spb.Responses != nil {
1208 for _, verbs := range s.Paths {
1209 var maps []swaggerResponsesObject
1210 if verbs.Delete != nil {
1211 maps = append(maps, verbs.Delete.Responses)
1212 }
1213 if verbs.Get != nil {
1214 maps = append(maps, verbs.Get.Responses)
1215 }
1216 if verbs.Post != nil {
1217 maps = append(maps, verbs.Post.Responses)
1218 }
1219 if verbs.Put != nil {
1220 maps = append(maps, verbs.Put.Responses)
1221 }
1222 if verbs.Patch != nil {
1223 maps = append(maps, verbs.Patch.Responses)
1224 }
1225
1226 for k, v := range spb.Responses {
1227 for _, respMap := range maps {
1228 if _, ok := respMap[k]; ok {
1229 // Don't overwrite already existing Responses
1230 continue
1231 }
1232 respMap[k] = swaggerResponseObject{
1233 Description: v.Description,
Arjun E K57a7fcb2020-01-30 06:44:45 +00001234 Schema: swaggerSchemaFromProtoSchema(v.Schema, p.reg, customRefs, nil),
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001235 }
1236 }
1237 }
1238 }
1239 }
1240
1241 if spb.Extensions != nil {
1242 exts, err := processExtensions(spb.Extensions)
1243 if err != nil {
1244 return nil, err
1245 }
1246 s.extensions = exts
1247 }
1248
1249 // Additional fields on the OpenAPI v2 spec's "Swagger" object
1250 // should be added here, once supported in the proto.
1251 }
1252
1253 // Finally add any references added by users that aren't
1254 // otherwise rendered.
1255 addCustomRefs(s.Definitions, p.reg, customRefs)
1256
1257 return &s, nil
1258}
1259
1260func processExtensions(inputExts map[string]*structpb.Value) ([]extension, error) {
1261 exts := []extension{}
1262 for k, v := range inputExts {
1263 if !strings.HasPrefix(k, "x-") {
1264 return nil, fmt.Errorf("Extension keys need to start with \"x-\": %q", k)
1265 }
1266 ext, err := (&jsonpb.Marshaler{Indent: " "}).MarshalToString(v)
1267 if err != nil {
1268 return nil, err
1269 }
1270 exts = append(exts, extension{key: k, value: json.RawMessage(ext)})
1271 }
1272 sort.Slice(exts, func(i, j int) bool { return exts[i].key < exts[j].key })
1273 return exts, nil
1274}
1275
1276// updateSwaggerDataFromComments updates a Swagger object based on a comment
1277// from the proto file.
1278//
1279// First paragraph of a comment is used for summary. Remaining paragraphs of
1280// a comment are used for description. If 'Summary' field is not present on
1281// the passed swaggerObject, the summary and description are joined by \n\n.
1282//
1283// If there is a field named 'Info', its 'Summary' and 'Description' fields
1284// will be updated instead.
1285//
1286// If there is no 'Summary', the same behavior will be attempted on 'Title',
1287// but only if the last character is not a period.
Arjun E K57a7fcb2020-01-30 06:44:45 +00001288func updateSwaggerDataFromComments(reg *descriptor.Registry, swaggerObject interface{}, data interface{}, comment string, isPackageObject bool) error {
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001289 if len(comment) == 0 {
1290 return nil
1291 }
1292
Arjun E K57a7fcb2020-01-30 06:44:45 +00001293 // Checks whether the "use_go_templates" flag is set to true
1294 if reg.GetUseGoTemplate() {
1295 comment = goTemplateComments(comment, data, reg)
1296 }
1297
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001298 // Figure out what to apply changes to.
1299 swaggerObjectValue := reflect.ValueOf(swaggerObject)
1300 infoObjectValue := swaggerObjectValue.Elem().FieldByName("Info")
1301 if !infoObjectValue.CanSet() {
1302 // No such field? Apply summary and description directly to
1303 // passed object.
1304 infoObjectValue = swaggerObjectValue.Elem()
1305 }
1306
1307 // Figure out which properties to update.
1308 summaryValue := infoObjectValue.FieldByName("Summary")
1309 descriptionValue := infoObjectValue.FieldByName("Description")
1310 readOnlyValue := infoObjectValue.FieldByName("ReadOnly")
1311
1312 if readOnlyValue.Kind() == reflect.Bool && readOnlyValue.CanSet() && strings.Contains(comment, "Output only.") {
1313 readOnlyValue.Set(reflect.ValueOf(true))
1314 }
1315
1316 usingTitle := false
1317 if !summaryValue.CanSet() {
1318 summaryValue = infoObjectValue.FieldByName("Title")
1319 usingTitle = true
1320 }
1321
1322 paragraphs := strings.Split(comment, "\n\n")
1323
1324 // If there is a summary (or summary-equivalent) and it's empty, use the first
1325 // paragraph as summary, and the rest as description.
1326 if summaryValue.CanSet() {
1327 summary := strings.TrimSpace(paragraphs[0])
1328 description := strings.TrimSpace(strings.Join(paragraphs[1:], "\n\n"))
1329 if !usingTitle || (len(summary) > 0 && summary[len(summary)-1] != '.') {
1330 // overrides the schema value only if it's empty
1331 // keep the comment precedence when updating the package definition
1332 if summaryValue.Len() == 0 || isPackageObject {
1333 summaryValue.Set(reflect.ValueOf(summary))
1334 }
1335 if len(description) > 0 {
1336 if !descriptionValue.CanSet() {
1337 return fmt.Errorf("Encountered object type with a summary, but no description")
1338 }
1339 // overrides the schema value only if it's empty
1340 // keep the comment precedence when updating the package definition
1341 if descriptionValue.Len() == 0 || isPackageObject {
1342 descriptionValue.Set(reflect.ValueOf(description))
1343 }
1344 }
1345 return nil
1346 }
1347 }
1348
1349 // There was no summary field on the swaggerObject. Try to apply the
1350 // whole comment into description if the swagger object description is empty.
1351 if descriptionValue.CanSet() {
1352 if descriptionValue.Len() == 0 || isPackageObject {
1353 descriptionValue.Set(reflect.ValueOf(strings.Join(paragraphs, "\n\n")))
1354 }
1355 return nil
1356 }
1357
1358 return fmt.Errorf("no description nor summary property")
1359}
1360
1361func fieldProtoComments(reg *descriptor.Registry, msg *descriptor.Message, field *descriptor.Field) string {
1362 protoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "Field")
1363 for i, f := range msg.Fields {
1364 if f == field {
1365 return protoComments(reg, msg.File, msg.Outers, "MessageType", int32(msg.Index), protoPath, int32(i))
1366 }
1367 }
1368 return ""
1369}
1370
1371func enumValueProtoComments(reg *descriptor.Registry, enum *descriptor.Enum) string {
1372 protoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.EnumDescriptorProto)(nil)), "Value")
1373 var comments []string
1374 for idx, value := range enum.GetValue() {
1375 name := value.GetName()
1376 str := protoComments(reg, enum.File, enum.Outers, "EnumType", int32(enum.Index), protoPath, int32(idx))
1377 if str != "" {
1378 comments = append(comments, name+": "+str)
1379 }
1380 }
1381 if len(comments) > 0 {
1382 return "- " + strings.Join(comments, "\n - ")
1383 }
1384 return ""
1385}
1386
1387func protoComments(reg *descriptor.Registry, file *descriptor.File, outers []string, typeName string, typeIndex int32, fieldPaths ...int32) string {
1388 if file.SourceCodeInfo == nil {
1389 fmt.Fprintln(os.Stderr, "descriptor.File should not contain nil SourceCodeInfo")
1390 return ""
1391 }
1392
1393 outerPaths := make([]int32, len(outers))
1394 for i := range outers {
1395 location := ""
1396 if file.Package != nil {
1397 location = file.GetPackage()
1398 }
1399
1400 msg, err := reg.LookupMsg(location, strings.Join(outers[:i+1], "."))
1401 if err != nil {
1402 panic(err)
1403 }
1404 outerPaths[i] = int32(msg.Index)
1405 }
1406
1407 for _, loc := range file.SourceCodeInfo.Location {
1408 if !isProtoPathMatches(loc.Path, outerPaths, typeName, typeIndex, fieldPaths) {
1409 continue
1410 }
1411 comments := ""
1412 if loc.LeadingComments != nil {
1413 comments = strings.TrimRight(*loc.LeadingComments, "\n")
1414 comments = strings.TrimSpace(comments)
1415 // TODO(ivucica): this is a hack to fix "// " being interpreted as "//".
1416 // perhaps we should:
1417 // - split by \n
1418 // - determine if every (but first and last) line begins with " "
1419 // - trim every line only if that is the case
1420 // - join by \n
1421 comments = strings.Replace(comments, "\n ", "\n", -1)
1422 }
1423 return comments
1424 }
1425 return ""
1426}
1427
Arjun E K57a7fcb2020-01-30 06:44:45 +00001428func goTemplateComments(comment string, data interface{}, reg *descriptor.Registry) string {
1429 var temp bytes.Buffer
1430 tpl, err := template.New("").Funcs(template.FuncMap{
1431 // Allows importing documentation from a file
1432 "import": func(name string) string {
1433 file, err := ioutil.ReadFile(name)
1434 if err != nil {
1435 return err.Error()
1436 }
1437 // Runs template over imported file
1438 return goTemplateComments(string(file), data, reg)
1439 },
1440 // Grabs title and description from a field
1441 "fieldcomments": func(msg *descriptor.Message, field *descriptor.Field) string {
1442 return strings.Replace(fieldProtoComments(reg, msg, field), "\n", "<br>", -1)
1443 },
1444 }).Parse(comment)
1445 if err != nil {
1446 // If there is an error parsing the templating insert the error as string in the comment
1447 // to make it easier to debug the template error
1448 return err.Error()
1449 }
1450 err = tpl.Execute(&temp, data)
1451 if err != nil {
1452 // If there is an error executing the templating insert the error as string in the comment
1453 // to make it easier to debug the error
1454 return err.Error()
1455 }
1456 return temp.String()
1457}
1458
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001459var messageProtoPath = protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "MessageType")
1460var nestedProtoPath = protoPathIndex(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "NestedType")
1461var packageProtoPath = protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "Package")
1462var serviceProtoPath = protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "Service")
1463var methodProtoPath = protoPathIndex(reflect.TypeOf((*pbdescriptor.ServiceDescriptorProto)(nil)), "Method")
1464
1465func isProtoPathMatches(paths []int32, outerPaths []int32, typeName string, typeIndex int32, fieldPaths []int32) bool {
1466 if typeName == "Package" && typeIndex == packageProtoPath {
1467 // path for package comments is just [2], and all the other processing
1468 // is too complex for it.
1469 if len(paths) == 0 || typeIndex != paths[0] {
1470 return false
1471 }
1472 return true
1473 }
1474
1475 if len(paths) != len(outerPaths)*2+2+len(fieldPaths) {
1476 return false
1477 }
1478
1479 if typeName == "Method" {
1480 if paths[0] != serviceProtoPath || paths[2] != methodProtoPath {
1481 return false
1482 }
1483 paths = paths[2:]
1484 } else {
1485 typeNameDescriptor := reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil))
1486
1487 if len(outerPaths) > 0 {
1488 if paths[0] != messageProtoPath || paths[1] != outerPaths[0] {
1489 return false
1490 }
1491 paths = paths[2:]
1492 outerPaths = outerPaths[1:]
1493
1494 for i, v := range outerPaths {
1495 if paths[i*2] != nestedProtoPath || paths[i*2+1] != v {
1496 return false
1497 }
1498 }
1499 paths = paths[len(outerPaths)*2:]
1500
1501 if typeName == "MessageType" {
1502 typeName = "NestedType"
1503 }
1504 typeNameDescriptor = reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil))
1505 }
1506
1507 if paths[0] != protoPathIndex(typeNameDescriptor, typeName) || paths[1] != typeIndex {
1508 return false
1509 }
1510 paths = paths[2:]
1511 }
1512
1513 for i, v := range fieldPaths {
1514 if paths[i] != v {
1515 return false
1516 }
1517 }
1518 return true
1519}
1520
1521// protoPathIndex returns a path component for google.protobuf.descriptor.SourceCode_Location.
1522//
1523// Specifically, it returns an id as generated from descriptor proto which
1524// can be used to determine what type the id following it in the path is.
1525// For example, if we are trying to locate comments related to a field named
1526// `Address` in a message named `Person`, the path will be:
1527//
1528// [4, a, 2, b]
1529//
1530// While `a` gets determined by the order in which the messages appear in
1531// the proto file, and `b` is the field index specified in the proto
1532// file itself, the path actually needs to specify that `a` refers to a
1533// message and not, say, a service; and that `b` refers to a field and not
1534// an option.
1535//
1536// protoPathIndex figures out the values 4 and 2 in the above example. Because
1537// messages are top level objects, the value of 4 comes from field id for
1538// `MessageType` inside `google.protobuf.descriptor.FileDescriptor` message.
1539// This field has a message type `google.protobuf.descriptor.DescriptorProto`.
1540// And inside message `DescriptorProto`, there is a field named `Field` with id
1541// 2.
1542//
1543// Some code generators seem to be hardcoding these values; this method instead
1544// interprets them from `descriptor.proto`-derived Go source as necessary.
1545func protoPathIndex(descriptorType reflect.Type, what string) int32 {
1546 field, ok := descriptorType.Elem().FieldByName(what)
1547 if !ok {
1548 panic(fmt.Errorf("could not find protobuf descriptor type id for %s", what))
1549 }
1550 pbtag := field.Tag.Get("protobuf")
1551 if pbtag == "" {
1552 panic(fmt.Errorf("no Go tag 'protobuf' on protobuf descriptor for %s", what))
1553 }
1554 path, err := strconv.Atoi(strings.Split(pbtag, ",")[1])
1555 if err != nil {
1556 panic(fmt.Errorf("protobuf descriptor id for %s cannot be converted to a number: %s", what, err.Error()))
1557 }
1558
1559 return int32(path)
1560}
1561
1562// extractOperationOptionFromMethodDescriptor extracts the message of type
1563// swagger_options.Operation from a given proto method's descriptor.
1564func extractOperationOptionFromMethodDescriptor(meth *pbdescriptor.MethodDescriptorProto) (*swagger_options.Operation, error) {
1565 if meth.Options == nil {
1566 return nil, nil
1567 }
1568 if !proto.HasExtension(meth.Options, swagger_options.E_Openapiv2Operation) {
1569 return nil, nil
1570 }
1571 ext, err := proto.GetExtension(meth.Options, swagger_options.E_Openapiv2Operation)
1572 if err != nil {
1573 return nil, err
1574 }
1575 opts, ok := ext.(*swagger_options.Operation)
1576 if !ok {
1577 return nil, fmt.Errorf("extension is %T; want an Operation", ext)
1578 }
1579 return opts, nil
1580}
1581
1582// extractSchemaOptionFromMessageDescriptor extracts the message of type
1583// swagger_options.Schema from a given proto message's descriptor.
1584func extractSchemaOptionFromMessageDescriptor(msg *pbdescriptor.DescriptorProto) (*swagger_options.Schema, error) {
1585 if msg.Options == nil {
1586 return nil, nil
1587 }
1588 if !proto.HasExtension(msg.Options, swagger_options.E_Openapiv2Schema) {
1589 return nil, nil
1590 }
1591 ext, err := proto.GetExtension(msg.Options, swagger_options.E_Openapiv2Schema)
1592 if err != nil {
1593 return nil, err
1594 }
1595 opts, ok := ext.(*swagger_options.Schema)
1596 if !ok {
1597 return nil, fmt.Errorf("extension is %T; want a Schema", ext)
1598 }
1599 return opts, nil
1600}
1601
1602// extractSwaggerOptionFromFileDescriptor extracts the message of type
1603// swagger_options.Swagger from a given proto method's descriptor.
1604func extractSwaggerOptionFromFileDescriptor(file *pbdescriptor.FileDescriptorProto) (*swagger_options.Swagger, error) {
1605 if file.Options == nil {
1606 return nil, nil
1607 }
1608 if !proto.HasExtension(file.Options, swagger_options.E_Openapiv2Swagger) {
1609 return nil, nil
1610 }
1611 ext, err := proto.GetExtension(file.Options, swagger_options.E_Openapiv2Swagger)
1612 if err != nil {
1613 return nil, err
1614 }
1615 opts, ok := ext.(*swagger_options.Swagger)
1616 if !ok {
1617 return nil, fmt.Errorf("extension is %T; want a Swagger object", ext)
1618 }
1619 return opts, nil
1620}
1621
1622func extractJSONSchemaFromFieldDescriptor(fd *pbdescriptor.FieldDescriptorProto) (*swagger_options.JSONSchema, error) {
1623 if fd.Options == nil {
1624 return nil, nil
1625 }
1626 if !proto.HasExtension(fd.Options, swagger_options.E_Openapiv2Field) {
1627 return nil, nil
1628 }
1629 ext, err := proto.GetExtension(fd.Options, swagger_options.E_Openapiv2Field)
1630 if err != nil {
1631 return nil, err
1632 }
1633 opts, ok := ext.(*swagger_options.JSONSchema)
1634 if !ok {
1635 return nil, fmt.Errorf("extension is %T; want a JSONSchema object", ext)
1636 }
1637 return opts, nil
1638}
1639
1640func protoJSONSchemaToSwaggerSchemaCore(j *swagger_options.JSONSchema, reg *descriptor.Registry, refs refMap) schemaCore {
1641 ret := schemaCore{}
1642
1643 if j.GetRef() != "" {
1644 swaggerName := fullyQualifiedNameToSwaggerName(j.GetRef(), reg)
1645 if swaggerName != "" {
1646 ret.Ref = "#/definitions/" + swaggerName
1647 if refs != nil {
1648 refs[j.GetRef()] = struct{}{}
1649 }
1650 } else {
1651 ret.Ref += j.GetRef()
1652 }
1653 } else {
1654 f, t := protoJSONSchemaTypeToFormat(j.GetType())
1655 ret.Format = f
1656 ret.Type = t
1657 }
1658
1659 return ret
1660}
1661
Arjun E K57a7fcb2020-01-30 06:44:45 +00001662func updateSwaggerObjectFromJSONSchema(s *swaggerSchemaObject, j *swagger_options.JSONSchema, reg *descriptor.Registry, data interface{}) {
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001663 s.Title = j.GetTitle()
1664 s.Description = j.GetDescription()
Arjun E K57a7fcb2020-01-30 06:44:45 +00001665 if reg.GetUseGoTemplate() {
1666 s.Title = goTemplateComments(s.Title, data, reg)
1667 s.Description = goTemplateComments(s.Description, data, reg)
1668 }
1669
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001670 s.ReadOnly = j.GetReadOnly()
1671 s.MultipleOf = j.GetMultipleOf()
1672 s.Maximum = j.GetMaximum()
1673 s.ExclusiveMaximum = j.GetExclusiveMaximum()
1674 s.Minimum = j.GetMinimum()
1675 s.ExclusiveMinimum = j.GetExclusiveMinimum()
1676 s.MaxLength = j.GetMaxLength()
1677 s.MinLength = j.GetMinLength()
1678 s.Pattern = j.GetPattern()
1679 s.Default = j.GetDefault()
1680 s.MaxItems = j.GetMaxItems()
1681 s.MinItems = j.GetMinItems()
1682 s.UniqueItems = j.GetUniqueItems()
1683 s.MaxProperties = j.GetMaxProperties()
1684 s.MinProperties = j.GetMinProperties()
1685 s.Required = j.GetRequired()
1686 if overrideType := j.GetType(); len(overrideType) > 0 {
1687 s.Type = strings.ToLower(overrideType[0].String())
1688 }
1689}
1690
Arjun E K57a7fcb2020-01-30 06:44:45 +00001691func swaggerSchemaFromProtoSchema(s *swagger_options.Schema, reg *descriptor.Registry, refs refMap, data interface{}) swaggerSchemaObject {
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001692 ret := swaggerSchemaObject{
Arjun E K57a7fcb2020-01-30 06:44:45 +00001693 ExternalDocs: protoExternalDocumentationToSwaggerExternalDocumentation(s.GetExternalDocs(), reg, data),
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001694 }
1695
1696 ret.schemaCore = protoJSONSchemaToSwaggerSchemaCore(s.GetJsonSchema(), reg, refs)
Arjun E K57a7fcb2020-01-30 06:44:45 +00001697 updateSwaggerObjectFromJSONSchema(&ret, s.GetJsonSchema(), reg, data)
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001698
1699 if s != nil && s.Example != nil {
1700 ret.Example = json.RawMessage(s.Example.Value)
1701 }
1702
1703 return ret
1704}
1705
1706func protoJSONSchemaTypeToFormat(in []swagger_options.JSONSchema_JSONSchemaSimpleTypes) (string, string) {
1707 if len(in) == 0 {
1708 return "", ""
1709 }
1710
1711 // Can't support more than 1 type, just return the first element.
1712 // This is due to an inconsistency in the design of the openapiv2 proto
1713 // and that used in schemaCore. schemaCore uses the v3 definition of types,
1714 // which only allows a single string, while the openapiv2 proto uses the OpenAPI v2
1715 // definition, which defers to the JSON schema definition, which allows a string or an array.
1716 // Sources:
1717 // https://swagger.io/specification/#itemsObject
1718 // https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.2
1719 switch in[0] {
1720 case swagger_options.JSONSchema_UNKNOWN, swagger_options.JSONSchema_NULL:
1721 return "", ""
1722 case swagger_options.JSONSchema_OBJECT:
1723 return "object", ""
1724 case swagger_options.JSONSchema_ARRAY:
1725 return "array", ""
1726 case swagger_options.JSONSchema_BOOLEAN:
1727 return "boolean", "boolean"
1728 case swagger_options.JSONSchema_INTEGER:
1729 return "integer", "int32"
1730 case swagger_options.JSONSchema_NUMBER:
1731 return "number", "double"
1732 case swagger_options.JSONSchema_STRING:
1733 // NOTE: in swagger specifition, format should be empty on string type
1734 return "string", ""
1735 default:
1736 // Maybe panic?
1737 return "", ""
1738 }
1739}
1740
Arjun E K57a7fcb2020-01-30 06:44:45 +00001741func protoExternalDocumentationToSwaggerExternalDocumentation(in *swagger_options.ExternalDocumentation, reg *descriptor.Registry, data interface{}) *swaggerExternalDocumentationObject {
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001742 if in == nil {
1743 return nil
1744 }
1745
Arjun E K57a7fcb2020-01-30 06:44:45 +00001746 if reg.GetUseGoTemplate() {
1747 in.Description = goTemplateComments(in.Description, data, reg)
1748 }
1749
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001750 return &swaggerExternalDocumentationObject{
1751 Description: in.Description,
1752 URL: in.Url,
1753 }
1754}
1755
1756func addCustomRefs(d swaggerDefinitionsObject, reg *descriptor.Registry, refs refMap) {
1757 if len(refs) == 0 {
1758 return
1759 }
1760 msgMap := make(messageMap)
1761 enumMap := make(enumMap)
1762 for ref := range refs {
1763 if _, ok := d[fullyQualifiedNameToSwaggerName(ref, reg)]; ok {
1764 // Skip already existing definitions
1765 delete(refs, ref)
1766 continue
1767 }
1768 msg, err := reg.LookupMsg("", ref)
1769 if err == nil {
1770 msgMap[fullyQualifiedNameToSwaggerName(ref, reg)] = msg
1771 continue
1772 }
1773 enum, err := reg.LookupEnum("", ref)
1774 if err == nil {
1775 enumMap[fullyQualifiedNameToSwaggerName(ref, reg)] = enum
1776 continue
1777 }
1778
1779 // ?? Should be either enum or msg
1780 }
1781 renderMessagesAsDefinition(msgMap, d, reg, refs)
1782 renderEnumerationsAsDefinition(enumMap, d, reg)
1783
1784 // Run again in case any new refs were added
1785 addCustomRefs(d, reg, refs)
1786}
1787
Arjun E K57a7fcb2020-01-30 06:44:45 +00001788func lowerCamelCase(fieldName string, fields []*descriptor.Field) string {
1789 for _, oneField := range fields {
1790 if oneField.GetName() == fieldName {
1791 return oneField.GetJsonName()
1792 }
1793 }
1794 parameterString := gogen.CamelCase(fieldName)
Matteo Scandoloa6a3aee2019-11-26 13:30:14 -07001795 builder := &strings.Builder{}
1796 builder.WriteString(strings.ToLower(string(parameterString[0])))
1797 builder.WriteString(parameterString[1:])
1798 return builder.String()
1799}