Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend google.protobuf.EnumOptions for Schema #4931

Merged
merged 2 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion docs/docs/mapping/customizing_openapi_output.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ You can disable this behavior and exclude all protobuf comments from OpenAPI out

## Using proto options

You can define options on your Protocol Buffer services, operations, messages, and field definitions to customize your Open API output. For instance, to customize the [OpenAPI Schema Object](https://swagger.io/specification/v2/#schemaObject) for messages and fields:
You can define options on your Protocol Buffer services, operations, messages, enums and field definitions to customize your Open API output. For instance, to customize the [OpenAPI Schema Object](https://swagger.io/specification/v2/#schemaObject) for messages and fields:

```protobuf
import "protoc-gen-openapiv2/options/annotations.proto";
Expand Down Expand Up @@ -52,6 +52,32 @@ message ABitOfEverything {
string uuid = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {description: "The UUID field."}];
}
```
Enums can be customized like messages:

```protobuf
// NumericEnum is one or zero.
enum NumericEnum {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_enum) = {
description: "NumericEnum is one or zero."
title: "NumericEnum"
extensions: {
key: "x-a-bit-of-everything-foo"
value {
string_value: "bar"
}
}
external_docs: {
url: "https://github.com/grpc-ecosystem/grpc-gateway"
description: "Find out more about ABitOfEverything"
}
example: "\"ZERO\""
};
// ZERO means 0
ZERO = 0;
// ONE means 1
ONE = 1;
}
```

Operations can also be customized:

Expand Down
9 changes: 7 additions & 2 deletions examples/internal/clients/abe/api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6001,12 +6001,17 @@ definitions:
$ref: "#/definitions/examplepbErrorObject"
examplepbNumericEnum:
type: "string"
description: "NumericEnum is one or zero.\n\n - ZERO: ZERO means 0\n - ONE: ONE\
\ means 1"
externalDocs:
description: "Find out more about ABitOfEverything"
url: "https://github.com/grpc-ecosystem/grpc-gateway"
title: "NumericEnum"
description: "NumericEnum is one or zero."
example: "ZERO"
enum:
- "ZERO"
- "ONE"
default: "ZERO"
x-a-bit-of-everything-foo: "bar"
examplepbRequiredMessageTypeRequest:
type: "object"
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/

package abe
// ExamplepbNumericEnum : NumericEnum is one or zero. - ZERO: ZERO means 0 - ONE: ONE means 1
// ExamplepbNumericEnum : NumericEnum is one or zero.
type ExamplepbNumericEnum string

// List of examplepbNumericEnum
Expand Down
1,086 changes: 549 additions & 537 deletions examples/internal/proto/examplepb/a_bit_of_everything.pb.go

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions examples/internal/proto/examplepb/a_bit_of_everything.proto
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,19 @@ message MessageWithBody {

// NumericEnum is one or zero.
enum NumericEnum {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_enum) = {
description: "NumericEnum is one or zero."
title: "NumericEnum"
extensions: {
key: "x-a-bit-of-everything-foo"
value: {string_value: "bar"}
}
external_docs: {
url: "https://github.com/grpc-ecosystem/grpc-gateway"
description: "Find out more about ABitOfEverything"
}
example: "\"ZERO\""
};
// ZERO means 0
ZERO = 0;
// ONE means 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7813,12 +7813,19 @@
},
"examplepbNumericEnum": {
"type": "string",
"example": "ZERO",
"enum": [
"ZERO",
"ONE"
],
"default": "ZERO",
"description": "NumericEnum is one or zero.\n\n - ZERO: ZERO means 0\n - ONE: ONE means 1"
"description": "NumericEnum is one or zero.",
"title": "NumericEnum",
"externalDocs": {
"description": "Find out more about ABitOfEverything",
"url": "https://github.com/grpc-ecosystem/grpc-gateway"
},
"x-a-bit-of-everything-foo": "bar"
},
"examplepbRequiredMessageTypeRequest": {
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,12 +355,19 @@
},
"examplepbNumericEnum": {
"type": "string",
"example": "ZERO",
"enum": [
"ZERO",
"ONE"
],
"default": "ZERO",
"description": "NumericEnum is one or zero.\n\n - ZERO: ZERO means 0\n - ONE: ONE means 1"
"description": "NumericEnum is one or zero.",
"title": "NumericEnum",
"externalDocs": {
"description": "Find out more about ABitOfEverything",
"url": "https://github.com/grpc-ecosystem/grpc-gateway"
},
"x-a-bit-of-everything-foo": "bar"
},
"pathenumPathEnum": {
"type": "string",
Expand Down
9 changes: 8 additions & 1 deletion examples/internal/proto/examplepb/stream.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -532,12 +532,19 @@
},
"examplepbNumericEnum": {
"type": "string",
"example": "ZERO",
"enum": [
"ZERO",
"ONE"
],
"default": "ZERO",
"description": "NumericEnum is one or zero.\n\n - ZERO: ZERO means 0\n - ONE: ONE means 1"
"description": "NumericEnum is one or zero.",
"title": "NumericEnum",
"externalDocs": {
"description": "Find out more about ABitOfEverything",
"url": "https://github.com/grpc-ecosystem/grpc-gateway"
},
"x-a-bit-of-everything-foo": "bar"
},
"pathenumPathEnum": {
"type": "string",
Expand Down
101 changes: 88 additions & 13 deletions protoc-gen-openapiv2/internal/genopenapi/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ var wktSchemas = map[string]schemaCore{
Items: (*openapiItemsObject)(&openapiSchemaObject{
schemaCore: schemaCore{
Type: "object",
}}),
},
}),
},
".google.protobuf.NullValue": {
Type: "string",
Expand Down Expand Up @@ -737,6 +738,7 @@ func shouldExcludeField(name string, excluded []descriptor.Parameter) bool {
}
return false
}

