Skip to content

Commit

Permalink
OpenAPIv2: Support request header parameters (#3010)
Browse files Browse the repository at this point in the history
* feat: support request header parameters

* docs: add openapi references to new parameters protos

Co-Authored-By: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>

* refactor: return error if http parameter type not defined

* docs: add note about breaking compatability with Open API V2

* docs: document custom http request header parameters

* chore: generate go

* test: fix failing required header parameter test

* test: remove unnecessary comment

Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
  • Loading branch information
krak3n and johanbrandhorst committed Nov 15, 2022
1 parent 7a1adab commit e59b2ad
Show file tree
Hide file tree
Showing 5 changed files with 1,125 additions and 487 deletions.
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

0 comments on commit e59b2ad

Please sign in to comment.