Skip to content

Commit

Permalink
Add support for method's deprecated option (grpc-ecosystem#4373)
Browse files Browse the repository at this point in the history
* process method deprecated option

* flag

* Apply suggestions from code review

Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>

* bazel flag

* docs

---------

Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
  • Loading branch information
vajexal and johanbrandhorst authored Jun 2, 2024
1 parent 3e8de20 commit 6707495
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 3 deletions.
109 changes: 109 additions & 0 deletions docs/docs/mapping/customizing_openapi_output.md
Original file line number Diff line number Diff line change
Expand Up @@ -934,4 +934,113 @@ plugins:
- preserve_rpc_order=true
```

### Enable RPC deprecation

With `enable_rpc_deprecation` option you can deprecate openapi method using standard method's option. Allowed values are: `true`, `false`.

For example, if you are using `buf`:

```yaml
version: v1
plugins:
- name: openapiv2
out: .
opt:
- enable_rpc_deprecation=true
```

or with `protoc`

```sh
protoc --openapiv2_out=. --openapiv2_opt=enable_rpc_deprecation=true ./path/to/file.proto
```

Input example:

```protobuf
syntax = "proto3";
package helloproto.v1;
import "google/api/annotations.proto";
option go_package = "helloproto/v1;helloproto";
service EchoService {
rpc Hello(HelloReq) returns (HelloResp) {
option deprecated = true;
option (google.api.http) = {get: "/api/hello"};
}
}
message HelloReq {
string name = 1;
}
message HelloResp {
string message = 1;
}
```

Output:

```yaml
swagger: "2.0"
info:
title: helloproto/v1/example.proto
version: version not set
tags:
- name: EchoService
consumes:
- application/json
produces:
- application/json
paths:
/api/hello:
get:
operationId: EchoService_Hello
responses:
"200":
description: A successful response.
schema:
$ref: '#/definitions/v1HelloResp'
default:
description: An unexpected error response.
schema:
$ref: '#/definitions/rpcStatus'
parameters:
- name: name
in: query
required: false
type: string
tags:
- EchoService
deprecated: true
definitions:
protobufAny:
type: object
properties:
'@type':
type: string
additionalProperties: {}
rpcStatus:
type: object
properties:
code:
type: integer
format: int32
message:
type: string
details:
type: array
items:
type: object
$ref: '#/definitions/protobufAny'
v1HelloResp:
type: object
properties:
message:
type: string
```

{% endraw %}
13 changes: 13 additions & 0 deletions internal/descriptor/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ type Registry struct {
// preserveRPCOrder, if true, will ensure the order of paths emitted in openapi swagger files mirror
// the order of RPC methods found in proto files. If false, emitted paths will be ordered alphabetically.
preserveRPCOrder bool

// enableRpcDeprecation whether to process grpc method's deprecated option
enableRpcDeprecation bool
}

type repeatedFieldSeparator struct {
Expand Down Expand Up @@ -875,3 +878,13 @@ func (r *Registry) SetPreserveRPCOrder(preserve bool) {
func (r *Registry) IsPreserveRPCOrder() bool {
return r.preserveRPCOrder
}

// SetEnableRpcDeprecation sets enableRpcDeprecation
func (r *Registry) SetEnableRpcDeprecation(enable bool) {
r.enableRpcDeprecation = enable
}

// GetEnableRpcDeprecation returns enableRpcDeprecation
func (r *Registry) GetEnableRpcDeprecation() bool {
return r.enableRpcDeprecation
}
12 changes: 11 additions & 1 deletion protoc-gen-openapiv2/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ def _run_proto_gen_openapi(
generate_unbound_methods,
visibility_restriction_selectors,
use_allof_for_refs,
disable_default_responses):
disable_default_responses,
enable_rpc_deprecation):
args = actions.args()

args.add("--plugin", "protoc-gen-openapiv2=%s" % protoc_gen_openapiv2.path)
Expand Down Expand Up @@ -148,6 +149,9 @@ def _run_proto_gen_openapi(
if disable_default_responses:
args.add("--openapiv2_opt", "disable_default_responses=true")

if enable_rpc_deprecation:
args.add("--openapiv2_opt", "enable_rpc_deprecation=true")

args.add("--openapiv2_opt", "repeated_path_param_separator=%s" % repeated_path_param_separator)

proto_file_infos = _direct_source_infos(proto_info)
Expand Down Expand Up @@ -255,6 +259,7 @@ def _proto_gen_openapi_impl(ctx):
visibility_restriction_selectors = ctx.attr.visibility_restriction_selectors,
use_allof_for_refs = ctx.attr.use_allof_for_refs,
disable_default_responses = ctx.attr.disable_default_responses,
enable_rpc_deprecation = ctx.attr.enable_rpc_deprecation,
),
),
),
Expand Down Expand Up @@ -410,6 +415,11 @@ protoc_gen_openapiv2 = rule(
" if you have to support custom response codes that are" +
" not 200.",
),
"enable_rpc_deprecation": attr.bool(
default = False,
mandatory = False,
doc = "whether to process grpc method's deprecated option.",
),
"_protoc": attr.label(
default = "@com_google_protobuf//:protoc",
executable = True,
Expand Down
10 changes: 8 additions & 2 deletions protoc-gen-openapiv2/internal/genopenapi/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,8 @@ func renderServices(services []*descriptor.Service, paths *openapiPathsObject, r
continue
}

deprecated := reg.GetEnableRpcDeprecation() && meth.GetOptions().GetDeprecated()

for bIdx, b := range meth.Bindings {
operationFunc := operationForMethod(b.HTTPMethod)
// Iterate over all the OpenAPI parameters
Expand Down Expand Up @@ -1508,6 +1510,7 @@ func renderServices(services []*descriptor.Service, paths *openapiPathsObject, r
operationObject := &openapiOperationObject{
Parameters: parameters,
Responses: openapiResponsesObject{},
Deprecated: deprecated,
}

if !reg.GetDisableDefaultResponses() {
Expand Down Expand Up @@ -1566,14 +1569,17 @@ func renderServices(services []*descriptor.Service, paths *openapiPathsObject, r
grpclog.Error(err)
return err
}

opts, err := getMethodOpenAPIOption(reg, meth)
if opts != nil {
if err != nil {
panic(err)
}
operationObject.ExternalDocs = protoExternalDocumentationToOpenAPIExternalDocumentation(opts.ExternalDocs, reg, meth)
// TODO(ivucica): this would be better supported by looking whether the method is deprecated in the proto file
operationObject.Deprecated = opts.Deprecated

if opts.Deprecated {
operationObject.Deprecated = true
}

if opts.Summary != "" {
operationObject.Summary = opts.Summary
Expand Down
124 changes: 124 additions & 0 deletions protoc-gen-openapiv2/internal/genopenapi/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10789,3 +10789,127 @@ func TestApiVisibilityOption(t *testing.T) {
t.Fatal("Definition should be excluded by api visibility option")
}
}

func TestRenderServicesOptionDeprecated(t *testing.T) {
testCases := [...]struct {
testName string
methodOptions descriptorpb.MethodOptions
openapiOperation *openapi_options.Operation
expectedDeprecated bool
}{
{
testName: "method option",
methodOptions: descriptorpb.MethodOptions{
Deprecated: proto.Bool(true),
},
expectedDeprecated: true,
},
{
testName: "openapi option",
openapiOperation: &openapi_options.Operation{
Deprecated: true,
},
expectedDeprecated: true,
},
{
testName: "empty openapi doesn't override method option",
methodOptions: descriptorpb.MethodOptions{
Deprecated: proto.Bool(true),
},
openapiOperation: &openapi_options.Operation{},
expectedDeprecated: true,
},
}

for _, tc := range testCases {
tc := tc

t.Run(tc.testName, func(t *testing.T) {
msgdesc := &descriptorpb.DescriptorProto{
Name: proto.String("ExampleMessage"),
}

meth := &descriptorpb.MethodDescriptorProto{
Name: proto.String("Example"),
InputType: proto.String("ExampleMessage"),
OutputType: proto.String("ExampleMessage"),
Options: &tc.methodOptions,
}

svc := &descriptorpb.ServiceDescriptorProto{
Name: proto.String("ExampleService"),
Method: []*descriptorpb.MethodDescriptorProto{meth},
}

msg := &descriptor.Message{
DescriptorProto: msgdesc,
}

file := descriptor.File{
FileDescriptorProto: &descriptorpb.FileDescriptorProto{
SourceCodeInfo: &descriptorpb.SourceCodeInfo{},
Name: proto.String("example.proto"),
Package: proto.String("example"),
MessageType: []*descriptorpb.DescriptorProto{msgdesc},
Service: []*descriptorpb.ServiceDescriptorProto{svc},
Options: &descriptorpb.FileOptions{
GoPackage: proto.String("github.com/grpc-ecosystem/grpc-gateway/runtime/internal/examplepb;example"),
},
},
GoPkg: descriptor.GoPackage{
Path: "example.com/path/to/example/example.pb",
Name: "example_pb",
},
Messages: []*descriptor.Message{msg},
Services: []*descriptor.Service{
{
ServiceDescriptorProto: svc,
Methods: []*descriptor.Method{
{
MethodDescriptorProto: meth,
RequestType: msg,
ResponseType: msg,
Bindings: []*descriptor.Binding{
{
HTTPMethod: "GET",
PathTmpl: httprule.Template{
Version: 1,
OpCodes: []int{0, 0},
Template: "/v1/echo",
},
},
},
},
},
},
},
}

if tc.openapiOperation != nil {
proto.SetExtension(
proto.Message(file.Services[0].Methods[0].Options),
openapi_options.E_Openapiv2Operation,
tc.openapiOperation,
)
}

reg := descriptor.NewRegistry()
reg.SetEnableRpcDeprecation(true)
fileCL := crossLinkFixture(&file)

if err := reg.Load(reqFromFile(fileCL)); err != nil {
t.Errorf("reg.Load(%#v) failed with %v; want success", file, err)
}

result, err := applyTemplate(param{File: fileCL, reg: reg})
if err != nil {
t.Fatalf("applyTemplate(%#v) failed with %v; want success", file, err)
}

got := result.getPathItemObject("/v1/echo").Get.Deprecated
if got != tc.expectedDeprecated {
t.Fatalf("Wrong deprecated field, got %v want %v", got, tc.expectedDeprecated)
}
})
}
}
3 changes: 3 additions & 0 deletions protoc-gen-openapiv2/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ var (
useAllOfForRefs = flag.Bool("use_allof_for_refs", false, "if set, will use allOf as container for $ref to preserve same-level properties.")
allowPatchFeature = flag.Bool("allow_patch_feature", true, "whether to hide update_mask fields in PATCH requests from the generated swagger file.")
preserveRPCOrder = flag.Bool("preserve_rpc_order", false, "if true, will ensure the order of paths emitted in openapi swagger files mirror the order of RPC methods found in proto files. If false, emitted paths will be ordered alphabetically.")
enableRpcDeprecation = flag.Bool("enable_rpc_deprecation", false, "whether to process grpc method's deprecated option.")

_ = flag.Bool("logtostderr", false, "Legacy glog compatibility. This flag is a no-op, you can safely remove it")
)
Expand Down Expand Up @@ -171,6 +172,8 @@ func main() {
reg.SetUseAllOfForRefs(*useAllOfForRefs)
reg.SetAllowPatchFeature(*allowPatchFeature)
reg.SetPreserveRPCOrder(*preserveRPCOrder)
reg.SetEnableRpcDeprecation(*enableRpcDeprecation)

if err := reg.SetRepeatedPathParamSeparator(*repeatedPathParamSeparator); err != nil {
emitError(err)
return
Expand Down

0 comments on commit 6707495

Please sign in to comment.