diff --git a/cmd/protoc-gen-openapi/examples/google/example/library/v1/openapi.yaml b/cmd/protoc-gen-openapi/examples/google/example/library/v1/openapi.yaml index 475f60ec..fa81f285 100644 --- a/cmd/protoc-gen-openapi/examples/google/example/library/v1/openapi.yaml +++ b/cmd/protoc-gen-openapi/examples/google/example/library/v1/openapi.yaml @@ -269,7 +269,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/MoveBookRequest' + $ref: '#/components/schemas/MoveBookRequestBody' required: true responses: "200": @@ -302,7 +302,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/MergeShelvesRequest' + $ref: '#/components/schemas/MergeShelvesRequestBody' required: true responses: "200": @@ -370,28 +370,20 @@ components: type: string description: A token to retrieve next page of results. Pass this value in the [ListShelvesRequest.page_token][google.example.library.v1.ListShelvesRequest.page_token] field in the subsequent call to `ListShelves` method to retrieve the next page of results. description: Response message for LibraryService.ListShelves. - MergeShelvesRequest: + MergeShelvesRequestBody: required: - - name - other_shelf_name type: object properties: - name: - type: string - description: The name of the shelf we're adding books to. other_shelf_name: type: string description: The name of the shelf we're removing books from and deleting. description: Describes the shelf being removed (other_shelf_name) and updated (name) in this merge. - MoveBookRequest: + MoveBookRequestBody: required: - - name - other_shelf_name type: object properties: - name: - type: string - description: The name of the book to move. other_shelf_name: type: string description: The name of the destination shelf. diff --git a/cmd/protoc-gen-openapi/examples/google/example/library/v1/openapi_json.yaml b/cmd/protoc-gen-openapi/examples/google/example/library/v1/openapi_json.yaml index f5a0697d..96c7c3b6 100644 --- a/cmd/protoc-gen-openapi/examples/google/example/library/v1/openapi_json.yaml +++ b/cmd/protoc-gen-openapi/examples/google/example/library/v1/openapi_json.yaml @@ -269,7 +269,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/MoveBookRequest' + $ref: '#/components/schemas/MoveBookRequestBody' required: true responses: "200": @@ -302,7 +302,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/MergeShelvesRequest' + $ref: '#/components/schemas/MergeShelvesRequestBody' required: true responses: "200": @@ -370,28 +370,20 @@ components: type: string description: A token to retrieve next page of results. Pass this value in the [ListShelvesRequest.page_token][google.example.library.v1.ListShelvesRequest.page_token] field in the subsequent call to `ListShelves` method to retrieve the next page of results. description: Response message for LibraryService.ListShelves. - MergeShelvesRequest: + MergeShelvesRequestBody: required: - - name - otherShelfName type: object properties: - name: - type: string - description: The name of the shelf we're adding books to. otherShelfName: type: string description: The name of the shelf we're removing books from and deleting. description: Describes the shelf being removed (other_shelf_name) and updated (name) in this merge. - MoveBookRequest: + MoveBookRequestBody: required: - - name - otherShelfName type: object properties: - name: - type: string - description: The name of the book to move. otherShelfName: type: string description: The name of the destination shelf. diff --git a/cmd/protoc-gen-openapi/examples/tests/mapfields/openapi.yaml b/cmd/protoc-gen-openapi/examples/tests/mapfields/openapi.yaml index 7e70d512..76dd12e7 100644 --- a/cmd/protoc-gen-openapi/examples/tests/mapfields/openapi.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/mapfields/openapi.yaml @@ -21,7 +21,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Message' + $ref: '#/components/schemas/UpdateMessageRequestBody' required: true responses: "200": @@ -81,5 +81,36 @@ components: format: int64 label: type: string + UpdateMessageRequestBody: + type: object + properties: + another_message: + $ref: '#/components/schemas/AnotherMessage' + sub_message: + $ref: '#/components/schemas/Message_SubMessage' + string_list: + type: array + items: + type: string + sub_message_list: + type: array + items: + $ref: '#/components/schemas/Message_SubMessage' + object_list: + type: array + items: + type: object + strings_map: + type: object + additionalProperties: + type: string + sub_messages_map: + type: object + additionalProperties: + $ref: '#/components/schemas/Message_SubMessage' + objects_map: + type: object + additionalProperties: + type: object tags: - name: Messaging diff --git a/cmd/protoc-gen-openapi/examples/tests/mapfields/openapi_json.yaml b/cmd/protoc-gen-openapi/examples/tests/mapfields/openapi_json.yaml index 5600f548..c01af272 100644 --- a/cmd/protoc-gen-openapi/examples/tests/mapfields/openapi_json.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/mapfields/openapi_json.yaml @@ -21,7 +21,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Message' + $ref: '#/components/schemas/UpdateMessageRequestBody' required: true responses: "200": @@ -81,5 +81,36 @@ components: format: int64 label: type: string + UpdateMessageRequestBody: + type: object + properties: + anotherMessage: + $ref: '#/components/schemas/AnotherMessage' + subMessage: + $ref: '#/components/schemas/Message_SubMessage' + stringList: + type: array + items: + type: string + subMessageList: + type: array + items: + $ref: '#/components/schemas/Message_SubMessage' + objectList: + type: array + items: + type: object + stringsMap: + type: object + additionalProperties: + type: string + subMessagesMap: + type: object + additionalProperties: + $ref: '#/components/schemas/Message_SubMessage' + objectsMap: + type: object + additionalProperties: + type: object tags: - name: Messaging diff --git a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi.yaml b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi.yaml index 21e30e73..8b8429cb 100644 --- a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi.yaml @@ -43,7 +43,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Message' + $ref: '#/components/schemas/CreateMessageRequestBody' required: true responses: "200": @@ -78,6 +78,16 @@ paths: $ref: '#/components/schemas/Message' components: schemas: + CreateMessageRequestBody: + type: object + properties: + user_id: + type: integer + format: uint64 + content: + type: string + maybe: + type: string Message: type: object properties: diff --git a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_json.yaml b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_json.yaml index eb124f04..a93cbb4b 100644 --- a/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_json.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/pathparams/openapi_json.yaml @@ -43,7 +43,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Message' + $ref: '#/components/schemas/CreateMessageRequestBody' required: true responses: "200": @@ -78,6 +78,16 @@ paths: $ref: '#/components/schemas/Message' components: schemas: + CreateMessageRequestBody: + type: object + properties: + userId: + type: integer + format: uint64 + content: + type: string + maybe: + type: string Message: type: object properties: diff --git a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml index 5dfbfed4..0f99f585 100644 --- a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml @@ -113,7 +113,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Message' + $ref: '#/components/schemas/CreateMessageRequestBody' required: true responses: "200": @@ -247,6 +247,46 @@ components: items: $ref: '#/components/schemas/AnyJSONValue' description: AnyJSONValue is a "catch all" type that can hold any JSON value, except null as this is not allowed in OpenAPI + CreateMessageRequestBody: + type: object + properties: + string_type: + type: string + recursive_type: + $ref: '#/components/schemas/RecursiveParent' + embedded_type: + $ref: '#/components/schemas/Message_EmbMessage' + sub_type: + $ref: '#/components/schemas/SubMessage' + repeated_type: + type: array + items: + type: string + repeated_sub_type: + type: array + items: + $ref: '#/components/schemas/SubMessage' + repeated_recursive_type: + type: array + items: + $ref: '#/components/schemas/RecursiveParent' + map_type: + type: object + additionalProperties: + type: string + body: + type: object + media: + type: array + items: + type: object + value_type: + $ref: '#/components/schemas/AnyJSONValue' + repeated_value_type: + type: array + items: + $ref: '#/components/schemas/AnyJSONValue' + description: Description of repeated value Message: type: object properties: diff --git a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml index 23caa5d4..ec0d7a6d 100644 --- a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml @@ -113,7 +113,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Message' + $ref: '#/components/schemas/CreateMessageRequestBody' required: true responses: "200": @@ -247,6 +247,46 @@ components: items: $ref: '#/components/schemas/AnyJSONValue' description: AnyJSONValue is a "catch all" type that can hold any JSON value, except null as this is not allowed in OpenAPI + CreateMessageRequestBody: + type: object + properties: + stringType: + type: string + recursiveType: + $ref: '#/components/schemas/RecursiveParent' + embeddedType: + $ref: '#/components/schemas/Message_EmbMessage' + subType: + $ref: '#/components/schemas/SubMessage' + repeatedType: + type: array + items: + type: string + repeatedSubType: + type: array + items: + $ref: '#/components/schemas/SubMessage' + repeatedRecursiveType: + type: array + items: + $ref: '#/components/schemas/RecursiveParent' + mapType: + type: object + additionalProperties: + type: string + body: + type: object + media: + type: array + items: + type: object + valueType: + $ref: '#/components/schemas/AnyJSONValue' + repeatedValueType: + type: array + items: + $ref: '#/components/schemas/AnyJSONValue' + description: Description of repeated value Message: type: object properties: diff --git a/cmd/protoc-gen-openapi/generator/openapi-v3.go b/cmd/protoc-gen-openapi/generator/openapi-v3.go index 52fd6ae4..a329de56 100644 --- a/cmd/protoc-gen-openapi/generator/openapi-v3.go +++ b/cmd/protoc-gen-openapi/generator/openapi-v3.go @@ -40,6 +40,13 @@ type Configuration struct { CircularDepth *int } +// Schema from a Message with modified name and/or fields +type ModifiedSchema struct { + Name string + Message *protogen.Message + FieldNames []string +} + const ( infoURL = "https://github.com/google/gnostic/tree/master/cmd/protoc-gen-openapi" protobufValueName = "AnyJSONValue" @@ -50,6 +57,7 @@ type OpenAPIv3Generator struct { conf Configuration plugin *protogen.Plugin + modifiedSchemas []ModifiedSchema requiredSchemas []string // Names of schemas that need to be generated. generatedSchemas []string // Names of schemas that have already been generated. linterRulePattern *regexp.Regexp @@ -127,6 +135,10 @@ func (g *OpenAPIv3Generator) buildDocumentV3() *v3.Document { g.requiredSchemas = g.requiredSchemas[count:len(g.requiredSchemas)] } + for _, schema := range g.modifiedSchemas { + g.addSchemaToDocumentV3(d, schema.Message, schema.Name, schema.FieldNames) + } + allServers := []string{} // If paths methods has servers, but they're all the same, then move servers to path level @@ -276,7 +288,7 @@ func (g *OpenAPIv3Generator) addPathsToDocumentV3(d *v3.Document, file *protogen defaultHost := proto.GetExtension(service.Desc.Options(), annotations.E_DefaultHost).(string) op, path2 := g.buildOperationV3( - file, operationID, service.GoName, comment, defaultHost, path, body, inputMessage, outputMessage) + file, operationID, service.GoName, method.GoName, comment, defaultHost, path, body, inputMessage, outputMessage) g.addOperationV3(d, op, path2, methodName) } } @@ -470,6 +482,7 @@ func (g *OpenAPIv3Generator) buildOperationV3( file *protogen.File, operationID string, tagName string, + methodName string, description string, defaultHost string, path string, @@ -623,10 +636,27 @@ func (g *OpenAPIv3Generator) buildOperationV3( var requestSchema *v3.SchemaOrReference if bodyField == "*" { - // Pass the entire request message as the request body. - typeName := fullMessageTypeName(inputMessage.Desc) - requestSchema = g.schemaOrReferenceForType(typeName) + if string(inputMessage.Desc.FullName()) != "google.api.HttpBody" && haveCommonItem(coveredParameters, g.getMessageFieldNames(inputMessage)) { + // Add a new modified schema with fields that are not covered by path parameters + schema := ModifiedSchema{} + for _, field := range inputMessage.Fields { + fieldName := string(field.Desc.Name()) + if !contains(coveredParameters, fieldName) { + schema.FieldNames = append(schema.FieldNames, fieldName) + } + } + schema.Name = methodName + "RequestBody" + schema.Message = inputMessage + g.modifiedSchemas = append(g.modifiedSchemas, schema) + // Pass it as the request body. + typeName := schema.Name + requestSchema = g.schemaOrReferenceForType(typeName) + } else { + // Pass the entire request message as the request body. + typeName := fullMessageTypeName(inputMessage.Desc) + requestSchema = g.schemaOrReferenceForType(typeName) + } } else { // If body refers to a message field, use that type. for _, field := range inputMessage.Fields { @@ -892,70 +922,53 @@ func (g *OpenAPIv3Generator) schemaOrReferenceForField(field protoreflect.FieldD return kindSchema } -// addSchemasToDocumentV3 adds info from one file descriptor. -func (g *OpenAPIv3Generator) addSchemasToDocumentV3(d *v3.Document, messages []*protogen.Message) { - // For each message, generate a definition. - for _, message := range messages { - if message.Messages != nil { - g.addSchemasToDocumentV3(d, message.Messages) - } - - typeName := fullMessageTypeName(message.Desc) - - // Only generate this if we need it and haven't already generated it. - if !contains(g.requiredSchemas, typeName) || - contains(g.generatedSchemas, typeName) { - continue - } - - g.generatedSchemas = append(g.generatedSchemas, typeName) +// addSchemaToDocumentV3 adds a single schema +func (g *OpenAPIv3Generator) addSchemaToDocumentV3(d *v3.Document, message *protogen.Message, name string, fieldNames []string) { + // google.protobuf.Value is handled like a special value when doing transcoding. + // It's interpreted as a "catch all" JSON value, that can be anything. + if message.Desc != nil && message.Desc.FullName() == "google.protobuf.Value" { + // Add the schema to the components.schema list. + description := protobufValueName + ` is a "catch all" type that can hold any JSON value, except null as this is not allowed in OpenAPI` - // google.protobuf.Value is handled like a special value when doing transcoding. - // It's interpreted as a "catch all" JSON value, that can be anything. - if message.Desc != nil && message.Desc.FullName() == "google.protobuf.Value" { - // Add the schema to the components.schema list. - description := protobufValueName + ` is a "catch all" type that can hold any JSON value, except null as this is not allowed in OpenAPI` - - d.Components.Schemas.AdditionalProperties = append(d.Components.Schemas.AdditionalProperties, - &v3.NamedSchemaOrReference{ - Name: protobufValueName, - Value: &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{ - Description: description, - OneOf: []*v3.SchemaOrReference{ - // type is not allow to be null in OpenAPI - { - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{Type: "string"}, - }, - }, { - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{Type: "number"}, - }, - }, { - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{Type: "integer"}, - }, - }, { - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{Type: "boolean"}, - }, - }, { - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{Type: "object"}, - }, - }, { - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{ - Type: "array", - Items: &v3.ItemsItem{ - SchemaOrReference: []*v3.SchemaOrReference{{ - Oneof: &v3.SchemaOrReference_Reference{ - Reference: &v3.Reference{XRef: "#/components/schemas/" + protobufValueName}, - }, - }}, - }, + d.Components.Schemas.AdditionalProperties = append(d.Components.Schemas.AdditionalProperties, + &v3.NamedSchemaOrReference{ + Name: protobufValueName, + Value: &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Description: description, + OneOf: []*v3.SchemaOrReference{ + // type is not allow to be null in OpenAPI + { + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string"}, + }, + }, { + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "number"}, + }, + }, { + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "integer"}, + }, + }, { + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "boolean"}, + }, + }, { + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "object"}, + }, + }, { + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "array", + Items: &v3.ItemsItem{ + SchemaOrReference: []*v3.SchemaOrReference{{ + Oneof: &v3.SchemaOrReference_Reference{ + Reference: &v3.Reference{XRef: "#/components/schemas/" + protobufValueName}, + }, + }}, }, }, }, @@ -964,84 +977,130 @@ func (g *OpenAPIv3Generator) addSchemasToDocumentV3(d *v3.Document, messages []* }, }, }, - ) - continue - } + }, + ) + return + } - // Get the message description from the comments. - messageDescription := g.filterCommentString(message.Comments.Leading, true) + // Get the message description from the comments. + messageDescription := g.filterCommentString(message.Comments.Leading, true) - // Build an array holding the fields of the message. - definitionProperties := &v3.Properties{ - AdditionalProperties: make([]*v3.NamedSchemaOrReference, 0), + // Build an array holding the fields of the message. + definitionProperties := &v3.Properties{ + AdditionalProperties: make([]*v3.NamedSchemaOrReference, 0), + } + + var required []string + for _, field := range message.Fields { + // If the name is not in fieldNames, we don't need this field + if (!contains(fieldNames, string(field.Desc.Name()))) { + continue } - var required []string - for _, field := range message.Fields { - // Check the field annotations to see if this is a readonly or writeonly field. - inputOnly := false - outputOnly := false - extension := proto.GetExtension(field.Desc.Options(), annotations.E_FieldBehavior) - if extension != nil { - switch v := extension.(type) { - case []annotations.FieldBehavior: - for _, vv := range v { - switch vv { - case annotations.FieldBehavior_OUTPUT_ONLY: - outputOnly = true - case annotations.FieldBehavior_INPUT_ONLY: - inputOnly = true - case annotations.FieldBehavior_REQUIRED: - required = append(required, g.formatFieldName(field)) - } + // Check the field annotations to see if this is a readonly or writeonly field. + inputOnly := false + outputOnly := false + extension := proto.GetExtension(field.Desc.Options(), annotations.E_FieldBehavior) + if extension != nil { + switch v := extension.(type) { + case []annotations.FieldBehavior: + for _, vv := range v { + switch vv { + case annotations.FieldBehavior_OUTPUT_ONLY: + outputOnly = true + case annotations.FieldBehavior_INPUT_ONLY: + inputOnly = true + case annotations.FieldBehavior_REQUIRED: + required = append(required, g.formatFieldName(field)) } - default: - log.Printf("unsupported extension type %T", extension) } + default: + log.Printf("unsupported extension type %T", extension) } + } - // The field is either described by a reference or a schema. - fieldSchema := g.schemaOrReferenceForField(field.Desc) - if fieldSchema == nil { - continue - } + // The field is either described by a reference or a schema. + fieldSchema := g.schemaOrReferenceForField(field.Desc) + if fieldSchema == nil { + continue + } - if schema, ok := fieldSchema.Oneof.(*v3.SchemaOrReference_Schema); ok { - // Get the field description from the comments. - schema.Schema.Description = g.filterCommentString(field.Comments.Leading, true) - if outputOnly { - schema.Schema.ReadOnly = true - } - if inputOnly { - schema.Schema.WriteOnly = true - } + if schema, ok := fieldSchema.Oneof.(*v3.SchemaOrReference_Schema); ok { + // Get the field description from the comments. + schema.Schema.Description = g.filterCommentString(field.Comments.Leading, true) + if outputOnly { + schema.Schema.ReadOnly = true + } + if inputOnly { + schema.Schema.WriteOnly = true } - - definitionProperties.AdditionalProperties = append( - definitionProperties.AdditionalProperties, - &v3.NamedSchemaOrReference{ - Name: g.formatFieldName(field), - Value: fieldSchema, - }, - ) } - // Add the schema to the components.schema list. - d.Components.Schemas.AdditionalProperties = append(d.Components.Schemas.AdditionalProperties, + + definitionProperties.AdditionalProperties = append( + definitionProperties.AdditionalProperties, &v3.NamedSchemaOrReference{ - Name: g.formatMessageName(message), - Value: &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{ - Type: "object", - Description: messageDescription, - Properties: definitionProperties, - Required: required, - }, + Name: g.formatFieldName(field), + Value: fieldSchema, + }, + ) + } + // Add the schema to the components.schema list. + d.Components.Schemas.AdditionalProperties = append(d.Components.Schemas.AdditionalProperties, + &v3.NamedSchemaOrReference{ + Name: name, + Value: &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "object", + Description: messageDescription, + Properties: definitionProperties, + Required: required, }, }, }, - ) + }, + ) +} + +// addSchemasToDocumentV3 adds info from one file descriptor. +func (g *OpenAPIv3Generator) addSchemasToDocumentV3(d *v3.Document, messages []*protogen.Message) { + // For each message, generate a definition. + for _, message := range messages { + if message.Messages != nil { + g.addSchemasToDocumentV3(d, message.Messages) + } + + typeName := fullMessageTypeName(message.Desc) + + // Only generate this if we need it and haven't already generated it. + if !contains(g.requiredSchemas, typeName) || + contains(g.generatedSchemas, typeName) { + continue + } + + g.generatedSchemas = append(g.generatedSchemas, typeName) + + g.addSchemaToDocumentV3(d, message, g.formatMessageName(message), g.getMessageFieldNames(message)) + } +} + +// getMessageFieldNames gets the names of all fields from a message +func (g *OpenAPIv3Generator) getMessageFieldNames(message *protogen.Message) []string { + fieldNames := []string{} + for _, field := range message.Fields { + fieldNames = append(fieldNames, string(field.Desc.Name())) } + return fieldNames +} + +// haveCommonItem returns true if 2 string arrays have a common item +func haveCommonItem(a1 []string, a2 []string) bool { + for _, item := range a1 { + if contains(a2, item) { + return true + } + } + return false } // contains returns true if an array contains a specified string.