diff --git a/apps/protoc-gen-openapi/examples/tests/mapfields/message.proto b/apps/protoc-gen-openapi/examples/tests/mapfields/message.proto index 4fda21b..11cdb4b 100644 --- a/apps/protoc-gen-openapi/examples/tests/mapfields/message.proto +++ b/apps/protoc-gen-openapi/examples/tests/mapfields/message.proto @@ -18,6 +18,7 @@ syntax = "proto3"; package tests.mapfields.message.v1; import "google/api/annotations.proto"; +import "google/protobuf/struct.proto"; option go_package = "github.com/google/gnostic/apps/protoc-gen-openapi/examples/tests/mapfields/message/v1;message"; @@ -29,7 +30,18 @@ service Messaging { }; } } + message Message { + message SubMessage { + int64 id = 1; + string label = 2; + } string message_id = 1; - map labels = 2; + SubMessage sub_message = 2; + repeated string string_list = 3; + repeated SubMessage sub_message_list = 4; + repeated google.protobuf.Struct object_list = 5; + map strings_map = 6; + map sub_messages_map = 7; + map objects_map = 8; } diff --git a/apps/protoc-gen-openapi/examples/tests/mapfields/openapi.yaml b/apps/protoc-gen-openapi/examples/tests/mapfields/openapi.yaml index aa403a6..89f2141 100644 --- a/apps/protoc-gen-openapi/examples/tests/mapfields/openapi.yaml +++ b/apps/protoc-gen-openapi/examples/tests/mapfields/openapi.yaml @@ -36,7 +36,38 @@ components: properties: message_id: type: string - labels: + sub_message: + $ref: '#/components/schemas/SubMessage' + string_list: + type: array + items: + type: string + sub_message_list: + type: array + items: + $ref: '#/components/schemas/SubMessage' + object_list: + type: array + items: + type: object + strings_map: type: object + additionalProperties: + type: string + sub_messages_map: + type: object + additionalProperties: + $ref: '#/components/schemas/SubMessage' + objects_map: + type: object + additionalProperties: + type: object + SubMessage: + properties: + id: + type: integer + format: int64 + label: + type: string tags: - name: Messaging diff --git a/apps/protoc-gen-openapi/examples/tests/mapfields/openapi_json.yaml b/apps/protoc-gen-openapi/examples/tests/mapfields/openapi_json.yaml index 0eb736d..a3bd375 100644 --- a/apps/protoc-gen-openapi/examples/tests/mapfields/openapi_json.yaml +++ b/apps/protoc-gen-openapi/examples/tests/mapfields/openapi_json.yaml @@ -36,7 +36,38 @@ components: properties: messageId: type: string - labels: + subMessage: + $ref: '#/components/schemas/SubMessage' + stringList: + type: array + items: + type: string + subMessageList: + type: array + items: + $ref: '#/components/schemas/SubMessage' + objectList: + type: array + items: + type: object + stringsMap: type: object + additionalProperties: + type: string + subMessagesMap: + type: object + additionalProperties: + $ref: '#/components/schemas/SubMessage' + objectsMap: + type: object + additionalProperties: + type: object + SubMessage: + properties: + id: + type: integer + format: int64 + label: + type: string tags: - name: Messaging diff --git a/apps/protoc-gen-openapi/generator/openapi-v3.go b/apps/protoc-gen-openapi/generator/openapi-v3.go index 8ff32bb..511c32d 100644 --- a/apps/protoc-gen-openapi/generator/openapi-v3.go +++ b/apps/protoc-gen-openapi/generator/openapi-v3.go @@ -44,7 +44,6 @@ type OpenAPIv3Generator struct { conf Configuration plugin *protogen.Plugin - singleService bool // 1 file with 1 service requiredSchemas []string // Names of schemas that need to be generated. generatedSchemas []string // Names of schemas that have already been generated. linterRulePattern *regexp.Regexp @@ -117,10 +116,11 @@ func (g *OpenAPIv3Generator) buildDocumentV3() *v3.Document { for len(g.requiredSchemas) > 0 { count := len(g.requiredSchemas) for _, file := range g.plugin.Files { - g.addSchemasToDocumentV3(d, file) + g.addSchemasToDocumentV3(d, file.Messages) } g.requiredSchemas = g.requiredSchemas[count:len(g.requiredSchemas)] } + // Sort the tags. { pairs := d.Tags @@ -411,7 +411,7 @@ func (g *OpenAPIv3Generator) buildOperationV3( var bodyFieldMessageTypeName string if bodyField == "*" { // Pass the entire request message as the request body. - bodyFieldMessageTypeName = fullMessageTypeName(inputMessage) + bodyFieldMessageTypeName = fullMessageTypeName(inputMessage.Desc) } else { // If body refers to a message field, use that type. for _, field := range inputMessage.Fields { @@ -420,7 +420,7 @@ func (g *OpenAPIv3Generator) buildOperationV3( case protoreflect.StringKind: bodyFieldScalarTypeName = "string" case protoreflect.MessageKind: - bodyFieldMessageTypeName = fullMessageTypeName(field.Message) + bodyFieldMessageTypeName = fullMessageTypeName(field.Message.Desc) default: log.Printf("unsupported field type %+v", field.Desc) } @@ -518,29 +518,13 @@ func (g *OpenAPIv3Generator) schemaReferenceForTypeName(typeName string) string return "#/components/schemas/" + g.formatMessageRef(lastPart) } -// itemsItemForTypeName is a helper constructor. -func itemsItemForTypeName(typeName string) *v3.ItemsItem { - return &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{{ - Oneof: &v3.SchemaOrReference_Schema{ - Schema: &v3.Schema{ - Type: typeName}}}}} -} - -// itemsItemForReference is a helper constructor. -func itemsItemForReference(xref string) *v3.ItemsItem { - return &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{{ - Oneof: &v3.SchemaOrReference_Reference{ - Reference: &v3.Reference{ - XRef: xref}}}}} -} - // fullMessageTypeName builds the full type name of a message. -func fullMessageTypeName(message *protogen.Message) string { - return "." + string(message.Desc.ParentFile().Package()) + "." + string(message.Desc.Name()) +func fullMessageTypeName(message protoreflect.MessageDescriptor) string { + return "." + string(message.ParentFile().Package()) + "." + string(message.Name()) } func (g *OpenAPIv3Generator) responseContentForMessage(outputMessage *protogen.Message) *v3.MediaTypes { - typeName := fullMessageTypeName(outputMessage) + typeName := fullMessageTypeName(outputMessage.Desc) if typeName == ".google.protobuf.Empty" { return &v3.MediaTypes{} @@ -568,7 +552,7 @@ func (g *OpenAPIv3Generator) responseContentForMessage(outputMessage *protogen.M Schema: &v3.SchemaOrReference{ Oneof: &v3.SchemaOrReference_Reference{ Reference: &v3.Reference{ - XRef: g.schemaReferenceForTypeName(fullMessageTypeName(outputMessage)), + XRef: g.schemaReferenceForTypeName(fullMessageTypeName(outputMessage.Desc)), }, }, }, @@ -578,11 +562,130 @@ func (g *OpenAPIv3Generator) responseContentForMessage(outputMessage *protogen.M } } +func (g *OpenAPIv3Generator) schemaOrReferenceForType(typeName string) *v3.SchemaOrReference { + switch typeName { + + case ".google.protobuf.Timestamp": + // Timestamps are serialized as strings + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string", Format: "RFC3339"}}} + + case ".google.type.Date": + // Dates are serialized as strings + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string", Format: "date"}}} + + case ".google.type.DateTime": + // DateTimes are serialized as strings + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string", Format: "date-time"}}} + + case ".google.protobuf.Struct": + // Struct is equivalent to a JSON object + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "object"}}} + + case ".google.protobuf.Empty": + // Empty is close to JSON undefined than null, so ignore this field + return nil //&v3.SchemaOrReference{Oneof: &v3.SchemaOrReference_Schema{Schema: &v3.Schema{Type: "null"}}} + + default: + ref := g.schemaReferenceForTypeName(typeName) + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Reference{ + Reference: &v3.Reference{XRef: ref}}} + } +} + +func (g *OpenAPIv3Generator) schemaOrReferenceForField(field protoreflect.FieldDescriptor) *v3.SchemaOrReference { + if field.IsMap() { + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "object", + AdditionalProperties: &v3.AdditionalPropertiesItem{ + Oneof: &v3.AdditionalPropertiesItem_SchemaOrReference{ + SchemaOrReference: g.schemaOrReferenceForField(field.MapValue())}}}}} + } + + var kindSchema *v3.SchemaOrReference + + kind := field.Kind() + + switch kind { + + case protoreflect.MessageKind: + typeName := fullMessageTypeName(field.Message()) + kindSchema = g.schemaOrReferenceForType(typeName) + if kindSchema == nil { + return nil + } + + case protoreflect.StringKind: + kindSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string"}}} + + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Uint32Kind, + protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Uint64Kind, + protoreflect.Sfixed32Kind, protoreflect.Fixed32Kind, protoreflect.Sfixed64Kind, + protoreflect.Fixed64Kind: + kindSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "integer", Format: kind.String()}}} + + case protoreflect.EnumKind: + kindSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "integer", Format: "enum"}}} + + case protoreflect.BoolKind: + kindSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "boolean"}}} + + case protoreflect.FloatKind, protoreflect.DoubleKind: + kindSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "number", Format: kind.String()}}} + + case protoreflect.BytesKind: + kindSchema = &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string", Format: "bytes"}}} + + default: + log.Printf("(TODO) Unsupported field type: %+v", fullMessageTypeName(field.Message())) + } + + if field.IsList() { + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{ + Type: "array", + Items: &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{kindSchema}}, + }, + }, + } + } + + return kindSchema +} + // addSchemasToDocumentV3 adds info from one file descriptor. -func (g *OpenAPIv3Generator) addSchemasToDocumentV3(d *v3.Document, file *protogen.File) { +func (g *OpenAPIv3Generator) addSchemasToDocumentV3(d *v3.Document, messages []*protogen.Message) { // For each message, generate a definition. - for _, message := range file.Messages { - typeName := fullMessageTypeName(message) + for _, message := range messages { + // Add any messages that are defined inside this message. + 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) { @@ -611,148 +714,26 @@ func (g *OpenAPIv3Generator) addSchemasToDocumentV3(d *v3.Document, file *protog log.Printf("unsupported extension type %T", extension) } } - // Get the field description from the comments. - fieldDescription := g.filterCommentString(field.Comments.Leading, true) + // The field is either described by a reference or a schema. - XRef := "" - fieldSchema := &v3.Schema{ - Description: fieldDescription, + fieldSchema := g.schemaOrReferenceForField(field.Desc) + if fieldSchema == nil { + continue } - if outputOnly { - fieldSchema.ReadOnly = true - } - if field.Desc.IsList() { - fieldSchema.Type = "array" - switch field.Desc.Kind() { - case protoreflect.MessageKind: - typeName := fullMessageTypeName(field.Message) - switch typeName { - case ".google.protobuf.Timestamp": - // Timestamps are serialized as strings - fieldSchema.Items = itemsItemForTypeName("string") - case ".google.type.Date": - // Dates are serialized as strings - fieldSchema.Items = itemsItemForTypeName("string") - case ".google.type.DateTime": - // DateTimes are serialized as strings - fieldSchema.Items = itemsItemForTypeName("string") - case ".google.protobuf.Struct": - // Struct is equivalent to a JSON object - fieldSchema.Items = itemsItemForTypeName("object") - case ".google.protobuf.Empty": - // Struct is close to JSON null, so ignore this field - continue - default: - // The field is described by a reference. - fieldSchema.Items = itemsItemForReference( - g.schemaReferenceForTypeName(typeName)) - } - case protoreflect.StringKind: - fieldSchema.Items = itemsItemForTypeName("string") - case protoreflect.Int32Kind, - protoreflect.Sint32Kind, - protoreflect.Uint32Kind, - protoreflect.Int64Kind, - protoreflect.Sint64Kind, - protoreflect.Uint64Kind, - protoreflect.Sfixed32Kind, - protoreflect.Fixed32Kind, - protoreflect.Sfixed64Kind, - protoreflect.Fixed64Kind: - fieldSchema.Items = itemsItemForTypeName("integer") - case protoreflect.EnumKind: - fieldSchema.Items = itemsItemForTypeName("integer") - case protoreflect.BoolKind: - fieldSchema.Items = itemsItemForTypeName("boolean") - case protoreflect.FloatKind, protoreflect.DoubleKind: - fieldSchema.Items = itemsItemForTypeName("number") - case protoreflect.BytesKind: - fieldSchema.Items = itemsItemForTypeName("string") - default: - log.Printf("(TODO) Unsupported array type: %+v", fullMessageTypeName(field.Message)) - } - } else if field.Desc.IsMap() && - field.Desc.MapKey().Kind() == protoreflect.StringKind && - field.Desc.MapValue().Kind() == protoreflect.StringKind { - fieldSchema.Type = "object" - } else { - k := field.Desc.Kind() - switch k { - case protoreflect.MessageKind: - typeName := fullMessageTypeName(field.Message) - switch typeName { - case ".google.protobuf.Timestamp": - // Timestamps are serialized as strings - fieldSchema.Type = "string" - fieldSchema.Format = "RFC3339" - case ".google.type.Date": - // Dates are serialized as strings - fieldSchema.Type = "string" - fieldSchema.Format = "date" - case ".google.type.DateTime": - // DateTimes are serialized as strings - fieldSchema.Type = "string" - fieldSchema.Format = "date-time" - case ".google.protobuf.Struct": - // Struct is equivalent to a JSON object - fieldSchema.Type = "object" - case ".google.protobuf.Empty": - // Struct is close to JSON null, so ignore this field - continue - default: - // The field is described by a reference. - XRef = g.schemaReferenceForTypeName(typeName) - } - case protoreflect.StringKind: - fieldSchema.Type = "string" - case protoreflect.Int32Kind, - protoreflect.Sint32Kind, - protoreflect.Uint32Kind, - protoreflect.Int64Kind, - protoreflect.Sint64Kind, - protoreflect.Uint64Kind, - protoreflect.Sfixed32Kind, - protoreflect.Fixed32Kind, - protoreflect.Sfixed64Kind, - protoreflect.Fixed64Kind: - fieldSchema.Type = "integer" - fieldSchema.Format = k.String() - case protoreflect.EnumKind: - fieldSchema.Type = "integer" - fieldSchema.Format = "enum" - case protoreflect.BoolKind: - fieldSchema.Type = "boolean" - case protoreflect.FloatKind, protoreflect.DoubleKind: - fieldSchema.Type = "number" - fieldSchema.Format = k.String() - case protoreflect.BytesKind: - fieldSchema.Type = "string" - fieldSchema.Format = "bytes" - default: - log.Printf("(TODO) Unsupported field type: %+v", fullMessageTypeName(field.Message)) - } - } - var value *v3.SchemaOrReference - if XRef != "" { - value = &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Reference{ - Reference: &v3.Reference{ - XRef: XRef, - }, - }, - } - } else { - value = &v3.SchemaOrReference{ - Oneof: &v3.SchemaOrReference_Schema{ - Schema: fieldSchema, - }, + + 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 } } + definitionProperties.AdditionalProperties = append( definitionProperties.AdditionalProperties, &v3.NamedSchemaOrReference{ Name: g.formatFieldName(field), - Value: value, + Value: fieldSchema, }, ) }