func filterOutExcludedFields(fields []string, excluded []descriptor.Parameter) []string {
var filtered []string
for _, f := range fields {
Expand Down Expand Up @@ -904,7 +906,7 @@ func primitiveSchema(t descriptorpb.FieldDescriptorProto_Type) (ftype, format st
}

// renderEnumerationsAsDefinition inserts enums into the definitions object.
func renderEnumerationsAsDefinition(enums enumMap, d openapiDefinitionsObject, reg *descriptor.Registry) {
func renderEnumerationsAsDefinition(enums enumMap, d openapiDefinitionsObject, reg *descriptor.Registry, customRefs refMap) {
for _, enum := range enums {
swgName, ok := fullyQualifiedNameToOpenAPIName(enum.FQEN(), reg)
if !ok {
Expand All @@ -926,12 +928,37 @@ func renderEnumerationsAsDefinition(enums enumMap, d openapiDefinitionsObject, r
Default: defaultValue,
},
}

if reg.GetEnumsAsInts() {
enumSchemaObject.Type = "integer"
enumSchemaObject.Format = "int32"
enumSchemaObject.Default = getEnumDefaultNumber(reg, enum)
enumSchemaObject.Enum = listEnumNumbers(reg, enum)
}
opts, err := getEnumOpenAPIOption(reg, enum)
if err != nil {
panic(err)
}
if opts != nil {
protoSchema := openapiSchemaFromProtoEnumSchema(opts, reg, customRefs, enum)
// Warning: Make sure not to overwrite any fields already set on the schema type.
// This is only a subset of the fields from JsonSchema since most of them only apply to arrays or objects not enums
enumSchemaObject.ExternalDocs = protoSchema.ExternalDocs
enumSchemaObject.ReadOnly = protoSchema.ReadOnly
enumSchemaObject.extensions = protoSchema.extensions
if protoSchema.Type != "" || protoSchema.Ref != "" {
enumSchemaObject.schemaCore = protoSchema.schemaCore
}
if protoSchema.Title != "" {
enumSchemaObject.Title = protoSchema.Title
}
if protoSchema.Description != "" {
enumSchemaObject.Description = protoSchema.Description
}
if protoSchema.Example != nil {
enumSchemaObject.Example = protoSchema.Example
}
}
if err := updateOpenAPIDataFromComments(reg, &enumSchemaObject, enum, enumComments, false); err != nil {
panic(err)
}
Expand Down Expand Up @@ -971,8 +998,10 @@ func lookupMsgAndOpenAPIName(location, name string, reg *descriptor.Registry) (*

// registriesSeen is used to memoise calls to resolveFullyQualifiedNameToOpenAPINames so
// we don't repeat it unnecessarily, since it can take some time.
var registriesSeen = map[*descriptor.Registry]map[string]string{}
var registriesSeenMutex sync.Mutex
var (
registriesSeen = map[*descriptor.Registry]map[string]string{}
registriesSeenMutex sync.Mutex
)

// Take the names of every proto message and generate a unique reference for each, according to the given strategy.
func resolveFullyQualifiedNameToOpenAPINames(messages []string, namingStrategy string) map[string]string {
Expand Down Expand Up @@ -1265,7 +1294,7 @@ func renderServices(services []*descriptor.Service, paths *openapiPathsObject, r
// extract any constraints specified in the path placeholders into ECMA regular expressions
pathParamRegexpMap := partsToRegexpMap(parts)
// Keep track of path parameter overrides
var pathParamNames = make(map[string]string)
pathParamNames := make(map[string]string)
for _, parameter := range pathParams {

var paramType, paramFormat, desc, collectionFormat string
Expand Down Expand Up @@ -1578,7 +1607,8 @@ func renderServices(services []*descriptor.Service, paths *openapiPathsObject, r
Key: "error",
Value: openapiSchemaObject{
schemaCore: schemaCore{
Ref: fmt.Sprintf("#/definitions/%s", statusDef)},
Ref: fmt.Sprintf("#/definitions/%s", statusDef),
},
},
})
}
Expand Down Expand Up @@ -1917,7 +1947,7 @@ func applyTemplate(p param) (*openapiSwaggerObject, error) {
if err := renderMessagesAsDefinition(messages, s.Definitions, p.reg, customRefs, nil); err != nil {
return nil, err
}
renderEnumerationsAsDefinition(enums, s.Definitions, p.reg)
renderEnumerationsAsDefinition(enums, s.Definitions, p.reg, requestResponseRefs)

// File itself might have some comments and metadata.
packageProtoPath := protoPathIndex(reflect.TypeOf((*descriptorpb.FileDescriptorProto)(nil)), "Package")
Expand Down Expand Up @@ -2651,11 +2681,13 @@ func goTemplateComments(comment string, data interface{}, reg *descriptor.Regist
return temp.String()
}

var messageProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.FileDescriptorProto)(nil)), "MessageType")
var nestedProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.DescriptorProto)(nil)), "NestedType")
var packageProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.FileDescriptorProto)(nil)), "Package")
var serviceProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.FileDescriptorProto)(nil)), "Service")
var methodProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.ServiceDescriptorProto)(nil)), "Method")
var (
messageProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.FileDescriptorProto)(nil)), "MessageType")
nestedProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.DescriptorProto)(nil)), "NestedType")
packageProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.FileDescriptorProto)(nil)), "Package")
serviceProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.FileDescriptorProto)(nil)), "Service")
methodProtoPath = protoPathIndex(reflect.TypeOf((*descriptorpb.ServiceDescriptorProto)(nil)), "Method")
)

