diff --git a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/message.proto b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/message.proto index 5489ed1c..c0161a9e 100644 --- a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/message.proto +++ b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/message.proto @@ -19,6 +19,7 @@ package tests.protobuftypes.message.v1; import "google/api/client.proto"; import "google/api/annotations.proto"; +import "google/api/httpbody.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/empty.proto"; @@ -32,11 +33,24 @@ service Messaging { body : "*" }; } + rpc CreateMessagesFromCSV(google.api.HttpBody) returns (google.api.HttpBody) { + option (google.api.http) = { + post : "/v1/messages:csv" + body : "*" + }; + } rpc ListMessages(google.protobuf.Empty) returns (google.protobuf.Value) { option (google.api.http) = { get : "/v1/messages" }; } + // OpenAPI does not allow requestBody in GET operations. + // But it should not convert it to query params either. + rpc ListMessagesCSV(google.api.HttpBody) returns (google.api.HttpBody) { + option (google.api.http) = { + get : "/v1/messages:csv" + }; + } rpc GetMessage(Message) returns (Message) { option (google.api.http) = { get : "/v1/messages/{message_id}" diff --git a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml index 1a995ce0..5dfbfed4 100644 --- a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi.yaml @@ -206,6 +206,34 @@ paths: application/json: schema: type: object + /v1/messages:csv: + get: + tags: + - Messaging + description: |- + OpenAPI does not allow requestBody in GET operations. + But it should not convert it to query params either. + operationId: Messaging_ListMessagesCSV + responses: + "200": + description: OK + content: + '*/*': {} + post: + tags: + - Messaging + operationId: Messaging_CreateMessagesFromCSV + requestBody: + content: + application/json: + schema: + type: string + required: true + responses: + "200": + description: OK + content: + '*/*': {} components: schemas: AnyJSONValue: diff --git a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml index f6e0ebd0..23caa5d4 100644 --- a/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml +++ b/cmd/protoc-gen-openapi/examples/tests/protobuftypes/openapi_json.yaml @@ -206,6 +206,34 @@ paths: application/json: schema: type: object + /v1/messages:csv: + get: + tags: + - Messaging + description: |- + OpenAPI does not allow requestBody in GET operations. + But it should not convert it to query params either. + operationId: Messaging_ListMessagesCSV + responses: + "200": + description: OK + content: + '*/*': {} + post: + tags: + - Messaging + operationId: Messaging_CreateMessagesFromCSV + requestBody: + content: + application/json: + schema: + type: string + required: true + responses: + "200": + description: OK + content: + '*/*': {} components: schemas: AnyJSONValue: diff --git a/cmd/protoc-gen-openapi/generator/openapi-v3.go b/cmd/protoc-gen-openapi/generator/openapi-v3.go index 9a0fd26a..f22dee25 100644 --- a/cmd/protoc-gen-openapi/generator/openapi-v3.go +++ b/cmd/protoc-gen-openapi/generator/openapi-v3.go @@ -573,7 +573,7 @@ func (g *OpenAPIv3Generator) buildOperationV3( } // Add any unhandled fields in the request message as query parameters. - if bodyField != "*" { + if bodyField != "*" && string(inputMessage.Desc.FullName()) != "google.api.HttpBody" { for _, field := range inputMessage.Fields { fieldName := string(field.Desc.Name()) if !contains(coveredParameters, fieldName) && fieldName != bodyField { @@ -734,7 +734,7 @@ func (g *OpenAPIv3Generator) responseContentForMessage(outputMessage *protogen.M return &v3.MediaTypes{ AdditionalProperties: []*v3.NamedMediaType{ { - Name: "application/octet-stream", + Name: "*/*", Value: &v3.MediaType{}, }, }, @@ -756,6 +756,13 @@ func (g *OpenAPIv3Generator) responseContentForMessage(outputMessage *protogen.M func (g *OpenAPIv3Generator) schemaOrReferenceForType(typeName string) *v3.SchemaOrReference { switch typeName { + // Even for GET requests, the google.api.HttpBody will contain POST body data + // This is based on how Envoy handles google.api.HttpBody + case ".google.api.HttpBody": + return &v3.SchemaOrReference{ + Oneof: &v3.SchemaOrReference_Schema{ + Schema: &v3.Schema{Type: "string"}}} + case ".google.protobuf.Timestamp": // Timestamps are serialized as strings return &v3.SchemaOrReference{ diff --git a/third_party/google/api/httpbody.proto b/third_party/google/api/httpbody.proto new file mode 100644 index 00000000..00c80aba --- /dev/null +++ b/third_party/google/api/httpbody.proto @@ -0,0 +1,81 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/any.proto"; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/httpbody;httpbody"; +option java_multiple_files = true; +option java_outer_classname = "HttpBodyProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Message that represents an arbitrary HTTP body. It should only be used for +// payload formats that can't be represented as JSON, such as raw binary or +// an HTML page. +// +// +// This message can be used both in streaming and non-streaming API methods in +// the request as well as the response. +// +// It can be used as a top-level request field, which is convenient if one +// wants to extract parameters from either the URL or HTTP template into the +// request fields and also want access to the raw HTTP body. +// +// Example: +// +// message GetResourceRequest { +// // A unique request id. +// string request_id = 1; +// +// // The raw HTTP body is bound to this field. +// google.api.HttpBody http_body = 2; +// +// } +// +// service ResourceService { +// rpc GetResource(GetResourceRequest) +// returns (google.api.HttpBody); +// rpc UpdateResource(google.api.HttpBody) +// returns (google.protobuf.Empty); +// +// } +// +// Example with streaming methods: +// +// service CaldavService { +// rpc GetCalendar(stream google.api.HttpBody) +// returns (stream google.api.HttpBody); +// rpc UpdateCalendar(stream google.api.HttpBody) +// returns (stream google.api.HttpBody); +// +// } +// +// Use of this type only changes how the request and response bodies are +// handled, all other features will continue to work unchanged. +message HttpBody { + // The HTTP Content-Type header value specifying the content type of the body. + string content_type = 1; + + // The HTTP request/response body as raw binary. + bytes data = 2; + + // Application specific response metadata. Must be set in the first response + // for streaming APIs. + repeated google.protobuf.Any extensions = 3; +}