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