diff --git a/Makefile b/Makefile index 2caf27e1765..e3f93ad9f6a 100644 --- a/Makefile +++ b/Makefile @@ -147,6 +147,9 @@ proto: buf generate \ --template examples/internal/proto/examplepb/enum_with_single_value.buf.gen.yaml \ --path examples/internal/proto/examplepb/enum_with_single_value.proto + buf generate \ + --template ./examples/internal/proto/examplepb/proto3_field_semantics.buf.gen.yaml \ + --path examples/internal/proto/examplepb/proto3_field_semantics.proto buf generate \ --template ./protoc-gen-openapiv2/options/buf.gen.yaml \ --path ./protoc-gen-openapiv2/options/annotations.proto \ diff --git a/buf.yaml b/buf.yaml index 8d3aa00dfed..624da13a8a4 100644 --- a/buf.yaml +++ b/buf.yaml @@ -22,6 +22,7 @@ lint: - examples/internal/proto/examplepb/opaque.proto - examples/internal/proto/examplepb/openapi_merge_a.proto - examples/internal/proto/examplepb/openapi_merge_b.proto + - examples/internal/proto/examplepb/proto3_field_semantics.proto - examples/internal/proto/examplepb/response_body_service.proto - examples/internal/proto/examplepb/stream.proto - examples/internal/proto/examplepb/unannotated_echo_service.proto @@ -68,6 +69,7 @@ lint: - examples/internal/proto/examplepb/opaque.proto - examples/internal/proto/examplepb/openapi_merge_a.proto - examples/internal/proto/examplepb/openapi_merge_b.proto + - examples/internal/proto/examplepb/proto3_field_semantics.proto - examples/internal/proto/examplepb/response_body_service.proto - examples/internal/proto/examplepb/stream.proto - examples/internal/proto/examplepb/unannotated_echo_service.proto @@ -100,6 +102,7 @@ lint: - examples/internal/proto/examplepb/excess_body.proto - examples/internal/proto/examplepb/non_standard_names.proto - examples/internal/proto/examplepb/opaque.proto + - examples/internal/proto/examplepb/proto3_field_semantics.proto - examples/internal/proto/examplepb/response_body_service.proto - examples/internal/proto/examplepb/stream.proto - examples/internal/proto/examplepb/unannotated_echo_service.proto @@ -126,6 +129,7 @@ lint: - examples/internal/proto/examplepb/opaque.proto - examples/internal/proto/examplepb/openapi_merge_a.proto - examples/internal/proto/examplepb/openapi_merge_b.proto + - examples/internal/proto/examplepb/proto3_field_semantics.proto - examples/internal/proto/examplepb/response_body_service.proto - examples/internal/proto/examplepb/stream.proto - examples/internal/proto/examplepb/unannotated_echo_service.proto diff --git a/examples/internal/proto/examplepb/BUILD.bazel b/examples/internal/proto/examplepb/BUILD.bazel index c45a0a07494..cca1d53cc3c 100644 --- a/examples/internal/proto/examplepb/BUILD.bazel +++ b/examples/internal/proto/examplepb/BUILD.bazel @@ -67,6 +67,7 @@ proto_library( "generated_output.proto", "ignore_comment.proto", "non_standard_names.proto", + "proto3_field_semantics.proto", "remove_internal_comment.proto", "response_body_service.proto", "stream.proto", diff --git a/examples/internal/proto/examplepb/proto3_field_semantics.buf.gen.yaml b/examples/internal/proto/examplepb/proto3_field_semantics.buf.gen.yaml new file mode 100644 index 00000000000..27fb25624de --- /dev/null +++ b/examples/internal/proto/examplepb/proto3_field_semantics.buf.gen.yaml @@ -0,0 +1,6 @@ +version: v2 +plugins: + - local: protoc-gen-openapiv2 + out: . + opt: + - use_proto3_field_semantics=true diff --git a/examples/internal/proto/examplepb/proto3_field_semantics.proto b/examples/internal/proto/examplepb/proto3_field_semantics.proto new file mode 100644 index 00000000000..3d17efd2cf8 --- /dev/null +++ b/examples/internal/proto/examplepb/proto3_field_semantics.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package grpc.gateway.examples.internal.proto.examplepb; + +import "google/api/annotations.proto"; + +option go_package = "github.com/grpc-ecosystem/grpc-gateway/v2/examples/internal/proto/examplepb"; + +service Proto3FieldSemanticsService { + rpc GetFilter(GetFilterRequest) returns (GetFilterResponse) { + option (google.api.http) = {get: "/v1/filter"}; + } +} + +message GetFilterRequest { + string name = 1; +} + +message GetFilterResponse { + Filter filter = 1; +} + +message Filter { + string name = 1; + + oneof filter_union { + FilterCondition condition = 2; + FilterGroup group = 3; + } +} + +message FilterCondition { + string field = 1; + string value = 2; +} + +message FilterGroup { + repeated Filter filters = 1; + string operator = 2; +} diff --git a/examples/internal/proto/examplepb/proto3_field_semantics.swagger.json b/examples/internal/proto/examplepb/proto3_field_semantics.swagger.json new file mode 100644 index 00000000000..98ff646eaea --- /dev/null +++ b/examples/internal/proto/examplepb/proto3_field_semantics.swagger.json @@ -0,0 +1,151 @@ +{ + "swagger": "2.0", + "info": { + "title": "examples/internal/proto/examplepb/proto3_field_semantics.proto", + "version": "version not set" + }, + "tags": [ + { + "name": "Proto3FieldSemanticsService" + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/v1/filter": { + "get": { + "operationId": "Proto3FieldSemanticsService_GetFilter", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/examplepbGetFilterResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "name", + "in": "query", + "required": true, + "type": "string" + } + ], + "tags": [ + "Proto3FieldSemanticsService" + ] + } + } + }, + "definitions": { + "examplepbFilter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "condition": { + "$ref": "#/definitions/examplepbFilterCondition" + }, + "group": { + "$ref": "#/definitions/examplepbFilterGroup" + } + }, + "required": [ + "name" + ] + }, + "examplepbFilterCondition": { + "type": "object", + "properties": { + "field": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "field", + "value" + ] + }, + "examplepbFilterGroup": { + "type": "object", + "properties": { + "filters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/examplepbFilter" + } + }, + "operator": { + "type": "string" + } + }, + "required": [ + "filters", + "operator" + ] + }, + "examplepbGetFilterResponse": { + "type": "object", + "properties": { + "filter": { + "$ref": "#/definitions/examplepbFilter" + } + }, + "required": [ + "filter" + ] + }, + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {}, + "required": [ + "typeUrl", + "value" + ] + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + }, + "required": [ + "code", + "message", + "details" + ] + } + } +} diff --git a/protoc-gen-openapiv2/internal/genopenapi/template.go b/protoc-gen-openapiv2/internal/genopenapi/template.go index 35456fa9c7a..4341b0e4091 100644 --- a/protoc-gen-openapiv2/internal/genopenapi/template.go +++ b/protoc-gen-openapiv2/internal/genopenapi/template.go @@ -3444,7 +3444,7 @@ func updateswaggerObjectFromJSONSchema(s *openapiSchemaObject, j *openapi_option func updateSwaggerObjectFromFieldBehavior(s *openapiSchemaObject, j []annotations.FieldBehavior, reg *descriptor.Registry, field *descriptor.Field) { required := false if reg.GetUseProto3FieldSemantics() { - required = !field.GetProto3Optional() + required = !field.GetProto3Optional() && field.OneofIndex == nil } for _, fb := range j { switch fb { diff --git a/protoc-gen-openapiv2/internal/genopenapi/template_test.go b/protoc-gen-openapiv2/internal/genopenapi/template_test.go index 783a3957ec8..f1b2ee51210 100644 --- a/protoc-gen-openapiv2/internal/genopenapi/template_test.go +++ b/protoc-gen-openapiv2/internal/genopenapi/template_test.go @@ -11968,6 +11968,13 @@ func Test_updateSwaggerObjectFromFieldBehavior(t *testing.T) { Number: proto.Int32(1), Proto3Optional: &boolTrue, }} + oneofIndex := int32(0) + proto3FieldOneof := &descriptor.Field{FieldDescriptorProto: &descriptorpb.FieldDescriptorProto{ + Name: proto.String("name"), + Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(), + Number: proto.Int32(1), + OneofIndex: &oneofIndex, + }} tests := []struct { name string args args @@ -12015,6 +12022,38 @@ func Test_updateSwaggerObjectFromFieldBehavior(t *testing.T) { }, required: []string{"name"}, }, + { + name: "Oneof field not required with proto3 field semantics", + args: args{ + s: &openapiSchemaObject{}, + j: []annotations.FieldBehavior{}, + reg: regWithProto3FieldSemantics, + field: proto3FieldOneof, + }, + required: nil, + }, + { + name: "Oneof field not required without proto3 field semantics", + args: args{ + s: &openapiSchemaObject{}, + j: []annotations.FieldBehavior{}, + reg: regWithNoProto3FieldSemantics, + field: proto3FieldOneof, + }, + required: nil, + }, + { + name: "Oneof field required when explicitly annotated REQUIRED", + args: args{ + s: &openapiSchemaObject{}, + j: []annotations.FieldBehavior{ + annotations.FieldBehavior_REQUIRED, + }, + reg: regWithProto3FieldSemantics, + field: proto3FieldOneof, + }, + required: []string{"name"}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {