github.com/grpc-ecosystem/grpc-gateway/v2@v2.19.1/protoc-gen-openapiv2/internal/genopenapi/template.go (about) 1 package genopenapi 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "math" 8 "net/textproto" 9 "os" 10 "reflect" 11 "regexp" 12 "sort" 13 "strconv" 14 "strings" 15 "sync" 16 "text/template" 17 "time" 18 19 "github.com/grpc-ecosystem/grpc-gateway/v2/internal/casing" 20 "github.com/grpc-ecosystem/grpc-gateway/v2/internal/descriptor" 21 openapi_options "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" 22 "google.golang.org/genproto/googleapis/api/annotations" 23 "google.golang.org/genproto/googleapis/api/visibility" 24 "google.golang.org/grpc/grpclog" 25 "google.golang.org/protobuf/encoding/protojson" 26 "google.golang.org/protobuf/proto" 27 "google.golang.org/protobuf/types/descriptorpb" 28 "google.golang.org/protobuf/types/known/structpb" 29 ) 30 31 // The OpenAPI specification does not allow for more than one endpoint with the same HTTP method and path. 32 // This prevents multiple gRPC service methods from sharing the same stripped version of the path and method. 33 // For example: `GET /v1/{name=organizations/*}/roles` and `GET /v1/{name=users/*}/roles` both get stripped to `GET /v1/{name}/roles`. 34 // We must make the URL unique by adding a suffix and an incrementing index to each path parameter 35 // to differentiate the endpoints. 36 // Since path parameter names do not affect the request contents (i.e. they're replaced in the path) 37 // this will be hidden from the real grpc gateway consumer. 38 const pathParamUniqueSuffixDeliminator = "_" 39 40 const paragraphDeliminator = "\n\n" 41 42 // wktSchemas are the schemas of well-known-types. 43 // The schemas must match with the behavior of the JSON unmarshaler in 44 // https://github.com/protocolbuffers/protobuf-go/blob/v1.25.0/encoding/protojson/well_known_types.go 45 var wktSchemas = map[string]schemaCore{ 46 ".google.protobuf.FieldMask": { 47 Type: "string", 48 }, 49 ".google.protobuf.Timestamp": { 50 Type: "string", 51 Format: "date-time", 52 }, 53 ".google.protobuf.Duration": { 54 Type: "string", 55 }, 56 ".google.protobuf.StringValue": { 57 Type: "string", 58 }, 59 ".google.protobuf.BytesValue": { 60 Type: "string", 61 Format: "byte", 62 }, 63 ".google.protobuf.Int32Value": { 64 Type: "integer", 65 Format: "int32", 66 }, 67 ".google.protobuf.UInt32Value": { 68 Type: "integer", 69 Format: "int64", 70 }, 71 ".google.protobuf.Int64Value": { 72 Type: "string", 73 Format: "int64", 74 }, 75 ".google.protobuf.UInt64Value": { 76 Type: "string", 77 Format: "uint64", 78 }, 79 ".google.protobuf.FloatValue": { 80 Type: "number", 81 Format: "float", 82 }, 83 ".google.protobuf.DoubleValue": { 84 Type: "number", 85 Format: "double", 86 }, 87 ".google.protobuf.BoolValue": { 88 Type: "boolean", 89 }, 90 ".google.protobuf.Empty": { 91 Type: "object", 92 }, 93 ".google.protobuf.Struct": { 94 Type: "object", 95 }, 96 ".google.protobuf.Value": {}, 97 ".google.protobuf.ListValue": { 98 Type: "array", 99 Items: (*openapiItemsObject)(&openapiSchemaObject{ 100 schemaCore: schemaCore{ 101 Type: "object", 102 }}), 103 }, 104 ".google.protobuf.NullValue": { 105 Type: "string", 106 }, 107 } 108 109 func listEnumNames(reg *descriptor.Registry, enum *descriptor.Enum) (names []string) { 110 for _, value := range enum.GetValue() { 111 if !isVisible(getEnumValueVisibilityOption(value), reg) { 112 continue 113 } 114 if reg.GetOmitEnumDefaultValue() && value.GetNumber() == 0 { 115 continue 116 } 117 names = append(names, value.GetName()) 118 } 119 120 if len(names) > 0 { 121 return names 122 } 123 124 return nil 125 } 126 127 func listEnumNumbers(reg *descriptor.Registry, enum *descriptor.Enum) (numbers []int) { 128 for _, value := range enum.GetValue() { 129 if reg.GetOmitEnumDefaultValue() && value.GetNumber() == 0 { 130 continue 131 } 132 if !isVisible(getEnumValueVisibilityOption(value), reg) { 133 continue 134 } 135 numbers = append(numbers, int(value.GetNumber())) 136 } 137 138 if len(numbers) > 0 { 139 return numbers 140 } 141 142 return nil 143 } 144 145 func getEnumDefault(reg *descriptor.Registry, enum *descriptor.Enum) interface{} { 146 if !reg.GetOmitEnumDefaultValue() { 147 for _, value := range enum.GetValue() { 148 if value.GetNumber() == 0 { 149 return value.GetName() 150 } 151 } 152 } 153 return nil 154 } 155 156 func getEnumDefaultNumber(reg *descriptor.Registry, enum *descriptor.Enum) interface{} { 157 if !reg.GetOmitEnumDefaultValue() { 158 for _, value := range enum.GetValue() { 159 if value.GetNumber() == 0 { 160 return int(value.GetNumber()) 161 } 162 } 163 } 164 return nil 165 } 166 167 // messageToQueryParameters converts a message to a list of OpenAPI query parameters. 168 func messageToQueryParameters(message *descriptor.Message, reg *descriptor.Registry, pathParams []descriptor.Parameter, body *descriptor.Body, httpMethod string) (params []openapiParameterObject, err error) { 169 for _, field := range message.Fields { 170 // When body is set to oneof field, we want to skip other fields in the oneof group. 171 if isBodySameOneOf(body, field) { 172 continue 173 } 174 175 if !isVisible(getFieldVisibilityOption(field), reg) { 176 continue 177 } 178 if reg.GetAllowPatchFeature() && field.GetTypeName() == ".google.protobuf.FieldMask" && field.GetName() == "update_mask" && httpMethod == "PATCH" && len(body.FieldPath) != 0 { 179 continue 180 } 181 182 p, err := queryParams(message, field, "", reg, pathParams, body, reg.GetRecursiveDepth()) 183 if err != nil { 184 return nil, err 185 } 186 params = append(params, p...) 187 } 188 return params, nil 189 } 190 191 func isBodySameOneOf(body *descriptor.Body, field *descriptor.Field) bool { 192 if field.OneofIndex == nil { 193 return false 194 } 195 196 if body == nil || len(body.FieldPath) == 0 { 197 return false 198 } 199 200 if body.FieldPath[0].Target.OneofIndex == nil { 201 return false 202 } 203 204 return *body.FieldPath[0].Target.OneofIndex == *field.OneofIndex 205 } 206 207 // queryParams converts a field to a list of OpenAPI query parameters recursively through the use of nestedQueryParams. 208 func queryParams(message *descriptor.Message, field *descriptor.Field, prefix string, reg *descriptor.Registry, pathParams []descriptor.Parameter, body *descriptor.Body, recursiveCount int) (params []openapiParameterObject, err error) { 209 return nestedQueryParams(message, field, prefix, reg, pathParams, body, newCycleChecker(recursiveCount)) 210 } 211 212 type cycleChecker struct { 213 m map[string]int 214 count int 215 } 216 217 func newCycleChecker(recursive int) *cycleChecker { 218 return &cycleChecker{ 219 m: make(map[string]int), 220 count: recursive, 221 } 222 } 223 224 // Check returns whether name is still within recursion 225 // toleration 226 func (c *cycleChecker) Check(name string) bool { 227 count, ok := c.m[name] 228 count += 1 229 isCycle := count > c.count 230 231 if isCycle { 232 return false 233 } 234 235 // provision map entry if not available 236 if !ok { 237 c.m[name] = 1 238 return true 239 } 240 241 c.m[name] = count 242 243 return true 244 } 245 246 func (c *cycleChecker) Branch() *cycleChecker { 247 copy := &cycleChecker{ 248 count: c.count, 249 m: make(map[string]int, len(c.m)), 250 } 251 252 for k, v := range c.m { 253 copy.m[k] = v 254 } 255 256 return copy 257 } 258 259 // nestedQueryParams converts a field to a list of OpenAPI query parameters recursively. 260 // This function is a helper function for queryParams, that keeps track of cyclical message references 261 // through the use of 262 // 263 // touched map[string]int 264 // 265 // If a cycle is discovered, an error is returned, as cyclical data structures are dangerous 266 // in query parameters. 267 func nestedQueryParams(message *descriptor.Message, field *descriptor.Field, prefix string, reg *descriptor.Registry, pathParams []descriptor.Parameter, body *descriptor.Body, cycle *cycleChecker) (params []openapiParameterObject, err error) { 268 // make sure the parameter is not already listed as a path parameter 269 for _, pathParam := range pathParams { 270 if pathParam.Target == field { 271 return nil, nil 272 } 273 } 274 // make sure the parameter is not already listed as a body parameter 275 if body != nil { 276 if body.FieldPath == nil { 277 return nil, nil 278 } 279 for _, fieldPath := range body.FieldPath { 280 if fieldPath.Target == field { 281 return nil, nil 282 } 283 } 284 } 285 schema := schemaOfField(field, reg, nil) 286 fieldType := field.GetTypeName() 287 if message.File != nil { 288 comments := fieldProtoComments(reg, message, field) 289 if err := updateOpenAPIDataFromComments(reg, &schema, message, comments, false); err != nil { 290 return nil, err 291 } 292 } 293 294 isEnum := field.GetType() == descriptorpb.FieldDescriptorProto_TYPE_ENUM 295 items := schema.Items 296 if schema.Type != "" || isEnum { 297 if schema.Type == "object" { 298 location := "" 299 if ix := strings.LastIndex(field.Message.FQMN(), "."); ix > 0 { 300 location = field.Message.FQMN()[0:ix] 301 } 302 if m, err := reg.LookupMsg(location, field.GetTypeName()); err == nil { 303 if opt := m.GetOptions(); opt != nil && opt.MapEntry != nil && *opt.MapEntry { 304 k := m.GetField()[0] 305 kType, err := getMapParamKey(k.GetType()) 306 if err != nil { 307 return nil, err 308 } 309 // This will generate a query in the format map_name[key_type] 310 fName := fmt.Sprintf("%s[%s]", *field.Name, kType) 311 field.Name = proto.String(fName) 312 schema.Type = schema.AdditionalProperties.schemaCore.Type 313 schema.Description = `This is a request variable of the map type. The query format is "map_name[key]=value", e.g. If the map name is Age, the key type is string, and the value type is integer, the query parameter is expressed as Age["bob"]=18` 314 } 315 } 316 } 317 if items != nil && (items.Type == "" || items.Type == "object") && !isEnum { 318 return nil, nil // TODO: currently, mapping object in query parameter is not supported 319 } 320 desc := mergeDescription(schema) 321 322 // verify if the field is required 323 required := false 324 for _, fieldName := range schema.Required { 325 if fieldName == reg.FieldName(field) { 326 required = true 327 break 328 } 329 } 330 // verify if the field is required in message options 331 if messageSchema, err := extractSchemaOptionFromMessageDescriptor(message.DescriptorProto); err == nil { 332 for _, fieldName := range messageSchema.GetJsonSchema().GetRequired() { 333 // Required fields can be field names or json_name values 334 if fieldName == field.GetJsonName() || fieldName == field.GetName() { 335 required = true 336 break 337 } 338 } 339 } 340 341 param := openapiParameterObject{ 342 Description: desc, 343 In: "query", 344 Default: schema.Default, 345 Type: schema.Type, 346 Items: schema.Items, 347 Format: schema.Format, 348 Pattern: schema.Pattern, 349 Required: required, 350 extensions: schema.extensions, 351 Enum: schema.Enum, 352 } 353 if param.Type == "array" { 354 param.CollectionFormat = "multi" 355 } 356 357 param.Name = prefix + reg.FieldName(field) 358 359 if isEnum { 360 enum, err := reg.LookupEnum("", fieldType) 361 if err != nil { 362 return nil, fmt.Errorf("unknown enum type %s", fieldType) 363 } 364 if items != nil { // array 365 param.Items = &openapiItemsObject{ 366 schemaCore: schemaCore{ 367 Type: "string", 368 Enum: listEnumNames(reg, enum), 369 }, 370 } 371 if reg.GetEnumsAsInts() { 372 param.Items.Type = "integer" 373 param.Items.Enum = listEnumNumbers(reg, enum) 374 } 375 } else { 376 param.Type = "string" 377 param.Enum = listEnumNames(reg, enum) 378 param.Default = getEnumDefault(reg, enum) 379 if reg.GetEnumsAsInts() { 380 param.Type = "integer" 381 param.Enum = listEnumNumbers(reg, enum) 382 param.Default = getEnumDefaultNumber(reg, enum) 383 } 384 } 385 valueComments := enumValueProtoComments(reg, enum) 386 if valueComments != "" { 387 param.Description = strings.TrimLeft(param.Description+"\n\n "+valueComments, "\n") 388 } 389 } 390 return []openapiParameterObject{param}, nil 391 } 392 393 // nested type, recurse 394 msg, err := reg.LookupMsg("", fieldType) 395 if err != nil { 396 return nil, fmt.Errorf("unknown message type %s", fieldType) 397 } 398 399 // Check for cyclical message reference: 400 if ok := cycle.Check(*msg.Name); !ok { 401 return nil, fmt.Errorf("exceeded recursive count (%d) for query parameter %q", cycle.count, fieldType) 402 } 403 404 // Construct a new map with the message name so a cycle further down the recursive path can be detected. 405 // Do not keep anything in the original touched reference and do not pass that reference along. This will 406 // prevent clobbering adjacent records while recursing. 407 touchedOut := cycle.Branch() 408 409 for _, nestedField := range msg.Fields { 410 if !isVisible(getFieldVisibilityOption(nestedField), reg) { 411 continue 412 } 413 414 fieldName := reg.FieldName(field) 415 p, err := nestedQueryParams(msg, nestedField, prefix+fieldName+".", reg, pathParams, body, touchedOut) 416 if err != nil { 417 return nil, err 418 } 419 params = append(params, p...) 420 } 421 return params, nil 422 } 423 424 func getMapParamKey(t descriptorpb.FieldDescriptorProto_Type) (string, error) { 425 tType, f, ok := primitiveSchema(t) 426 if !ok || f == "byte" || f == "float" || f == "double" { 427 return "", fmt.Errorf("unsupported type: %q", f) 428 } 429 return tType, nil 430 } 431 432 // findServicesMessagesAndEnumerations discovers all messages and enums defined in the RPC methods of the service. 433 func findServicesMessagesAndEnumerations(s []*descriptor.Service, reg *descriptor.Registry, m messageMap, ms messageMap, e enumMap, refs refMap) { 434 for _, svc := range s { 435 for _, meth := range svc.Methods { 436 // Request may be fully included in query 437 { 438 if !isVisible(getMethodVisibilityOption(meth), reg) { 439 continue 440 } 441 442 swgReqName, ok := fullyQualifiedNameToOpenAPIName(meth.RequestType.FQMN(), reg) 443 if !ok { 444 grpclog.Errorf("couldn't resolve OpenAPI name for FQMN %q", meth.RequestType.FQMN()) 445 continue 446 } 447 if _, ok := refs[fmt.Sprintf("#/definitions/%s", swgReqName)]; ok { 448 if !skipRenderingRef(meth.RequestType.FQMN()) { 449 m[swgReqName] = meth.RequestType 450 } 451 } 452 } 453 454 swgRspName, ok := fullyQualifiedNameToOpenAPIName(meth.ResponseType.FQMN(), reg) 455 if !ok && !skipRenderingRef(meth.ResponseType.FQMN()) { 456 grpclog.Errorf("couldn't resolve OpenAPI name for FQMN %q", meth.ResponseType.FQMN()) 457 continue 458 } 459 460 findNestedMessagesAndEnumerations(meth.RequestType, reg, m, e) 461 462 if !skipRenderingRef(meth.ResponseType.FQMN()) { 463 m[swgRspName] = meth.ResponseType 464 } 465 findNestedMessagesAndEnumerations(meth.ResponseType, reg, m, e) 466 } 467 } 468 } 469 470 // findNestedMessagesAndEnumerations those can be generated by the services. 471 func findNestedMessagesAndEnumerations(message *descriptor.Message, reg *descriptor.Registry, m messageMap, e enumMap) { 472 // Iterate over all the fields that 473 for _, t := range message.Fields { 474 if !isVisible(getFieldVisibilityOption(t), reg) { 475 continue 476 } 477 478 fieldType := t.GetTypeName() 479 // If the type is an empty string then it is a proto primitive 480 if fieldType != "" { 481 if _, ok := m[fieldType]; !ok { 482 msg, err := reg.LookupMsg("", fieldType) 483 if err != nil { 484 enum, err := reg.LookupEnum("", fieldType) 485 if err != nil { 486 panic(err) 487 } 488 e[fieldType] = enum 489 continue 490 } 491 m[fieldType] = msg 492 findNestedMessagesAndEnumerations(msg, reg, m, e) 493 } 494 } 495 } 496 } 497 498 func skipRenderingRef(refName string) bool { 499 _, ok := wktSchemas[refName] 500 return ok 501 } 502 503 func renderMessageAsDefinition(msg *descriptor.Message, reg *descriptor.Registry, customRefs refMap, pathParams []descriptor.Parameter) (openapiSchemaObject, error) { 504 schema := openapiSchemaObject{ 505 schemaCore: schemaCore{ 506 Type: "object", 507 }, 508 } 509 msgComments := protoComments(reg, msg.File, msg.Outers, "MessageType", int32(msg.Index)) 510 if err := updateOpenAPIDataFromComments(reg, &schema, msg, msgComments, false); err != nil { 511 return openapiSchemaObject{}, err 512 } 513 opts, err := getMessageOpenAPIOption(reg, msg) 514 if err != nil { 515 return openapiSchemaObject{}, err 516 } 517 if opts != nil { 518 protoSchema := openapiSchemaFromProtoSchema(opts, reg, customRefs, msg) 519 520 // Warning: Make sure not to overwrite any fields already set on the schema type. 521 schema.ExternalDocs = protoSchema.ExternalDocs 522 schema.ReadOnly = protoSchema.ReadOnly 523 schema.MultipleOf = protoSchema.MultipleOf 524 schema.Maximum = protoSchema.Maximum 525 schema.ExclusiveMaximum = protoSchema.ExclusiveMaximum 526 schema.Minimum = protoSchema.Minimum 527 schema.ExclusiveMinimum = protoSchema.ExclusiveMinimum 528 schema.MaxLength = protoSchema.MaxLength 529 schema.MinLength = protoSchema.MinLength 530 schema.Pattern = protoSchema.Pattern 531 schema.Default = protoSchema.Default 532 schema.MaxItems = protoSchema.MaxItems 533 schema.MinItems = protoSchema.MinItems 534 schema.UniqueItems = protoSchema.UniqueItems 535 schema.MaxProperties = protoSchema.MaxProperties 536 schema.MinProperties = protoSchema.MinProperties 537 schema.Required = protoSchema.Required 538 schema.XNullable = protoSchema.XNullable 539 schema.extensions = protoSchema.extensions 540 if protoSchema.schemaCore.Type != "" || protoSchema.schemaCore.Ref != "" { 541 schema.schemaCore = protoSchema.schemaCore 542 } 543 if protoSchema.Title != "" { 544 schema.Title = protoSchema.Title 545 } 546 if protoSchema.Description != "" { 547 schema.Description = protoSchema.Description 548 } 549 if protoSchema.Example != nil { 550 schema.Example = protoSchema.Example 551 } 552 } 553 554 schema.Required = filterOutExcludedFields(schema.Required, pathParams) 555 556 for _, f := range msg.Fields { 557 if !isVisible(getFieldVisibilityOption(f), reg) { 558 continue 559 } 560 561 if shouldExcludeField(f.GetName(), pathParams) { 562 continue 563 } 564 subPathParams := subPathParams(f.GetName(), pathParams) 565 fieldSchema, err := renderFieldAsDefinition(f, reg, customRefs, subPathParams) 566 if err != nil { 567 return openapiSchemaObject{}, err 568 } 569 comments := fieldProtoComments(reg, msg, f) 570 if err := updateOpenAPIDataFromComments(reg, &fieldSchema, f, comments, false); err != nil { 571 return openapiSchemaObject{}, err 572 } 573 574 if requiredIdx := find(schema.Required, *f.Name); requiredIdx != -1 && reg.GetUseJSONNamesForFields() { 575 schema.Required[requiredIdx] = f.GetJsonName() 576 } 577 578 if fieldSchema.Required != nil { 579 schema.Required = getUniqueFields(schema.Required, fieldSchema.Required) 580 schema.Required = append(schema.Required, fieldSchema.Required...) 581 // To avoid populating both the field schema require and message schema require, unset the field schema require. 582 // See issue #2635. 583 fieldSchema.Required = nil 584 } 585 586 if reg.GetUseAllOfForRefs() { 587 if fieldSchema.Ref != "" { 588 // Per the JSON Reference syntax: Any members other than "$ref" in a JSON Reference object SHALL be ignored. 589 // https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03#section-3 590 // However, use allOf to specify Title/Description/Example/readOnly fields. 591 if fieldSchema.Title != "" || fieldSchema.Description != "" || len(fieldSchema.Example) > 0 || fieldSchema.ReadOnly { 592 fieldSchema = openapiSchemaObject{ 593 Title: fieldSchema.Title, 594 Description: fieldSchema.Description, 595 schemaCore: schemaCore{ 596 Example: fieldSchema.Example, 597 }, 598 ReadOnly: fieldSchema.ReadOnly, 599 AllOf: []allOfEntry{{Ref: fieldSchema.Ref}}, 600 } 601 } else { 602 fieldSchema = openapiSchemaObject{schemaCore: schemaCore{Ref: fieldSchema.Ref}} 603 } 604 } 605 } 606 607 kv := keyVal{Value: fieldSchema} 608 kv.Key = reg.FieldName(f) 609 if schema.Properties == nil { 610 schema.Properties = &openapiSchemaObjectProperties{} 611 } 612 *schema.Properties = append(*schema.Properties, kv) 613 } 614 615 if msg.FQMN() == ".google.protobuf.Any" { 616 transformAnyForJSON(&schema, reg.GetUseJSONNamesForFields()) 617 } 618 619 return schema, nil 620 } 621 622 func renderFieldAsDefinition(f *descriptor.Field, reg *descriptor.Registry, refs refMap, pathParams []descriptor.Parameter) (openapiSchemaObject, error) { 623 if len(pathParams) == 0 { 624 return schemaOfField(f, reg, refs), nil 625 } 626 location := "" 627 if ix := strings.LastIndex(f.Message.FQMN(), "."); ix > 0 { 628 location = f.Message.FQMN()[0:ix] 629 } 630 msg, err := reg.LookupMsg(location, f.GetTypeName()) 631 if err != nil { 632 return openapiSchemaObject{}, err 633 } 634 schema, err := renderMessageAsDefinition(msg, reg, refs, pathParams) 635 if err != nil { 636 return openapiSchemaObject{}, err 637 } 638 comments := fieldProtoComments(reg, f.Message, f) 639 if len(comments) > 0 { 640 // Use title and description from field instead of nested message if present. 641 paragraphs := strings.Split(comments, paragraphDeliminator) 642 schema.Title = strings.TrimSpace(paragraphs[0]) 643 schema.Description = strings.TrimSpace(strings.Join(paragraphs[1:], paragraphDeliminator)) 644 } 645 return schema, nil 646 } 647 648 // transformAnyForJSON should be called when the schema object represents a google.protobuf.Any, and will replace the 649 // Properties slice with a single value for '@type'. We mutate the incorrectly named field so that we inherit the same 650 // documentation as specified on the original field in the protobuf descriptors. 651 func transformAnyForJSON(schema *openapiSchemaObject, useJSONNames bool) { 652 var typeFieldName string 653 if useJSONNames { 654 typeFieldName = "typeUrl" 655 } else { 656 typeFieldName = "type_url" 657 } 658 659 for _, property := range *schema.Properties { 660 if property.Key == typeFieldName { 661 schema.AdditionalProperties = &openapiSchemaObject{} 662 schema.Properties = &openapiSchemaObjectProperties{keyVal{ 663 Key: "@type", 664 Value: property.Value, 665 }} 666 break 667 } 668 } 669 } 670 671 func renderMessagesAsDefinition(messages messageMap, d openapiDefinitionsObject, reg *descriptor.Registry, customRefs refMap, pathParams []descriptor.Parameter) error { 672 for name, msg := range messages { 673 swgName, ok := fullyQualifiedNameToOpenAPIName(msg.FQMN(), reg) 674 if !ok { 675 return fmt.Errorf("can't resolve OpenAPI name from %q", msg.FQMN()) 676 } 677 if skipRenderingRef(name) { 678 continue 679 } 680 681 if opt := msg.GetOptions(); opt != nil && opt.MapEntry != nil && *opt.MapEntry { 682 continue 683 } 684 var err error 685 d[swgName], err = renderMessageAsDefinition(msg, reg, customRefs, pathParams) 686 if err != nil { 687 return err 688 } 689 } 690 return nil 691 } 692 693 // isVisible checks if a field/RPC is visible based on the visibility restriction 694 // combined with the `visibility_restriction_selectors`. 695 // Elements with an overlap on `visibility_restriction_selectors` are visible, those without are not visible. 696 // Elements without `google.api.VisibilityRule` annotations entirely are always visible. 697 func isVisible(r *visibility.VisibilityRule, reg *descriptor.Registry) bool { 698 if r == nil { 699 return true 700 } 701 702 restrictions := strings.Split(strings.TrimSpace(r.Restriction), ",") 703 // No restrictions results in the element always being visible 704 if len(restrictions) == 0 { 705 return true 706 } 707 708 for _, restriction := range restrictions { 709 if reg.GetVisibilityRestrictionSelectors()[strings.TrimSpace(restriction)] { 710 return true 711 } 712 } 713 714 return false 715 } 716 717 func shouldExcludeField(name string, excluded []descriptor.Parameter) bool { 718 for _, p := range excluded { 719 if len(p.FieldPath) == 1 && name == p.FieldPath[0].Name { 720 return true 721 } 722 } 723 return false 724 } 725 func filterOutExcludedFields(fields []string, excluded []descriptor.Parameter) []string { 726 var filtered []string 727 for _, f := range fields { 728 if !shouldExcludeField(f, excluded) { 729 filtered = append(filtered, f) 730 } 731 } 732 return filtered 733 } 734 735 // schemaOfField returns a OpenAPI Schema Object for a protobuf field. 736 func schemaOfField(f *descriptor.Field, reg *descriptor.Registry, refs refMap) openapiSchemaObject { 737 const ( 738 singular = 0 739 array = 1 740 object = 2 741 ) 742 var ( 743 core schemaCore 744 aggregate int 745 ) 746 747 fd := f.FieldDescriptorProto 748 location := "" 749 if ix := strings.LastIndex(f.Message.FQMN(), "."); ix > 0 { 750 location = f.Message.FQMN()[0:ix] 751 } 752 if m, err := reg.LookupMsg(location, f.GetTypeName()); err == nil { 753 if opt := m.GetOptions(); opt != nil && opt.MapEntry != nil && *opt.MapEntry { 754 fd = m.GetField()[1] 755 aggregate = object 756 } 757 } 758 if fd.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REPEATED { 759 aggregate = array 760 } 761 762 var props *openapiSchemaObjectProperties 763 764 switch ft := fd.GetType(); ft { 765 case descriptorpb.FieldDescriptorProto_TYPE_ENUM, descriptorpb.FieldDescriptorProto_TYPE_MESSAGE, descriptorpb.FieldDescriptorProto_TYPE_GROUP: 766 if wktSchema, ok := wktSchemas[fd.GetTypeName()]; ok { 767 core = wktSchema 768 if fd.GetTypeName() == ".google.protobuf.Empty" { 769 props = &openapiSchemaObjectProperties{} 770 } 771 } else { 772 swgRef, ok := fullyQualifiedNameToOpenAPIName(fd.GetTypeName(), reg) 773 if !ok { 774 panic(fmt.Sprintf("can't resolve OpenAPI ref from typename %q", fd.GetTypeName())) 775 } 776 core = schemaCore{ 777 Ref: "#/definitions/" + swgRef, 778 } 779 if refs != nil { 780 refs[fd.GetTypeName()] = struct{}{} 781 } 782 } 783 default: 784 ftype, format, ok := primitiveSchema(ft) 785 if ok { 786 core = schemaCore{Type: ftype, Format: format} 787 } else { 788 core = schemaCore{Type: ft.String(), Format: "UNKNOWN"} 789 } 790 } 791 792 ret := openapiSchemaObject{} 793 794 switch aggregate { 795 case array: 796 if _, ok := wktSchemas[fd.GetTypeName()]; !ok && fd.GetType() == descriptorpb.FieldDescriptorProto_TYPE_MESSAGE { 797 core.Type = "object" 798 } 799 ret = openapiSchemaObject{ 800 schemaCore: schemaCore{ 801 Type: "array", 802 Items: (*openapiItemsObject)(&openapiSchemaObject{schemaCore: core}), 803 }, 804 } 805 case object: 806 ret = openapiSchemaObject{ 807 schemaCore: schemaCore{ 808 Type: "object", 809 }, 810 AdditionalProperties: &openapiSchemaObject{Properties: props, schemaCore: core}, 811 } 812 default: 813 ret = openapiSchemaObject{ 814 schemaCore: core, 815 Properties: props, 816 } 817 } 818 819 if j, err := getFieldOpenAPIOption(reg, f); err == nil { 820 updateswaggerObjectFromJSONSchema(&ret, j, reg, f) 821 } 822 823 if j, err := getFieldBehaviorOption(reg, f); err == nil { 824 updateSwaggerObjectFromFieldBehavior(&ret, j, reg, f) 825 } 826 827 for i, required := range ret.Required { 828 if required == f.GetName() { 829 ret.Required[i] = reg.FieldName(f) 830 } 831 } 832 833 if reg.GetProto3OptionalNullable() && f.GetProto3Optional() { 834 ret.XNullable = true 835 } 836 837 return ret 838 } 839 840 // primitiveSchema returns a pair of "Type" and "Format" in JSON Schema for 841 // the given primitive field type. 842 // The last return parameter is true iff the field type is actually primitive. 843 func primitiveSchema(t descriptorpb.FieldDescriptorProto_Type) (ftype, format string, ok bool) { 844 switch t { 845 case descriptorpb.FieldDescriptorProto_TYPE_DOUBLE: 846 return "number", "double", true 847 case descriptorpb.FieldDescriptorProto_TYPE_FLOAT: 848 return "number", "float", true 849 case descriptorpb.FieldDescriptorProto_TYPE_INT64: 850 return "string", "int64", true 851 case descriptorpb.FieldDescriptorProto_TYPE_UINT64: 852 // 64bit integer types are marshaled as string in the default JSONPb marshaler. 853 // TODO(yugui) Add an option to declare 64bit integers as int64. 854 // 855 // NOTE: uint64 is not a predefined format of integer type in OpenAPI spec. 856 // So we cannot expect that uint64 is commonly supported by OpenAPI processor. 857 return "string", "uint64", true 858 case descriptorpb.FieldDescriptorProto_TYPE_INT32: 859 return "integer", "int32", true 860 case descriptorpb.FieldDescriptorProto_TYPE_FIXED64: 861 // Ditto. 862 return "string", "uint64", true 863 case descriptorpb.FieldDescriptorProto_TYPE_FIXED32: 864 // Ditto. 865 return "integer", "int64", true 866 case descriptorpb.FieldDescriptorProto_TYPE_BOOL: 867 // NOTE: in OpenAPI specification, format should be empty on boolean type 868 return "boolean", "", true 869 case descriptorpb.FieldDescriptorProto_TYPE_STRING: 870 // NOTE: in OpenAPI specification, can be empty on string type 871 // see: https://swagger.io/specification/v2/#data-types 872 return "string", "", true 873 case descriptorpb.FieldDescriptorProto_TYPE_BYTES: 874 return "string", "byte", true 875 case descriptorpb.FieldDescriptorProto_TYPE_UINT32: 876 // Ditto. 877 return "integer", "int64", true 878 case descriptorpb.FieldDescriptorProto_TYPE_SFIXED32: 879 return "integer", "int32", true 880 case descriptorpb.FieldDescriptorProto_TYPE_SFIXED64: 881 return "string", "int64", true 882 case descriptorpb.FieldDescriptorProto_TYPE_SINT32: 883 return "integer", "int32", true 884 case descriptorpb.FieldDescriptorProto_TYPE_SINT64: 885 return "string", "int64", true 886 default: 887 return "", "", false 888 } 889 } 890 891 // renderEnumerationsAsDefinition inserts enums into the definitions object. 892 func renderEnumerationsAsDefinition(enums enumMap, d openapiDefinitionsObject, reg *descriptor.Registry) { 893 for _, enum := range enums { 894 swgName, ok := fullyQualifiedNameToOpenAPIName(enum.FQEN(), reg) 895 if !ok { 896 panic(fmt.Sprintf("can't resolve OpenAPI name from FQEN %q", enum.FQEN())) 897 } 898 enumComments := protoComments(reg, enum.File, enum.Outers, "EnumType", int32(enum.Index)) 899 900 // it may be necessary to sort the result of the GetValue function. 901 enumNames := listEnumNames(reg, enum) 902 defaultValue := getEnumDefault(reg, enum) 903 valueComments := enumValueProtoComments(reg, enum) 904 if valueComments != "" { 905 enumComments = strings.TrimLeft(enumComments+"\n\n "+valueComments, "\n") 906 } 907 enumSchemaObject := openapiSchemaObject{ 908 schemaCore: schemaCore{ 909 Type: "string", 910 Enum: enumNames, 911 Default: defaultValue, 912 }, 913 } 914 if reg.GetEnumsAsInts() { 915 enumSchemaObject.Type = "integer" 916 enumSchemaObject.Format = "int32" 917 enumSchemaObject.Default = getEnumDefaultNumber(reg, enum) 918 enumSchemaObject.Enum = listEnumNumbers(reg, enum) 919 } 920 if err := updateOpenAPIDataFromComments(reg, &enumSchemaObject, enum, enumComments, false); err != nil { 921 panic(err) 922 } 923 924 d[swgName] = enumSchemaObject 925 } 926 } 927 928 // Take in a FQMN or FQEN and return a OpenAPI safe version of the FQMN and 929 // a boolean indicating if FQMN was properly resolved. 930 func fullyQualifiedNameToOpenAPIName(fqn string, reg *descriptor.Registry) (string, bool) { 931 registriesSeenMutex.Lock() 932 defer registriesSeenMutex.Unlock() 933 if mapping, present := registriesSeen[reg]; present { 934 ret, ok := mapping[fqn] 935 return ret, ok 936 } 937 mapping := resolveFullyQualifiedNameToOpenAPINames(append(reg.GetAllFQMNs(), append(reg.GetAllFQENs(), reg.GetAllFQMethNs()...)...), reg.GetOpenAPINamingStrategy()) 938 registriesSeen[reg] = mapping 939 ret, ok := mapping[fqn] 940 return ret, ok 941 } 942 943 // Lookup message type by location.name and return a openapiv2-safe version 944 // of its FQMN. 945 func lookupMsgAndOpenAPIName(location, name string, reg *descriptor.Registry) (*descriptor.Message, string, error) { 946 msg, err := reg.LookupMsg(location, name) 947 if err != nil { 948 return nil, "", err 949 } 950 swgName, ok := fullyQualifiedNameToOpenAPIName(msg.FQMN(), reg) 951 if !ok { 952 return nil, "", fmt.Errorf("can't map OpenAPI name from FQMN %q", msg.FQMN()) 953 } 954 return msg, swgName, nil 955 } 956 957 // registriesSeen is used to memoise calls to resolveFullyQualifiedNameToOpenAPINames so 958 // we don't repeat it unnecessarily, since it can take some time. 959 var registriesSeen = map[*descriptor.Registry]map[string]string{} 960 var registriesSeenMutex sync.Mutex 961 962 // Take the names of every proto message and generate a unique reference for each, according to the given strategy. 963 func resolveFullyQualifiedNameToOpenAPINames(messages []string, namingStrategy string) map[string]string { 964 strategyFn := LookupNamingStrategy(namingStrategy) 965 if strategyFn == nil { 966 return nil 967 } 968 return strategyFn(messages) 969 } 970 971 var canRegexp = regexp.MustCompile("{([a-zA-Z][a-zA-Z0-9_.]*)([^}]*)}") 972 973 // templateToParts will split a URL template as defined by https://github.com/googleapis/googleapis/blob/master/google/api/http.proto 974 // into a string slice with each part as an element of the slice for use by `partsToOpenAPIPath` and `partsToRegexpMap`. 975 func templateToParts(path string, reg *descriptor.Registry, fields []*descriptor.Field, msgs []*descriptor.Message) []string { 976 // It seems like the right thing to do here is to just use 977 // strings.Split(path, "/") but that breaks badly when you hit a url like 978 // /{my_field=prefix/*}/ and end up with 2 sections representing my_field. 979 // Instead do the right thing and write a small pushdown (counter) automata 980 // for it. 981 var parts []string 982 depth := 0 983 buffer := "" 984 jsonBuffer := "" 985 pathLoop: 986 for i, char := range path { 987 switch char { 988 case '{': 989 // Push on the stack 990 depth++ 991 buffer += string(char) 992 jsonBuffer = "" 993 jsonBuffer += string(char) 994 case '}': 995 if depth == 0 { 996 panic("Encountered } without matching { before it.") 997 } 998 // Pop from the stack 999 depth-- 1000 buffer += string(char) 1001 if reg.GetUseJSONNamesForFields() && 1002 len(jsonBuffer) > 1 { 1003 jsonSnakeCaseName := jsonBuffer[1:] 1004 jsonCamelCaseName := lowerCamelCase(jsonSnakeCaseName, fields, msgs) 1005 prev := buffer[:len(buffer)-len(jsonSnakeCaseName)-2] 1006 buffer = strings.Join([]string{prev, "{", jsonCamelCaseName, "}"}, "") 1007 jsonBuffer = "" 1008 } 1009 case '/': 1010 if depth == 0 { 1011 parts = append(parts, buffer) 1012 buffer = "" 1013 // Since the stack was empty when we hit the '/' we are done with this 1014 // section. 1015 continue 1016 } 1017 buffer += string(char) 1018 jsonBuffer += string(char) 1019 case ':': 1020 if depth == 0 { 1021 // As soon as we find a ":" outside a variable, 1022 // everything following is a verb 1023 parts = append(parts, buffer) 1024 buffer = path[i:] 1025 break pathLoop 1026 } 1027 buffer += string(char) 1028 jsonBuffer += string(char) 1029 default: 1030 buffer += string(char) 1031 jsonBuffer += string(char) 1032 } 1033 } 1034 1035 // Now append the last element to parts 1036 parts = append(parts, buffer) 1037 1038 return parts 1039 } 1040 1041 // partsToOpenAPIPath converts each path part of the form /path/{string_value=strprefix/*} which is defined in 1042 // https://github.com/googleapis/googleapis/blob/master/google/api/http.proto to the OpenAPI expected form /path/{string_value}. 1043 // For example this would replace the path segment of "{foo=bar/*}" with "{foo}" or "prefix{bang=bash/**}" with "prefix{bang}". 1044 // OpenAPI 2 only allows simple path parameters with the constraints on that parameter specified in the OpenAPI 1045 // schema's "pattern" instead of in the path parameter itself. 1046 func partsToOpenAPIPath(parts []string, overrides map[string]string) string { 1047 for index, part := range parts { 1048 part = canRegexp.ReplaceAllString(part, "{$1}") 1049 if override, ok := overrides[part]; ok { 1050 part = override 1051 } 1052 parts[index] = part 1053 } 1054 if last := len(parts) - 1; strings.HasPrefix(parts[last], ":") { 1055 // Last item is a verb (":" LITERAL). 1056 return strings.Join(parts[:last], "/") + parts[last] 1057 } 1058 return strings.Join(parts, "/") 1059 } 1060 1061 // partsToRegexpMap returns a map of parameter name to ECMA 262 patterns 1062 // which is what the "pattern" field on an OpenAPI parameter expects. 1063 // See https://swagger.io/specification/v2/ (Parameter Object) and 1064 // https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.3. 1065 // The expression is generated based on expressions defined by https://github.com/googleapis/googleapis/blob/master/google/api/http.proto 1066 // "Path Template Syntax" section which allow for a "param_name=foobar/*/bang/**" style expressions inside 1067 // the path parameter placeholders that indicate constraints on the values of those parameters. 1068 // This function will scan the split parts of a path template for parameters and 1069 // outputs a map of the name of the parameter to a ECMA regular expression. See the http.proto file for descriptions 1070 // of the supported syntax. This function will ignore any path parameters that don't contain a "=" after the 1071 // parameter name. For supported parameters, we assume "*" represent all characters except "/" as it's 1072 // intended to match a single path element and we assume "**" matches any character as it's intended to match multiple 1073 // path elements. 1074 // For example "{name=organizations/*/roles/*}" would produce the regular expression for the "name" parameter of 1075 // "organizations/[^/]+/roles/[^/]+" or "{bar=bing/*/bang/**}" would produce the regular expression for the "bar" 1076 // parameter of "bing/[^/]+/bang/.+". 1077 // 1078 // Note that OpenAPI does not actually support path parameters with "/", see https://github.com/OAI/OpenAPI-Specification/issues/892 1079 func partsToRegexpMap(parts []string) map[string]string { 1080 regExps := make(map[string]string) 1081 for _, part := range parts { 1082 if strings.Contains(part, "/") { 1083 grpclog.Warningf("Path parameter %q contains '/', which is not supported in OpenAPI", part) 1084 } 1085 if submatch := canRegexp.FindStringSubmatch(part); len(submatch) > 2 { 1086 if strings.HasPrefix(submatch[2], "=") { // this part matches the standard and should be made into a regular expression 1087 // assume the string's characters other than "**" and "*" are literals (not necessarily a good assumption 100% of the times, but it will support most use cases) 1088 regex := submatch[2][1:] 1089 regex = strings.ReplaceAll(regex, "**", ".+") // ** implies any character including "/" 1090 regex = strings.ReplaceAll(regex, "*", "[^/]+") // * implies any character except "/" 1091 regExps[submatch[1]] = regex 1092 } 1093 } 1094 } 1095 return regExps 1096 } 1097 1098 func renderServiceTags(services []*descriptor.Service, reg *descriptor.Registry) []openapiTagObject { 1099 var tags []openapiTagObject 1100 for _, svc := range services { 1101 if !isVisible(getServiceVisibilityOption(svc), reg) { 1102 continue 1103 } 1104 tagName := svc.GetName() 1105 if pkg := svc.File.GetPackage(); pkg != "" && reg.IsIncludePackageInTags() { 1106 tagName = pkg + "." + tagName 1107 } 1108 1109 tag := openapiTagObject{ 1110 Name: tagName, 1111 } 1112 1113 opts, err := getServiceOpenAPIOption(reg, svc) 1114 if err != nil { 1115 grpclog.Error(err) 1116 return nil 1117 } 1118 if opts != nil { 1119 tag.Description = opts.Description 1120 if reg.GetUseGoTemplate() { 1121 tag.Description = goTemplateComments(tag.Description, svc, reg) 1122 } 1123 if opts.ExternalDocs != nil { 1124 tag.ExternalDocs = &openapiExternalDocumentationObject{ 1125 Description: opts.ExternalDocs.Description, 1126 URL: opts.ExternalDocs.Url, 1127 } 1128 if reg.GetUseGoTemplate() { 1129 tag.ExternalDocs.Description = goTemplateComments(opts.ExternalDocs.Description, svc, reg) 1130 } 1131 } 1132 if opts.GetName() != "" { 1133 tag.Name = opts.GetName() 1134 } 1135 } 1136 tags = append(tags, tag) 1137 } 1138 return tags 1139 } 1140 1141 func renderServices(services []*descriptor.Service, paths *openapiPathsObject, reg *descriptor.Registry, requestResponseRefs, customRefs refMap, msgs []*descriptor.Message, defs openapiDefinitionsObject) error { 1142 // Correctness of svcIdx and methIdx depends on 'services' containing the services in the same order as the 'file.Service' array. 1143 svcBaseIdx := 0 1144 var lastFile *descriptor.File = nil 1145 for svcIdx, svc := range services { 1146 if svc.File != lastFile { 1147 lastFile = svc.File 1148 svcBaseIdx = svcIdx 1149 } 1150 1151 if !isVisible(getServiceVisibilityOption(svc), reg) { 1152 continue 1153 } 1154 1155 for methIdx, meth := range svc.Methods { 1156 if !isVisible(getMethodVisibilityOption(meth), reg) { 1157 continue 1158 } 1159 1160 for bIdx, b := range meth.Bindings { 1161 operationFunc := operationForMethod(b.HTTPMethod) 1162 // Iterate over all the OpenAPI parameters 1163 parameters := openapiParametersObject{} 1164 // split the path template into its parts 1165 parts := templateToParts(b.PathTmpl.Template, reg, meth.RequestType.Fields, msgs) 1166 // extract any constraints specified in the path placeholders into ECMA regular expressions 1167 pathParamRegexpMap := partsToRegexpMap(parts) 1168 // Keep track of path parameter overrides 1169 var pathParamNames = make(map[string]string) 1170 for _, parameter := range b.PathParams { 1171 1172 var paramType, paramFormat, desc, collectionFormat string 1173 var defaultValue interface{} 1174 var enumNames interface{} 1175 var items *openapiItemsObject 1176 var minItems *int 1177 var extensions []extension 1178 switch pt := parameter.Target.GetType(); pt { 1179 case descriptorpb.FieldDescriptorProto_TYPE_GROUP, descriptorpb.FieldDescriptorProto_TYPE_MESSAGE: 1180 if descriptor.IsWellKnownType(parameter.Target.GetTypeName()) { 1181 if parameter.IsRepeated() { 1182 return errors.New("only primitive and enum types are allowed in repeated path parameters") 1183 } 1184 schema := schemaOfField(parameter.Target, reg, customRefs) 1185 paramType = schema.Type 1186 paramFormat = schema.Format 1187 desc = schema.Description 1188 defaultValue = schema.Default 1189 extensions = schema.extensions 1190 } else { 1191 return errors.New("only primitive and well-known types are allowed in path parameters") 1192 } 1193 case descriptorpb.FieldDescriptorProto_TYPE_ENUM: 1194 enum, err := reg.LookupEnum("", parameter.Target.GetTypeName()) 1195 if err != nil { 1196 return err 1197 } 1198 paramType = "string" 1199 paramFormat = "" 1200 enumNames = listEnumNames(reg, enum) 1201 if reg.GetEnumsAsInts() { 1202 paramType = "integer" 1203 paramFormat = "" 1204 enumNames = listEnumNumbers(reg, enum) 1205 } 1206 1207 schema := schemaOfField(parameter.Target, reg, customRefs) 1208 desc = schema.Description 1209 defaultValue = schema.Default 1210 extensions = schema.extensions 1211 default: 1212 var ok bool 1213 paramType, paramFormat, ok = primitiveSchema(pt) 1214 if !ok { 1215 return fmt.Errorf("unknown field type %v", pt) 1216 } 1217 1218 schema := schemaOfField(parameter.Target, reg, customRefs) 1219 desc = schema.Description 1220 defaultValue = schema.Default 1221 extensions = schema.extensions 1222 // If there is no mandatory format based on the field, 1223 // allow it to be overridden by the user 1224 if paramFormat == "" { 1225 paramFormat = schema.Format 1226 } 1227 } 1228 1229 if parameter.IsRepeated() { 1230 core := schemaCore{Type: paramType, Format: paramFormat} 1231 if parameter.IsEnum() { 1232 core.Enum = enumNames 1233 enumNames = nil 1234 } 1235 items = (*openapiItemsObject)(&openapiSchemaObject{schemaCore: core}) 1236 paramType = "array" 1237 paramFormat = "" 1238 collectionFormat = reg.GetRepeatedPathParamSeparatorName() 1239 minItems = new(int) 1240 *minItems = 1 1241 } 1242 1243 if desc == "" { 1244 desc = fieldProtoComments(reg, parameter.Target.Message, parameter.Target) 1245 } 1246 parameterString := parameter.String() 1247 if reg.GetUseJSONNamesForFields() { 1248 parameterString = lowerCamelCase(parameterString, meth.RequestType.Fields, msgs) 1249 } 1250 var pattern string 1251 if regExp, ok := pathParamRegexpMap[parameterString]; ok { 1252 pattern = regExp 1253 } 1254 if fc := getFieldConfiguration(reg, parameter.Target); fc != nil { 1255 pathParamName := fc.GetPathParamName() 1256 if pathParamName != "" && pathParamName != parameterString { 1257 pathParamNames["{"+parameterString+"}"] = "{" + pathParamName + "}" 1258 parameterString, _, _ = strings.Cut(pathParamName, "=") 1259 } 1260 } 1261 parameters = append(parameters, openapiParameterObject{ 1262 Name: parameterString, 1263 Description: desc, 1264 In: "path", 1265 Required: true, 1266 Default: defaultValue, 1267 // Parameters in gRPC-Gateway can only be strings? 1268 Type: paramType, 1269 Format: paramFormat, 1270 Enum: enumNames, 1271 Items: items, 1272 CollectionFormat: collectionFormat, 1273 MinItems: minItems, 1274 Pattern: pattern, 1275 extensions: extensions, 1276 }) 1277 } 1278 // Now check if there is a body parameter 1279 if b.Body != nil { 1280 // Recursively render fields as definitions as long as they contain path parameters. 1281 // Special case for top level body if we don't have a body field. 1282 var schema openapiSchemaObject 1283 desc := "" 1284 var bodyFieldName string 1285 schema = openapiSchemaObject{ 1286 schemaCore: schemaCore{}, 1287 } 1288 if len(b.Body.FieldPath) == 0 { 1289 // No field for body, use type. 1290 bodyFieldName = "body" 1291 wknSchemaCore, isWkn := wktSchemas[meth.RequestType.FQMN()] 1292 if isWkn { 1293 schema.schemaCore = wknSchemaCore 1294 // 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. 1295 if meth.RequestType.FQMN() == ".google.protobuf.Empty" { 1296 schema.Properties = &openapiSchemaObjectProperties{} 1297 } 1298 } else { 1299 messageSchema, err := renderMessageAsDefinition(meth.RequestType, reg, customRefs, b.PathParams) 1300 if err != nil { 1301 return err 1302 } 1303 if len(b.PathParams) == 0 { 1304 if err := schema.setRefFromFQN(meth.RequestType.FQMN(), reg); err != nil { 1305 return err 1306 } 1307 desc = messageSchema.Description 1308 } else { 1309 if meth.Name != nil { 1310 methFQN, ok := fullyQualifiedNameToOpenAPIName(meth.FQMN(), reg) 1311 if !ok { 1312 panic(fmt.Errorf("failed to resolve method FQN: '%s'", meth.FQMN())) 1313 } 1314 defName := methFQN + "Body" 1315 schema.Ref = fmt.Sprintf("#/definitions/%s", defName) 1316 defs[defName] = messageSchema 1317 } else { 1318 schema = messageSchema 1319 if schema.Properties == nil || len(*schema.Properties) == 0 { 1320 grpclog.Warningf("created a body with 0 properties in the message, this might be unintended: %s", *meth.RequestType) 1321 } 1322 } 1323 } 1324 } 1325 } else { 1326 // Body field path is limited to one path component. From google.api.HttpRule.body: 1327 // "NOTE: the referred field must be present at the top-level of the request message type." 1328 // Ref: https://github.com/googleapis/googleapis/blob/b3397f5febbf21dfc69b875ddabaf76bee765058/google/api/http.proto#L350-L352 1329 if len(b.Body.FieldPath) > 1 { 1330 return fmt.Errorf("Body of request %q is not a top level field: '%v'.", meth.Service.GetName(), b.Body.FieldPath) 1331 } 1332 bodyField := b.Body.FieldPath[0] 1333 if reg.GetUseJSONNamesForFields() { 1334 bodyFieldName = lowerCamelCase(bodyField.Name, meth.RequestType.Fields, msgs) 1335 } else { 1336 bodyFieldName = bodyField.Name 1337 } 1338 // Align pathParams with body field path. 1339 pathParams := subPathParams(bodyField.Name, b.PathParams) 1340 var err error 1341 schema, err = renderFieldAsDefinition(bodyField.Target, reg, customRefs, pathParams) 1342 if err != nil { 1343 return err 1344 } 1345 if schema.Title != "" { 1346 desc = mergeDescription(schema) 1347 } else { 1348 desc = fieldProtoComments(reg, bodyField.Target.Message, bodyField.Target) 1349 } 1350 } 1351 1352 if meth.GetClientStreaming() { 1353 desc += " (streaming inputs)" 1354 } 1355 parameters = append(parameters, openapiParameterObject{ 1356 Name: bodyFieldName, 1357 Description: desc, 1358 In: "body", 1359 Required: true, 1360 Schema: &schema, 1361 }) 1362 } 1363 1364 // add the parameters to the query string 1365 queryParams, err := messageToQueryParameters(meth.RequestType, reg, b.PathParams, b.Body, b.HTTPMethod) 1366 if err != nil { 1367 return err 1368 } 1369 parameters = append(parameters, queryParams...) 1370 1371 path := partsToOpenAPIPath(parts, pathParamNames) 1372 1373 pathItemObject, ok := getPathItemObject(*paths, path) 1374 1375 if !ok { 1376 pathItemObject = openapiPathItemObject{} 1377 } else { 1378 // handle case where we have an existing mapping for the same path and method 1379 existingOperationObject := operationFunc(&pathItemObject) 1380 if existingOperationObject != nil { 1381 var firstPathParameter *openapiParameterObject 1382 var firstParamIndex int 1383 for index, param := range parameters { 1384 param := param 1385 if param.In == "path" { 1386 firstPathParameter = ¶m 1387 firstParamIndex = index 1388 break 1389 } 1390 } 1391 if firstPathParameter == nil { 1392 // Without a path parameter, there is nothing to vary to support multiple mappings of the same path/method. 1393 // Previously this did not log an error and only overwrote the mapping, we now log the error but 1394 // still overwrite the mapping 1395 grpclog.Errorf("Duplicate mapping for path %s %s", b.HTTPMethod, path) 1396 } else { 1397 newPathCount := 0 1398 var newPath string 1399 var newPathElement string 1400 // Iterate until there is not an existing operation that matches the same escaped path. 1401 // Most of the time this will only be a single iteration, but a large API could technically have 1402 // a pretty large amount of these if it used similar patterns for all its functions. 1403 for existingOperationObject != nil { 1404 newPathCount += 1 1405 newPathElement = firstPathParameter.Name + pathParamUniqueSuffixDeliminator + strconv.Itoa(newPathCount) 1406 newPath = strings.ReplaceAll(path, "{"+firstPathParameter.Name+"}", "{"+newPathElement+"}") 1407 1408 if newPathItemObject, ok := getPathItemObject(*paths, newPath); ok { 1409 existingOperationObject = operationFunc(&newPathItemObject) 1410 } else { 1411 existingOperationObject = nil 1412 } 1413 } 1414 // update the pathItemObject we are adding to with the new path 1415 pathItemObject, _ = getPathItemObject(*paths, newPath) 1416 firstPathParameter.Name = newPathElement 1417 path = newPath 1418 parameters[firstParamIndex] = *firstPathParameter 1419 } 1420 } 1421 } 1422 1423 methProtoPath := protoPathIndex(reflect.TypeOf((*descriptorpb.ServiceDescriptorProto)(nil)), "Method") 1424 desc := "A successful response." 1425 var responseSchema openapiSchemaObject 1426 1427 if b.ResponseBody == nil || len(b.ResponseBody.FieldPath) == 0 { 1428 responseSchema = openapiSchemaObject{ 1429 schemaCore: schemaCore{}, 1430 } 1431 1432 // Don't link to a full definition for 1433 // empty; it's overly verbose. 1434 // schema.Properties{} renders it as 1435 // well, without a definition 1436 wknSchemaCore, isWkn := wktSchemas[meth.ResponseType.FQMN()] 1437 if !isWkn { 1438 if err := responseSchema.setRefFromFQN(meth.ResponseType.FQMN(), reg); err != nil { 1439 return err 1440 } 1441 } else { 1442 responseSchema.schemaCore = wknSchemaCore 1443 1444 // 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. 1445 if meth.ResponseType.FQMN() == ".google.protobuf.Empty" { 1446 responseSchema.Properties = &openapiSchemaObjectProperties{} 1447 } 1448 } 1449 } else { 1450 // This is resolving the value of response_body in the google.api.HttpRule 1451 lastField := b.ResponseBody.FieldPath[len(b.ResponseBody.FieldPath)-1] 1452 responseSchema = schemaOfField(lastField.Target, reg, customRefs) 1453 if responseSchema.Description != "" { 1454 desc = responseSchema.Description 1455 } else { 1456 desc = fieldProtoComments(reg, lastField.Target.Message, lastField.Target) 1457 } 1458 } 1459 if meth.GetServerStreaming() { 1460 desc += "(streaming responses)" 1461 responseSchema.Type = "object" 1462 swgRef, _ := fullyQualifiedNameToOpenAPIName(meth.ResponseType.FQMN(), reg) 1463 responseSchema.Title = "Stream result of " + swgRef 1464 1465 props := openapiSchemaObjectProperties{ 1466 keyVal{ 1467 Key: "result", 1468 Value: openapiSchemaObject{ 1469 schemaCore: schemaCore{ 1470 Ref: responseSchema.Ref, 1471 }, 1472 }, 1473 }, 1474 } 1475 if !reg.GetDisableDefaultErrors() { 1476 statusDef, hasStatus := fullyQualifiedNameToOpenAPIName(".google.rpc.Status", reg) 1477 if hasStatus { 1478 props = append(props, keyVal{ 1479 Key: "error", 1480 Value: openapiSchemaObject{ 1481 schemaCore: schemaCore{ 1482 Ref: fmt.Sprintf("#/definitions/%s", statusDef)}, 1483 }, 1484 }) 1485 } 1486 } 1487 1488 // Special case HttpBody responses, they will be unformatted bytes 1489 if meth.ResponseType.FQMN() == ".google.api.HttpBody" { 1490 responseSchema.Type = "string" 1491 responseSchema.Format = "binary" 1492 responseSchema.Title = "Free form byte stream" 1493 // The error response is still JSON, but technically the full response 1494 // is still unformatted, so don't include the error response structure. 1495 props = nil 1496 } 1497 1498 responseSchema.Properties = &props 1499 responseSchema.Ref = "" 1500 } 1501 1502 operationObject := &openapiOperationObject{ 1503 Parameters: parameters, 1504 Responses: openapiResponsesObject{}, 1505 } 1506 1507 if !reg.GetDisableDefaultResponses() { 1508 operationObject.Responses["200"] = openapiResponseObject{ 1509 Description: desc, 1510 Schema: responseSchema, 1511 Headers: openapiHeadersObject{}, 1512 } 1513 } 1514 1515 if !reg.GetDisableServiceTags() { 1516 tag := svc.GetName() 1517 if pkg := svc.File.GetPackage(); pkg != "" && reg.IsIncludePackageInTags() { 1518 tag = pkg + "." + tag 1519 } 1520 operationObject.Tags = []string{tag} 1521 } 1522 1523 if !reg.GetDisableDefaultErrors() { 1524 errDef, hasErrDef := fullyQualifiedNameToOpenAPIName(".google.rpc.Status", reg) 1525 if hasErrDef { 1526 // https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#responses-object 1527 operationObject.Responses["default"] = openapiResponseObject{ 1528 Description: "An unexpected error response.", 1529 Schema: openapiSchemaObject{ 1530 schemaCore: schemaCore{ 1531 Ref: fmt.Sprintf("#/definitions/%s", errDef), 1532 }, 1533 }, 1534 } 1535 } 1536 } 1537 operationObject.OperationID = fmt.Sprintf("%s_%s", svc.GetName(), meth.GetName()) 1538 if reg.GetSimpleOperationIDs() { 1539 operationObject.OperationID = meth.GetName() 1540 } 1541 if bIdx != 0 { 1542 // OperationID must be unique in an OpenAPI v2 definition. 1543 operationObject.OperationID += strconv.Itoa(bIdx + 1) 1544 } 1545 1546 // Fill reference map with referenced request messages 1547 for _, param := range operationObject.Parameters { 1548 if param.Schema != nil && param.Schema.Ref != "" { 1549 requestResponseRefs[param.Schema.Ref] = struct{}{} 1550 } 1551 } 1552 1553 methComments := protoComments(reg, svc.File, nil, "Service", int32(svcIdx-svcBaseIdx), methProtoPath, int32(methIdx)) 1554 if err := updateOpenAPIDataFromComments(reg, operationObject, meth, methComments, false); err != nil { 1555 panic(err) 1556 } 1557 1558 svcOpts, err := getServiceOpenAPIOption(reg, svc) 1559 if err != nil { 1560 grpclog.Error(err) 1561 return err 1562 } 1563 opts, err := getMethodOpenAPIOption(reg, meth) 1564 if opts != nil { 1565 if err != nil { 1566 panic(err) 1567 } 1568 operationObject.ExternalDocs = protoExternalDocumentationToOpenAPIExternalDocumentation(opts.ExternalDocs, reg, meth) 1569 // TODO(ivucica): this would be better supported by looking whether the method is deprecated in the proto file 1570 operationObject.Deprecated = opts.Deprecated 1571 1572 if opts.Summary != "" { 1573 operationObject.Summary = opts.Summary 1574 } 1575 if opts.Description != "" { 1576 operationObject.Description = opts.Description 1577 } 1578 if len(opts.Tags) > 0 { 1579 operationObject.Tags = make([]string, len(opts.Tags)) 1580 copy(operationObject.Tags, opts.Tags) 1581 } else if svcOpts.GetName() != "" { 1582 operationObject.Tags = []string{svcOpts.GetName()} 1583 } 1584 if opts.OperationId != "" { 1585 operationObject.OperationID = opts.OperationId 1586 } 1587 if opts.Security != nil { 1588 newSecurity := []openapiSecurityRequirementObject{} 1589 if operationObject.Security != nil { 1590 newSecurity = *operationObject.Security 1591 } 1592 for _, secReq := range opts.Security { 1593 newSecReq := openapiSecurityRequirementObject{} 1594 for secReqKey, secReqValue := range secReq.SecurityRequirement { 1595 if secReqValue == nil { 1596 continue 1597 } 1598 1599 newSecReqValue := make([]string, len(secReqValue.Scope)) 1600 copy(newSecReqValue, secReqValue.Scope) 1601 newSecReq[secReqKey] = newSecReqValue 1602 } 1603 1604 if len(newSecReq) > 0 { 1605 newSecurity = append(newSecurity, newSecReq) 1606 } 1607 } 1608 operationObject.Security = &newSecurity 1609 } 1610 if opts.Responses != nil { 1611 for name, resp := range opts.Responses { 1612 // Merge response data into default response if available. 1613 respObj := operationObject.Responses[name] 1614 if resp.Description != "" { 1615 respObj.Description = resp.Description 1616 } 1617 if resp.Schema != nil { 1618 respObj.Schema = openapiSchemaFromProtoSchema(resp.Schema, reg, customRefs, meth) 1619 } 1620 if resp.Examples != nil { 1621 respObj.Examples = openapiExamplesFromProtoExamples(resp.Examples) 1622 } 1623 if resp.Headers != nil { 1624 hdrs, err := processHeaders(resp.Headers) 1625 if err != nil { 1626 return err 1627 } 1628 respObj.Headers = hdrs 1629 } 1630 if resp.Extensions != nil { 1631 exts, err := processExtensions(resp.Extensions) 1632 if err != nil { 1633 return err 1634 } 1635 respObj.extensions = exts 1636 } 1637 operationObject.Responses[name] = respObj 1638 } 1639 } 1640 1641 if opts.Extensions != nil { 1642 exts, err := processExtensions(opts.Extensions) 1643 if err != nil { 1644 return err 1645 } 1646 operationObject.extensions = exts 1647 } 1648 1649 if len(opts.Consumes) > 0 { 1650 operationObject.Consumes = make([]string, len(opts.Consumes)) 1651 copy(operationObject.Consumes, opts.Consumes) 1652 } 1653 1654 if len(opts.Produces) > 0 { 1655 operationObject.Produces = make([]string, len(opts.Produces)) 1656 copy(operationObject.Produces, opts.Produces) 1657 } 1658 1659 if params := opts.Parameters; params != nil && len(params.Headers) > 0 { 1660 for _, header := range params.Headers { 1661 param := openapiParameterObject{ 1662 In: "header", 1663 Name: header.Name, 1664 Description: header.Description, 1665 Required: header.Required, 1666 Format: header.Format, 1667 } 1668 1669 switch header.Type { 1670 case openapi_options.HeaderParameter_STRING: 1671 param.Type = "string" 1672 case openapi_options.HeaderParameter_NUMBER: 1673 param.Type = "number" 1674 case openapi_options.HeaderParameter_INTEGER: 1675 param.Type = "integer" 1676 case openapi_options.HeaderParameter_BOOLEAN: 1677 param.Type = "boolean" 1678 default: 1679 return fmt.Errorf("invalid header parameter type: %+v", header.Type) 1680 } 1681 1682 operationObject.Parameters = append(operationObject.Parameters, param) 1683 } 1684 } 1685 1686 // TODO(ivucica): add remaining fields of operation object 1687 } 1688 1689 switch b.HTTPMethod { 1690 case "DELETE": 1691 pathItemObject.Delete = operationObject 1692 case "GET": 1693 pathItemObject.Get = operationObject 1694 case "POST": 1695 pathItemObject.Post = operationObject 1696 case "PUT": 1697 pathItemObject.Put = operationObject 1698 case "PATCH": 1699 pathItemObject.Patch = operationObject 1700 case "HEAD": 1701 pathItemObject.Head = operationObject 1702 case "OPTIONS": 1703 pathItemObject.Options = operationObject 1704 } 1705 1706 updatePaths(paths, path, pathItemObject) 1707 } 1708 } 1709 } 1710 1711 // Success! return nil on the error object 1712 return nil 1713 } 1714 1715 // Returns the openapiPathItemObject associated with a path. If path is not present, returns 1716 // empty openapiPathItemObject and false. 1717 func getPathItemObject(paths openapiPathsObject, path string) (openapiPathItemObject, bool) { 1718 for _, pathData := range paths { 1719 if pathData.Path == path { 1720 return pathData.PathItemObject, true 1721 } 1722 } 1723 1724 return openapiPathItemObject{}, false 1725 } 1726 1727 // If a path already exists in openapiPathsObject, updates that path's openapiPathItemObject. If not, 1728 // appends a new path and openapiPathItemObject to the openapiPathsObject. 1729 func updatePaths(paths *openapiPathsObject, path string, pathItemObject openapiPathItemObject) { 1730 for i, p := range *paths { 1731 if p.Path == path { 1732 (*paths)[i].PathItemObject = pathItemObject 1733 return 1734 } 1735 } 1736 *paths = append(*paths, pathData{ 1737 Path: path, 1738 PathItemObject: pathItemObject, 1739 }) 1740 } 1741 1742 func mergeDescription(schema openapiSchemaObject) string { 1743 desc := schema.Description 1744 if schema.Title != "" { // join title because title of parameter object will be ignored 1745 desc = strings.TrimSpace(schema.Title + paragraphDeliminator + schema.Description) 1746 } 1747 return desc 1748 } 1749 1750 func operationForMethod(httpMethod string) func(*openapiPathItemObject) *openapiOperationObject { 1751 switch httpMethod { 1752 case "GET": 1753 return func(obj *openapiPathItemObject) *openapiOperationObject { return obj.Get } 1754 case "POST": 1755 return func(obj *openapiPathItemObject) *openapiOperationObject { return obj.Post } 1756 case "PUT": 1757 return func(obj *openapiPathItemObject) *openapiOperationObject { return obj.Put } 1758 case "DELETE": 1759 return func(obj *openapiPathItemObject) *openapiOperationObject { return obj.Delete } 1760 case "PATCH": 1761 return func(obj *openapiPathItemObject) *openapiOperationObject { return obj.Patch } 1762 case "HEAD": 1763 return func(obj *openapiPathItemObject) *openapiOperationObject { return obj.Head } 1764 case "OPTIONS": 1765 return func(obj *openapiPathItemObject) *openapiOperationObject { return obj.Options } 1766 default: 1767 return func(obj *openapiPathItemObject) *openapiOperationObject { return nil } 1768 } 1769 } 1770 1771 // This function is called with a param which contains the entire definition of a method. 1772 func applyTemplate(p param) (*openapiSwaggerObject, error) { 1773 // Create the basic template object. This is the object that everything is 1774 // defined off of. 1775 s := openapiSwaggerObject{ 1776 // OpenAPI 2.0 is the version of this document 1777 Swagger: "2.0", 1778 Consumes: []string{"application/json"}, 1779 Produces: []string{"application/json"}, 1780 Paths: openapiPathsObject{}, 1781 Definitions: make(openapiDefinitionsObject), 1782 Info: openapiInfoObject{ 1783 Title: *p.File.Name, 1784 Version: "version not set", 1785 }, 1786 } 1787 1788 // Loops through all the services and their exposed GET/POST/PUT/DELETE definitions 1789 // and create entries for all of them. 1790 // Also adds custom user specified references to second map. 1791 requestResponseRefs, customRefs := refMap{}, refMap{} 1792 if err := renderServices(p.Services, &s.Paths, p.reg, requestResponseRefs, customRefs, p.Messages, s.Definitions); err != nil { 1793 panic(err) 1794 } 1795 1796 messages := messageMap{} 1797 streamingMessages := messageMap{} 1798 enums := enumMap{} 1799 1800 if !p.reg.GetDisableDefaultErrors() { 1801 // Add the error type to the message map 1802 runtimeError, swgRef, err := lookupMsgAndOpenAPIName("google.rpc", "Status", p.reg) 1803 if err == nil { 1804 messages[swgRef] = runtimeError 1805 } else { 1806 // just in case there is an error looking up runtimeError 1807 grpclog.Error(err) 1808 } 1809 } 1810 1811 // Find all the service's messages and enumerations that are defined (recursively) 1812 // and write request, response and other custom (but referenced) types out as definition objects. 1813 findServicesMessagesAndEnumerations(p.Services, p.reg, messages, streamingMessages, enums, requestResponseRefs) 1814 if err := renderMessagesAsDefinition(messages, s.Definitions, p.reg, customRefs, nil); err != nil { 1815 return nil, err 1816 } 1817 renderEnumerationsAsDefinition(enums, s.Definitions, p.reg) 1818 1819 // File itself might have some comments and metadata. 1820 packageProtoPath := protoPathIndex(reflect.TypeOf((*descriptorpb.FileDescriptorProto)(nil)), "Package") 1821 packageComments := protoComments(p.reg, p.File, nil, "Package", packageProtoPath) 1822 if err := updateOpenAPIDataFromComments(p.reg, &s, p, packageComments, true); err != nil { 1823 return nil, err 1824 } 1825 1826 // There may be additional options in the OpenAPI option in the proto. 1827 spb, err := getFileOpenAPIOption(p.reg, p.File) 1828 if err != nil { 1829 return nil, err 1830 } 1831 if spb != nil { 1832 if spb.Swagger != "" { 1833 s.Swagger = spb.Swagger 1834 } 1835 if spb.Info != nil { 1836 if spb.Info.Title != "" { 1837 s.Info.Title = spb.Info.Title 1838 } 1839 if spb.Info.Description != "" { 1840 s.Info.Description = spb.Info.Description 1841 } 1842 if spb.Info.TermsOfService != "" { 1843 s.Info.TermsOfService = spb.Info.TermsOfService 1844 } 1845 if spb.Info.Version != "" { 1846 s.Info.Version = spb.Info.Version 1847 } 1848 if spb.Info.Contact != nil { 1849 if s.Info.Contact == nil { 1850 s.Info.Contact = &openapiContactObject{} 1851 } 1852 if spb.Info.Contact.Name != "" { 1853 s.Info.Contact.Name = spb.Info.Contact.Name 1854 } 1855 if spb.Info.Contact.Url != "" { 1856 s.Info.Contact.URL = spb.Info.Contact.Url 1857 } 1858 if spb.Info.Contact.Email != "" { 1859 s.Info.Contact.Email = spb.Info.Contact.Email 1860 } 1861 } 1862 if spb.Info.License != nil { 1863 if s.Info.License == nil { 1864 s.Info.License = &openapiLicenseObject{} 1865 } 1866 if spb.Info.License.Name != "" { 1867 s.Info.License.Name = spb.Info.License.Name 1868 } 1869 if spb.Info.License.Url != "" { 1870 s.Info.License.URL = spb.Info.License.Url 1871 } 1872 } 1873 if spb.Info.Extensions != nil { 1874 exts, err := processExtensions(spb.Info.Extensions) 1875 if err != nil { 1876 return nil, err 1877 } 1878 s.Info.extensions = exts 1879 } 1880 } 1881 if spb.Host != "" { 1882 s.Host = spb.Host 1883 } 1884 if spb.BasePath != "" { 1885 s.BasePath = spb.BasePath 1886 } 1887 if len(spb.Schemes) > 0 { 1888 s.Schemes = make([]string, len(spb.Schemes)) 1889 for i, scheme := range spb.Schemes { 1890 s.Schemes[i] = strings.ToLower(scheme.String()) 1891 } 1892 } 1893 if len(spb.Consumes) > 0 { 1894 s.Consumes = make([]string, len(spb.Consumes)) 1895 copy(s.Consumes, spb.Consumes) 1896 } 1897 if len(spb.Produces) > 0 { 1898 s.Produces = make([]string, len(spb.Produces)) 1899 copy(s.Produces, spb.Produces) 1900 } 1901 if spb.SecurityDefinitions != nil && spb.SecurityDefinitions.Security != nil { 1902 if s.SecurityDefinitions == nil { 1903 s.SecurityDefinitions = openapiSecurityDefinitionsObject{} 1904 } 1905 for secDefKey, secDefValue := range spb.SecurityDefinitions.Security { 1906 var newSecDefValue openapiSecuritySchemeObject 1907 if oldSecDefValue, ok := s.SecurityDefinitions[secDefKey]; !ok { 1908 newSecDefValue = openapiSecuritySchemeObject{} 1909 } else { 1910 newSecDefValue = oldSecDefValue 1911 } 1912 if secDefValue.Type != openapi_options.SecurityScheme_TYPE_INVALID { 1913 switch secDefValue.Type { 1914 case openapi_options.SecurityScheme_TYPE_BASIC: 1915 newSecDefValue.Type = "basic" 1916 case openapi_options.SecurityScheme_TYPE_API_KEY: 1917 newSecDefValue.Type = "apiKey" 1918 case openapi_options.SecurityScheme_TYPE_OAUTH2: 1919 newSecDefValue.Type = "oauth2" 1920 } 1921 } 1922 if secDefValue.Description != "" { 1923 newSecDefValue.Description = secDefValue.Description 1924 } 1925 if secDefValue.Name != "" { 1926 newSecDefValue.Name = secDefValue.Name 1927 } 1928 if secDefValue.In != openapi_options.SecurityScheme_IN_INVALID { 1929 switch secDefValue.In { 1930 case openapi_options.SecurityScheme_IN_QUERY: 1931 newSecDefValue.In = "query" 1932 case openapi_options.SecurityScheme_IN_HEADER: 1933 newSecDefValue.In = "header" 1934 } 1935 } 1936 if secDefValue.Flow != openapi_options.SecurityScheme_FLOW_INVALID { 1937 switch secDefValue.Flow { 1938 case openapi_options.SecurityScheme_FLOW_IMPLICIT: 1939 newSecDefValue.Flow = "implicit" 1940 case openapi_options.SecurityScheme_FLOW_PASSWORD: 1941 newSecDefValue.Flow = "password" 1942 case openapi_options.SecurityScheme_FLOW_APPLICATION: 1943 newSecDefValue.Flow = "application" 1944 case openapi_options.SecurityScheme_FLOW_ACCESS_CODE: 1945 newSecDefValue.Flow = "accessCode" 1946 } 1947 } 1948 if secDefValue.AuthorizationUrl != "" { 1949 newSecDefValue.AuthorizationURL = secDefValue.AuthorizationUrl 1950 } 1951 if secDefValue.TokenUrl != "" { 1952 newSecDefValue.TokenURL = secDefValue.TokenUrl 1953 } 1954 if secDefValue.Scopes != nil { 1955 if newSecDefValue.Scopes == nil { 1956 newSecDefValue.Scopes = openapiScopesObject{} 1957 } 1958 for scopeKey, scopeDesc := range secDefValue.Scopes.Scope { 1959 newSecDefValue.Scopes[scopeKey] = scopeDesc 1960 } 1961 } 1962 if secDefValue.Extensions != nil { 1963 exts, err := processExtensions(secDefValue.Extensions) 1964 if err != nil { 1965 return nil, err 1966 } 1967 newSecDefValue.extensions = exts 1968 } 1969 s.SecurityDefinitions[secDefKey] = newSecDefValue 1970 } 1971 } 1972 if spb.Security != nil { 1973 var newSecurity []openapiSecurityRequirementObject 1974 if s.Security != nil { 1975 newSecurity = s.Security 1976 } 1977 for _, secReq := range spb.Security { 1978 newSecReq := openapiSecurityRequirementObject{} 1979 for secReqKey, secReqValue := range secReq.SecurityRequirement { 1980 if secReqValue == nil { 1981 return nil, fmt.Errorf("malformed security requirement spec for key %q; value is required", secReqKey) 1982 } 1983 newSecReqValue := make([]string, len(secReqValue.Scope)) 1984 copy(newSecReqValue, secReqValue.Scope) 1985 newSecReq[secReqKey] = newSecReqValue 1986 } 1987 newSecurity = append(newSecurity, newSecReq) 1988 } 1989 s.Security = newSecurity 1990 } 1991 s.ExternalDocs = protoExternalDocumentationToOpenAPIExternalDocumentation(spb.ExternalDocs, p.reg, spb) 1992 // Populate all Paths with Responses set at top level, 1993 // preferring Responses already set over those at the top level. 1994 if spb.Responses != nil { 1995 for _, verbs := range s.Paths { 1996 var maps []openapiResponsesObject 1997 if verbs.PathItemObject.Delete != nil { 1998 maps = append(maps, verbs.PathItemObject.Delete.Responses) 1999 } 2000 if verbs.PathItemObject.Get != nil { 2001 maps = append(maps, verbs.PathItemObject.Get.Responses) 2002 } 2003 if verbs.PathItemObject.Post != nil { 2004 maps = append(maps, verbs.PathItemObject.Post.Responses) 2005 } 2006 if verbs.PathItemObject.Put != nil { 2007 maps = append(maps, verbs.PathItemObject.Put.Responses) 2008 } 2009 if verbs.PathItemObject.Patch != nil { 2010 maps = append(maps, verbs.PathItemObject.Patch.Responses) 2011 } 2012 2013 for k, v := range spb.Responses { 2014 for _, respMap := range maps { 2015 if _, ok := respMap[k]; ok { 2016 // Don't overwrite already existing Responses 2017 continue 2018 } 2019 respMap[k] = openapiResponseObject{ 2020 Description: v.Description, 2021 Schema: openapiSchemaFromProtoSchema(v.Schema, p.reg, customRefs, nil), 2022 Examples: openapiExamplesFromProtoExamples(v.Examples), 2023 } 2024 } 2025 } 2026 } 2027 } 2028 2029 if spb.Extensions != nil { 2030 exts, err := processExtensions(spb.Extensions) 2031 if err != nil { 2032 return nil, err 2033 } 2034 s.extensions = exts 2035 } 2036 2037 if spb.Tags != nil { 2038 for _, v := range spb.Tags { 2039 newTag := openapiTagObject{} 2040 newTag.Name = v.Name 2041 newTag.Description = v.Description 2042 if p.reg.GetUseGoTemplate() { 2043 newTag.Description = goTemplateComments(newTag.Description, nil, p.reg) 2044 } 2045 if v.ExternalDocs != nil { 2046 newTag.ExternalDocs = &openapiExternalDocumentationObject{ 2047 Description: v.ExternalDocs.Description, 2048 URL: v.ExternalDocs.Url, 2049 } 2050 if p.reg.GetUseGoTemplate() { 2051 newTag.ExternalDocs.Description = goTemplateComments(v.ExternalDocs.Description, nil, p.reg) 2052 } 2053 } 2054 if v.Extensions != nil { 2055 exts, err := processExtensions(v.Extensions) 2056 if err != nil { 2057 return nil, err 2058 } 2059 newTag.extensions = exts 2060 } 2061 s.Tags = append(s.Tags, newTag) 2062 } 2063 } 2064 2065 // Additional fields on the OpenAPI v2 spec's "OpenAPI" object 2066 // should be added here, once supported in the proto. 2067 } 2068 2069 if !p.reg.GetDisableServiceTags() { 2070 s.Tags = mergeTags(s.Tags, renderServiceTags(p.Services, p.reg)) 2071 } 2072 2073 // Finally add any references added by users that aren't 2074 // otherwise rendered. 2075 if err := addCustomRefs(s.Definitions, p.reg, customRefs); err != nil { 2076 return nil, err 2077 } 2078 2079 return &s, nil 2080 } 2081 2082 func mergeTags(existingTags []openapiTagObject, tags []openapiTagObject) []openapiTagObject { 2083 for _, tag := range tags { 2084 matched := false 2085 for i, existingTag := range existingTags { 2086 if existingTag.Name == tag.Name { 2087 if existingTag.Description == "" { 2088 existingTags[i].Description = tag.Description 2089 } 2090 if existingTag.ExternalDocs == nil { 2091 existingTags[i].ExternalDocs = tag.ExternalDocs 2092 } else if tag.ExternalDocs != nil { 2093 if existingTag.ExternalDocs.Description == "" { 2094 existingTags[i].ExternalDocs.Description = tag.ExternalDocs.Description 2095 } 2096 if existingTag.ExternalDocs.URL == "" { 2097 existingTags[i].ExternalDocs.URL = tag.ExternalDocs.URL 2098 } 2099 } 2100 if existingTag.extensions == nil { 2101 existingTags[i].extensions = tag.extensions 2102 } else if tag.extensions != nil { 2103 for _, ext := range tag.extensions { 2104 matchedExt := false 2105 for _, existingExt := range existingTag.extensions { 2106 if existingExt.key == ext.key { 2107 matchedExt = true 2108 break 2109 } 2110 } 2111 if !matchedExt { 2112 existingTags[i].extensions = append(existingTags[i].extensions, ext) 2113 } 2114 } 2115 } 2116 matched = true 2117 break 2118 } 2119 } 2120 if !matched { 2121 existingTags = append(existingTags, tag) 2122 } 2123 } 2124 return existingTags 2125 } 2126 2127 func processExtensions(inputExts map[string]*structpb.Value) ([]extension, error) { 2128 exts := make([]extension, 0, len(inputExts)) 2129 for k, v := range inputExts { 2130 if !strings.HasPrefix(k, "x-") { 2131 return nil, fmt.Errorf("extension keys need to start with \"x-\": %q", k) 2132 } 2133 ext, err := (&protojson.MarshalOptions{Indent: " "}).Marshal(v) 2134 if err != nil { 2135 return nil, err 2136 } 2137 exts = append(exts, extension{key: k, value: ext}) 2138 } 2139 sort.Slice(exts, func(i, j int) bool { return exts[i].key < exts[j].key }) 2140 return exts, nil 2141 } 2142 2143 func validateHeaderTypeAndFormat(headerType, format string) error { 2144 // The type of the object. The value MUST be one of "string", "number", "integer", "boolean", or "array" 2145 // See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#headerObject 2146 // Note: currently not implementing array as we are only implementing this in the operation response context 2147 switch headerType { 2148 // the format property is an open string-valued property, and can have any value to support documentation needs 2149 // primary check for format is to ensure that the number/integer formats are extensions of the specified type 2150 // See: https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#dataTypeFormat 2151 case "string": 2152 return nil 2153 case "number": 2154 switch format { 2155 case "uint", 2156 "uint8", 2157 "uint16", 2158 "uint32", 2159 "uint64", 2160 "int", 2161 "int8", 2162 "int16", 2163 "int32", 2164 "int64", 2165 "float", 2166 "float32", 2167 "float64", 2168 "complex64", 2169 "complex128", 2170 "double", 2171 "byte", 2172 "rune", 2173 "uintptr", 2174 "": 2175 return nil 2176 default: 2177 return fmt.Errorf("the provided format %q is not a valid extension of the type %q", format, headerType) 2178 } 2179 case "integer": 2180 switch format { 2181 case "uint", 2182 "uint8", 2183 "uint16", 2184 "uint32", 2185 "uint64", 2186 "int", 2187 "int8", 2188 "int16", 2189 "int32", 2190 "int64", 2191 "": 2192 return nil 2193 default: 2194 return fmt.Errorf("the provided format %q is not a valid extension of the type %q", format, headerType) 2195 } 2196 case "boolean": 2197 return nil 2198 } 2199 return fmt.Errorf("the provided header type %q is not supported", headerType) 2200 } 2201 2202 func validateDefaultValueTypeAndFormat(headerType string, defaultValue string, format string) error { 2203 switch headerType { 2204 case "string": 2205 if !isQuotedString(defaultValue) { 2206 return fmt.Errorf("the provided default value %q does not match provider type %q, or is not properly quoted with escaped quotations", defaultValue, headerType) 2207 } 2208 switch format { 2209 case "date-time": 2210 unquoteTime := strings.Trim(defaultValue, `"`) 2211 if _, err := time.Parse(time.RFC3339, unquoteTime); err != nil { 2212 return fmt.Errorf("the provided default value %q is not a valid RFC3339 date-time string", defaultValue) 2213 } 2214 case "date": 2215 const layoutRFC3339Date = "2006-01-02" 2216 unquoteDate := strings.Trim(defaultValue, `"`) 2217 if _, err := time.Parse(layoutRFC3339Date, unquoteDate); err != nil { 2218 return fmt.Errorf("the provided default value %q is not a valid RFC3339 date-time string", defaultValue) 2219 } 2220 } 2221 case "number": 2222 if err := isJSONNumber(defaultValue, headerType); err != nil { 2223 return err 2224 } 2225 case "integer": 2226 switch format { 2227 case "int32": 2228 if _, err := strconv.ParseInt(defaultValue, 0, 32); err != nil { 2229 return fmt.Errorf("the provided default value %q does not match provided format %q", defaultValue, format) 2230 } 2231 case "uint32": 2232 if _, err := strconv.ParseUint(defaultValue, 0, 32); err != nil { 2233 return fmt.Errorf("the provided default value %q does not match provided format %q", defaultValue, format) 2234 } 2235 case "int64": 2236 if _, err := strconv.ParseInt(defaultValue, 0, 64); err != nil { 2237 return fmt.Errorf("the provided default value %q does not match provided format %q", defaultValue, format) 2238 } 2239 case "uint64": 2240 if _, err := strconv.ParseUint(defaultValue, 0, 64); err != nil { 2241 return fmt.Errorf("the provided default value %q does not match provided format %q", defaultValue, format) 2242 } 2243 default: 2244 if _, err := strconv.ParseInt(defaultValue, 0, 64); err != nil { 2245 return fmt.Errorf("the provided default value %q does not match provided type %q", defaultValue, headerType) 2246 } 2247 } 2248 case "boolean": 2249 if !isBool(defaultValue) { 2250 return fmt.Errorf("the provided default value %q does not match provider type %q", defaultValue, headerType) 2251 } 2252 } 2253 return nil 2254 } 2255 2256 func isQuotedString(s string) bool { 2257 return len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' 2258 } 2259 2260 func isJSONNumber(s string, t string) error { 2261 val, err := strconv.ParseFloat(s, 64) 2262 if err != nil { 2263 return fmt.Errorf("the provided default value %q does not match provider type %q", s, t) 2264 } 2265 // Floating point values that cannot be represented as sequences of digits (such as Infinity and NaN) are not permitted. 2266 // See: https://tools.ietf.org/html/rfc4627#section-2.4 2267 if math.IsInf(val, 0) || math.IsNaN(val) { 2268 return fmt.Errorf("the provided number %q is not a valid JSON number", s) 2269 } 2270 2271 return nil 2272 } 2273 2274 func isBool(s string) bool { 2275 // Unable to use strconv.ParseBool because it returns truthy values https://golang.org/pkg/strconv/#example_ParseBool 2276 // per https://swagger.io/specification/v2/#data-types 2277 // type: boolean represents two values: true and false. Note that truthy and falsy values such as "true", "", 0 or null are not considered boolean values. 2278 return s == "true" || s == "false" 2279 } 2280 2281 func processHeaders(inputHdrs map[string]*openapi_options.Header) (openapiHeadersObject, error) { 2282 hdrs := make(map[string]openapiHeaderObject, len(inputHdrs)) 2283 for k, v := range inputHdrs { 2284 header := textproto.CanonicalMIMEHeaderKey(k) 2285 ret := openapiHeaderObject{ 2286 Description: v.Description, 2287 Format: v.Format, 2288 Pattern: v.Pattern, 2289 } 2290 if err := validateHeaderTypeAndFormat(v.Type, v.Format); err != nil { 2291 return nil, err 2292 } 2293 ret.Type = v.Type 2294 if v.Default != "" { 2295 if err := validateDefaultValueTypeAndFormat(v.Type, v.Default, v.Format); err != nil { 2296 return nil, err 2297 } 2298 ret.Default = RawExample(v.Default) 2299 } 2300 hdrs[header] = ret 2301 } 2302 return hdrs, nil 2303 } 2304 2305 func removeInternalComments(comment string) string { 2306 c := []string{} 2307 for len(comment) > 0 { 2308 open := strings.SplitN(comment, "(--", 2) 2309 if len(open) == 1 { 2310 c = append(c, open[0]) 2311 break 2312 } 2313 ex := strings.TrimRight(open[0], " \t") 2314 // Trim only one line prior to all spaces 2315 switch { 2316 case strings.HasSuffix(ex, "\r\n"): 2317 ex = strings.TrimSuffix(ex, "\r\n") 2318 case strings.HasSuffix(ex, "\n"): 2319 ex = strings.TrimSuffix(ex, "\n") 2320 } 2321 if ex != "" { 2322 c = append(c, ex) 2323 } 2324 comment = open[1] 2325 2326 close := strings.SplitN(comment, "--)", 2) 2327 if len(close) > 1 { 2328 comment = close[1] 2329 } else { 2330 break 2331 } 2332 } 2333 return strings.Join(c, "") 2334 } 2335 2336 // updateOpenAPIDataFromComments updates a OpenAPI object based on a comment 2337 // from the proto file. 2338 // 2339 // First paragraph of a comment is used for summary. Remaining paragraphs of 2340 // a comment are used for description. If 'Summary' field is not present on 2341 // the passed swaggerObject, the summary and description are joined by \n\n. 2342 // 2343 // If there is a field named 'Info', its 'Summary' and 'Description' fields 2344 // will be updated instead. 2345 // 2346 // If there is no 'Summary', the same behavior will be attempted on 'Title', 2347 // but only if the last character is not a period. 2348 func updateOpenAPIDataFromComments(reg *descriptor.Registry, swaggerObject interface{}, data interface{}, comment string, isPackageObject bool) error { 2349 if len(comment) == 0 { 2350 return nil 2351 } 2352 2353 // Checks whether the "ignore_comments" flag is set to true 2354 if reg.GetIgnoreComments() { 2355 return nil 2356 } 2357 2358 // Checks whether the "remove_internal_comments" flag is set to true 2359 if reg.GetRemoveInternalComments() { 2360 comment = removeInternalComments(comment) 2361 } 2362 2363 // Checks whether the "use_go_templates" flag is set to true 2364 if reg.GetUseGoTemplate() { 2365 comment = goTemplateComments(comment, data, reg) 2366 } 2367 2368 // Figure out what to apply changes to. 2369 swaggerObjectValue := reflect.ValueOf(swaggerObject) 2370 infoObjectValue := swaggerObjectValue.Elem().FieldByName("Info") 2371 if !infoObjectValue.CanSet() { 2372 // No such field? Apply summary and description directly to 2373 // passed object. 2374 infoObjectValue = swaggerObjectValue.Elem() 2375 } 2376 2377 // Figure out which properties to update. 2378 summaryValue := infoObjectValue.FieldByName("Summary") 2379 descriptionValue := infoObjectValue.FieldByName("Description") 2380 readOnlyValue := infoObjectValue.FieldByName("ReadOnly") 2381 2382 if readOnlyValue.Kind() == reflect.Bool && readOnlyValue.CanSet() && strings.Contains(comment, "Output only.") { 2383 readOnlyValue.Set(reflect.ValueOf(true)) 2384 } 2385 2386 usingTitle := false 2387 if !summaryValue.CanSet() { 2388 summaryValue = infoObjectValue.FieldByName("Title") 2389 usingTitle = true 2390 } 2391 2392 paragraphs := strings.Split(comment, paragraphDeliminator) 2393 2394 // If there is a summary (or summary-equivalent) and it's empty, use the first 2395 // paragraph as summary, and the rest as description. 2396 if summaryValue.CanSet() { 2397 summary := strings.TrimSpace(paragraphs[0]) 2398 description := strings.TrimSpace(strings.Join(paragraphs[1:], paragraphDeliminator)) 2399 if !usingTitle || (len(summary) > 0 && summary[len(summary)-1] != '.') { 2400 // overrides the schema value only if it's empty 2401 // keep the comment precedence when updating the package definition 2402 if summaryValue.Len() == 0 || isPackageObject { 2403 summaryValue.Set(reflect.ValueOf(summary)) 2404 } 2405 if len(description) > 0 { 2406 if !descriptionValue.CanSet() { 2407 return errors.New("encountered object type with a summary, but no description") 2408 } 2409 // overrides the schema value only if it's empty 2410 // keep the comment precedence when updating the package definition 2411 if descriptionValue.Len() == 0 || isPackageObject { 2412 descriptionValue.Set(reflect.ValueOf(description)) 2413 } 2414 } 2415 return nil 2416 } 2417 } 2418 2419 // There was no summary field on the swaggerObject. Try to apply the 2420 // whole comment into description if the OpenAPI object description is empty. 2421 if descriptionValue.CanSet() { 2422 if descriptionValue.Len() == 0 || isPackageObject { 2423 descriptionValue.Set(reflect.ValueOf(strings.Join(paragraphs, paragraphDeliminator))) 2424 } 2425 return nil 2426 } 2427 2428 return errors.New("no description nor summary property") 2429 } 2430 2431 func fieldProtoComments(reg *descriptor.Registry, msg *descriptor.Message, field *descriptor.Field) string { 2432 protoPath := protoPathIndex(reflect.TypeOf((*descriptorpb.DescriptorProto)(nil)), "Field") 2433 for i, f := range msg.Fields { 2434 if f == field { 2435 return protoComments(reg, msg.File, msg.Outers, "MessageType", int32(msg.Index), protoPath, int32(i)) 2436 } 2437 } 2438 return "" 2439 } 2440 2441 func enumValueProtoComments(reg *descriptor.Registry, enum *descriptor.Enum) string { 2442 protoPath := protoPathIndex(reflect.TypeOf((*descriptorpb.EnumDescriptorProto)(nil)), "Value") 2443 var comments []string 2444 for idx, value := range enum.GetValue() { 2445 if reg.GetOmitEnumDefaultValue() && value.GetNumber() == 0 { 2446 continue 2447 } 2448 if !isVisible(getEnumValueVisibilityOption(value), reg) { 2449 continue 2450 } 2451 name := value.GetName() 2452 if reg.GetEnumsAsInts() { 2453 name = strconv.Itoa(int(value.GetNumber())) 2454 } 2455 if str := protoComments(reg, enum.File, enum.Outers, "EnumType", int32(enum.Index), protoPath, int32(idx)); str != "" { 2456 comments = append(comments, name+": "+str) 2457 } 2458 } 2459 if len(comments) > 0 { 2460 return "- " + strings.Join(comments, "\n - ") 2461 } 2462 return "" 2463 } 2464 2465 func protoComments(reg *descriptor.Registry, file *descriptor.File, outers []string, typeName string, typeIndex int32, fieldPaths ...int32) string { 2466 if file.SourceCodeInfo == nil { 2467 fmt.Fprintln(os.Stderr, file.GetName(), "descriptor.File should not contain nil SourceCodeInfo") 2468 return "" 2469 } 2470 2471 outerPaths := make([]int32, len(outers)) 2472 for i := range outers { 2473 location := "" 2474 if file.Package != nil { 2475 location = file.GetPackage() 2476 } 2477 2478 msg, err := reg.LookupMsg(location, strings.Join(outers[:i+1], ".")) 2479 if err != nil { 2480 panic(err) 2481 } 2482 outerPaths[i] = int32(msg.Index) 2483 } 2484 2485 for _, loc := range file.SourceCodeInfo.Location { 2486 if !isProtoPathMatches(loc.Path, outerPaths, typeName, typeIndex, fieldPaths) { 2487 continue 2488 } 2489 comments := "" 2490 if loc.LeadingComments != nil { 2491 comments = strings.TrimRight(*loc.LeadingComments, "\n") 2492 comments = strings.TrimSpace(comments) 2493 // TODO(ivucica): this is a hack to fix "// " being interpreted as "//". 2494 // perhaps we should: 2495 // - split by \n 2496 // - determine if every (but first and last) line begins with " " 2497 // - trim every line only if that is the case 2498 // - join by \n 2499 comments = strings.ReplaceAll(comments, "\n ", "\n") 2500 comments = removeInternalComments(comments) 2501 } 2502 if loc.TrailingComments != nil { 2503 trailing := strings.TrimSpace(*loc.TrailingComments) 2504 if comments == "" { 2505 comments = trailing 2506 } else { 2507 comments += "\n\n" + trailing 2508 } 2509 } 2510 return comments 2511 } 2512 return "" 2513 } 2514 2515 func goTemplateComments(comment string, data interface{}, reg *descriptor.Registry) string { 2516 var temp bytes.Buffer 2517 tpl, err := template.New("").Funcs(template.FuncMap{ 2518 // Allows importing documentation from a file 2519 "import": func(name string) string { 2520 file, err := os.ReadFile(name) 2521 if err != nil { 2522 return err.Error() 2523 } 2524 // Runs template over imported file 2525 return goTemplateComments(string(file), data, reg) 2526 }, 2527 // Grabs title and description from a field 2528 "fieldcomments": func(msg *descriptor.Message, field *descriptor.Field) string { 2529 return strings.ReplaceAll(fieldProtoComments(reg, msg, field), "\n", "<br>") 2530 }, 2531 "arg": func(name string) string { 2532 if v, f := reg.GetGoTemplateArgs()[name]; f { 2533 return v 2534 } 2535 return fmt.Sprintf("goTemplateArg %s not found", name) 2536 }, 2537 }).Parse(comment) 2538 if err != nil { 2539 // If there is an error parsing the templating insert the error as string in the comment 2540 // to make it easier to debug the template error 2541 return err.Error() 2542 } 2543 if err := tpl.Execute(&temp, data); err != nil { 2544 // If there is an error executing the templating insert the error as string in the comment 2545 // to make it easier to debug the error 2546 return err.Error() 2547 } 2548 return temp.String() 2549 } 2550 2551 var messageProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.FileDescriptorProto)(nil)), "MessageType") 2552 var nestedProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.DescriptorProto)(nil)), "NestedType") 2553 var packageProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.FileDescriptorProto)(nil)), "Package") 2554 var serviceProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.FileDescriptorProto)(nil)), "Service") 2555 var methodProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.ServiceDescriptorProto)(nil)), "Method") 2556 2557 func isProtoPathMatches(paths []int32, outerPaths []int32, typeName string, typeIndex int32, fieldPaths []int32) bool { 2558 if typeName == "Package" && typeIndex == packageProtoPath { 2559 // path for package comments is just [2], and all the other processing 2560 // is too complex for it. 2561 if len(paths) == 0 || typeIndex != paths[0] { 2562 return false 2563 } 2564 return true 2565 } 2566 2567 if len(paths) != len(outerPaths)*2+2+len(fieldPaths) { 2568 return false 2569 } 2570 2571 if typeName == "Method" { 2572 if paths[0] != serviceProtoPath || paths[2] != methodProtoPath { 2573 return false 2574 } 2575 paths = paths[2:] 2576 } else { 2577 typeNameDescriptor := reflect.TypeOf((*descriptorpb.FileDescriptorProto)(nil)) 2578 2579 if len(outerPaths) > 0 { 2580 if paths[0] != messageProtoPath || paths[1] != outerPaths[0] { 2581 return false 2582 } 2583 paths = paths[2:] 2584 outerPaths = outerPaths[1:] 2585 2586 for i, v := range outerPaths { 2587 if paths[i*2] != nestedProtoPath || paths[i*2+1] != v { 2588 return false 2589 } 2590 } 2591 paths = paths[len(outerPaths)*2:] 2592 2593 if typeName == "MessageType" { 2594 typeName = "NestedType" 2595 } 2596 typeNameDescriptor = reflect.TypeOf((*descriptorpb.DescriptorProto)(nil)) 2597 } 2598 2599 if paths[0] != protoPathIndex(typeNameDescriptor, typeName) || paths[1] != typeIndex { 2600 return false 2601 } 2602 paths = paths[2:] 2603 } 2604 2605 for i, v := range fieldPaths { 2606 if paths[i] != v { 2607 return false 2608 } 2609 } 2610 return true 2611 } 2612 2613 // protoPathIndex returns a path component for google.protobuf.descriptor.SourceCode_Location. 2614 // 2615 // Specifically, it returns an id as generated from descriptor proto which 2616 // can be used to determine what type the id following it in the path is. 2617 // For example, if we are trying to locate comments related to a field named 2618 // `Address` in a message named `Person`, the path will be: 2619 // 2620 // [4, a, 2, b] 2621 // 2622 // While `a` gets determined by the order in which the messages appear in 2623 // the proto file, and `b` is the field index specified in the proto 2624 // file itself, the path actually needs to specify that `a` refers to a 2625 // message and not, say, a service; and that `b` refers to a field and not 2626 // an option. 2627 // 2628 // protoPathIndex figures out the values 4 and 2 in the above example. Because 2629 // messages are top level objects, the value of 4 comes from field id for 2630 // `MessageType` inside `google.protobuf.descriptor.FileDescriptor` message. 2631 // This field has a message type `google.protobuf.descriptor.DescriptorProto`. 2632 // And inside message `DescriptorProto`, there is a field named `Field` with id 2633 // 2. 2634 // 2635 // Some code generators seem to be hardcoding these values; this method instead 2636 // interprets them from `descriptor.proto`-derived Go source as necessary. 2637 func protoPathIndex(descriptorType reflect.Type, what string) int32 { 2638 field, ok := descriptorType.Elem().FieldByName(what) 2639 if !ok { 2640 panic(fmt.Errorf("could not find protobuf descriptor type id for %s", what)) 2641 } 2642 pbtag := field.Tag.Get("protobuf") 2643 if pbtag == "" { 2644 panic(fmt.Errorf("no Go tag 'protobuf' on protobuf descriptor for %s", what)) 2645 } 2646 path, err := strconv.ParseInt(strings.Split(pbtag, ",")[1], 10, 32) 2647 if err != nil { 2648 panic(fmt.Errorf("protobuf descriptor id for %s cannot be converted to a number: %s", what, err.Error())) 2649 } 2650 return int32(path) 2651 } 2652 2653 // extractOperationOptionFromMethodDescriptor extracts the message of type 2654 // openapi_options.Operation from a given proto method's descriptor. 2655 func extractOperationOptionFromMethodDescriptor(meth *descriptorpb.MethodDescriptorProto) (*openapi_options.Operation, error) { 2656 if meth.Options == nil { 2657 return nil, nil 2658 } 2659 if !proto.HasExtension(meth.Options, openapi_options.E_Openapiv2Operation) { 2660 return nil, nil 2661 } 2662 ext := proto.GetExtension(meth.Options, openapi_options.E_Openapiv2Operation) 2663 opts, ok := ext.(*openapi_options.Operation) 2664 if !ok { 2665 return nil, fmt.Errorf("extension is %T; want an Operation", ext) 2666 } 2667 return opts, nil 2668 } 2669 2670 // extractSchemaOptionFromMessageDescriptor extracts the message of type 2671 // openapi_options.Schema from a given proto message's descriptor. 2672 func extractSchemaOptionFromMessageDescriptor(msg *descriptorpb.DescriptorProto) (*openapi_options.Schema, error) { 2673 if msg.Options == nil { 2674 return nil, nil 2675 } 2676 if !proto.HasExtension(msg.Options, openapi_options.E_Openapiv2Schema) { 2677 return nil, nil 2678 } 2679 ext := proto.GetExtension(msg.Options, openapi_options.E_Openapiv2Schema) 2680 opts, ok := ext.(*openapi_options.Schema) 2681 if !ok { 2682 return nil, fmt.Errorf("extension is %T; want a Schema", ext) 2683 } 2684 return opts, nil 2685 } 2686 2687 // extractTagOptionFromServiceDescriptor extracts the tag of type 2688 // openapi_options.Tag from a given proto service's descriptor. 2689 func extractTagOptionFromServiceDescriptor(svc *descriptorpb.ServiceDescriptorProto) (*openapi_options.Tag, error) { 2690 if svc.Options == nil { 2691 return nil, nil 2692 } 2693 if !proto.HasExtension(svc.Options, openapi_options.E_Openapiv2Tag) { 2694 return nil, nil 2695 } 2696 ext := proto.GetExtension(svc.Options, openapi_options.E_Openapiv2Tag) 2697 opts, ok := ext.(*openapi_options.Tag) 2698 if !ok { 2699 return nil, fmt.Errorf("extension is %T; want a Tag", ext) 2700 } 2701 return opts, nil 2702 } 2703 2704 // extractOpenAPIOptionFromFileDescriptor extracts the message of type 2705 // openapi_options.OpenAPI from a given proto method's descriptor. 2706 func extractOpenAPIOptionFromFileDescriptor(file *descriptorpb.FileDescriptorProto) (*openapi_options.Swagger, error) { 2707 if file.Options == nil { 2708 return nil, nil 2709 } 2710 if !proto.HasExtension(file.Options, openapi_options.E_Openapiv2Swagger) { 2711 return nil, nil 2712 } 2713 ext := proto.GetExtension(file.Options, openapi_options.E_Openapiv2Swagger) 2714 opts, ok := ext.(*openapi_options.Swagger) 2715 if !ok { 2716 return nil, fmt.Errorf("extension is %T; want a OpenAPI object", ext) 2717 } 2718 return opts, nil 2719 } 2720 2721 func extractJSONSchemaFromFieldDescriptor(fd *descriptorpb.FieldDescriptorProto) (*openapi_options.JSONSchema, error) { 2722 if fd.Options == nil { 2723 return nil, nil 2724 } 2725 if !proto.HasExtension(fd.Options, openapi_options.E_Openapiv2Field) { 2726 return nil, nil 2727 } 2728 ext := proto.GetExtension(fd.Options, openapi_options.E_Openapiv2Field) 2729 opts, ok := ext.(*openapi_options.JSONSchema) 2730 if !ok { 2731 return nil, fmt.Errorf("extension is %T; want a JSONSchema object", ext) 2732 } 2733 return opts, nil 2734 } 2735 2736 func extractFieldBehaviorFromFieldDescriptor(fd *descriptorpb.FieldDescriptorProto) ([]annotations.FieldBehavior, error) { 2737 if fd.Options == nil { 2738 return nil, nil 2739 } 2740 if !proto.HasExtension(fd.Options, annotations.E_FieldBehavior) { 2741 return nil, nil 2742 } 2743 ext := proto.GetExtension(fd.Options, annotations.E_FieldBehavior) 2744 opts, ok := ext.([]annotations.FieldBehavior) 2745 if !ok { 2746 return nil, fmt.Errorf("extension is %T; want a []FieldBehavior object", ext) 2747 } 2748 return opts, nil 2749 } 2750 2751 func getFieldVisibilityOption(fd *descriptor.Field) *visibility.VisibilityRule { 2752 if fd.Options == nil { 2753 return nil 2754 } 2755 if !proto.HasExtension(fd.Options, visibility.E_FieldVisibility) { 2756 return nil 2757 } 2758 ext := proto.GetExtension(fd.Options, visibility.E_FieldVisibility) 2759 opts, ok := ext.(*visibility.VisibilityRule) 2760 if !ok { 2761 return nil 2762 } 2763 return opts 2764 } 2765 2766 func getServiceVisibilityOption(fd *descriptor.Service) *visibility.VisibilityRule { 2767 if fd.Options == nil { 2768 return nil 2769 } 2770 if !proto.HasExtension(fd.Options, visibility.E_ApiVisibility) { 2771 return nil 2772 } 2773 ext := proto.GetExtension(fd.Options, visibility.E_ApiVisibility) 2774 opts, ok := ext.(*visibility.VisibilityRule) 2775 if !ok { 2776 return nil 2777 } 2778 return opts 2779 } 2780 2781 func getMethodVisibilityOption(fd *descriptor.Method) *visibility.VisibilityRule { 2782 if fd.Options == nil { 2783 return nil 2784 } 2785 if !proto.HasExtension(fd.Options, visibility.E_MethodVisibility) { 2786 return nil 2787 } 2788 ext := proto.GetExtension(fd.Options, visibility.E_MethodVisibility) 2789 opts, ok := ext.(*visibility.VisibilityRule) 2790 if !ok { 2791 return nil 2792 } 2793 return opts 2794 } 2795 2796 func getEnumValueVisibilityOption(fd *descriptorpb.EnumValueDescriptorProto) *visibility.VisibilityRule { 2797 if fd.Options == nil { 2798 return nil 2799 } 2800 if !proto.HasExtension(fd.Options, visibility.E_ValueVisibility) { 2801 return nil 2802 } 2803 ext := proto.GetExtension(fd.Options, visibility.E_ValueVisibility) 2804 opts, ok := ext.(*visibility.VisibilityRule) 2805 if !ok { 2806 return nil 2807 } 2808 return opts 2809 } 2810 2811 func getMethodOpenAPIOption(reg *descriptor.Registry, meth *descriptor.Method) (*openapi_options.Operation, error) { 2812 opts, err := extractOperationOptionFromMethodDescriptor(meth.MethodDescriptorProto) 2813 if err != nil { 2814 return nil, err 2815 } 2816 if opts != nil { 2817 return opts, nil 2818 } 2819 opts, ok := reg.GetOpenAPIMethodOption(meth.FQMN()) 2820 if !ok { 2821 return nil, nil 2822 } 2823 return opts, nil 2824 } 2825 2826 func getMessageOpenAPIOption(reg *descriptor.Registry, msg *descriptor.Message) (*openapi_options.Schema, error) { 2827 opts, err := extractSchemaOptionFromMessageDescriptor(msg.DescriptorProto) 2828 if err != nil { 2829 return nil, err 2830 } 2831 if opts != nil { 2832 return opts, nil 2833 } 2834 opts, ok := reg.GetOpenAPIMessageOption(msg.FQMN()) 2835 if !ok { 2836 return nil, nil 2837 } 2838 return opts, nil 2839 } 2840 2841 func getServiceOpenAPIOption(reg *descriptor.Registry, svc *descriptor.Service) (*openapi_options.Tag, error) { 2842 if opts, ok := reg.GetOpenAPIServiceOption(svc.FQSN()); ok { 2843 return opts, nil 2844 } 2845 opts, err := extractTagOptionFromServiceDescriptor(svc.ServiceDescriptorProto) 2846 if err != nil { 2847 return nil, err 2848 } 2849 return opts, nil 2850 } 2851 2852 func getFileOpenAPIOption(reg *descriptor.Registry, file *descriptor.File) (*openapi_options.Swagger, error) { 2853 opts, err := extractOpenAPIOptionFromFileDescriptor(file.FileDescriptorProto) 2854 if err != nil { 2855 return nil, err 2856 } 2857 if opts != nil { 2858 return opts, nil 2859 } 2860 opts, ok := reg.GetOpenAPIFileOption(*file.Name) 2861 if !ok { 2862 return nil, nil 2863 } 2864 return opts, nil 2865 } 2866 2867 func getFieldOpenAPIOption(reg *descriptor.Registry, fd *descriptor.Field) (*openapi_options.JSONSchema, error) { 2868 opts, err := extractJSONSchemaFromFieldDescriptor(fd.FieldDescriptorProto) 2869 if err != nil { 2870 return nil, err 2871 } 2872 if opts != nil { 2873 return opts, nil 2874 } 2875 opts, ok := reg.GetOpenAPIFieldOption(fd.FQFN()) 2876 if !ok { 2877 return nil, nil 2878 } 2879 return opts, nil 2880 } 2881 2882 func getFieldBehaviorOption(reg *descriptor.Registry, fd *descriptor.Field) ([]annotations.FieldBehavior, error) { 2883 opts, err := extractFieldBehaviorFromFieldDescriptor(fd.FieldDescriptorProto) 2884 if err != nil { 2885 return nil, err 2886 } 2887 if opts != nil { 2888 return opts, nil 2889 } 2890 return opts, nil 2891 } 2892 2893 func protoJSONSchemaToOpenAPISchemaCore(j *openapi_options.JSONSchema, reg *descriptor.Registry, refs refMap) schemaCore { 2894 ret := schemaCore{} 2895 2896 if j.GetRef() != "" { 2897 openapiName, ok := fullyQualifiedNameToOpenAPIName(j.GetRef(), reg) 2898 if ok { 2899 ret.Ref = "#/definitions/" + openapiName 2900 if refs != nil { 2901 refs[j.GetRef()] = struct{}{} 2902 } 2903 } else { 2904 ret.Ref += j.GetRef() 2905 } 2906 } else { 2907 f, t := protoJSONSchemaTypeToFormat(j.GetType()) 2908 ret.Format = f 2909 ret.Type = t 2910 } 2911 2912 return ret 2913 } 2914 2915 func updateswaggerObjectFromJSONSchema(s *openapiSchemaObject, j *openapi_options.JSONSchema, reg *descriptor.Registry, data interface{}) { 2916 s.Title = j.GetTitle() 2917 s.Description = j.GetDescription() 2918 if reg.GetUseGoTemplate() { 2919 s.Title = goTemplateComments(s.Title, data, reg) 2920 s.Description = goTemplateComments(s.Description, data, reg) 2921 } 2922 if s.Type == "array" { 2923 s.Items.MaxLength = j.GetMaxLength() 2924 s.Items.MinLength = j.GetMinLength() 2925 s.Items.Pattern = j.GetPattern() 2926 s.Items.Default = j.GetDefault() 2927 s.Items.UniqueItems = j.GetUniqueItems() 2928 s.Items.MaxProperties = j.GetMaxProperties() 2929 s.Items.MinProperties = j.GetMinProperties() 2930 s.Items.Required = j.GetRequired() 2931 s.Items.Minimum = j.GetMinimum() 2932 s.Items.Maximum = j.GetMaximum() 2933 s.Items.ReadOnly = j.GetReadOnly() 2934 s.Items.MultipleOf = j.GetMultipleOf() 2935 s.Items.ExclusiveMaximum = j.GetExclusiveMaximum() 2936 s.Items.ExclusiveMinimum = j.GetExclusiveMinimum() 2937 s.Items.Enum = j.GetEnum() 2938 2939 if j.GetDefault() == "" { 2940 s.Items.Default = nil 2941 } 2942 if len(j.GetEnum()) == 0 { 2943 s.Items.Enum = nil 2944 } 2945 if j.GetFormat() != "" { 2946 s.Items.Format = j.GetFormat() 2947 } 2948 } else { 2949 s.MaxLength = j.GetMaxLength() 2950 s.MinLength = j.GetMinLength() 2951 s.Pattern = j.GetPattern() 2952 s.Default = j.GetDefault() 2953 s.UniqueItems = j.GetUniqueItems() 2954 s.MaxProperties = j.GetMaxProperties() 2955 s.MinProperties = j.GetMinProperties() 2956 s.Required = j.GetRequired() 2957 s.Minimum = j.GetMinimum() 2958 s.Maximum = j.GetMaximum() 2959 s.ReadOnly = j.GetReadOnly() 2960 s.MultipleOf = j.GetMultipleOf() 2961 s.ExclusiveMaximum = j.GetExclusiveMaximum() 2962 s.ExclusiveMinimum = j.GetExclusiveMinimum() 2963 s.Enum = j.GetEnum() 2964 2965 if j.GetDefault() == "" { 2966 s.Default = nil 2967 } 2968 if len(j.GetEnum()) == 0 { 2969 s.Enum = nil 2970 } 2971 if j.GetFormat() != "" { 2972 s.Format = j.GetFormat() 2973 } 2974 } 2975 s.MaxItems = j.GetMaxItems() 2976 s.MinItems = j.GetMinItems() 2977 2978 if j.GetExtensions() != nil { 2979 exts, err := processExtensions(j.GetExtensions()) 2980 if err != nil { 2981 panic(err) 2982 } 2983 s.extensions = exts 2984 } 2985 if overrideType := j.GetType(); len(overrideType) > 0 { 2986 s.Type = strings.ToLower(overrideType[0].String()) 2987 } 2988 if j.GetExample() != "" { 2989 s.Example = RawExample(j.GetExample()) 2990 } 2991 } 2992 2993 func updateSwaggerObjectFromFieldBehavior(s *openapiSchemaObject, j []annotations.FieldBehavior, reg *descriptor.Registry, field *descriptor.Field) { 2994 for _, fb := range j { 2995 switch fb { 2996 case annotations.FieldBehavior_REQUIRED: 2997 if reg.GetUseJSONNamesForFields() { 2998 s.Required = append(s.Required, *field.JsonName) 2999 } else { 3000 s.Required = append(s.Required, *field.Name) 3001 } 3002 case annotations.FieldBehavior_OUTPUT_ONLY: 3003 s.ReadOnly = true 3004 case annotations.FieldBehavior_FIELD_BEHAVIOR_UNSPECIFIED: 3005 case annotations.FieldBehavior_OPTIONAL: 3006 case annotations.FieldBehavior_INPUT_ONLY: 3007 // OpenAPI v3 supports a writeOnly property, but this is not supported in Open API v2 3008 case annotations.FieldBehavior_IMMUTABLE: 3009 } 3010 } 3011 } 3012 3013 func openapiSchemaFromProtoSchema(s *openapi_options.Schema, reg *descriptor.Registry, refs refMap, data interface{}) openapiSchemaObject { 3014 ret := openapiSchemaObject{ 3015 ExternalDocs: protoExternalDocumentationToOpenAPIExternalDocumentation(s.GetExternalDocs(), reg, data), 3016 } 3017 3018 ret.schemaCore = protoJSONSchemaToOpenAPISchemaCore(s.GetJsonSchema(), reg, refs) 3019 updateswaggerObjectFromJSONSchema(&ret, s.GetJsonSchema(), reg, data) 3020 3021 if s != nil && s.Example != "" { 3022 ret.Example = RawExample(s.Example) 3023 } 3024 3025 return ret 3026 } 3027 3028 func openapiExamplesFromProtoExamples(in map[string]string) map[string]interface{} { 3029 if len(in) == 0 { 3030 return nil 3031 } 3032 out := make(map[string]interface{}, len(in)) 3033 for mimeType, exampleStr := range in { 3034 switch mimeType { 3035 case "application/json": 3036 // JSON example objects are rendered raw. 3037 out[mimeType] = RawExample(exampleStr) 3038 default: 3039 // All other mimetype examples are rendered as strings. 3040 out[mimeType] = exampleStr 3041 } 3042 } 3043 return out 3044 } 3045 3046 func protoJSONSchemaTypeToFormat(in []openapi_options.JSONSchema_JSONSchemaSimpleTypes) (string, string) { 3047 if len(in) == 0 { 3048 return "", "" 3049 } 3050 3051 // Can't support more than 1 type, just return the first element. 3052 // This is due to an inconsistency in the design of the openapiv2 proto 3053 // and that used in schemaCore. schemaCore uses the v3 definition of types, 3054 // which only allows a single string, while the openapiv2 proto uses the OpenAPI v2 3055 // definition, which defers to the JSON schema definition, which allows a string or an array. 3056 // Sources: 3057 // https://swagger.io/specification/#itemsObject 3058 // https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.2 3059 switch in[0] { 3060 case openapi_options.JSONSchema_UNKNOWN, openapi_options.JSONSchema_NULL: 3061 return "", "" 3062 case openapi_options.JSONSchema_OBJECT: 3063 return "object", "" 3064 case openapi_options.JSONSchema_ARRAY: 3065 return "array", "" 3066 case openapi_options.JSONSchema_BOOLEAN: 3067 // NOTE: in OpenAPI specification, format should be empty on boolean type 3068 return "boolean", "" 3069 case openapi_options.JSONSchema_INTEGER: 3070 return "integer", "int32" 3071 case openapi_options.JSONSchema_NUMBER: 3072 return "number", "double" 3073 case openapi_options.JSONSchema_STRING: 3074 // NOTE: in OpenAPI specification, format should be empty on string type 3075 return "string", "" 3076 default: 3077 // Maybe panic? 3078 return "", "" 3079 } 3080 } 3081 3082 func protoExternalDocumentationToOpenAPIExternalDocumentation(in *openapi_options.ExternalDocumentation, reg *descriptor.Registry, data interface{}) *openapiExternalDocumentationObject { 3083 if in == nil { 3084 return nil 3085 } 3086 3087 if reg.GetUseGoTemplate() { 3088 in.Description = goTemplateComments(in.Description, data, reg) 3089 } 3090 3091 return &openapiExternalDocumentationObject{ 3092 Description: in.Description, 3093 URL: in.Url, 3094 } 3095 } 3096 3097 func addCustomRefs(d openapiDefinitionsObject, reg *descriptor.Registry, refs refMap) error { 3098 if len(refs) == 0 { 3099 return nil 3100 } 3101 msgMap := make(messageMap) 3102 enumMap := make(enumMap) 3103 for ref := range refs { 3104 swgName, swgOk := fullyQualifiedNameToOpenAPIName(ref, reg) 3105 if !swgOk { 3106 grpclog.Errorf("can't resolve OpenAPI name from CustomRef %q", ref) 3107 continue 3108 } 3109 if _, ok := d[swgName]; ok { 3110 // Skip already existing definitions 3111 delete(refs, ref) 3112 continue 3113 } 3114 msg, err := reg.LookupMsg("", ref) 3115 if err == nil { 3116 msgMap[swgName] = msg 3117 continue 3118 } 3119 enum, err := reg.LookupEnum("", ref) 3120 if err == nil { 3121 enumMap[swgName] = enum 3122 continue 3123 } 3124 3125 // ?? Should be either enum or msg 3126 } 3127 if err := renderMessagesAsDefinition(msgMap, d, reg, refs, nil); err != nil { 3128 return err 3129 } 3130 renderEnumerationsAsDefinition(enumMap, d, reg) 3131 3132 // Run again in case any new refs were added 3133 return addCustomRefs(d, reg, refs) 3134 } 3135 3136 func lowerCamelCase(fieldName string, fields []*descriptor.Field, msgs []*descriptor.Message) string { 3137 for _, oneField := range fields { 3138 if oneField.GetName() == fieldName { 3139 return oneField.GetJsonName() 3140 } 3141 } 3142 messageNameToFieldsToJSONName := make(map[string]map[string]string, len(msgs)) 3143 fieldNameToType := make(map[string]string) 3144 for _, msg := range msgs { 3145 fieldNameToJSONName := make(map[string]string) 3146 for _, oneField := range msg.GetField() { 3147 fieldNameToJSONName[oneField.GetName()] = oneField.GetJsonName() 3148 fieldNameToType[oneField.GetName()] = oneField.GetTypeName() 3149 } 3150 messageNameToFieldsToJSONName[msg.GetName()] = fieldNameToJSONName 3151 } 3152 if strings.Contains(fieldName, ".") { 3153 fieldNames := strings.Split(fieldName, ".") 3154 fieldNamesWithCamelCase := make([]string, 0) 3155 for i := 0; i < len(fieldNames)-1; i++ { 3156 fieldNamesWithCamelCase = append(fieldNamesWithCamelCase, casing.JSONCamelCase(fieldNames[i])) 3157 } 3158 prefix := strings.Join(fieldNamesWithCamelCase, ".") 3159 reservedJSONName := getReservedJSONName(fieldName, messageNameToFieldsToJSONName, fieldNameToType) 3160 if reservedJSONName != "" { 3161 return prefix + "." + reservedJSONName 3162 } 3163 } 3164 return casing.JSONCamelCase(fieldName) 3165 } 3166 3167 func getReservedJSONName(fieldName string, messageNameToFieldsToJSONName map[string]map[string]string, fieldNameToType map[string]string) string { 3168 if len(strings.Split(fieldName, ".")) == 2 { 3169 fieldNames := strings.Split(fieldName, ".") 3170 firstVariable := fieldNames[0] 3171 firstType := fieldNameToType[firstVariable] 3172 firstTypeShortNames := strings.Split(firstType, ".") 3173 firstTypeShortName := firstTypeShortNames[len(firstTypeShortNames)-1] 3174 return messageNameToFieldsToJSONName[firstTypeShortName][fieldNames[1]] 3175 } 3176 fieldNames := strings.Split(fieldName, ".") 3177 return getReservedJSONName(strings.Join(fieldNames[1:], "."), messageNameToFieldsToJSONName, fieldNameToType) 3178 } 3179 3180 func find(a []string, x string) int { 3181 // This is a linear search but we are dealing with a small number of fields 3182 for i, n := range a { 3183 if x == n { 3184 return i 3185 } 3186 } 3187 return -1 3188 } 3189 3190 // Make a deep copy of the outer parameters that has paramName as the first component, 3191 // but remove the first component of the field path. 3192 func subPathParams(paramName string, outerParams []descriptor.Parameter) []descriptor.Parameter { 3193 var innerParams []descriptor.Parameter 3194 for _, p := range outerParams { 3195 if len(p.FieldPath) > 1 && p.FieldPath[0].Name == paramName { 3196 subParam := descriptor.Parameter{ 3197 FieldPath: p.FieldPath[1:], 3198 Target: p.Target, 3199 Method: p.Method, 3200 } 3201 innerParams = append(innerParams, subParam) 3202 } 3203 } 3204 return innerParams 3205 } 3206 3207 func getFieldConfiguration(reg *descriptor.Registry, fd *descriptor.Field) *openapi_options.JSONSchema_FieldConfiguration { 3208 if j, err := getFieldOpenAPIOption(reg, fd); err == nil { 3209 return j.GetFieldConfiguration() 3210 } 3211 return nil 3212 }