From f336fbcc3f21acf89e935c2353e7ead734ec31f5 Mon Sep 17 00:00:00 2001 From: Michael Chinn <1105686+mechinn@users.noreply.github.com> Date: Wed, 23 Jan 2019 10:22:47 -0500 Subject: [PATCH] make generated swagger json match gateway behavior for server streams (#850) * #579 fixing protoc-gen-swagger to wrap server stream messages with "result" object to match gateway behavior * #579 fixing test example swagger json * change streamdefinitions to x-stream-definitions and add test * move runtime internal to root of repo for use in protoc-gen-swagger, adding error to stream wrapper * adding comment explaining AddStreamError * fix bazel --- Makefile | 3 +- examples/proto/examplepb/stream.swagger.json | 362 ++++++++++++++++++ {runtime/internal => internal}/BUILD.bazel | 6 +- .../internal => internal}/stream_chunk.pb.go | 39 +- .../internal => internal}/stream_chunk.proto | 0 protoc-gen-swagger/genswagger/BUILD.bazel | 2 + protoc-gen-swagger/genswagger/generator.go | 3 + protoc-gen-swagger/genswagger/template.go | 127 +++++- .../genswagger/template_test.go | 75 +++- protoc-gen-swagger/genswagger/types.go | 1 + protoc-gen-swagger/main.go | 5 + runtime/BUILD.bazel | 4 +- runtime/handler.go | 2 +- runtime/handler_test.go | 2 +- 14 files changed, 586 insertions(+), 45 deletions(-) create mode 100644 examples/proto/examplepb/stream.swagger.json rename {runtime/internal => internal}/BUILD.bazel (66%) rename {runtime/internal => internal}/stream_chunk.pb.go (64%) rename {runtime/internal => internal}/stream_chunk.proto (100%) diff --git a/Makefile b/Makefile index 8d7156cc378..d627f84511d 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ SWAGGER_PLUGIN_FLAGS?= GOOGLEAPIS_DIR=third_party/googleapis OUTPUT_DIR=_output -RUNTIME_PROTO=runtime/internal/stream_chunk.proto +RUNTIME_PROTO=internal/stream_chunk.proto RUNTIME_GO=$(RUNTIME_PROTO:.proto=.pb.go) OPENAPIV2_PROTO=protoc-gen-swagger/options/openapiv2.proto protoc-gen-swagger/options/annotations.proto @@ -63,6 +63,7 @@ endif SWAGGER_EXAMPLES=examples/proto/examplepb/echo_service.proto \ examples/proto/examplepb/a_bit_of_everything.proto \ examples/proto/examplepb/wrappers.proto \ + examples/proto/examplepb/stream.proto \ examples/proto/examplepb/unannotated_echo_service.proto \ examples/proto/examplepb/response_body_service.proto diff --git a/examples/proto/examplepb/stream.swagger.json b/examples/proto/examplepb/stream.swagger.json new file mode 100644 index 00000000000..a441c36be48 --- /dev/null +++ b/examples/proto/examplepb/stream.swagger.json @@ -0,0 +1,362 @@ +{ + "swagger": "2.0", + "info": { + "title": "examples/proto/examplepb/stream.proto", + "version": "version not set" + }, + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/v1/example/a_bit_of_everything": { + "get": { + "operationId": "List", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "$ref": "#/x-stream-definitions/examplepbABitOfEverything" + } + } + }, + "tags": [ + "StreamService" + ] + } + }, + "/v1/example/a_bit_of_everything/bulk": { + "post": { + "operationId": "BulkCreate", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "properties": {} + } + } + }, + "parameters": [ + { + "name": "body", + "description": " (streaming inputs)", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/examplepbABitOfEverything" + } + } + ], + "tags": [ + "StreamService" + ] + } + }, + "/v1/example/a_bit_of_everything/echo": { + "post": { + "operationId": "BulkEcho", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "$ref": "#/x-stream-definitions/subStringMessage" + } + } + }, + "parameters": [ + { + "name": "body", + "description": " (streaming inputs)", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/subStringMessage" + } + } + ], + "tags": [ + "StreamService" + ] + } + } + }, + "definitions": { + "ABitOfEverythingNested": { + "type": "object", + "example": { + "ok": "TRUE" + }, + "properties": { + "name": { + "type": "string", + "description": "name is nested field." + }, + "amount": { + "type": "integer", + "format": "int64" + }, + "ok": { + "$ref": "#/definitions/NestedDeepEnum" + } + }, + "description": "Nested is nested type." + }, + "MessagePathEnumNestedPathEnum": { + "type": "string", + "enum": [ + "GHI", + "JKL" + ], + "default": "GHI" + }, + "NestedDeepEnum": { + "type": "string", + "enum": [ + "FALSE", + "TRUE" + ], + "default": "FALSE", + "description": "DeepEnum is one or zero.\n\n - FALSE: FALSE is false.\n - TRUE: TRUE is true." + }, + "examplepbABitOfEverything": { + "type": "object", + "example": { + "uuid": "0cf361e1-4b44-483d-a159-54dabdf7e814" + }, + "properties": { + "single_nested": { + "$ref": "#/definitions/ABitOfEverythingNested" + }, + "uuid": { + "type": "string", + "minLength": 1, + "pattern": "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, + "nested": { + "type": "array", + "items": { + "$ref": "#/definitions/ABitOfEverythingNested" + } + }, + "float_value": { + "type": "number", + "format": "float" + }, + "double_value": { + "type": "number", + "format": "double" + }, + "int64_value": { + "type": "string", + "format": "int64" + }, + "uint64_value": { + "type": "string", + "format": "uint64" + }, + "int32_value": { + "type": "integer", + "format": "int32" + }, + "fixed64_value": { + "type": "string", + "format": "uint64" + }, + "fixed32_value": { + "type": "integer", + "format": "int64" + }, + "bool_value": { + "type": "boolean", + "format": "boolean" + }, + "string_value": { + "type": "string" + }, + "bytes_value": { + "type": "string", + "format": "byte" + }, + "uint32_value": { + "type": "integer", + "format": "int64" + }, + "enum_value": { + "$ref": "#/definitions/examplepbNumericEnum" + }, + "path_enum_value": { + "$ref": "#/definitions/pathenumPathEnum" + }, + "nested_path_enum_value": { + "$ref": "#/definitions/MessagePathEnumNestedPathEnum" + }, + "sfixed32_value": { + "type": "integer", + "format": "int32" + }, + "sfixed64_value": { + "type": "string", + "format": "int64" + }, + "sint32_value": { + "type": "integer", + "format": "int32" + }, + "sint64_value": { + "type": "string", + "format": "int64" + }, + "repeated_string_value": { + "type": "array", + "items": { + "type": "string" + } + }, + "oneof_empty": { + "properties": {} + }, + "oneof_string": { + "type": "string" + }, + "map_value": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/examplepbNumericEnum" + } + }, + "mapped_string_value": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "mapped_nested_value": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ABitOfEverythingNested" + } + }, + "nonConventionalNameValue": { + "type": "string" + }, + "timestamp_value": { + "type": "string", + "format": "date-time" + }, + "repeated_enum_value": { + "type": "array", + "items": { + "$ref": "#/definitions/examplepbNumericEnum" + }, + "title": "repeated enum value. it is comma-separated in query" + } + }, + "description": "Intentionaly complicated message type to cover many features of Protobuf.", + "title": "A bit of everything", + "externalDocs": { + "description": "Find out more about ABitOfEverything", + "url": "https://github.com/grpc-ecosystem/grpc-gateway" + }, + "required": [ + "uuid" + ] + }, + "examplepbNumericEnum": { + "type": "string", + "enum": [ + "ZERO", + "ONE" + ], + "default": "ZERO", + "description": "NumericEnum is one or zero.\n\n - ZERO: ZERO means 0\n - ONE: ONE means 1" + }, + "pathenumPathEnum": { + "type": "string", + "enum": [ + "ABC", + "DEF" + ], + "default": "ABC" + }, + "protobufAny": { + "type": "object", + "properties": { + "type_url": { + "type": "string", + "description": "A URL/resource name whose content describes the type of the\nserialized protocol buffer message.\n\nFor URLs which use the scheme `http`, `https`, or no scheme, the\nfollowing restrictions and interpretations apply:\n\n* If no scheme is provided, `https` is assumed.\n* The last segment of the URL's path must represent the fully\n qualified name of the type (as in `path/google.protobuf.Duration`).\n The name should be in a canonical form (e.g., leading \".\" is\n not accepted).\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics." + }, + "value": { + "type": "string", + "format": "byte", + "description": "Must be a valid serialized protocol buffer of the above specified type." + } + }, + "description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }" + }, + "runtimeStreamError": { + "type": "object", + "properties": { + "grpc_code": { + "type": "integer", + "format": "int32" + }, + "http_code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "http_status": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "subStringMessage": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + } + }, + "x-stream-definitions": { + "examplepbABitOfEverything": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/examplepbABitOfEverything" + }, + "error": { + "$ref": "#/definitions/runtimeStreamError" + } + }, + "title": "Stream result of examplepbABitOfEverything" + }, + "subStringMessage": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/subStringMessage" + }, + "error": { + "$ref": "#/definitions/runtimeStreamError" + } + }, + "title": "Stream result of subStringMessage" + } + } +} diff --git a/runtime/internal/BUILD.bazel b/internal/BUILD.bazel similarity index 66% rename from runtime/internal/BUILD.bazel rename to internal/BUILD.bazel index 63565730423..76cafe6ec7f 100644 --- a/runtime/internal/BUILD.bazel +++ b/internal/BUILD.bazel @@ -1,7 +1,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") -package(default_visibility = ["//runtime:__subpackages__"]) +package(default_visibility = ["//visibility:public"]) proto_library( name = "internal_proto", @@ -11,12 +11,12 @@ proto_library( go_proto_library( name = "internal_go_proto", - importpath = "github.com/grpc-ecosystem/grpc-gateway/runtime/internal", + importpath = "github.com/grpc-ecosystem/grpc-gateway/internal", proto = ":internal_proto", ) go_library( name = "go_default_library", embed = [":internal_go_proto"], - importpath = "github.com/grpc-ecosystem/grpc-gateway/runtime/internal", + importpath = "github.com/grpc-ecosystem/grpc-gateway/internal", ) diff --git a/runtime/internal/stream_chunk.pb.go b/internal/stream_chunk.pb.go similarity index 64% rename from runtime/internal/stream_chunk.pb.go rename to internal/stream_chunk.pb.go index f4af074808b..c6cfb5c1c02 100644 --- a/runtime/internal/stream_chunk.pb.go +++ b/internal/stream_chunk.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// source: runtime/internal/stream_chunk.proto +// source: internal/stream_chunk.proto package internal @@ -36,7 +36,7 @@ func (m *StreamError) Reset() { *m = StreamError{} } func (m *StreamError) String() string { return proto.CompactTextString(m) } func (*StreamError) ProtoMessage() {} func (*StreamError) Descriptor() ([]byte, []int) { - return fileDescriptor_stream_chunk_6adb10b40700458b, []int{0} + return fileDescriptor_stream_chunk_cac6867f460b29a5, []int{0} } func (m *StreamError) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_StreamError.Unmarshal(m, b) @@ -96,24 +96,23 @@ func init() { } func init() { - proto.RegisterFile("runtime/internal/stream_chunk.proto", fileDescriptor_stream_chunk_6adb10b40700458b) + proto.RegisterFile("internal/stream_chunk.proto", fileDescriptor_stream_chunk_cac6867f460b29a5) } -var fileDescriptor_stream_chunk_6adb10b40700458b = []byte{ - // 226 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x34, 0x90, 0xc1, 0x4e, 0x84, 0x30, - 0x10, 0x86, 0x83, 0xeb, 0xba, 0xbb, 0xc3, 0x8d, 0xec, 0xa1, 0xea, 0x41, 0xa2, 0x17, 0x4e, 0x25, - 0xd1, 0x27, 0x50, 0xe3, 0x0b, 0xb0, 0x37, 0x2f, 0x9b, 0x59, 0x18, 0x0b, 0x11, 0x5a, 0x32, 0x1d, - 0x62, 0x78, 0x2d, 0x9f, 0xd0, 0xb4, 0xc8, 0xb1, 0xdf, 0xf7, 0xff, 0x93, 0x3f, 0x85, 0x27, 0x9e, - 0xac, 0x74, 0x03, 0x95, 0x9d, 0x15, 0x62, 0x8b, 0x7d, 0xe9, 0x85, 0x09, 0x87, 0x73, 0xdd, 0x4e, - 0xf6, 0x5b, 0x8f, 0xec, 0xc4, 0x65, 0x47, 0xc3, 0x63, 0xad, 0x0d, 0x0a, 0xfd, 0xe0, 0xac, 0xff, - 0x1b, 0x77, 0xb7, 0xc6, 0x39, 0xd3, 0x53, 0x19, 0x33, 0x97, 0xe9, 0xab, 0x44, 0x3b, 0x2f, 0x85, - 0xc7, 0xdf, 0x04, 0xd2, 0x53, 0xbc, 0xf3, 0xc1, 0xec, 0x38, 0xbb, 0x87, 0x43, 0x38, 0x71, 0xae, - 0x5d, 0x43, 0x2a, 0xc9, 0x93, 0x62, 0x5b, 0xed, 0x03, 0x78, 0x77, 0x0d, 0x05, 0xd9, 0x8a, 0x8c, - 0x8b, 0xbc, 0x5a, 0x64, 0x00, 0x51, 0x2a, 0xd8, 0x0d, 0xe4, 0x3d, 0x1a, 0x52, 0x9b, 0x3c, 0x29, - 0x0e, 0xd5, 0xfa, 0xcc, 0x1e, 0x20, 0x8d, 0x35, 0x2f, 0x28, 0x93, 0x57, 0xd7, 0xd1, 0x42, 0x40, - 0xa7, 0x48, 0x32, 0x0d, 0xbb, 0x86, 0x04, 0xbb, 0xde, 0xab, 0x6d, 0xbe, 0x29, 0xd2, 0xe7, 0xa3, - 0x5e, 0x16, 0xeb, 0x75, 0xb1, 0x7e, 0xb5, 0x73, 0xb5, 0x86, 0xde, 0xe0, 0x73, 0xbf, 0x7e, 0xc2, - 0xe5, 0x26, 0x46, 0x5e, 0xfe, 0x02, 0x00, 0x00, 0xff, 0xff, 0x16, 0x75, 0x92, 0x08, 0x1f, 0x01, - 0x00, 0x00, +var fileDescriptor_stream_chunk_cac6867f460b29a5 = []byte{ + // 223 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x34, 0x90, 0x41, 0x4e, 0xc3, 0x30, + 0x10, 0x45, 0x15, 0x4a, 0x69, 0x3b, 0xd9, 0x45, 0x5d, 0x18, 0xba, 0x20, 0x62, 0x95, 0x95, 0x23, + 0xc1, 0x09, 0x00, 0x71, 0x81, 0x74, 0xc7, 0xa6, 0x9a, 0x26, 0x83, 0x13, 0x91, 0xd8, 0xd1, 0x78, + 0x22, 0x94, 0x6b, 0x71, 0xc2, 0xca, 0x8e, 0xb2, 0xf4, 0x7b, 0x7f, 0xbe, 0xbe, 0x0c, 0xa7, 0xce, + 0x0a, 0xb1, 0xc5, 0xbe, 0xf4, 0xc2, 0x84, 0xc3, 0xa5, 0x6e, 0x27, 0xfb, 0xab, 0x47, 0x76, 0xe2, + 0xb2, 0xa3, 0xe1, 0xb1, 0xd6, 0x06, 0x85, 0xfe, 0x70, 0xd6, 0x3c, 0x59, 0xe9, 0x06, 0x7a, 0x7a, + 0x34, 0xce, 0x99, 0x9e, 0xca, 0x98, 0xb9, 0x4e, 0x3f, 0x25, 0xda, 0x79, 0x39, 0x78, 0xf9, 0x4f, + 0x20, 0x3d, 0xc7, 0x9e, 0x2f, 0x66, 0xc7, 0xd9, 0x09, 0x0e, 0xa1, 0xe2, 0x52, 0xbb, 0x86, 0x54, + 0x92, 0x27, 0xc5, 0xb6, 0xda, 0x07, 0xf0, 0xe9, 0x1a, 0x0a, 0xb2, 0x15, 0x19, 0x17, 0x79, 0xb7, + 0xc8, 0x00, 0xa2, 0x54, 0xb0, 0x1b, 0xc8, 0x7b, 0x34, 0xa4, 0x36, 0x79, 0x52, 0x1c, 0xaa, 0xf5, + 0x99, 0x3d, 0x43, 0x1a, 0xcf, 0xbc, 0xa0, 0x4c, 0x5e, 0xdd, 0x47, 0x0b, 0x01, 0x9d, 0x23, 0xc9, + 0x34, 0xec, 0x1a, 0x12, 0xec, 0x7a, 0xaf, 0xb6, 0xf9, 0xa6, 0x48, 0x5f, 0x8f, 0x7a, 0x59, 0xac, + 0xd7, 0xc5, 0xfa, 0xdd, 0xce, 0xd5, 0x1a, 0xfa, 0x80, 0xef, 0xfd, 0xfa, 0x09, 0xd7, 0x87, 0x18, + 0x79, 0xbb, 0x05, 0x00, 0x00, 0xff, 0xff, 0x0d, 0x7d, 0xa5, 0x18, 0x17, 0x01, 0x00, 0x00, } diff --git a/runtime/internal/stream_chunk.proto b/internal/stream_chunk.proto similarity index 100% rename from runtime/internal/stream_chunk.proto rename to internal/stream_chunk.proto diff --git a/protoc-gen-swagger/genswagger/BUILD.bazel b/protoc-gen-swagger/genswagger/BUILD.bazel index 683b5eeec06..be398fb0dc1 100644 --- a/protoc-gen-swagger/genswagger/BUILD.bazel +++ b/protoc-gen-swagger/genswagger/BUILD.bazel @@ -12,11 +12,13 @@ go_library( ], importpath = "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/genswagger", deps = [ + "//internal:go_default_library", "//protoc-gen-grpc-gateway/descriptor:go_default_library", "//protoc-gen-grpc-gateway/generator:go_default_library", "//protoc-gen-swagger/options:go_default_library", "@com_github_golang_glog//:go_default_library", "@com_github_golang_protobuf//proto:go_default_library", + "@io_bazel_rules_go//proto/wkt:any_go_proto", "@io_bazel_rules_go//proto/wkt:compiler_plugin_go_proto", "@io_bazel_rules_go//proto/wkt:descriptor_go_proto", ], diff --git a/protoc-gen-swagger/genswagger/generator.go b/protoc-gen-swagger/genswagger/generator.go index 8eba8ac66a4..b69f7e42e3e 100644 --- a/protoc-gen-swagger/genswagger/generator.go +++ b/protoc-gen-swagger/genswagger/generator.go @@ -47,6 +47,9 @@ func mergeTargetFile(targets []*wrapper, mergeFileName string) *wrapper { for k, v := range f.swagger.Definitions { mergedTarget.swagger.Definitions[k] = v } + for k, v := range f.swagger.StreamDefinitions { + mergedTarget.swagger.StreamDefinitions[k] = v + } for k, v := range f.swagger.Paths { mergedTarget.swagger.Paths[k] = v } diff --git a/protoc-gen-swagger/genswagger/template.go b/protoc-gen-swagger/genswagger/template.go index e8675963b75..f5a6e031164 100644 --- a/protoc-gen-swagger/genswagger/template.go +++ b/protoc-gen-swagger/genswagger/template.go @@ -1,6 +1,8 @@ package genswagger import ( + "bytes" + "compress/gzip" "encoding/json" "fmt" "os" @@ -10,8 +12,12 @@ import ( "strings" "sync" + "github.com/golang/glog" "github.com/golang/protobuf/proto" pbdescriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" + plugin "github.com/golang/protobuf/protoc-gen-go/plugin" + "github.com/golang/protobuf/ptypes/any" + "github.com/grpc-ecosystem/grpc-gateway/internal" "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway/descriptor" swagger_options "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options" ) @@ -180,7 +186,7 @@ func queryParams(message *descriptor.Message, field *descriptor.Field, prefix st } // findServicesMessagesAndEnumerations discovers all messages and enums defined in the RPC methods of the service. -func findServicesMessagesAndEnumerations(s []*descriptor.Service, reg *descriptor.Registry, m messageMap, e enumMap, refs refMap) { +func findServicesMessagesAndEnumerations(s []*descriptor.Service, reg *descriptor.Registry, m messageMap, ms messageMap, e enumMap, refs refMap) { for _, svc := range s { for _, meth := range svc.Methods { // Request may be fully included in query @@ -193,6 +199,20 @@ func findServicesMessagesAndEnumerations(s []*descriptor.Service, reg *descripto if !skipRenderingRef(meth.ResponseType.FQMN()) { m[fullyQualifiedNameToSwaggerName(meth.ResponseType.FQMN(), reg)] = meth.ResponseType + if meth.GetServerStreaming() { + runtimeStreamError := fullyQualifiedNameToSwaggerName(".grpc.gateway.runtime.StreamError", reg) + glog.V(1).Infof("StreamError FQMN: %s", runtimeStreamError) + streamError, err := reg.LookupMsg(".grpc.gateway.runtime", "StreamError") + if err == nil { + glog.V(1).Infof("StreamError: %v", streamError) + m[runtimeStreamError] = streamError + findNestedMessagesAndEnumerations(streamError, reg, m, e) + } else { + //just in case there is an error looking up StreamError + glog.Error(err) + } + ms[fullyQualifiedNameToSwaggerName(meth.ResponseType.FQMN(), reg)] = meth.ResponseType + } } findNestedMessagesAndEnumerations(meth.ResponseType, reg, m, e) } @@ -306,6 +326,92 @@ func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, } } +//AddStreamError Adds internal.StreamError and Any to registry for stream responses +func AddStreamError(reg *descriptor.Registry) error { + //load internal protos + any, err := fileDescriptorProtoFromProtoDescriptor(&any.Any{}) + if err != nil { + return err + } + streamError, err := fileDescriptorProtoFromProtoDescriptor(&internal.StreamError{}) + if err != nil { + return err + } + if err := reg.Load(&plugin.CodeGeneratorRequest{ + ProtoFile: []*pbdescriptor.FileDescriptorProto{ + any, + streamError, + }, + }); err != nil { + return err + } + return nil +} + +type protoDescriptor interface { + Descriptor() ([]byte, []int) +} + +func fileDescriptorProtoFromProtoDescriptor(pd protoDescriptor) (*pbdescriptor.FileDescriptorProto, error) { + pdd, _ := pd.Descriptor() + r, err := gzip.NewReader(bytes.NewReader(pdd)) + if err != nil { + return nil, err + } + var buf bytes.Buffer + _, err = buf.ReadFrom(r) + if err != nil { + return nil, err + } + err = r.Close() + if err != nil { + return nil, err + } + fdp := &pbdescriptor.FileDescriptorProto{} + if err := proto.Unmarshal(buf.Bytes(), fdp); err != nil { + return nil, err + } + //hide the fact that we are loading this from the pb.go instead of the proto directly + fdp.SourceCodeInfo = &pbdescriptor.SourceCodeInfo{} + return fdp, nil +} + +func renderMessagesAsStreamDefinition(messages messageMap, d swaggerDefinitionsObject, reg *descriptor.Registry) { + for name, msg := range messages { + if skipRenderingRef(name) { + continue + } + + if opt := msg.GetOptions(); opt != nil && opt.MapEntry != nil && *opt.MapEntry { + continue + } + d[fullyQualifiedNameToSwaggerName(msg.FQMN(), reg)] = swaggerSchemaObject{ + schemaCore: schemaCore{ + Type: "object", + }, + Title: fmt.Sprintf("Stream result of %s", fullyQualifiedNameToSwaggerName(msg.FQMN(), reg)), + Properties: &swaggerSchemaObjectProperties{ + keyVal{ + Key: "result", + Value: swaggerSchemaObject{ + schemaCore: schemaCore{ + Ref: fmt.Sprintf("#/definitions/%s", fullyQualifiedNameToSwaggerName(msg.FQMN(), reg)), + }, + }, + }, + keyVal{ + Key: "error", + Value: swaggerSchemaObject{ + schemaCore: schemaCore{ + Ref: fmt.Sprintf("#/definitions/%s", fullyQualifiedNameToSwaggerName(".grpc.gateway.runtime.StreamError", reg)), + }, + }, + }, + }, + } + } +} + // schemaOfField returns a swagger Schema Object for a protobuf field. func schemaOfField(f *descriptor.Field, reg *descriptor.Registry, refs refMap) swaggerSchemaObject { const ( @@ -766,6 +872,8 @@ func renderServices(services []*descriptor.Service, paths swaggerPathsObject, re } if meth.GetServerStreaming() { desc += "(streaming responses)" + // Use the streamdefinition which wraps the message in a "result" + responseSchema.Ref = strings.Replace(responseSchema.Ref, `#/definitions/`, `#/x-stream-definitions/`, 1) } operationObject := &swaggerOperationObject{ Tags: []string{svc.GetName()}, @@ -882,12 +990,13 @@ func applyTemplate(p param) (*swaggerObject, error) { // defined off of. s := swaggerObject{ // Swagger 2.0 is the version of this document - Swagger: "2.0", - Schemes: []string{"http", "https"}, - Consumes: []string{"application/json"}, - Produces: []string{"application/json"}, - Paths: make(swaggerPathsObject), - Definitions: make(swaggerDefinitionsObject), + Swagger: "2.0", + Schemes: []string{"http", "https"}, + Consumes: []string{"application/json"}, + Produces: []string{"application/json"}, + Paths: make(swaggerPathsObject), + Definitions: make(swaggerDefinitionsObject), + StreamDefinitions: make(swaggerDefinitionsObject), Info: swaggerInfoObject{ Title: *p.File.Name, Version: "version not set", @@ -905,9 +1014,11 @@ func applyTemplate(p param) (*swaggerObject, error) { // Find all the service's messages and enumerations that are defined (recursively) // and write request, response and other custom (but referenced) types out as definition objects. m := messageMap{} + ms := messageMap{} e := enumMap{} - findServicesMessagesAndEnumerations(p.Services, p.reg, m, e, requestResponseRefs) + findServicesMessagesAndEnumerations(p.Services, p.reg, m, ms, e, requestResponseRefs) renderMessagesAsDefinition(m, s.Definitions, p.reg, customRefs) + renderMessagesAsStreamDefinition(ms, s.StreamDefinitions, p.reg) renderEnumerationsAsDefinition(e, s.Definitions, p.reg) // File itself might have some comments and metadata. diff --git a/protoc-gen-swagger/genswagger/template_test.go b/protoc-gen-swagger/genswagger/template_test.go index b653ce51ce9..4e20a3ccd25 100644 --- a/protoc-gen-swagger/genswagger/template_test.go +++ b/protoc-gen-swagger/genswagger/template_test.go @@ -280,7 +280,6 @@ func TestApplyTemplateSimple(t *testing.T) { } func TestApplyTemplateRequestWithoutClientStreaming(t *testing.T) { - t.Skip() msgdesc := &protodescriptor.DescriptorProto{ Name: proto.String("ExampleMessage"), Field: []*protodescriptor.FieldDescriptorProto{ @@ -405,7 +404,9 @@ func TestApplyTemplateRequestWithoutClientStreaming(t *testing.T) { }, }, } - result, err := applyTemplate(param{File: crossLinkFixture(&file)}) + reg := descriptor.NewRegistry() + reg.Load(&plugin.CodeGeneratorRequest{ProtoFile: []*protodescriptor.FileDescriptorProto{file.FileDescriptorProto}}) + result, err := applyTemplate(param{File: crossLinkFixture(&file), reg: reg}) if err != nil { t.Errorf("applyTemplate(%#v) failed with %v; want success", file, err) return @@ -425,9 +426,6 @@ func TestApplyTemplateRequestWithoutClientStreaming(t *testing.T) { if want, got := []string{"application/json"}, result.Produces; !reflect.DeepEqual(got, want) { t.Errorf("applyTemplate(%#v).Produces = %s want to be %s", file, got, want) } - if want, got, name := "Generated for ExampleService.Echo - ", result.Paths["/v1/echo"].Post.Summary, "Paths[/v1/echo].Post.Summary"; !reflect.DeepEqual(got, want) { - t.Errorf("applyTemplate(%#v).%s = %s want to be %s", file, name, got, want) - } // If there was a failure, print out the input and the json result for debugging. if t.Failed() { @@ -437,7 +435,6 @@ func TestApplyTemplateRequestWithoutClientStreaming(t *testing.T) { } func TestApplyTemplateRequestWithClientStreaming(t *testing.T) { - t.Skip() msgdesc := &protodescriptor.DescriptorProto{ Name: proto.String("ExampleMessage"), Field: []*protodescriptor.FieldDescriptorProto{ @@ -561,11 +558,71 @@ func TestApplyTemplateRequestWithClientStreaming(t *testing.T) { }, }, } - _, err := applyTemplate(param{File: crossLinkFixture(&file)}) - if err == nil { - t.Errorf("applyTemplate(%#v) should have failed cause swagger doesn't support streaming", file) + reg := descriptor.NewRegistry() + if err := AddStreamError(reg); err != nil { + t.Errorf("AddStreamError(%#v) failed with %v; want success", reg, err) return } + reg.Load(&plugin.CodeGeneratorRequest{ProtoFile: []*protodescriptor.FileDescriptorProto{file.FileDescriptorProto}}) + result, err := applyTemplate(param{File: crossLinkFixture(&file), reg: reg}) + if err != nil { + t.Errorf("applyTemplate(%#v) failed with %v; want success", file, err) + return + } + + // Only ExampleMessage must be present, not NestedMessage + if want, got, name := 3, len(result.Definitions), "len(Definitions)"; !reflect.DeepEqual(got, want) { + t.Errorf("applyTemplate(%#v).%s = %d want to be %d", file, name, got, want) + } + // stream ExampleMessage must be present + if want, got, name := 1, len(result.StreamDefinitions), "len(StreamDefinitions)"; !reflect.DeepEqual(got, want) { + t.Errorf("applyTemplate(%#v).%s = %d want to be %d", file, name, got, want) + } else { + streamExampleExampleMessage := result.StreamDefinitions["exampleExampleMessage"] + if want, got, name := "object", streamExampleExampleMessage.Type, `StreamDefinitions["exampleExampleMessage"].Type`; !reflect.DeepEqual(got, want) { + t.Errorf("applyTemplate(%#v).%s = %s want to be %s", file, name, got, want) + } + if want, got, name := "Stream result of exampleExampleMessage", streamExampleExampleMessage.Title, `StreamDefinitions["exampleExampleMessage"].Title`; !reflect.DeepEqual(got, want) { + t.Errorf("applyTemplate(%#v).%s = %s want to be %s", file, name, got, want) + } + streamExampleExampleMessageProperties := *(streamExampleExampleMessage.Properties) + if want, got, name := 2, len(streamExampleExampleMessageProperties), `len(StreamDefinitions["exampleExampleMessage"].Properties)`; !reflect.DeepEqual(got, want) { + t.Errorf("applyTemplate(%#v).%s = %d want to be %d", file, name, got, want) + } else { + resultProperty := streamExampleExampleMessageProperties[0] + if want, got, name := "result", resultProperty.Key, `(*(StreamDefinitions["exampleExampleMessage"].Properties))[0].Key`; !reflect.DeepEqual(got, want) { + t.Errorf("applyTemplate(%#v).%s = %s want to be %s", file, name, got, want) + } + result := resultProperty.Value.(swaggerSchemaObject) + if want, got, name := "#/definitions/exampleExampleMessage", result.Ref, `((*(StreamDefinitions["exampleExampleMessage"].Properties))[0].Value.(swaggerSchemaObject)).Ref`; !reflect.DeepEqual(got, want) { + t.Errorf("applyTemplate(%#v).%s = %s want to be %s", file, name, got, want) + } + errorProperty := streamExampleExampleMessageProperties[1] + if want, got, name := "error", errorProperty.Key, `(*(StreamDefinitions["exampleExampleMessage"].Properties))[0].Key`; !reflect.DeepEqual(got, want) { + t.Errorf("applyTemplate(%#v).%s = %s want to be %s", file, name, got, want) + } + err := errorProperty.Value.(swaggerSchemaObject) + if want, got, name := "#/definitions/runtimeStreamError", err.Ref, `((*(StreamDefinitions["exampleExampleMessage"].Properties))[0].Value.(swaggerSchemaObject)).Ref`; !reflect.DeepEqual(got, want) { + t.Errorf("applyTemplate(%#v).%s = %s want to be %s", file, name, got, want) + } + } + } + if want, got, name := 1, len(result.Paths["/v1/echo"].Post.Responses), "len(Paths[/v1/echo].Post.Responses)"; !reflect.DeepEqual(got, want) { + t.Errorf("applyTemplate(%#v).%s = %d want to be %d", file, name, got, want) + } else { + if want, got, name := "A successful response.(streaming responses)", result.Paths["/v1/echo"].Post.Responses["200"].Description, `result.Paths["/v1/echo"].Post.Responses["200"].Description`; !reflect.DeepEqual(got, want) { + t.Errorf("applyTemplate(%#v).%s = %s want to be %s", file, name, got, want) + } + if want, got, name := "#/x-stream-definitions/exampleExampleMessage", result.Paths["/v1/echo"].Post.Responses["200"].Schema.Ref, `result.Paths["/v1/echo"].Post.Responses["200"].Description`; !reflect.DeepEqual(got, want) { + t.Errorf("applyTemplate(%#v).%s = %s want to be %s", file, name, got, want) + } + } + + // If there was a failure, print out the input and the json result for debugging. + if t.Failed() { + t.Errorf("had: %s", file) + t.Errorf("got: %s", fmt.Sprint(result)) + } } func TestApplyTemplateRequestWithUnusedReferences(t *testing.T) { diff --git a/protoc-gen-swagger/genswagger/types.go b/protoc-gen-swagger/genswagger/types.go index 6125c80f314..d2bed9aa92c 100644 --- a/protoc-gen-swagger/genswagger/types.go +++ b/protoc-gen-swagger/genswagger/types.go @@ -57,6 +57,7 @@ type swaggerObject struct { Produces []string `json:"produces"` Paths swaggerPathsObject `json:"paths"` Definitions swaggerDefinitionsObject `json:"definitions"` + StreamDefinitions swaggerDefinitionsObject `json:"x-stream-definitions,omitempty"` SecurityDefinitions swaggerSecurityDefinitionsObject `json:"securityDefinitions,omitempty"` Security []swaggerSecurityRequirementObject `json:"security,omitempty"` ExternalDocs *swaggerExternalDocumentationObject `json:"externalDocs,omitempty"` diff --git a/protoc-gen-swagger/main.go b/protoc-gen-swagger/main.go index 174e4fad384..a4d11fd1ef5 100644 --- a/protoc-gen-swagger/main.go +++ b/protoc-gen-swagger/main.go @@ -91,6 +91,11 @@ func main() { g := genswagger.New(reg) + if err := genswagger.AddStreamError(reg); err != nil { + emitError(err) + return + } + if err := reg.Load(req); err != nil { emitError(err) return diff --git a/runtime/BUILD.bazel b/runtime/BUILD.bazel index 059e2033535..c99f83e5851 100644 --- a/runtime/BUILD.bazel +++ b/runtime/BUILD.bazel @@ -24,7 +24,7 @@ go_library( ], importpath = "github.com/grpc-ecosystem/grpc-gateway/runtime", deps = [ - "//runtime/internal:go_default_library", + "//internal:go_default_library", "//utilities:go_default_library", "@com_github_golang_protobuf//jsonpb:go_default_library_gen", "@com_github_golang_protobuf//proto:go_default_library", @@ -60,7 +60,7 @@ go_test( embed = [":go_default_library"], deps = [ "//examples/proto/examplepb:go_default_library", - "//runtime/internal:go_default_library", + "//internal:go_default_library", "//utilities:go_default_library", "@com_github_golang_protobuf//jsonpb:go_default_library_gen", "@com_github_golang_protobuf//proto:go_default_library", diff --git a/runtime/handler.go b/runtime/handler.go index 8ad9d766e1c..58ba686b81b 100644 --- a/runtime/handler.go +++ b/runtime/handler.go @@ -9,7 +9,7 @@ import ( "context" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes/any" - "github.com/grpc-ecosystem/grpc-gateway/runtime/internal" + "github.com/grpc-ecosystem/grpc-gateway/internal" "google.golang.org/grpc/codes" "google.golang.org/grpc/grpclog" "google.golang.org/grpc/status" diff --git a/runtime/handler_test.go b/runtime/handler_test.go index 6a712a4e64e..c9caa25cfc0 100644 --- a/runtime/handler_test.go +++ b/runtime/handler_test.go @@ -10,8 +10,8 @@ import ( "context" "github.com/golang/protobuf/proto" pb "github.com/grpc-ecosystem/grpc-gateway/examples/proto/examplepb" + "github.com/grpc-ecosystem/grpc-gateway/internal" "github.com/grpc-ecosystem/grpc-gateway/runtime" - "github.com/grpc-ecosystem/grpc-gateway/runtime/internal" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status"