Skip to content

Commit e59b2ad

Browse files
OpenAPIv2: Support request header parameters (#3010)
* feat: support request header parameters * docs: add openapi references to new parameters protos Co-Authored-By: Johan Brandhorst-Satzkorn <[email protected]> * 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 <[email protected]>
1 parent 7a1adab commit e59b2ad

File tree

5 files changed

+1125
-487
lines changed

5 files changed

+1125
-487
lines changed

docs/docs/mapping/customizing_openapi_output.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,4 +734,93 @@ definitions:
734734
type: string
735735
```
736736

737+
### Custom HTTP Header Request Parameters
738+
739+
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.
740+
741+
**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`.
742+
743+
```protobuf
744+
syntax = "proto3";
745+
746+
package helloproto.v1;
747+
748+
import "google/api/annotations.proto";
749+
import "protoc-gen-openapiv2/options/annotations.proto";
750+
751+
option go_package = "helloproto/v1;helloproto";
752+
753+
service EchoService {
754+
rpc Hello(HelloReq) returns (HelloResp) {
755+
option (google.api.http) = {get: "/api/hello"};
756+
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
757+
parameters: {
758+
headers: {
759+
name: "X-Foo";
760+
description: "Foo Header";
761+
type: STRING,
762+
required: true;
763+
};
764+
headers: {
765+
name: "X-Bar";
766+
description: "Bar Header";
767+
type: NUMBER,
768+
};
769+
};
770+
};
771+
}
772+
}
773+
774+
message HelloReq {
775+
string name = 1;
776+
}
777+
778+
message HelloResp {
779+
string message = 1;
780+
}
781+
```
782+
783+
Output:
784+
785+
```yaml
786+
swagger: "2.0"
787+
info:
788+
title: helloproto/v1/hello.proto
789+
version: version not set
790+
consumes:
791+
- application/json
792+
produces:
793+
- application/json
794+
paths:
795+
/api/hello:
796+
get:
797+
operationId: Hello
798+
responses:
799+
"200":
800+
description: A successful response.
801+
schema:
802+
$ref: "#/definitions/helloproto.v1.HelloResp"
803+
parameters:
804+
- name: name
805+
in: query
806+
required: false
807+
type: string
808+
- name: X-Foo
809+
description: Foo Header
810+
in: header
811+
required: true
812+
type: string
813+
- name: X-Bar
814+
description: Bar Header
815+
in: header
816+
required: false
817+
type: number
818+
definitions:
819+
helloproto.v1.HelloResp:
820+
type: object
821+
properties:
822+
message:
823+
type: string
824+
```
825+
737826
{% endraw %}

protoc-gen-openapiv2/internal/genopenapi/template.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1514,6 +1514,33 @@ func renderServices(services []*descriptor.Service, paths openapiPathsObject, re
15141514
copy(operationObject.Produces, opts.Produces)
15151515
}
15161516

1517+
if params := opts.Parameters; params != nil && len(params.Headers) > 0 {
1518+
for _, header := range params.Headers {
1519+
param := openapiParameterObject{
1520+
In: "header",
1521+
Name: header.Name,
1522+
Description: header.Description,
1523+
Required: header.Required,
1524+
Format: header.Format,
1525+
}
1526+
1527+
switch header.Type {
1528+
case openapi_options.HeaderParameter_STRING:
1529+
param.Type = "string"
1530+
case openapi_options.HeaderParameter_NUMBER:
1531+
param.Type = "number"
1532+
case openapi_options.HeaderParameter_INTEGER:
1533+
param.Type = "integer"
1534+
case openapi_options.HeaderParameter_BOOLEAN:
1535+
param.Type = "boolean"
1536+
default:
1537+
return fmt.Errorf("invalid header parameter type: %+v", header.Type)
1538+
}
1539+
1540+
operationObject.Parameters = append(operationObject.Parameters, param)
1541+
}
1542+
}
1543+
15171544
// TODO(ivucica): add remaining fields of operation object
15181545
}
15191546

protoc-gen-openapiv2/internal/genopenapi/template_test.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7976,6 +7976,214 @@ func TestRenderServicesWithColonInSegment(t *testing.T) {
79767976
}
79777977
}
79787978

