Skip to content

Commit

Permalink
Extend google.protobuf.EnumOptions for Schema (#4931)
Browse files Browse the repository at this point in the history
* schema option for enum

Signed-off-by: Lukas Hoehl <[email protected]>

* create new type EnumSchema for enum options

Signed-off-by: Lukas Hoehl <[email protected]>

---------

Signed-off-by: Lukas Hoehl <[email protected]>
  • Loading branch information
hown3d authored Nov 27, 2024
1 parent 3bb2501 commit 8515b77
Show file tree
Hide file tree
Showing 13 changed files with 1,312 additions and 911 deletions.
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

0 comments on commit 8515b77

Please sign in to comment.