func isProtoPathMatches(paths []int32, outerPaths []int32, typeName string, typeIndex int32, fieldPaths []int32) bool {
if typeName == "Package" && typeIndex == packageProtoPath {
Expand Down Expand Up @@ -2787,6 +2819,23 @@ func extractSchemaOptionFromMessageDescriptor(msg *descriptorpb.DescriptorProto)
return opts, nil
}

// extractEnumSchemaOptionFromEnumDescriptor extracts the message of type
// openapi_options.EnumSchema from a given proto enum's descriptor.
func extractEnumSchemaOptionFromEnumDescriptor(enum *descriptorpb.EnumDescriptorProto) (*openapi_options.EnumSchema, error) {
if enum.Options == nil {
return nil, nil
}
if !proto.HasExtension(enum.Options, openapi_options.E_Openapiv2Enum) {
return nil, nil
}
ext := proto.GetExtension(enum.Options, openapi_options.E_Openapiv2Enum)
opts, ok := ext.(*openapi_options.EnumSchema)
if !ok {
return nil, fmt.Errorf("extension is %T; want a EnumSchema", ext)
}
return opts, nil
}

// extractTagOptionFromServiceDescriptor extracts the tag of type
// openapi_options.Tag from a given proto service's descriptor.
func extractTagOptionFromServiceDescriptor(svc *descriptorpb.ServiceDescriptorProto) (*openapi_options.Tag, error) {
Expand Down Expand Up @@ -2941,6 +2990,14 @@ func getMessageOpenAPIOption(reg *descriptor.Registry, msg *descriptor.Message)
return opts, nil
}

func getEnumOpenAPIOption(reg *descriptor.Registry, enum *descriptor.Enum) (*openapi_options.EnumSchema, error) {
opts, err := extractEnumSchemaOptionFromEnumDescriptor(enum.EnumDescriptorProto)
if err != nil {
return nil, err
}
return opts, nil
}

func getServiceOpenAPIOption(reg *descriptor.Registry, svc *descriptor.Service) (*openapi_options.Tag, error) {
if opts, ok := reg.GetOpenAPIServiceOption(svc.FQSN()); ok {
return opts, nil
Expand Down Expand Up @@ -3112,6 +3169,24 @@ func updateSwaggerObjectFromFieldBehavior(s *openapiSchemaObject, j []annotation
}
}

func openapiSchemaFromProtoEnumSchema(s *openapi_options.EnumSchema, reg *descriptor.Registry, refs refMap, data interface{}) openapiSchemaObject {
ret := openapiSchemaObject{
ExternalDocs: protoExternalDocumentationToOpenAPIExternalDocumentation(s.GetExternalDocs(), reg, data),
}
jsonSchema := &openapi_options.JSONSchema{
Ref: s.Ref,
Title: s.Title,
Extensions: s.Extensions,
Description: s.Description,
Default: s.Default,
ReadOnly: s.ReadOnly,
Example: s.Example,
}
ret.schemaCore = protoJSONSchemaToOpenAPISchemaCore(jsonSchema, reg, refs)
updateswaggerObjectFromJSONSchema(&ret, jsonSchema, reg, data)
return ret
}

func openapiSchemaFromProtoSchema(s *openapi_options.Schema, reg *descriptor.Registry, refs refMap, data interface{}) openapiSchemaObject {
ret := openapiSchemaObject{
ExternalDocs: protoExternalDocumentationToOpenAPIExternalDocumentation(s.GetExternalDocs(), reg, data),
Expand Down Expand Up @@ -3229,7 +3304,7 @@ func addCustomRefs(d openapiDefinitionsObject, reg *descriptor.Registry, refs re
if err := renderMessagesAsDefinition(msgMap, d, reg, refs, nil); err != nil {
return err
}
renderEnumerationsAsDefinition(enumMap, d, reg)
renderEnumerationsAsDefinition(enumMap, d, reg, refs)

// Run again in case any new refs were added
return addCustomRefs(d, reg, refs)
Expand Down
Loading
Loading