7979+
func TestRenderServiceWithHeaderParameters(t *testing.T) {
7980+
file := func() descriptor.File {
7981+
msgdesc := &descriptorpb.DescriptorProto{
7982+
Name: proto.String("ExampleMessage"),
7983+
}
7984+
7985+
meth := &descriptorpb.MethodDescriptorProto{
7986+
Name: proto.String("Example"),
7987+
InputType: proto.String("ExampleMessage"),
7988+
OutputType: proto.String("ExampleMessage"),
7989+
Options: &descriptorpb.MethodOptions{},
7990+
}
7991+
7992+
svc := &descriptorpb.ServiceDescriptorProto{
7993+
Name: proto.String("ExampleService"),
7994+
Method: []*descriptorpb.MethodDescriptorProto{meth},
7995+
}
7996+
7997+
msg := &descriptor.Message{
7998+
DescriptorProto: msgdesc,
7999+
}
8000+
8001+
return descriptor.File{
8002+
FileDescriptorProto: &descriptorpb.FileDescriptorProto{
8003+
SourceCodeInfo: &descriptorpb.SourceCodeInfo{},
8004+
Name: proto.String("example.proto"),
8005+
Package: proto.String("example"),
8006+
MessageType: []*descriptorpb.DescriptorProto{msgdesc},
8007+
Service: []*descriptorpb.ServiceDescriptorProto{svc},
8008+
Options: &descriptorpb.FileOptions{
8009+
GoPackage: proto.String("github.com/grpc-ecosystem/grpc-gateway/runtime/internal/examplepb;example"),
8010+
},
8011+
},
8012+
GoPkg: descriptor.GoPackage{
8013+
Path: "example.com/path/to/example/example.pb",
8014+
Name: "example_pb",
8015+
},
8016+
Messages: []*descriptor.Message{msg},
8017+
Services: []*descriptor.Service{
8018+
{
8019+
ServiceDescriptorProto: svc,
8020+
Methods: []*descriptor.Method{
8021+
{
8022+
MethodDescriptorProto: meth,
8023+
RequestType: msg,
8024+
ResponseType: msg,
8025+
Bindings: []*descriptor.Binding{
8026+
{
8027+
HTTPMethod: "GET",
8028+
PathTmpl: httprule.Template{
8029+
Version: 1,
8030+
OpCodes: []int{0, 0},
8031+
Template: "/v1/echo",
8032+
},
8033+
},
8034+
},
8035+
},
8036+
},
8037+
},
8038+
},
8039+
}
8040+
}
8041+
8042+
type test struct {
8043+
file func() descriptor.File
8044+
openapiOperation *openapi_options.Operation
8045+
parameters openapiParametersObject
8046+
}
8047+
8048+
tests := map[string]*test{
8049+
"type string": {
8050+
file: file,
8051+
openapiOperation: &openapi_options.Operation{
8052+
Parameters: &openapi_options.Parameters{
8053+
Headers: []*openapi_options.HeaderParameter{
8054+
{
8055+
Name: "X-Custom-Header",
8056+
Type: openapi_options.HeaderParameter_STRING,
8057+
},
8058+
},
8059+
},
8060+
},
8061+
parameters: openapiParametersObject{
8062+
{
8063+
Name: "X-Custom-Header",
8064+
In: "header",
8065+
Type: "string",
8066+
},
8067+
},
8068+
},
8069+
"type integer": {
8070+
file: file,
8071+
openapiOperation: &openapi_options.Operation{
8072+
Parameters: &openapi_options.Parameters{
8073+
Headers: []*openapi_options.HeaderParameter{
8074+
{
8075+
Name: "X-Custom-Header",
8076+
Type: openapi_options.HeaderParameter_INTEGER,
8077+
},
8078+
},
8079+
},
8080+
},
8081+
parameters: openapiParametersObject{
8082+
{
8083+
Name: "X-Custom-Header",
8084+
In: "header",
8085+
Type: "integer",
8086+
},
8087+
},
8088+
},
8089+
"type number": {
8090+
file: file,
8091+
openapiOperation: &openapi_options.Operation{
8092+
Parameters: &openapi_options.Parameters{
8093+
Headers: []*openapi_options.HeaderParameter{
8094+
{
8095+
Name: "X-Custom-Header",
8096+
Type: openapi_options.HeaderParameter_NUMBER,
8097+
},
8098+
},
8099+
},
8100+
},
8101+
parameters: openapiParametersObject{
8102+
{
8103+
Name: "X-Custom-Header",
8104+
In: "header",
8105+
Type: "number",
8106+
},
8107+
},
8108+
},
8109+
"type boolean": {
8110+
file: file,
8111+
openapiOperation: &openapi_options.Operation{
8112+
Parameters: &openapi_options.Parameters{
8113+
Headers: []*openapi_options.HeaderParameter{
8114+
{
8115+
Name: "X-Custom-Header",
8116+
Type: openapi_options.HeaderParameter_BOOLEAN,
8117+
},
8118+
},
8119+
},
8120+
},
8121+
parameters: openapiParametersObject{
8122+
{
8123+
Name: "X-Custom-Header",
8124+
In: "header",
8125+
Type: "boolean",
8126+
},
8127+
},
8128+
},
8129+
"header required": {
8130+
file: file,
8131+
openapiOperation: &openapi_options.Operation{
8132+
Parameters: &openapi_options.Parameters{
8133+
Headers: []*openapi_options.HeaderParameter{
8134+
{
8135+
Name: "X-Custom-Header",
8136+
Required: true,
8137+
Type: openapi_options.HeaderParameter_STRING,
8138+
},
8139+
},
8140+
},
8141+
},
8142+
parameters: openapiParametersObject{
8143+
{
8144+
Name: "X-Custom-Header",
8145+
In: "header",
8146+
Required: true,
8147+
Type: "string",
8148+
},
8149+
},
8150+
},
8151+
}
8152+
8153+
for name, test := range tests {
8154+
test := test
8155+
8156+
t.Run(name, func(t *testing.T) {
8157+
file := test.file()
8158+
8159+
proto.SetExtension(
8160+
proto.Message(file.Services[0].Methods[0].Options),
8161+
openapi_options.E_Openapiv2Operation,
8162+
test.openapiOperation)
8163+
8164+
reg := descriptor.NewRegistry()
8165+
8166+
fileCL := crossLinkFixture(&file)
8167+
8168+
err := reg.Load(reqFromFile(fileCL))
8169+
if err != nil {
8170+
t.Errorf("reg.Load(%#v) failed with %v; want success", file, err)
8171+
}
8172+
8173+
result, err := applyTemplate(param{File: fileCL, reg: reg})
8174+
if err != nil {
8175+
t.Fatalf("applyTemplate(%#v) failed with %v; want success", file, err)
8176+
}
8177+
8178+
params := result.Paths["/v1/echo"].Get.Parameters
8179+
8180+
if !reflect.DeepEqual(params, test.parameters) {
8181+
t.Errorf("expected %+v, got %+v", test.parameters, params)
8182+
}
8183+
})
8184+
}
8185+
}
8186+
79798187
func GetPaths(req *openapiSwaggerObject) []string {
79808188
paths := make([]string, len(req.Paths))
79818189
i := 0

0 commit comments

Comments
 (0)