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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support request header parameters #3010

Merged
89 changes: 89 additions & 0 deletions docs/docs/mapping/customizing_openapi_output.md
Expand Up @@ -734,4 +734,93 @@ definitions:
type: string
```

### Custom HTTP Header Request Parameters

By default the parameters for each operation are generated from the protocol buffer definition however you can extend the parameters to include extra HTTP headers if required.

**NOTE**: These annotations do not alter the behaviour of the gateway and must be coupled with custom header parsing behaviour in the application. Also be aware that adding header parameters can alter the forwards and backwards compatibility of the schema. You must also set a type for your header which can be one of `STRING`, `INTEGER`, `NUMBER` or `BOOLEAN`.

```protobuf
syntax = "proto3";

package helloproto.v1;

import "google/api/annotations.proto";
import "protoc-gen-openapiv2/options/annotations.proto";

option go_package = "helloproto/v1;helloproto";

service EchoService {
rpc Hello(HelloReq) returns (HelloResp) {
option (google.api.http) = {get: "/api/hello"};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
parameters: {
headers: {
name: "X-Foo";
description: "Foo Header";
type: STRING,
required: true;
};
headers: {
name: "X-Bar";
description: "Bar Header";
type: NUMBER,
};
};
};
}
}

message HelloReq {
string name = 1;
}

message HelloResp {
string message = 1;
}
```

Output:

```yaml
swagger: "2.0"
info:
title: helloproto/v1/hello.proto
version: version not set
consumes:
- application/json
produces:
- application/json
paths:
/api/hello:
get:
operationId: Hello
responses:
"200":
description: A successful response.
schema:
$ref: "#/definitions/helloproto.v1.HelloResp"
parameters:
- name: name
in: query
required: false
type: string
- name: X-Foo
description: Foo Header
in: header
required: true
type: string
- name: X-Bar
description: Bar Header
in: header
required: false
type: number
definitions:
helloproto.v1.HelloResp:
type: object
properties:
message:
type: string
```

{% endraw %}
27 changes: 27 additions & 0 deletions protoc-gen-openapiv2/internal/genopenapi/template.go
Expand Up @@ -1514,6 +1514,33 @@ func renderServices(services []*descriptor.Service, paths openapiPathsObject, re
copy(operationObject.Produces, opts.Produces)
}

if params := opts.Parameters; params != nil && len(params.Headers) > 0 {
for _, header := range params.Headers {
param := openapiParameterObject{
In: "header",
Name: header.Name,
Description: header.Description,
Required: header.Required,
Format: header.Format,
}

switch header.Type {
case openapi_options.HeaderParameter_STRING:
param.Type = "string"
case openapi_options.HeaderParameter_NUMBER:
param.Type = "number"
case openapi_options.HeaderParameter_INTEGER:
param.Type = "integer"
case openapi_options.HeaderParameter_BOOLEAN:
param.Type = "boolean"
default:
return fmt.Errorf("invalid header parameter type: %+v", header.Type)
}

operationObject.Parameters = append(operationObject.Parameters, param)
}
}

// TODO(ivucica): add remaining fields of operation object
}

Expand Down
208 changes: 208 additions & 0 deletions protoc-gen-openapiv2/internal/genopenapi/template_test.go
Expand Up @@ -7976,6 +7976,214 @@ func TestRenderServicesWithColonInSegment(t *testing.T) {
}
}

func TestRenderServiceWithHeaderParameters(t *testing.T) {
file := func() descriptor.File {
msgdesc := &descriptorpb.DescriptorProto{
Name: proto.String("ExampleMessage"),
}

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

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

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

return 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",
},
},
},
},
},
},
},
}
}

type test struct {
file func() descriptor.File
openapiOperation *openapi_options.Operation
parameters openapiParametersObject
}

tests := map[string]*test{
"type string": {
file: file,
openapiOperation: &openapi_options.Operation{
Parameters: &openapi_options.Parameters{
Headers: []*openapi_options.HeaderParameter{
{
Name: "X-Custom-Header",
Type: openapi_options.HeaderParameter_STRING,
},
},
},
},
parameters: openapiParametersObject{
{
Name: "X-Custom-Header",
In: "header",
Type: "string",
},
},
},
"type integer": {
file: file,
openapiOperation: &openapi_options.Operation{
Parameters: &openapi_options.Parameters{
Headers: []*openapi_options.HeaderParameter{
{
Name: "X-Custom-Header",
Type: openapi_options.HeaderParameter_INTEGER,
},
},
},
},
parameters: openapiParametersObject{
{
Name: "X-Custom-Header",
In: "header",
Type: "integer",
},
},
},
"type number": {
file: file,
openapiOperation: &openapi_options.Operation{
Parameters: &openapi_options.Parameters{
Headers: []*openapi_options.HeaderParameter{
{
Name: "X-Custom-Header",
Type: openapi_options.HeaderParameter_NUMBER,
},
},
},
},
parameters: openapiParametersObject{
{
Name: "X-Custom-Header",
In: "header",
Type: "number",
},
},
},
"type boolean": {
file: file,
openapiOperation: &openapi_options.Operation{
Parameters: &openapi_options.Parameters{
Headers: []*openapi_options.HeaderParameter{
{
Name: "X-Custom-Header",
Type: openapi_options.HeaderParameter_BOOLEAN,
},
},
},
},
parameters: openapiParametersObject{
{
Name: "X-Custom-Header",
In: "header",
Type: "boolean",
},
},
},
"header required": {
file: file,
openapiOperation: &openapi_options.Operation{
Parameters: &openapi_options.Parameters{
Headers: []*openapi_options.HeaderParameter{
{
Name: "X-Custom-Header",
Required: true,
Type: openapi_options.HeaderParameter_STRING,
},
},
},
},
parameters: openapiParametersObject{
{
Name: "X-Custom-Header",
In: "header",
Required: true,
Type: "string",
},
},
},
}

for name, test := range tests {
test := test

t.Run(name, func(t *testing.T) {
file := test.file()

proto.SetExtension(
proto.Message(file.Services[0].Methods[0].Options),
openapi_options.E_Openapiv2Operation,
test.openapiOperation)

reg := descriptor.NewRegistry()

fileCL := crossLinkFixture(&file)

err := reg.Load(reqFromFile(fileCL))
if 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)
}

params := result.Paths["/v1/echo"].Get.Parameters

if !reflect.DeepEqual(params, test.parameters) {
t.Errorf("expected %+v, got %+v", test.parameters, params)
}
})
}
}

func GetPaths(req *openapiSwaggerObject) []string {
paths := make([]string, len(req.Paths))
i := 0
Expand Down