From 5ae7f118fb0ace0685308d8fa6de0c2eb3906f2f Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Thu, 26 Apr 2018 23:33:58 +0200 Subject: [PATCH] Add support for basic gRPC API Configuration YAML files (#521) * Add support for basic gRPC API Configuration YAML files Separate gRPC API Configuration files as described by google at https://cloud.google.com/endpoints/docs/grpc/grpc-service-config can be used to separate the description of the basic API surface in .proto files from their mapping to HTTP REST. In grpc-gateway this allows exposing gRPC services without having to add any annotations to their .proto files. The format of gRPC API Configuration YAML files is defined by the proto message schema google.api.service which can be found as service.proto in the googleapis repository. This patch exposes the Http part of the service configuration to the gateway generator. To enable this all HttpRules contained in the Http part of the description are loaded into the generator registry as a map from service method to HttpRules called externalHttpRules. During loading of the proto files we can now use these externalHttpRules to look for additional rules to apply to a service method in addition to the ones from the annotations found in the proto file. As the types are the same no fundamentally new codepaths are required in either of the generators. google.api.service is quite a complex protobuf message with lots of dependencies on other google api. To not have to pull in all other dependent types this patch constructs a reduced service message type with only the Http field. This relies on protobufs backwards compatibility guarantees which allows us to remove all other fields. Also we do not need all protobuf features on this custom message. To load the actual YAML file as a protobuf message this patch uses a new dependency on github.com/ghodss/yaml to convert the YAML into JSON which we can then unmarshal into protobuf using jsonpb. * Add a "Generating for unannotated protos" section to README.md Create a new documentation page describing how to use grpc-gateway without annotations by the use of gRPC API Configuration files. This new section explains the use of gRPC API Configuration YAML files to expose unannotated .proto files in contrast to the annotation approach. Adjusted other parts of the documentation to hint/link at that capability. * Add unannotated example for e2e testing The e2e test is basically just the echo example but with an additional inclusion of the google Duration WKT to ensure we properly generate the imports for those even if the annotation WKT is not used. To make this work we have to adjust the makefile to pass the additional grpc api configuration file. We also have to provide a basic server implementation and use the generated client from the tests. --- Makefile | 37 +- README.md | 12 +- WORKSPACE | 12 + docs/_config.yaml | 6 + docs/_docs/examples.md | 7 +- docs/_docs/features.md | 1 + docs/_docs/grpcapiconfiguration.md | 149 ++++++++ docs/_docs/usage.md | 3 + examples/clients/unannotatedecho/.gitignore | 1 + .../unannotatedecho/.swagger-codegen-ignore | 1 + examples/clients/unannotatedecho/BUILD.bazel | 16 + .../clients/unannotatedecho/api_client.go | 164 +++++++++ .../clients/unannotatedecho/api_response.go | 44 +++ .../clients/unannotatedecho/configuration.go | 67 ++++ .../examplepb_unannotated_simple_message.go | 22 ++ .../unannotated_echo_service_api.go | 290 ++++++++++++++++ examples/integration/BUILD.bazel | 1 + examples/integration/client_test.go | 34 ++ examples/proto/examplepb/BUILD.bazel | 1 + examples/proto/examplepb/echo_service.pb.go | 2 + .../examplepb/unannotated_echo_service.pb.go | 232 +++++++++++++ .../unannotated_echo_service.pb.gw.go | 317 ++++++++++++++++++ .../examplepb/unannotated_echo_service.proto | 37 ++ .../unannotated_echo_service.swagger.json | 170 ++++++++++ .../examplepb/unannotated_echo_service.yaml | 15 + examples/server/BUILD.bazel | 1 + examples/server/unannotatedecho.go | 40 +++ .../descriptor/BUILD.bazel | 5 + .../descriptor/grpc_api_configuration.go | 71 ++++ .../descriptor/grpc_api_configuration_test.go | 164 +++++++++ .../descriptor/grpc_api_service.go | 31 ++ .../descriptor/registry.go | 25 +- .../descriptor/services.go | 47 ++- protoc-gen-grpc-gateway/descriptor/types.go | 18 + .../gengateway/generator.go | 3 +- protoc-gen-grpc-gateway/main.go | 16 +- protoc-gen-swagger/main.go | 15 +- 37 files changed, 2034 insertions(+), 43 deletions(-) create mode 100644 docs/_docs/grpcapiconfiguration.md create mode 100644 examples/clients/unannotatedecho/.gitignore create mode 100644 examples/clients/unannotatedecho/.swagger-codegen-ignore create mode 100644 examples/clients/unannotatedecho/BUILD.bazel create mode 100644 examples/clients/unannotatedecho/api_client.go create mode 100644 examples/clients/unannotatedecho/api_response.go create mode 100644 examples/clients/unannotatedecho/configuration.go create mode 100644 examples/clients/unannotatedecho/examplepb_unannotated_simple_message.go create mode 100644 examples/clients/unannotatedecho/unannotated_echo_service_api.go create mode 100644 examples/proto/examplepb/unannotated_echo_service.pb.go create mode 100644 examples/proto/examplepb/unannotated_echo_service.pb.gw.go create mode 100644 examples/proto/examplepb/unannotated_echo_service.proto create mode 100644 examples/proto/examplepb/unannotated_echo_service.swagger.json create mode 100644 examples/proto/examplepb/unannotated_echo_service.yaml create mode 100644 examples/server/unannotatedecho.go create mode 100644 protoc-gen-grpc-gateway/descriptor/grpc_api_configuration.go create mode 100644 protoc-gen-grpc-gateway/descriptor/grpc_api_configuration_test.go create mode 100644 protoc-gen-grpc-gateway/descriptor/grpc_api_service.go diff --git a/Makefile b/Makefile index ac5bd6f5f37..8f0079da0f2 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,7 @@ GATEWAY_PLUGIN_SRC= utilities/doc.go \ protoc-gen-grpc-gateway/httprule/types.go \ protoc-gen-grpc-gateway/main.go GATEWAY_PLUGIN_FLAGS?= +SWAGGER_PLUGIN_FLAGS?= GOOGLEAPIS_DIR=third_party/googleapis OUTPUT_DIR=_output @@ -49,18 +50,24 @@ OPENAPIV2_PROTO=protoc-gen-swagger/options/openapiv2.proto protoc-gen-swagger/op OPENAPIV2_GO=$(OPENAPIV2_PROTO:.proto=.pb.go) PKGMAP=Mgoogle/protobuf/descriptor.proto=$(GO_PLUGIN_PKG)/descriptor,Mexamples/proto/sub/message.proto=$(PKG)/examples/proto/sub -ADDITIONAL_FLAGS= +ADDITIONAL_GW_FLAGS= ifneq "$(GATEWAY_PLUGIN_FLAGS)" "" - ADDITIONAL_FLAGS=,$(GATEWAY_PLUGIN_FLAGS) + ADDITIONAL_GW_FLAGS=,$(GATEWAY_PLUGIN_FLAGS) +endif +ADDITIONAL_SWG_FLAGS= +ifneq "$(SWAGGER_PLUGIN_FLAGS)" "" + ADDITIONAL_SWG_FLAGS=,$(SWAGGER_PLUGIN_FLAGS) 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/wrappers.proto \ + examples/proto/examplepb/unannotated_echo_service.proto EXAMPLES=examples/proto/examplepb/echo_service.proto \ examples/proto/examplepb/a_bit_of_everything.proto \ examples/proto/examplepb/stream.proto \ examples/proto/examplepb/flow_combination.proto \ - examples/proto/examplepb/wrappers.proto + examples/proto/examplepb/wrappers.proto \ + examples/proto/examplepb/unannotated_echo_service.proto EXAMPLE_SVCSRCS=$(EXAMPLES:.proto=.pb.go) EXAMPLE_GWSRCS=$(EXAMPLES:.proto=.pb.gw.go) EXAMPLE_SWAGGERSRCS=$(SWAGGER_EXAMPLES:.proto=.swagger.json) @@ -89,7 +96,13 @@ ABE_EXAMPLE_SRCS=$(EXAMPLE_CLIENT_DIR)/abe/a_bit_of_everything_nested.go \ $(EXAMPLE_CLIENT_DIR)/abe/nested_deep_enum.go \ $(EXAMPLE_CLIENT_DIR)/abe/protobuf_empty.go \ $(EXAMPLE_CLIENT_DIR)/abe/sub_string_message.go -EXAMPLE_CLIENT_SRCS=$(ECHO_EXAMPLE_SRCS) $(ABE_EXAMPLE_SRCS) +UNANNOTATED_ECHO_EXAMPLE_SPEC=examples/proto/examplepb/unannotated_echo_service.swagger.json +UNANNOTATED_ECHO_EXAMPLE_SRCS=$(EXAMPLE_CLIENT_DIR)/unannotatedecho/api_client.go \ + $(EXAMPLE_CLIENT_DIR)/unannotatedecho/api_response.go \ + $(EXAMPLE_CLIENT_DIR)/unannotatedecho/configuration.go \ + $(EXAMPLE_CLIENT_DIR)/unannotatedecho/echo_service_api.go \ + $(EXAMPLE_CLIENT_DIR)/unannotatedecho/examplepb_simple_message.go +EXAMPLE_CLIENT_SRCS=$(ECHO_EXAMPLE_SRCS) $(ABE_EXAMPLE_SRCS) $(UNANNOTATED_ECHO_EXAMPLE_SRCS) SWAGGER_CODEGEN=swagger-codegen PROTOC_INC_PATH=$(dir $(shell which protoc))/../include @@ -120,10 +133,14 @@ $(EXAMPLE_DEPSRCS): $(GO_PLUGIN) $(EXAMPLE_DEPS) mkdir -p $(OUTPUT_DIR) protoc -I $(PROTOC_INC_PATH) -I. --plugin=$(GO_PLUGIN) --go_out=$(PKGMAP),plugins=grpc:$(OUTPUT_DIR) $(@:.pb.go=.proto) cp $(OUTPUT_DIR)/$(PKG)/$@ $@ || cp $(OUTPUT_DIR)/$@ $@ + +$(EXAMPLE_GWSRCS): ADDITIONAL_GW_FLAGS:=$(ADDITIONAL_GW_FLAGS),grpc_api_configuration=examples/proto/examplepb/unannotated_echo_service.yaml $(EXAMPLE_GWSRCS): $(GATEWAY_PLUGIN) $(EXAMPLES) - protoc -I $(PROTOC_INC_PATH) -I. -I$(GOOGLEAPIS_DIR) --plugin=$(GATEWAY_PLUGIN) --grpc-gateway_out=logtostderr=true,$(PKGMAP)$(ADDITIONAL_FLAGS):. $(EXAMPLES) + protoc -I $(PROTOC_INC_PATH) -I. -I$(GOOGLEAPIS_DIR) --plugin=$(GATEWAY_PLUGIN) --grpc-gateway_out=logtostderr=true,$(PKGMAP)$(ADDITIONAL_GW_FLAGS):. $(EXAMPLES) + +$(EXAMPLE_SWAGGERSRCS): ADDITIONAL_SWG_FLAGS:=$(ADDITIONAL_SWG_FLAGS),grpc_api_configuration=examples/proto/examplepb/unannotated_echo_service.yaml $(EXAMPLE_SWAGGERSRCS): $(SWAGGER_PLUGIN) $(SWAGGER_EXAMPLES) - protoc -I $(PROTOC_INC_PATH) -I. -I$(GOOGLEAPIS_DIR) --plugin=$(SWAGGER_PLUGIN) --swagger_out=logtostderr=true,$(PKGMAP):. $(SWAGGER_EXAMPLES) + protoc -I $(PROTOC_INC_PATH) -I. -I$(GOOGLEAPIS_DIR) --plugin=$(SWAGGER_PLUGIN) --swagger_out=logtostderr=true,$(PKGMAP)$(ADDITIONAL_SWG_FLAGS):. $(SWAGGER_EXAMPLES) $(ECHO_EXAMPLE_SRCS): $(ECHO_EXAMPLE_SPEC) $(SWAGGER_CODEGEN) generate -i $(ECHO_EXAMPLE_SPEC) \ @@ -137,6 +154,12 @@ $(ABE_EXAMPLE_SRCS): $(ABE_EXAMPLE_SPEC) @rm -f $(EXAMPLE_CLIENT_DIR)/abe/README.md \ $(EXAMPLE_CLIENT_DIR)/abe/git_push.sh \ $(EXAMPLE_CLIENT_DIR)/abe/.travis.yml +$(UNANNOTATED_ECHO_EXAMPLE_SRCS): $(UNANNOTATED_ECHO_EXAMPLE_SPEC) + $(SWAGGER_CODEGEN) generate -i $(UNANNOTATED_ECHO_EXAMPLE_SPEC) \ + -l go -o examples/clients/unannotatedecho --additional-properties packageName=unannotatedecho + @rm -f $(EXAMPLE_CLIENT_DIR)/unannotatedecho/README.md \ + $(EXAMPLE_CLIENT_DIR)/unannotatedecho/git_push.sh \ + $(EXAMPLE_CLIENT_DIR)/unannotatedecho/.travis.yml examples: $(EXAMPLE_SVCSRCS) $(EXAMPLE_GWSRCS) $(EXAMPLE_DEPSRCS) $(EXAMPLE_SWAGGERSRCS) $(EXAMPLE_CLIENT_SRCS) test: examples diff --git a/README.md b/README.md index 4f489132c5d..ccb7d7409c4 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,9 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. + } } ``` + + If you do not want to modify the proto file for use with grpc-gateway you can alternatively use an external [gRPC API Configuration](https://cloud.google.com/endpoints/docs/grpc/grpc-service-config) file. [Check our documentation](https://grpc-ecosystem.github.io/grpc-gateway/docs/grpcapiconfiguration.html) for more information. + 3. Generate gRPC stub ```sh @@ -196,9 +199,10 @@ This parameter can be useful to pass request scoped context between the gateway ## More Examples More examples are available under `examples` directory. -* `examplepb/echo_service.proto`, `examplepb/a_bit_of_everything.proto`: service definition - * `examplepb/echo_service.pb.go`, `examplepb/a_bit_of_everything.pb.go`: [generated] stub of the service - * `examplepb/echo_service.pb.gw.go`, `examplepb/a_bit_of_everything.pb.gw.go`: [generated] reverse proxy for the service +* `proto/examplepb/echo_service.proto`, `proto/examplepb/a_bit_of_everything.proto`, `proto/examplepb/unannotated_echo_service.proto`: service definition + * `proto/examplepb/echo_service.pb.go`, `proto/examplepb/a_bit_of_everything.pb.go`, `proto/examplepb/unannotated_echo_service.pb.go`: [generated] stub of the service + * `proto/examplepb/echo_service.pb.gw.go`, `proto/examplepb/a_bit_of_everything.pb.gw.go`, `proto/examplepb/uannotated_echo_service.pb.gw.go`: [generated] reverse proxy for the service + * `proto/examplepb/unannotated_echo_service.yaml`: gRPC API Configuration for ```unannotated_echo_service.proto``` * `server/main.go`: service implementation * `main.go`: entrypoint of the generated reverse proxy @@ -215,6 +219,7 @@ To use the same port for custom HTTP handlers (e.g. serving `swagger.json`), gRP * Mapping HTTP headers with `Grpc-Metadata-` prefix to gRPC metadata (prefixed with `grpcgateway-`) * Optionally emitting API definition for [Swagger](http://swagger.io). * Setting [gRPC timeouts](http://www.grpc.io/docs/guides/wire.html) through inbound HTTP `Grpc-Timeout` header. +* Partial support for [gRPC API Configuration]((https://cloud.google.com/endpoints/docs/grpc/grpc-service-config)) files as an alternative to annotation. ### Want to support But not yet. @@ -238,6 +243,7 @@ But patch is welcome. * HTTP headers that start with 'Grpc-Metadata-' are mapped to gRPC metadata (prefixed with `grpcgateway-`) * While configurable, the default {un,}marshaling uses [jsonpb](https://godoc.org/github.com/golang/protobuf/jsonpb) with `OrigName: true`. + # Contribution See [CONTRIBUTING.md](http://github.com/grpc-ecosystem/grpc-gateway/blob/master/CONTRIBUTING.md). diff --git a/WORKSPACE b/WORKSPACE index 5727c218261..225d0181767 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -36,6 +36,18 @@ go_repository( importpath = "github.com/go-resty/resty", ) +go_repository( + name = "com_github_ghodss_yaml", + commit = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7", + importpath = "github.com/ghodss/yaml", +) + +go_repository( + name = "in_gopkg_yaml_v2", + commit = "eb3733d160e74a9c7e442f435eb3bea458e1d19f", + importpath = "gopkg.in/yaml.v2", +) + load("//:repositories.bzl", "repositories") repositories() diff --git a/docs/_config.yaml b/docs/_config.yaml index e1dd48b56e7..48031995698 100644 --- a/docs/_config.yaml +++ b/docs/_config.yaml @@ -6,6 +6,12 @@ collections: docs: output: true +defaults: + - scope: + path: "" + values: + layout: "default" + plugins: - jekyll-toc diff --git a/docs/_docs/examples.md b/docs/_docs/examples.md index adbb49ad7a8..78d93d720db 100644 --- a/docs/_docs/examples.md +++ b/docs/_docs/examples.md @@ -5,9 +5,10 @@ category: documentation # Examples Examples are available under `examples` directory. -* `examplepb/echo_service.proto`, `examplepb/a_bit_of_everything.proto`: service definition - * `examplepb/echo_service.pb.go`, `examplepb/a_bit_of_everything.pb.go`: [generated] stub of the service - * `examplepb/echo_service.pb.gw.go`, `examplepb/a_bit_of_everything.pb.gw.go`: [generated] reverse proxy for the service +* `proto/examplepb/echo_service.proto`, `proto/examplepb/a_bit_of_everything.proto`, `proto/examplepb/unannotated_echo_service.proto`: service definition + * `proto/examplepb/echo_service.pb.go`, `proto/examplepb/a_bit_of_everything.pb.go`, `proto/examplepb/unannotated_echo_service.pb.go`: [generated] stub of the service + * `proto/examplepb/echo_service.pb.gw.go`, `proto/examplepb/a_bit_of_everything.pb.gw.go`, `proto/examplepb/uannotated_echo_service.pb.gw.go`: [generated] reverse proxy for the service + * `proto/examplepb/unannotated_echo_service.yaml`: gRPC API Configuration for ```unannotated_echo_service.proto``` * `server/main.go`: service implementation * `main.go`: entrypoint of the generated reverse proxy diff --git a/docs/_docs/features.md b/docs/_docs/features.md index 27fce3c8301..0bae3608cbb 100644 --- a/docs/_docs/features.md +++ b/docs/_docs/features.md @@ -14,6 +14,7 @@ category: documentation * Mapping HTTP headers with `Grpc-Metadata-` prefix to gRPC metadata (prefixed with `grpcgateway-`) * Optionally emitting API definition for [Swagger](http://swagger.io). * Setting [gRPC timeouts](http://www.grpc.io/docs/guides/wire.html) through inbound HTTP `Grpc-Timeout` header. +* Partial support for [gRPC API Configuration](https://cloud.google.com/endpoints/docs/grpc/grpc-service-config) files as an alternative to annotation. ## Want to support But not yet. diff --git a/docs/_docs/grpcapiconfiguration.md b/docs/_docs/grpcapiconfiguration.md new file mode 100644 index 00000000000..44de68ca029 --- /dev/null +++ b/docs/_docs/grpcapiconfiguration.md @@ -0,0 +1,149 @@ +--- +title: Usage without annotations (gRPC API Configuration) +category: documentation +order: 100 +--- + +# gRPC API Configuration +In some sitations annotating the .proto file of a service is not an option. For example you might not have control over the .proto file or you might want to expose the same gRPC API multiple times in completely different ways. + +Google Cloud Platform a way to do this for services hosted with them called ["gRPC API Configuration"](https://cloud.google.com/endpoints/docs/grpc/grpc-service-config). It can be used to define the behavior of a gRPC API service without modifications to the service itself in the form of [YAML](https://en.wikipedia.org/wiki/YAML) configuration files. + +grpc-gateway generators implement the [HTTP rules part](https://cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#httprule) of this specification. This allows you to take a completely unannotated service proto file, add a YAML file describing its HTTP endpoints and use them together like a annotated proto file with the grpc-gateway generators. + +## Usage of gRPC API Configuration YAML files +The following is equivalent to the basic [usage example](usage.html) but without direct annotation for grpc-gateway in the .proto file. Only some steps require minor changes to use a gRPC API Configuration YAML file instead: + +1. Define your service in gRPC as usual + + your_service.proto: + ```protobuf + syntax = "proto3"; + package example; + message StringMessage { + string value = 1; + } + + service YourService { + rpc Echo(StringMessage) returns (StringMessage) {} + } + ``` + +2. Instead of annotating the .proto file in this step leave it untouched and create a `your_service.yaml` with the following content: + ```yaml + type: google.api.Service + config_version: 3 + + http: + rules: + - selector: example.YourService.Echo + post: /v1/example/echo + body: "*" + ``` + Use a [linter](http://www.yamllint.com/) to validate your YAML. + +3. Generate gRPC stub as before + + ```sh + protoc -I/usr/local/include -I. \ + -I$GOPATH/src \ + -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ + --go_out=plugins=grpc:. \ + path/to/your_service.proto + ``` + + It will generate a stub file `path/to/your_service.pb.go`. +4. Implement your service in gRPC as usual + 1. (Optional) Generate gRPC stub in the language you want. + + e.g. + ```sh + protoc -I/usr/local/include -I. \ + -I$GOPATH/src \ + -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ + --ruby_out=. \ + path/to/your/service_proto + + protoc -I/usr/local/include -I. \ + -I$GOPATH/src \ + -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ + --plugin=protoc-gen-grpc=grpc_ruby_plugin \ + --grpc-ruby_out=. \ + path/to/your/service.proto + ``` + 2. Add the googleapis-common-protos gem (or your language equivalent) as a dependency to your project. + 3. Implement your service + +5. Generate reverse-proxy. Here we have to pass the path to the `your_service.yaml` in addition to the .proto file: + ```sh + protoc -I/usr/local/include -I. \ + -I$GOPATH/src \ + -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ + --grpc-gateway_out=logtostderr=true,grpc_api_configuration=path/to/your_service.yaml:. \ + path/to/your_service.proto + ``` + + It will generate a reverse proxy `path/to/your_service.pb.gw.go` that is identical to the one produced for the annotated proto. + + Note: After generating the code for each of the stubs, in order to build the code, you will want to run ```go get .``` from the directory containing the stubs. + +6. Write an entrypoint + + Now you need to write an entrypoint of the proxy server. This step is the whether the file is annotated or not. + ```go + package main + + import ( + "flag" + "net/http" + + "github.com/golang/glog" + "golang.org/x/net/context" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "google.golang.org/grpc" + + gw "path/to/your_service_package" + ) + + var ( + echoEndpoint = flag.String("echo_endpoint", "localhost:9090", "endpoint of YourService") + ) + + func run() error { + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + mux := runtime.NewServeMux() + opts := []grpc.DialOption{grpc.WithInsecure()} + err := gw.RegisterYourServiceHandlerFromEndpoint(ctx, mux, *echoEndpoint, opts) + if err != nil { + return err + } + + return http.ListenAndServe(":8080", mux) + } + + func main() { + flag.Parse() + defer glog.Flush() + + if err := run(); err != nil { + glog.Fatal(err) + } + } + ``` + +7. (Optional) Generate swagger definitions + +Swagger generation in this step is equivalent to gateway generation. Again pass the path to the yaml file in addition to the proto: + + ```sh + protoc -I/usr/local/include -I. \ + -I$GOPATH/src \ + -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ + --swagger_out=logtostderr=true,grpc_api_configuration=path/to/your_service.yaml:. \ + path/to/your_service.proto + ``` + +All other steps work as before. If you want you can remove the googleapis include path in step 3 and 4 as the unannotated proto no longer requires them. diff --git a/docs/_docs/usage.md b/docs/_docs/usage.md index 7ad6c26187d..8862c523273 100644 --- a/docs/_docs/usage.md +++ b/docs/_docs/usage.md @@ -67,6 +67,9 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. + } } ``` + + If you do not want to modify the proto file for use with grpc-gateway you can alternatively use an external [gRPC API Configuration](https://cloud.google.com/endpoints/docs/grpc/grpc-service-config) file. [Check our documentation](grpcapiconfiguration.html) for more information. + 3. Generate gRPC stub ```sh diff --git a/examples/clients/unannotatedecho/.gitignore b/examples/clients/unannotatedecho/.gitignore new file mode 100644 index 00000000000..2f88269126d --- /dev/null +++ b/examples/clients/unannotatedecho/.gitignore @@ -0,0 +1 @@ +/docs diff --git a/examples/clients/unannotatedecho/.swagger-codegen-ignore b/examples/clients/unannotatedecho/.swagger-codegen-ignore new file mode 100644 index 00000000000..6c7b69a0156 --- /dev/null +++ b/examples/clients/unannotatedecho/.swagger-codegen-ignore @@ -0,0 +1 @@ +.gitignore diff --git a/examples/clients/unannotatedecho/BUILD.bazel b/examples/clients/unannotatedecho/BUILD.bazel new file mode 100644 index 00000000000..611c6c7052d --- /dev/null +++ b/examples/clients/unannotatedecho/BUILD.bazel @@ -0,0 +1,16 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "go_default_library", + srcs = [ + "api_client.go", + "api_response.go", + "configuration.go", + "examplepb_unannotated_simple_message.go", + "unannotated_echo_service_api.go", + ], + importpath = "github.com/grpc-ecosystem/grpc-gateway/examples/clients/unannotatedecho", + deps = ["@com_github_go_resty_resty//:go_default_library"], +) diff --git a/examples/clients/unannotatedecho/api_client.go b/examples/clients/unannotatedecho/api_client.go new file mode 100644 index 00000000000..aa4c1f14e42 --- /dev/null +++ b/examples/clients/unannotatedecho/api_client.go @@ -0,0 +1,164 @@ +/* + * examples/proto/examplepb/unannotated_echo_service.proto + * + * Unannotated Echo Service Similar to echo_service.proto but without annotations. See unannotated_echo_service.yaml for the equivalent of the annotations in gRPC API configuration format. Echo Service API consists of a single service which returns a message. + * + * OpenAPI spec version: version not set + * + * Generated by: https://github.com/swagger-api/swagger-codegen.git + */ + +package unannotatedecho + +import ( + "bytes" + "fmt" + "path/filepath" + "reflect" + "strings" + "net/url" + "io/ioutil" + "github.com/go-resty/resty" +) + +type APIClient struct { + config *Configuration +} + +func (c *APIClient) SelectHeaderContentType(contentTypes []string) string { + + if len(contentTypes) == 0 { + return "" + } + if contains(contentTypes, "application/json") { + return "application/json" + } + return contentTypes[0] // use the first content type specified in 'consumes' +} + +func (c *APIClient) SelectHeaderAccept(accepts []string) string { + + if len(accepts) == 0 { + return "" + } + if contains(accepts, "application/json") { + return "application/json" + } + return strings.Join(accepts, ",") +} + +func contains(haystack []string, needle string) bool { + for _, a := range haystack { + if strings.ToLower(a) == strings.ToLower(needle) { + return true + } + } + return false +} + +func (c *APIClient) CallAPI(path string, method string, + postBody interface{}, + headerParams map[string]string, + queryParams url.Values, + formParams map[string]string, + fileName string, + fileBytes []byte) (*resty.Response, error) { + + rClient := c.prepareClient() + request := c.prepareRequest(rClient, postBody, headerParams, queryParams, formParams, fileName, fileBytes) + + switch strings.ToUpper(method) { + case "GET": + response, err := request.Get(path) + return response, err + case "POST": + response, err := request.Post(path) + return response, err + case "PUT": + response, err := request.Put(path) + return response, err + case "PATCH": + response, err := request.Patch(path) + return response, err + case "DELETE": + response, err := request.Delete(path) + return response, err + } + + return nil, fmt.Errorf("invalid method %v", method) +} + +func (c *APIClient) ParameterToString(obj interface{}, collectionFormat string) string { + delimiter := "" + switch collectionFormat { + case "pipes": + delimiter = "|" + case "ssv": + delimiter = " " + case "tsv": + delimiter = "\t" + case "csv": + delimiter = "," + } + + if reflect.TypeOf(obj).Kind() == reflect.Slice { + return strings.Trim(strings.Replace(fmt.Sprint(obj), " ", delimiter, -1), "[]") + } + + return fmt.Sprintf("%v", obj) +} + +func (c *APIClient) prepareClient() *resty.Client { + + rClient := resty.New() + + rClient.SetDebug(c.config.Debug) + if c.config.Transport != nil { + rClient.SetTransport(c.config.Transport) + } + + if c.config.Timeout != nil { + rClient.SetTimeout(*c.config.Timeout) + } + rClient.SetLogger(ioutil.Discard) + return rClient +} + +func (c *APIClient) prepareRequest( + rClient *resty.Client, + postBody interface{}, + headerParams map[string]string, + queryParams url.Values, + formParams map[string]string, + fileName string, + fileBytes []byte) *resty.Request { + + + request := rClient.R() + request.SetBody(postBody) + + if c.config.UserAgent != "" { + request.SetHeader("User-Agent", c.config.UserAgent) + } + + // add header parameter, if any + if len(headerParams) > 0 { + request.SetHeaders(headerParams) + } + + // add query parameter, if any + if len(queryParams) > 0 { + request.SetMultiValueQueryParams(queryParams) + } + + // add form parameter, if any + if len(formParams) > 0 { + request.SetFormData(formParams) + } + + if len(fileBytes) > 0 && fileName != "" { + _, fileNm := filepath.Split(fileName) + request.SetFileReader("file", fileNm, bytes.NewReader(fileBytes)) + } + return request +} diff --git a/examples/clients/unannotatedecho/api_response.go b/examples/clients/unannotatedecho/api_response.go new file mode 100644 index 00000000000..8d7af71022e --- /dev/null +++ b/examples/clients/unannotatedecho/api_response.go @@ -0,0 +1,44 @@ +/* + * examples/proto/examplepb/unannotated_echo_service.proto + * + * Unannotated Echo Service Similar to echo_service.proto but without annotations. See unannotated_echo_service.yaml for the equivalent of the annotations in gRPC API configuration format. Echo Service API consists of a single service which returns a message. + * + * OpenAPI spec version: version not set + * + * Generated by: https://github.com/swagger-api/swagger-codegen.git + */ + +package unannotatedecho + +import ( + "net/http" +) + +type APIResponse struct { + *http.Response `json:"-"` + Message string `json:"message,omitempty"` + // Operation is the name of the swagger operation. + Operation string `json:"operation,omitempty"` + // RequestURL is the request URL. This value is always available, even if the + // embedded *http.Response is nil. + RequestURL string `json:"url,omitempty"` + // Method is the HTTP method used for the request. This value is always + // available, even if the embedded *http.Response is nil. + Method string `json:"method,omitempty"` + // Payload holds the contents of the response body (which may be nil or empty). + // This is provided here as the raw response.Body() reader will have already + // been drained. + Payload []byte `json:"-"` +} + +func NewAPIResponse(r *http.Response) *APIResponse { + + response := &APIResponse{Response: r} + return response +} + +func NewAPIResponseWithError(errorMessage string) *APIResponse { + + response := &APIResponse{Message: errorMessage} + return response +} diff --git a/examples/clients/unannotatedecho/configuration.go b/examples/clients/unannotatedecho/configuration.go new file mode 100644 index 00000000000..d8ed5f04501 --- /dev/null +++ b/examples/clients/unannotatedecho/configuration.go @@ -0,0 +1,67 @@ +/* + * examples/proto/examplepb/unannotated_echo_service.proto + * + * Unannotated Echo Service Similar to echo_service.proto but without annotations. See unannotated_echo_service.yaml for the equivalent of the annotations in gRPC API configuration format. Echo Service API consists of a single service which returns a message. + * + * OpenAPI spec version: version not set + * + * Generated by: https://github.com/swagger-api/swagger-codegen.git + */ + +package unannotatedecho + +import ( + "encoding/base64" + "net/http" + "time" +) + + +type Configuration struct { + Username string `json:"userName,omitempty"` + Password string `json:"password,omitempty"` + APIKeyPrefix map[string]string `json:"APIKeyPrefix,omitempty"` + APIKey map[string]string `json:"APIKey,omitempty"` + Debug bool `json:"debug,omitempty"` + DebugFile string `json:"debugFile,omitempty"` + OAuthToken string `json:"oAuthToken,omitempty"` + BasePath string `json:"basePath,omitempty"` + Host string `json:"host,omitempty"` + Scheme string `json:"scheme,omitempty"` + AccessToken string `json:"accessToken,omitempty"` + DefaultHeader map[string]string `json:"defaultHeader,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + APIClient *APIClient + Transport *http.Transport + Timeout *time.Duration `json:"timeout,omitempty"` +} + +func NewConfiguration() *Configuration { + cfg := &Configuration{ + BasePath: "http://localhost", + DefaultHeader: make(map[string]string), + APIKey: make(map[string]string), + APIKeyPrefix: make(map[string]string), + UserAgent: "Swagger-Codegen/1.0.0/go", + APIClient: &APIClient{}, + } + + cfg.APIClient.config = cfg + return cfg +} + +func (c *Configuration) GetBasicAuthEncodedString() string { + return base64.StdEncoding.EncodeToString([]byte(c.Username + ":" + c.Password)) +} + +func (c *Configuration) AddDefaultHeader(key string, value string) { + c.DefaultHeader[key] = value +} + +func (c *Configuration) GetAPIKeyWithPrefix(APIKeyIdentifier string) string { + if c.APIKeyPrefix[APIKeyIdentifier] != "" { + return c.APIKeyPrefix[APIKeyIdentifier] + " " + c.APIKey[APIKeyIdentifier] + } + + return c.APIKey[APIKeyIdentifier] +} diff --git a/examples/clients/unannotatedecho/examplepb_unannotated_simple_message.go b/examples/clients/unannotatedecho/examplepb_unannotated_simple_message.go new file mode 100644 index 00000000000..7791baa682b --- /dev/null +++ b/examples/clients/unannotatedecho/examplepb_unannotated_simple_message.go @@ -0,0 +1,22 @@ +/* + * examples/proto/examplepb/unannotated_echo_service.proto + * + * Unannotated Echo Service Similar to echo_service.proto but without annotations. See unannotated_echo_service.yaml for the equivalent of the annotations in gRPC API configuration format. Echo Service API consists of a single service which returns a message. + * + * OpenAPI spec version: version not set + * + * Generated by: https://github.com/swagger-api/swagger-codegen.git + */ + +package unannotatedecho + +// UnannotatedSimpleMessage represents a simple message sent to the unannotated Echo service. +type ExamplepbUnannotatedSimpleMessage struct { + + // Id represents the message identifier. + Id string `json:"id,omitempty"` + + Num string `json:"num,omitempty"` + + Duration string `json:"duration,omitempty"` +} diff --git a/examples/clients/unannotatedecho/unannotated_echo_service_api.go b/examples/clients/unannotatedecho/unannotated_echo_service_api.go new file mode 100644 index 00000000000..99f02f08d03 --- /dev/null +++ b/examples/clients/unannotatedecho/unannotated_echo_service_api.go @@ -0,0 +1,290 @@ +/* + * examples/proto/examplepb/unannotated_echo_service.proto + * + * Unannotated Echo Service Similar to echo_service.proto but without annotations. See unannotated_echo_service.yaml for the equivalent of the annotations in gRPC API configuration format. Echo Service API consists of a single service which returns a message. + * + * OpenAPI spec version: version not set + * + * Generated by: https://github.com/swagger-api/swagger-codegen.git + */ + +package unannotatedecho + +import ( + "net/url" + "strings" + "encoding/json" + "fmt" +) + +type UnannotatedEchoServiceApi struct { + Configuration *Configuration +} + +func NewUnannotatedEchoServiceApi() *UnannotatedEchoServiceApi { + configuration := NewConfiguration() + return &UnannotatedEchoServiceApi{ + Configuration: configuration, + } +} + +func NewUnannotatedEchoServiceApiWithBasePath(basePath string) *UnannotatedEchoServiceApi { + configuration := NewConfiguration() + configuration.BasePath = basePath + + return &UnannotatedEchoServiceApi{ + Configuration: configuration, + } +} + +/** + * Echo method receives a simple message and returns it. + * The message posted as the id parameter will also be returned. + * + * @param id + * @return *ExamplepbUnannotatedSimpleMessage + */ +func (a UnannotatedEchoServiceApi) Echo(id string) (*ExamplepbUnannotatedSimpleMessage, *APIResponse, error) { + + var localVarHttpMethod = strings.ToUpper("Post") + // create path and map variables + localVarPath := a.Configuration.BasePath + "/v1/example/echo/{id}" + localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", fmt.Sprintf("%v", id), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := make(map[string]string) + var localVarPostBody interface{} + var localVarFileName string + var localVarFileBytes []byte + // add default headers if any + for key := range a.Configuration.DefaultHeader { + localVarHeaderParams[key] = a.Configuration.DefaultHeader[key] + } + + // to determine the Content-Type header + localVarHttpContentTypes := []string{ "application/json", } + + // set Content-Type header + localVarHttpContentType := a.Configuration.APIClient.SelectHeaderContentType(localVarHttpContentTypes) + if localVarHttpContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHttpContentType + } + // to determine the Accept header + localVarHttpHeaderAccepts := []string{ + "application/json", + } + + // set Accept header + localVarHttpHeaderAccept := a.Configuration.APIClient.SelectHeaderAccept(localVarHttpHeaderAccepts) + if localVarHttpHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHttpHeaderAccept + } + var successPayload = new(ExamplepbUnannotatedSimpleMessage) + localVarHttpResponse, err := a.Configuration.APIClient.CallAPI(localVarPath, localVarHttpMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFileName, localVarFileBytes) + + var localVarURL, _ = url.Parse(localVarPath) + localVarURL.RawQuery = localVarQueryParams.Encode() + var localVarAPIResponse = &APIResponse{Operation: "Echo", Method: localVarHttpMethod, RequestURL: localVarURL.String()} + if localVarHttpResponse != nil { + localVarAPIResponse.Response = localVarHttpResponse.RawResponse + localVarAPIResponse.Payload = localVarHttpResponse.Body() + } + + if err != nil { + return successPayload, localVarAPIResponse, err + } + err = json.Unmarshal(localVarHttpResponse.Body(), &successPayload) + return successPayload, localVarAPIResponse, err +} + +/** + * Echo method receives a simple message and returns it. + * The message posted as the id parameter will also be returned. + * + * @param id + * @param num + * @param duration + * @return *ExamplepbUnannotatedSimpleMessage + */ +func (a UnannotatedEchoServiceApi) Echo2(id string, num string, duration string) (*ExamplepbUnannotatedSimpleMessage, *APIResponse, error) { + + var localVarHttpMethod = strings.ToUpper("Get") + // create path and map variables + localVarPath := a.Configuration.BasePath + "/v1/example/echo/{id}/{num}" + localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", fmt.Sprintf("%v", id), -1) + localVarPath = strings.Replace(localVarPath, "{"+"num"+"}", fmt.Sprintf("%v", num), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := make(map[string]string) + var localVarPostBody interface{} + var localVarFileName string + var localVarFileBytes []byte + // add default headers if any + for key := range a.Configuration.DefaultHeader { + localVarHeaderParams[key] = a.Configuration.DefaultHeader[key] + } + localVarQueryParams.Add("duration", a.Configuration.APIClient.ParameterToString(duration, "")) + + // to determine the Content-Type header + localVarHttpContentTypes := []string{ "application/json", } + + // set Content-Type header + localVarHttpContentType := a.Configuration.APIClient.SelectHeaderContentType(localVarHttpContentTypes) + if localVarHttpContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHttpContentType + } + // to determine the Accept header + localVarHttpHeaderAccepts := []string{ + "application/json", + } + + // set Accept header + localVarHttpHeaderAccept := a.Configuration.APIClient.SelectHeaderAccept(localVarHttpHeaderAccepts) + if localVarHttpHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHttpHeaderAccept + } + var successPayload = new(ExamplepbUnannotatedSimpleMessage) + localVarHttpResponse, err := a.Configuration.APIClient.CallAPI(localVarPath, localVarHttpMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFileName, localVarFileBytes) + + var localVarURL, _ = url.Parse(localVarPath) + localVarURL.RawQuery = localVarQueryParams.Encode() + var localVarAPIResponse = &APIResponse{Operation: "Echo2", Method: localVarHttpMethod, RequestURL: localVarURL.String()} + if localVarHttpResponse != nil { + localVarAPIResponse.Response = localVarHttpResponse.RawResponse + localVarAPIResponse.Payload = localVarHttpResponse.Body() + } + + if err != nil { + return successPayload, localVarAPIResponse, err + } + err = json.Unmarshal(localVarHttpResponse.Body(), &successPayload) + return successPayload, localVarAPIResponse, err +} + +/** + * EchoBody method receives a simple message and returns it. + * + * @param body + * @return *ExamplepbUnannotatedSimpleMessage + */ +func (a UnannotatedEchoServiceApi) EchoBody(body ExamplepbUnannotatedSimpleMessage) (*ExamplepbUnannotatedSimpleMessage, *APIResponse, error) { + + var localVarHttpMethod = strings.ToUpper("Post") + // create path and map variables + localVarPath := a.Configuration.BasePath + "/v1/example/echo_body" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := make(map[string]string) + var localVarPostBody interface{} + var localVarFileName string + var localVarFileBytes []byte + // add default headers if any + for key := range a.Configuration.DefaultHeader { + localVarHeaderParams[key] = a.Configuration.DefaultHeader[key] + } + + // to determine the Content-Type header + localVarHttpContentTypes := []string{ "application/json", } + + // set Content-Type header + localVarHttpContentType := a.Configuration.APIClient.SelectHeaderContentType(localVarHttpContentTypes) + if localVarHttpContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHttpContentType + } + // to determine the Accept header + localVarHttpHeaderAccepts := []string{ + "application/json", + } + + // set Accept header + localVarHttpHeaderAccept := a.Configuration.APIClient.SelectHeaderAccept(localVarHttpHeaderAccepts) + if localVarHttpHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHttpHeaderAccept + } + // body params + localVarPostBody = &body + var successPayload = new(ExamplepbUnannotatedSimpleMessage) + localVarHttpResponse, err := a.Configuration.APIClient.CallAPI(localVarPath, localVarHttpMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFileName, localVarFileBytes) + + var localVarURL, _ = url.Parse(localVarPath) + localVarURL.RawQuery = localVarQueryParams.Encode() + var localVarAPIResponse = &APIResponse{Operation: "EchoBody", Method: localVarHttpMethod, RequestURL: localVarURL.String()} + if localVarHttpResponse != nil { + localVarAPIResponse.Response = localVarHttpResponse.RawResponse + localVarAPIResponse.Payload = localVarHttpResponse.Body() + } + + if err != nil { + return successPayload, localVarAPIResponse, err + } + err = json.Unmarshal(localVarHttpResponse.Body(), &successPayload) + return successPayload, localVarAPIResponse, err +} + +/** + * EchoDelete method receives a simple message and returns it. + * + * @param id Id represents the message identifier. + * @param num + * @param duration + * @return *ExamplepbUnannotatedSimpleMessage + */ +func (a UnannotatedEchoServiceApi) EchoDelete(id string, num string, duration string) (*ExamplepbUnannotatedSimpleMessage, *APIResponse, error) { + + var localVarHttpMethod = strings.ToUpper("Delete") + // create path and map variables + localVarPath := a.Configuration.BasePath + "/v1/example/echo_delete" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := make(map[string]string) + var localVarPostBody interface{} + var localVarFileName string + var localVarFileBytes []byte + // add default headers if any + for key := range a.Configuration.DefaultHeader { + localVarHeaderParams[key] = a.Configuration.DefaultHeader[key] + } + localVarQueryParams.Add("id", a.Configuration.APIClient.ParameterToString(id, "")) + localVarQueryParams.Add("num", a.Configuration.APIClient.ParameterToString(num, "")) + localVarQueryParams.Add("duration", a.Configuration.APIClient.ParameterToString(duration, "")) + + // to determine the Content-Type header + localVarHttpContentTypes := []string{ "application/json", } + + // set Content-Type header + localVarHttpContentType := a.Configuration.APIClient.SelectHeaderContentType(localVarHttpContentTypes) + if localVarHttpContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHttpContentType + } + // to determine the Accept header + localVarHttpHeaderAccepts := []string{ + "application/json", + } + + // set Accept header + localVarHttpHeaderAccept := a.Configuration.APIClient.SelectHeaderAccept(localVarHttpHeaderAccepts) + if localVarHttpHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHttpHeaderAccept + } + var successPayload = new(ExamplepbUnannotatedSimpleMessage) + localVarHttpResponse, err := a.Configuration.APIClient.CallAPI(localVarPath, localVarHttpMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFileName, localVarFileBytes) + + var localVarURL, _ = url.Parse(localVarPath) + localVarURL.RawQuery = localVarQueryParams.Encode() + var localVarAPIResponse = &APIResponse{Operation: "EchoDelete", Method: localVarHttpMethod, RequestURL: localVarURL.String()} + if localVarHttpResponse != nil { + localVarAPIResponse.Response = localVarHttpResponse.RawResponse + localVarAPIResponse.Payload = localVarHttpResponse.Body() + } + + if err != nil { + return successPayload, localVarAPIResponse, err + } + err = json.Unmarshal(localVarHttpResponse.Body(), &successPayload) + return successPayload, localVarAPIResponse, err +} + diff --git a/examples/integration/BUILD.bazel b/examples/integration/BUILD.bazel index 37ef3bc6abc..c603d01e14f 100644 --- a/examples/integration/BUILD.bazel +++ b/examples/integration/BUILD.bazel @@ -11,6 +11,7 @@ go_test( deps = [ "//examples/clients/abe:go_default_library", "//examples/clients/echo:go_default_library", + "//examples/clients/unannotatedecho:go_default_library", "//examples/gateway:go_default_library", "//examples/proto/examplepb:go_default_library", "//examples/proto/sub:go_default_library", diff --git a/examples/integration/client_test.go b/examples/integration/client_test.go index 1e6b3b77ed3..b49a2f31fff 100644 --- a/examples/integration/client_test.go +++ b/examples/integration/client_test.go @@ -6,6 +6,7 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/examples/clients/abe" "github.com/grpc-ecosystem/grpc-gateway/examples/clients/echo" + "github.com/grpc-ecosystem/grpc-gateway/examples/clients/unannotatedecho" ) func TestClientIntegration(t *testing.T) { @@ -160,3 +161,36 @@ func testABEClientCreateBody(t *testing.T, cl *abe.ABitOfEverythingServiceApi) { t.Errorf("resp = %#v; want %#v", got, want) } } + +func TestUnannotatedEchoClient(t *testing.T) { + if testing.Short() { + t.Skip() + return + } + + cl := unannotatedecho.NewUnannotatedEchoServiceApiWithBasePath("http://localhost:8080") + resp, _, err := cl.Echo("foo") + if err != nil { + t.Errorf(`cl.Echo("foo") failed with %v; want success`, err) + } + if got, want := resp.Id, "foo"; got != want { + t.Errorf("resp.Id = %q; want %q", got, want) + } +} + +func TestUnannotatedEchoBodyClient(t *testing.T) { + if testing.Short() { + t.Skip() + return + } + + cl := unannotatedecho.NewUnannotatedEchoServiceApiWithBasePath("http://localhost:8080") + req := unannotatedecho.ExamplepbUnannotatedSimpleMessage{Id: "foo"} + resp, _, err := cl.EchoBody(req) + if err != nil { + t.Errorf("cl.EchoBody(%#v) failed with %v; want success", req, err) + } + if got, want := resp.Id, "foo"; got != want { + t.Errorf("resp.Id = %q; want %q", got, want) + } +} diff --git a/examples/proto/examplepb/BUILD.bazel b/examples/proto/examplepb/BUILD.bazel index 3b64196f240..066ccd467a9 100644 --- a/examples/proto/examplepb/BUILD.bazel +++ b/examples/proto/examplepb/BUILD.bazel @@ -17,6 +17,7 @@ proto_library( "echo_service.proto", "flow_combination.proto", "stream.proto", + "unannotated_echo_service.proto", "wrappers.proto", ], deps = [ diff --git a/examples/proto/examplepb/echo_service.pb.go b/examples/proto/examplepb/echo_service.pb.go index a5d16e85ff1..2e61c94ed01 100644 --- a/examples/proto/examplepb/echo_service.pb.go +++ b/examples/proto/examplepb/echo_service.pb.go @@ -15,6 +15,7 @@ It is generated from these files: examples/proto/examplepb/stream.proto examples/proto/examplepb/flow_combination.proto examples/proto/examplepb/wrappers.proto + examples/proto/examplepb/unannotated_echo_service.proto It has these top-level messages: SimpleMessage @@ -27,6 +28,7 @@ It has these top-level messages: NestedProto SingleNestedProto Wrappers + UnannotatedSimpleMessage */ package examplepb diff --git a/examples/proto/examplepb/unannotated_echo_service.pb.go b/examples/proto/examplepb/unannotated_echo_service.pb.go new file mode 100644 index 00000000000..4e3aebeba13 --- /dev/null +++ b/examples/proto/examplepb/unannotated_echo_service.pb.go @@ -0,0 +1,232 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: examples/proto/examplepb/unannotated_echo_service.proto + +package examplepb + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" +import google_protobuf2 "github.com/golang/protobuf/ptypes/duration" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// UnannotatedSimpleMessage represents a simple message sent to the unannotated Echo service. +type UnannotatedSimpleMessage struct { + // Id represents the message identifier. + Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` + Num int64 `protobuf:"varint,2,opt,name=num" json:"num,omitempty"` + Duration *google_protobuf2.Duration `protobuf:"bytes,3,opt,name=duration" json:"duration,omitempty"` +} + +func (m *UnannotatedSimpleMessage) Reset() { *m = UnannotatedSimpleMessage{} } +func (m *UnannotatedSimpleMessage) String() string { return proto.CompactTextString(m) } +func (*UnannotatedSimpleMessage) ProtoMessage() {} +func (*UnannotatedSimpleMessage) Descriptor() ([]byte, []int) { return fileDescriptor5, []int{0} } + +func (m *UnannotatedSimpleMessage) GetId() string { + if m != nil { + return m.Id + } + return "" +} + +func (m *UnannotatedSimpleMessage) GetNum() int64 { + if m != nil { + return m.Num + } + return 0 +} + +func (m *UnannotatedSimpleMessage) GetDuration() *google_protobuf2.Duration { + if m != nil { + return m.Duration + } + return nil +} + +func init() { + proto.RegisterType((*UnannotatedSimpleMessage)(nil), "grpc.gateway.examples.examplepb.UnannotatedSimpleMessage") +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// Client API for UnannotatedEchoService service + +type UnannotatedEchoServiceClient interface { + // Echo method receives a simple message and returns it. + // + // The message posted as the id parameter will also be + // returned. + Echo(ctx context.Context, in *UnannotatedSimpleMessage, opts ...grpc.CallOption) (*UnannotatedSimpleMessage, error) + // EchoBody method receives a simple message and returns it. + EchoBody(ctx context.Context, in *UnannotatedSimpleMessage, opts ...grpc.CallOption) (*UnannotatedSimpleMessage, error) + // EchoDelete method receives a simple message and returns it. + EchoDelete(ctx context.Context, in *UnannotatedSimpleMessage, opts ...grpc.CallOption) (*UnannotatedSimpleMessage, error) +} + +type unannotatedEchoServiceClient struct { + cc *grpc.ClientConn +} + +func NewUnannotatedEchoServiceClient(cc *grpc.ClientConn) UnannotatedEchoServiceClient { + return &unannotatedEchoServiceClient{cc} +} + +func (c *unannotatedEchoServiceClient) Echo(ctx context.Context, in *UnannotatedSimpleMessage, opts ...grpc.CallOption) (*UnannotatedSimpleMessage, error) { + out := new(UnannotatedSimpleMessage) + err := grpc.Invoke(ctx, "/grpc.gateway.examples.examplepb.UnannotatedEchoService/Echo", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *unannotatedEchoServiceClient) EchoBody(ctx context.Context, in *UnannotatedSimpleMessage, opts ...grpc.CallOption) (*UnannotatedSimpleMessage, error) { + out := new(UnannotatedSimpleMessage) + err := grpc.Invoke(ctx, "/grpc.gateway.examples.examplepb.UnannotatedEchoService/EchoBody", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *unannotatedEchoServiceClient) EchoDelete(ctx context.Context, in *UnannotatedSimpleMessage, opts ...grpc.CallOption) (*UnannotatedSimpleMessage, error) { + out := new(UnannotatedSimpleMessage) + err := grpc.Invoke(ctx, "/grpc.gateway.examples.examplepb.UnannotatedEchoService/EchoDelete", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for UnannotatedEchoService service + +type UnannotatedEchoServiceServer interface { + // Echo method receives a simple message and returns it. + // + // The message posted as the id parameter will also be + // returned. + Echo(context.Context, *UnannotatedSimpleMessage) (*UnannotatedSimpleMessage, error) + // EchoBody method receives a simple message and returns it. + EchoBody(context.Context, *UnannotatedSimpleMessage) (*UnannotatedSimpleMessage, error) + // EchoDelete method receives a simple message and returns it. + EchoDelete(context.Context, *UnannotatedSimpleMessage) (*UnannotatedSimpleMessage, error) +} + +func RegisterUnannotatedEchoServiceServer(s *grpc.Server, srv UnannotatedEchoServiceServer) { + s.RegisterService(&_UnannotatedEchoService_serviceDesc, srv) +} + +func _UnannotatedEchoService_Echo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnannotatedSimpleMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UnannotatedEchoServiceServer).Echo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/grpc.gateway.examples.examplepb.UnannotatedEchoService/Echo", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UnannotatedEchoServiceServer).Echo(ctx, req.(*UnannotatedSimpleMessage)) + } + return interceptor(ctx, in, info, handler) +} + +func _UnannotatedEchoService_EchoBody_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnannotatedSimpleMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UnannotatedEchoServiceServer).EchoBody(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/grpc.gateway.examples.examplepb.UnannotatedEchoService/EchoBody", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UnannotatedEchoServiceServer).EchoBody(ctx, req.(*UnannotatedSimpleMessage)) + } + return interceptor(ctx, in, info, handler) +} + +func _UnannotatedEchoService_EchoDelete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnannotatedSimpleMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UnannotatedEchoServiceServer).EchoDelete(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/grpc.gateway.examples.examplepb.UnannotatedEchoService/EchoDelete", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UnannotatedEchoServiceServer).EchoDelete(ctx, req.(*UnannotatedSimpleMessage)) + } + return interceptor(ctx, in, info, handler) +} + +var _UnannotatedEchoService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "grpc.gateway.examples.examplepb.UnannotatedEchoService", + HandlerType: (*UnannotatedEchoServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Echo", + Handler: _UnannotatedEchoService_Echo_Handler, + }, + { + MethodName: "EchoBody", + Handler: _UnannotatedEchoService_EchoBody_Handler, + }, + { + MethodName: "EchoDelete", + Handler: _UnannotatedEchoService_EchoDelete_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "examples/proto/examplepb/unannotated_echo_service.proto", +} + +func init() { + proto.RegisterFile("examples/proto/examplepb/unannotated_echo_service.proto", fileDescriptor5) +} + +var fileDescriptor5 = []byte{ + // 268 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x32, 0x4f, 0xad, 0x48, 0xcc, + 0x2d, 0xc8, 0x49, 0x2d, 0xd6, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0xd7, 0x87, 0x72, 0x0b, 0x92, 0xf4, + 0x4b, 0xf3, 0x12, 0xf3, 0xf2, 0xf2, 0x4b, 0x12, 0x4b, 0x52, 0x53, 0xe2, 0x53, 0x93, 0x33, 0xf2, + 0xe3, 0x8b, 0x53, 0x8b, 0xca, 0x32, 0x93, 0x53, 0xf5, 0xc0, 0x0a, 0x85, 0xe4, 0xd3, 0x8b, 0x0a, + 0x92, 0xf5, 0xd2, 0x13, 0x4b, 0x52, 0xcb, 0x13, 0x2b, 0xf5, 0x60, 0xa6, 0xe8, 0xc1, 0xf5, 0x4b, + 0xc9, 0xa5, 0xe7, 0xe7, 0xa7, 0xe7, 0xa4, 0x42, 0xcc, 0x4d, 0x2a, 0x4d, 0xd3, 0x4f, 0x29, 0x2d, + 0x4a, 0x2c, 0xc9, 0xcc, 0xcf, 0x83, 0x18, 0xa0, 0x54, 0xcc, 0x25, 0x11, 0x8a, 0xb0, 0x22, 0x38, + 0x13, 0xa4, 0xcd, 0x37, 0xb5, 0xb8, 0x38, 0x31, 0x3d, 0x55, 0x88, 0x8f, 0x8b, 0x29, 0x33, 0x45, + 0x82, 0x51, 0x81, 0x51, 0x83, 0x33, 0x88, 0x29, 0x33, 0x45, 0x48, 0x80, 0x8b, 0x39, 0xaf, 0x34, + 0x57, 0x82, 0x49, 0x81, 0x51, 0x83, 0x39, 0x08, 0xc4, 0x14, 0x32, 0xe5, 0xe2, 0x80, 0x99, 0x27, + 0xc1, 0xac, 0xc0, 0xa8, 0xc1, 0x6d, 0x24, 0xa9, 0x07, 0xb1, 0x50, 0x0f, 0x66, 0xa1, 0x9e, 0x0b, + 0x54, 0x41, 0x10, 0x5c, 0xa9, 0xd1, 0x3c, 0x66, 0x2e, 0x31, 0x24, 0x5b, 0x5d, 0x93, 0x33, 0xf2, + 0x83, 0x21, 0xde, 0x12, 0xaa, 0xe1, 0x62, 0x01, 0x71, 0x85, 0x2c, 0xf5, 0x08, 0xf8, 0x4c, 0x0f, + 0x97, 0xb3, 0xa5, 0xc8, 0xd7, 0x2a, 0xd4, 0xc0, 0xc8, 0xc5, 0x01, 0xb2, 0xde, 0x29, 0x3f, 0xa5, + 0x72, 0x80, 0x9c, 0xd0, 0xc4, 0xc8, 0xc5, 0x05, 0x72, 0x82, 0x4b, 0x6a, 0x4e, 0x6a, 0x49, 0xea, + 0xc0, 0x38, 0xc2, 0x89, 0x3b, 0x8a, 0x13, 0xae, 0x2a, 0x89, 0x0d, 0x1c, 0x95, 0xc6, 0x80, 0x00, + 0x00, 0x00, 0xff, 0xff, 0x9a, 0xa7, 0x7c, 0x41, 0xa5, 0x02, 0x00, 0x00, +} diff --git a/examples/proto/examplepb/unannotated_echo_service.pb.gw.go b/examples/proto/examplepb/unannotated_echo_service.pb.gw.go new file mode 100644 index 00000000000..65007db690a --- /dev/null +++ b/examples/proto/examplepb/unannotated_echo_service.pb.gw.go @@ -0,0 +1,317 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: examples/proto/examplepb/unannotated_echo_service.proto + +/* +Package examplepb is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package examplepb + +import ( + "io" + "net/http" + + "github.com/golang/protobuf/proto" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/grpc-ecosystem/grpc-gateway/utilities" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/status" +) + +var _ codes.Code +var _ io.Reader +var _ status.Status +var _ = runtime.String +var _ = utilities.NewDoubleArray + +var ( + filter_UnannotatedEchoService_Echo_0 = &utilities.DoubleArray{Encoding: map[string]int{"id": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} +) + +func request_UnannotatedEchoService_Echo_0(ctx context.Context, marshaler runtime.Marshaler, client UnannotatedEchoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UnannotatedSimpleMessage + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_UnannotatedEchoService_Echo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.Echo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +var ( + filter_UnannotatedEchoService_Echo_1 = &utilities.DoubleArray{Encoding: map[string]int{"id": 0, "num": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}} +) + +func request_UnannotatedEchoService_Echo_1(ctx context.Context, marshaler runtime.Marshaler, client UnannotatedEchoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UnannotatedSimpleMessage + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + val, ok = pathParams["num"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "num") + } + + protoReq.Num, err = runtime.Int64(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "num", err) + } + + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_UnannotatedEchoService_Echo_1); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.Echo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func request_UnannotatedEchoService_EchoBody_0(ctx context.Context, marshaler runtime.Marshaler, client UnannotatedEchoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UnannotatedSimpleMessage + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.EchoBody(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +var ( + filter_UnannotatedEchoService_EchoDelete_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_UnannotatedEchoService_EchoDelete_0(ctx context.Context, marshaler runtime.Marshaler, client UnannotatedEchoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq UnannotatedSimpleMessage + var metadata runtime.ServerMetadata + + if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_UnannotatedEchoService_EchoDelete_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.EchoDelete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +// RegisterUnannotatedEchoServiceHandlerFromEndpoint is same as RegisterUnannotatedEchoServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterUnannotatedEchoServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.Dial(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterUnannotatedEchoServiceHandler(ctx, mux, conn) +} + +// RegisterUnannotatedEchoServiceHandler registers the http handlers for service UnannotatedEchoService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterUnannotatedEchoServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterUnannotatedEchoServiceHandlerClient(ctx, mux, NewUnannotatedEchoServiceClient(conn)) +} + +// RegisterUnannotatedEchoServiceHandler registers the http handlers for service UnannotatedEchoService to "mux". +// The handlers forward requests to the grpc endpoint over the given implementation of "UnannotatedEchoServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "UnannotatedEchoServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "UnannotatedEchoServiceClient" to call the correct interceptors. +func RegisterUnannotatedEchoServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client UnannotatedEchoServiceClient) error { + + mux.Handle("POST", pattern_UnannotatedEchoService_Echo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + if cn, ok := w.(http.CloseNotifier); ok { + go func(done <-chan struct{}, closed <-chan bool) { + select { + case <-done: + case <-closed: + cancel() + } + }(ctx.Done(), cn.CloseNotify()) + } + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UnannotatedEchoService_Echo_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_UnannotatedEchoService_Echo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_UnannotatedEchoService_Echo_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + if cn, ok := w.(http.CloseNotifier); ok { + go func(done <-chan struct{}, closed <-chan bool) { + select { + case <-done: + case <-closed: + cancel() + } + }(ctx.Done(), cn.CloseNotify()) + } + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UnannotatedEchoService_Echo_1(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_UnannotatedEchoService_Echo_1(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_UnannotatedEchoService_EchoBody_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + if cn, ok := w.(http.CloseNotifier); ok { + go func(done <-chan struct{}, closed <-chan bool) { + select { + case <-done: + case <-closed: + cancel() + } + }(ctx.Done(), cn.CloseNotify()) + } + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UnannotatedEchoService_EchoBody_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_UnannotatedEchoService_EchoBody_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("DELETE", pattern_UnannotatedEchoService_EchoDelete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + if cn, ok := w.(http.CloseNotifier); ok { + go func(done <-chan struct{}, closed <-chan bool) { + select { + case <-done: + case <-closed: + cancel() + } + }(ctx.Done(), cn.CloseNotify()) + } + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UnannotatedEchoService_EchoDelete_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_UnannotatedEchoService_EchoDelete_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_UnannotatedEchoService_Echo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "example", "echo", "id"}, "")) + + pattern_UnannotatedEchoService_Echo_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 1, 0, 4, 1, 5, 4}, []string{"v1", "example", "echo", "id", "num"}, "")) + + pattern_UnannotatedEchoService_EchoBody_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "example", "echo_body"}, "")) + + pattern_UnannotatedEchoService_EchoDelete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "example", "echo_delete"}, "")) +) + +var ( + forward_UnannotatedEchoService_Echo_0 = runtime.ForwardResponseMessage + + forward_UnannotatedEchoService_Echo_1 = runtime.ForwardResponseMessage + + forward_UnannotatedEchoService_EchoBody_0 = runtime.ForwardResponseMessage + + forward_UnannotatedEchoService_EchoDelete_0 = runtime.ForwardResponseMessage +) diff --git a/examples/proto/examplepb/unannotated_echo_service.proto b/examples/proto/examplepb/unannotated_echo_service.proto new file mode 100644 index 00000000000..9187f78e2de --- /dev/null +++ b/examples/proto/examplepb/unannotated_echo_service.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; +option go_package = "examplepb"; + +// Unannotated Echo Service +// Similar to echo_service.proto but without annotations. See +// unannotated_echo_service.yaml for the equivalent of the annotations in +// gRPC API configuration format. +// +// Echo Service API consists of a single service which returns +// a message. +package grpc.gateway.examples.examplepb; + +// Do not need annotations.proto, can still use well known types as usual +import "google/protobuf/duration.proto"; + +// UnannotatedSimpleMessage represents a simple message sent to the unannotated Echo service. +message UnannotatedSimpleMessage { + // Id represents the message identifier. + string id = 1; + int64 num = 2; + google.protobuf.Duration duration = 3; +} + +// Echo service responds to incoming echo requests. +service UnannotatedEchoService { + // Echo method receives a simple message and returns it. + // + // The message posted as the id parameter will also be + // returned. + rpc Echo(UnannotatedSimpleMessage) returns (UnannotatedSimpleMessage); + + // EchoBody method receives a simple message and returns it. + rpc EchoBody(UnannotatedSimpleMessage) returns (UnannotatedSimpleMessage); + + // EchoDelete method receives a simple message and returns it. + rpc EchoDelete(UnannotatedSimpleMessage) returns (UnannotatedSimpleMessage); +} diff --git a/examples/proto/examplepb/unannotated_echo_service.swagger.json b/examples/proto/examplepb/unannotated_echo_service.swagger.json new file mode 100644 index 00000000000..9bfa9f28ba6 --- /dev/null +++ b/examples/proto/examplepb/unannotated_echo_service.swagger.json @@ -0,0 +1,170 @@ +{ + "swagger": "2.0", + "info": { + "title": "examples/proto/examplepb/unannotated_echo_service.proto", + "description": "Unannotated Echo Service\nSimilar to echo_service.proto but without annotations. See\nunannotated_echo_service.yaml for the equivalent of the annotations in\ngRPC API configuration format.\n\nEcho Service API consists of a single service which returns\na message.", + "version": "version not set" + }, + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/v1/example/echo/{id}": { + "post": { + "summary": "Echo method receives a simple message and returns it.", + "description": "The message posted as the id parameter will also be\nreturned.", + "operationId": "Echo", + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/examplepbUnannotatedSimpleMessage" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "UnannotatedEchoService" + ] + } + }, + "/v1/example/echo/{id}/{num}": { + "get": { + "summary": "Echo method receives a simple message and returns it.", + "description": "The message posted as the id parameter will also be\nreturned.", + "operationId": "Echo2", + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/examplepbUnannotatedSimpleMessage" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "num", + "in": "path", + "required": true, + "type": "string", + "format": "int64" + }, + { + "name": "duration", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "UnannotatedEchoService" + ] + } + }, + "/v1/example/echo_body": { + "post": { + "summary": "EchoBody method receives a simple message and returns it.", + "operationId": "EchoBody", + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/examplepbUnannotatedSimpleMessage" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/examplepbUnannotatedSimpleMessage" + } + } + ], + "tags": [ + "UnannotatedEchoService" + ] + } + }, + "/v1/example/echo_delete": { + "delete": { + "summary": "EchoDelete method receives a simple message and returns it.", + "operationId": "EchoDelete", + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/examplepbUnannotatedSimpleMessage" + } + } + }, + "parameters": [ + { + "name": "id", + "description": "Id represents the message identifier.", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "num", + "in": "query", + "required": false, + "type": "string", + "format": "int64" + }, + { + "name": "duration", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "UnannotatedEchoService" + ] + } + } + }, + "definitions": { + "examplepbUnannotatedSimpleMessage": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Id represents the message identifier." + }, + "num": { + "type": "string", + "format": "int64" + }, + "duration": { + "type": "string" + } + }, + "description": "UnannotatedSimpleMessage represents a simple message sent to the unannotated Echo service." + } + } +} diff --git a/examples/proto/examplepb/unannotated_echo_service.yaml b/examples/proto/examplepb/unannotated_echo_service.yaml new file mode 100644 index 00000000000..cd3a978d897 --- /dev/null +++ b/examples/proto/examplepb/unannotated_echo_service.yaml @@ -0,0 +1,15 @@ +type: google.api.Service +config_version: 3 + +http: + rules: + - selector: grpc.gateway.examples.examplepb.UnannotatedEchoService.Echo + post: "/v1/example/echo/{id}" + additional_bindings: + - get: "/v1/example/echo/{id}/{num}" + - selector: grpc.gateway.examples.examplepb.UnannotatedEchoService.EchoBody + post: "/v1/example/echo_body" + body: "*" + - selector: grpc.gateway.examples.examplepb.UnannotatedEchoService.EchoDelete + delete: "/v1/example/echo_delete" + diff --git a/examples/server/BUILD.bazel b/examples/server/BUILD.bazel index 6d924fe47f4..61fff8ab6ec 100644 --- a/examples/server/BUILD.bazel +++ b/examples/server/BUILD.bazel @@ -9,6 +9,7 @@ go_library( "echo.go", "flow_combination.go", "main.go", + "unannotatedecho.go", ], importpath = "github.com/grpc-ecosystem/grpc-gateway/examples/server", deps = [ diff --git a/examples/server/unannotatedecho.go b/examples/server/unannotatedecho.go new file mode 100644 index 00000000000..bf3bff0dfc1 --- /dev/null +++ b/examples/server/unannotatedecho.go @@ -0,0 +1,40 @@ +package server + +import ( + "context" + "github.com/golang/glog" + examples "github.com/grpc-ecosystem/grpc-gateway/examples/proto/examplepb" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +// Implements of UnannotatedEchoServiceServer + +type unannotatedEchoServer struct{} + +func newUnannotatedEchoServer() examples.UnannotatedEchoServiceServer { + return new(unannotatedEchoServer) +} + +func (s *unannotatedEchoServer) Echo(ctx context.Context, msg *examples.UnannotatedSimpleMessage) (*examples.UnannotatedSimpleMessage, error) { + glog.Info(msg) + return msg, nil +} + +func (s *unannotatedEchoServer) EchoBody(ctx context.Context, msg *examples.UnannotatedSimpleMessage) (*examples.UnannotatedSimpleMessage, error) { + glog.Info(msg) + grpc.SendHeader(ctx, metadata.New(map[string]string{ + "foo": "foo1", + "bar": "bar1", + })) + grpc.SetTrailer(ctx, metadata.New(map[string]string{ + "foo": "foo2", + "bar": "bar2", + })) + return msg, nil +} + +func (s *unannotatedEchoServer) EchoDelete(ctx context.Context, msg *examples.UnannotatedSimpleMessage) (*examples.UnannotatedSimpleMessage, error) { + glog.Info(msg) + return msg, nil +} diff --git a/protoc-gen-grpc-gateway/descriptor/BUILD.bazel b/protoc-gen-grpc-gateway/descriptor/BUILD.bazel index 7c68fa88582..93670c52729 100644 --- a/protoc-gen-grpc-gateway/descriptor/BUILD.bazel +++ b/protoc-gen-grpc-gateway/descriptor/BUILD.bazel @@ -5,6 +5,8 @@ package(default_visibility = ["//:generators"]) go_library( name = "go_default_library", srcs = [ + "grpc_api_configuration.go", + "grpc_api_service.go", "registry.go", "services.go", "types.go", @@ -12,7 +14,9 @@ go_library( importpath = "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway/descriptor", deps = [ "//protoc-gen-grpc-gateway/httprule:go_default_library", + "@com_github_ghodss_yaml//:go_default_library", "@com_github_golang_glog//:go_default_library", + "@com_github_golang_protobuf//jsonpb:go_default_library", "@com_github_golang_protobuf//proto:go_default_library", "@com_github_golang_protobuf//protoc-gen-go/descriptor:go_default_library", "@com_github_golang_protobuf//protoc-gen-go/generator:go_default_library", @@ -25,6 +29,7 @@ go_test( name = "go_default_test", size = "small", srcs = [ + "grpc_api_configuration_test.go", "registry_test.go", "services_test.go", "types_test.go", diff --git a/protoc-gen-grpc-gateway/descriptor/grpc_api_configuration.go b/protoc-gen-grpc-gateway/descriptor/grpc_api_configuration.go new file mode 100644 index 00000000000..ca68ed74db1 --- /dev/null +++ b/protoc-gen-grpc-gateway/descriptor/grpc_api_configuration.go @@ -0,0 +1,71 @@ +package descriptor + +import ( + "bytes" + "fmt" + "io/ioutil" + "strings" + + "github.com/ghodss/yaml" + "github.com/golang/protobuf/jsonpb" +) + +func loadGrpcAPIServiceFromYAML(yamlFileContents []byte, yamlSourceLogName string) (*GrpcAPIService, error) { + jsonContents, err := yaml.YAMLToJSON(yamlFileContents) + if err != nil { + return nil, fmt.Errorf("Failed to convert gRPC API Configuration from YAML in '%v' to JSON: %v", yamlSourceLogName, err) + } + + // As our GrpcAPIService is incomplete accept unkown fields. + unmarshaler := jsonpb.Unmarshaler{ + AllowUnknownFields: true, + } + + serviceConfiguration := GrpcAPIService{} + if err := unmarshaler.Unmarshal(bytes.NewReader(jsonContents), &serviceConfiguration); err != nil { + return nil, fmt.Errorf("Failed to parse gRPC API Configuration from YAML in '%v': %v", yamlSourceLogName, err) + } + + return &serviceConfiguration, nil +} + +func registerHTTPRulesFromGrpcAPIService(registry *Registry, service *GrpcAPIService, sourceLogName string) error { + if service.HTTP == nil { + // Nothing to do + return nil + } + + for _, rule := range service.HTTP.GetRules() { + selector := "." + strings.Trim(rule.GetSelector(), " ") + if strings.ContainsAny(selector, "*, ") { + return fmt.Errorf("Selector '%v' in %v must specify a single service method without wildcards", rule.GetSelector(), sourceLogName) + } + + registry.AddExternalHTTPRule(selector, rule) + } + + return nil +} + +// LoadGrpcAPIServiceFromYAML loads a gRPC API Configuration from the given YAML file +// and registers the HttpRule descriptions contained in it as externalHTTPRules in +// the given registry. This must be done before loading the proto file. +// +// You can learn more about gRPC API Service descriptions from google's documentation +// at https://cloud.google.com/endpoints/docs/grpc/grpc-service-config +// +// Note that for the purposes of the gateway generator we only consider a subset of all +// available features google supports in their service descriptions. +func (r *Registry) LoadGrpcAPIServiceFromYAML(yamlFile string) error { + yamlFileContents, err := ioutil.ReadFile(yamlFile) + if err != nil { + return fmt.Errorf("Failed to read gRPC API Configuration description from '%v': %v", yamlFile, err) + } + + service, err := loadGrpcAPIServiceFromYAML(yamlFileContents, yamlFile) + if err != nil { + return err + } + + return registerHTTPRulesFromGrpcAPIService(r, service, yamlFile) +} diff --git a/protoc-gen-grpc-gateway/descriptor/grpc_api_configuration_test.go b/protoc-gen-grpc-gateway/descriptor/grpc_api_configuration_test.go new file mode 100644 index 00000000000..cc6cb441202 --- /dev/null +++ b/protoc-gen-grpc-gateway/descriptor/grpc_api_configuration_test.go @@ -0,0 +1,164 @@ +package descriptor + +import ( + "strings" + "testing" +) + +func TestLoadGrpcAPIServiceFromYAMLEmpty(t *testing.T) { + service, err := loadGrpcAPIServiceFromYAML([]byte(``), "empty") + if err != nil { + t.Fatal(err) + } + + if service == nil { + t.Fatal("No service returned") + } + + if service.HTTP != nil { + t.Fatal("HTTP not empty") + } +} + +func TestLoadGrpcAPIServiceFromYAMLInvalidType(t *testing.T) { + // Ideally this would fail but for now this test documents that it doesn't + service, err := loadGrpcAPIServiceFromYAML([]byte(`type: not.the.right.type`), "invalidtype") + if err != nil { + t.Fatal(err) + } + + if service == nil { + t.Fatal("No service returned") + } +} + +func TestLoadGrpcAPIServiceFromYAMLSingleRule(t *testing.T) { + service, err := loadGrpcAPIServiceFromYAML([]byte(` +type: google.api.Service +config_version: 3 + +http: + rules: + - selector: grpctest.YourService.Echo + post: /v1/myecho + body: "*" +`), "example") + if err != nil { + t.Fatal(err) + } + + if service.HTTP == nil { + t.Fatal("HTTP is empty") + } + + if len(service.HTTP.GetRules()) != 1 { + t.Fatalf("Have %v rules instead of one. Got: %v", len(service.HTTP.GetRules()), service.HTTP.GetRules()) + } + + rule := service.HTTP.GetRules()[0] + if rule.GetSelector() != "grpctest.YourService.Echo" { + t.Errorf("Rule has unexpected selector '%v'", rule.GetSelector()) + } + if rule.GetPost() != "/v1/myecho" { + t.Errorf("Rule has unexpected post '%v'", rule.GetPost()) + } + if rule.GetBody() != "*" { + t.Errorf("Rule has unexpected body '%v'", rule.GetBody()) + } +} + +func TestLoadGrpcAPIServiceFromYAMLRejectInvalidYAML(t *testing.T) { + service, err := loadGrpcAPIServiceFromYAML([]byte(` +type: google.api.Service +config_version: 3 + +http: + rules: + - selector: grpctest.YourService.Echo + - post: thislinebreakstheselectorblockabovewiththeleadingdash + body: "*" +`), "invalidyaml") + if err == nil { + t.Fatal(err) + } + + if !strings.Contains(err.Error(), "line 7") { + t.Errorf("Expected yaml error to be detected in line 7. Got other error: %v", err) + } + + if service != nil { + t.Fatal("Service returned") + } +} + +func TestLoadGrpcAPIServiceFromYAMLMultipleWithAdditionalBindings(t *testing.T) { + service, err := loadGrpcAPIServiceFromYAML([]byte(` +type: google.api.Service +config_version: 3 + +http: + rules: + - selector: first.selector + post: /my/post/path + body: "*" + additional_bindings: + - post: /additional/post/path + - put: /additional/put/{value}/path + - delete: "{value}" + - patch: "/additional/patch/{value}" + - selector: some.other.service + delete: foo +`), "example") + if err != nil { + t.Fatalf("Failed to load service description from YAML: %v", err) + } + + if service == nil { + t.Fatal("No service returned") + } + + if service.HTTP == nil { + t.Fatal("HTTP is empty") + } + + if len(service.HTTP.GetRules()) != 2 { + t.Fatalf("%v service(s) returned when two were expected. Got: %v", len(service.HTTP.GetRules()), service.HTTP) + } + + first := service.HTTP.GetRules()[0] + if first.GetSelector() != "first.selector" { + t.Errorf("first.selector has unexpected selector '%v'", first.GetSelector()) + } + if first.GetBody() != "*" { + t.Errorf("first.selector has unexpected body '%v'", first.GetBody()) + } + if first.GetPost() != "/my/post/path" { + t.Errorf("first.selector has unexpected post '%v'", first.GetPost()) + } + if len(first.GetAdditionalBindings()) != 4 { + t.Fatalf("first.selector has unexpected number of bindings %v instead of four. Got: %v", len(first.GetAdditionalBindings()), first.GetAdditionalBindings()) + } + if first.GetAdditionalBindings()[0].GetPost() != "/additional/post/path" { + t.Errorf("first.selector additional binding 0 has unexpected post '%v'", first.GetAdditionalBindings()[0].GetPost()) + } + if first.GetAdditionalBindings()[1].GetPut() != "/additional/put/{value}/path" { + t.Errorf("first.selector additional binding 1 has unexpected put '%v'", first.GetAdditionalBindings()[0].GetPost()) + } + if first.GetAdditionalBindings()[2].GetDelete() != "{value}" { + t.Errorf("first.selector additional binding 2 has unexpected delete '%v'", first.GetAdditionalBindings()[0].GetPost()) + } + if first.GetAdditionalBindings()[3].GetPatch() != "/additional/patch/{value}" { + t.Errorf("first.selector additional binding 3 has unexpected patch '%v'", first.GetAdditionalBindings()[0].GetPost()) + } + + second := service.HTTP.GetRules()[1] + if second.GetSelector() != "some.other.service" { + t.Errorf("some.other.service has unexpected selector '%v'", second.GetSelector()) + } + if second.GetDelete() != "foo" { + t.Errorf("some.other.service has unexpected delete '%v'", second.GetDelete()) + } + if len(second.GetAdditionalBindings()) != 0 { + t.Errorf("some.other.service has %v additional bindings when it should not have any. Got: %v", len(second.GetAdditionalBindings()), second.GetAdditionalBindings()) + } +} diff --git a/protoc-gen-grpc-gateway/descriptor/grpc_api_service.go b/protoc-gen-grpc-gateway/descriptor/grpc_api_service.go new file mode 100644 index 00000000000..75b824057dd --- /dev/null +++ b/protoc-gen-grpc-gateway/descriptor/grpc_api_service.go @@ -0,0 +1,31 @@ +package descriptor + +import ( + "github.com/golang/protobuf/proto" + "google.golang.org/genproto/googleapis/api/annotations" +) + +// GrpcAPIService represents a stripped down version of google.api.Service . +// Compare to https://github.com/googleapis/googleapis/blob/master/google/api/service.proto +// The original imports 23 other protobuf files we are not interested in. If a significant +// subset (>50%) of these start being reproduced in this file we should swap to using the +// full generated version instead. +// +// For the purposes of the gateway generator we only consider a small subset of all +// available features google supports in their service descriptions. Thanks to backwards +// compatibility guarantees by protobuf it is safe for us to remove the other fields. +// We also only implement the absolute minimum of protobuf generator boilerplate to use +// our simplified version. These should be pretty stable too. +type GrpcAPIService struct { + // Http Rule. Named Http in the actual proto. Changed to suppress linter warning. + HTTP *annotations.Http `protobuf:"bytes,9,opt,name=http" json:"http,omitempty"` +} + +// ProtoMessage returns an empty GrpcAPIService element +func (*GrpcAPIService) ProtoMessage() {} + +// Reset resets the GrpcAPIService +func (m *GrpcAPIService) Reset() { *m = GrpcAPIService{} } + +// String returns the string representation of the GrpcAPIService +func (m *GrpcAPIService) String() string { return proto.CompactTextString(m) } diff --git a/protoc-gen-grpc-gateway/descriptor/registry.go b/protoc-gen-grpc-gateway/descriptor/registry.go index 16df86b4436..2596816e4dc 100644 --- a/protoc-gen-grpc-gateway/descriptor/registry.go +++ b/protoc-gen-grpc-gateway/descriptor/registry.go @@ -9,6 +9,7 @@ import ( "github.com/golang/glog" descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" plugin "github.com/golang/protobuf/protoc-gen-go/plugin" + "google.golang.org/genproto/googleapis/api/annotations" ) // Registry is a registry of information extracted from plugin.CodeGeneratorRequest. @@ -36,16 +37,20 @@ type Registry struct { // allowDeleteBody permits http delete methods to have a body allowDeleteBody bool + + // externalHttpRules is a mapping from fully qualified service method names to additional HttpRules applicable besides the ones found in annotations. + externalHTTPRules map[string][]*annotations.HttpRule } // NewRegistry returns a new Registry. func NewRegistry() *Registry { return &Registry{ - msgs: make(map[string]*Message), - enums: make(map[string]*Enum), - files: make(map[string]*File), - pkgMap: make(map[string]string), - pkgAliases: make(map[string]string), + msgs: make(map[string]*Message), + enums: make(map[string]*Enum), + files: make(map[string]*File), + pkgMap: make(map[string]string), + pkgAliases: make(map[string]string), + externalHTTPRules: make(map[string][]*annotations.HttpRule), } } @@ -205,6 +210,16 @@ func (r *Registry) LookupFile(name string) (*File, error) { return f, nil } +// LookupExternalHTTPRules looks up external http rules by fully qualified service method name +func (r *Registry) LookupExternalHTTPRules(qualifiedMethodName string) []*annotations.HttpRule { + return r.externalHTTPRules[qualifiedMethodName] +} + +// AddExternalHTTPRule adds an external http rule for the given fully qualified service method name +func (r *Registry) AddExternalHTTPRule(qualifiedMethodName string, rule *annotations.HttpRule) { + r.externalHTTPRules[qualifiedMethodName] = append(r.externalHTTPRules[qualifiedMethodName], rule) +} + // AddPkgMap adds a mapping from a .proto file to proto package name. func (r *Registry) AddPkgMap(file, protoPkg string) { r.pkgMap[file] = protoPkg diff --git a/protoc-gen-grpc-gateway/descriptor/services.go b/protoc-gen-grpc-gateway/descriptor/services.go index d3020ff28f4..c200e57e275 100644 --- a/protoc-gen-grpc-gateway/descriptor/services.go +++ b/protoc-gen-grpc-gateway/descriptor/services.go @@ -30,10 +30,14 @@ func (r *Registry) loadServices(file *File) error { glog.Errorf("Failed to extract HttpRule from %s.%s: %v", svc.GetName(), md.GetName(), err) return err } - if opts == nil { + optsList := r.LookupExternalHTTPRules((&Method{Service: svc, MethodDescriptorProto: md}).FQMN()) + if opts != nil { + optsList = append(optsList, opts) + } + if len(optsList) == 0 { glog.V(1).Infof("Found non-target method: %s.%s", svc.GetName(), md.GetName()) } - meth, err := r.newMethod(svc, md, opts) + meth, err := r.newMethod(svc, md, optsList) if err != nil { return err } @@ -49,7 +53,7 @@ func (r *Registry) loadServices(file *File) error { return nil } -func (r *Registry) newMethod(svc *Service, md *descriptor.MethodDescriptorProto, opts *options.HttpRule) (*Method, error) { +func (r *Registry) newMethod(svc *Service, md *descriptor.MethodDescriptorProto, optsList []*options.HttpRule) (*Method, error) { requestType, err := r.LookupMsg(svc.File.GetPackage(), md.GetInputType()) if err != nil { return nil, err @@ -141,23 +145,34 @@ func (r *Registry) newMethod(svc *Service, md *descriptor.MethodDescriptorProto, return b, nil } - b, err := newBinding(opts, 0) - if err != nil { - return nil, err - } - if b != nil { - meth.Bindings = append(meth.Bindings, b) - } - for i, additional := range opts.GetAdditionalBindings() { - if len(additional.AdditionalBindings) > 0 { - return nil, fmt.Errorf("additional_binding in additional_binding not allowed: %s.%s", svc.GetName(), meth.GetName()) - } - b, err := newBinding(additional, i+1) + applyOpts := func(opts *options.HttpRule) error { + b, err := newBinding(opts, len(meth.Bindings)) if err != nil { + return err + } + + if b != nil { + meth.Bindings = append(meth.Bindings, b) + } + for _, additional := range opts.GetAdditionalBindings() { + if len(additional.AdditionalBindings) > 0 { + return fmt.Errorf("additional_binding in additional_binding not allowed: %s.%s", svc.GetName(), meth.GetName()) + } + b, err := newBinding(additional, len(meth.Bindings)) + if err != nil { + return err + } + meth.Bindings = append(meth.Bindings, b) + } + + return nil + } + + for _, opts := range optsList { + if err := applyOpts(opts); err != nil { return nil, err } - meth.Bindings = append(meth.Bindings, b) } return meth, nil diff --git a/protoc-gen-grpc-gateway/descriptor/types.go b/protoc-gen-grpc-gateway/descriptor/types.go index bfdb2dc81d9..a84b85947ae 100644 --- a/protoc-gen-grpc-gateway/descriptor/types.go +++ b/protoc-gen-grpc-gateway/descriptor/types.go @@ -130,6 +130,16 @@ type Service struct { Methods []*Method } +// FQSN returns the fully qualified service name of this service. +func (s *Service) FQSN() string { + components := []string{""} + if s.File.Package != nil { + components = append(components, s.File.GetPackage()) + } + components = append(components, s.GetName()) + return strings.Join(components, ".") +} + // Method wraps descriptor.MethodDescriptorProto for richer features. type Method struct { // Service is the service which this method belongs to. @@ -143,6 +153,14 @@ type Method struct { Bindings []*Binding } +// FQMN returns a fully qualified rpc method name of this method. +func (m *Method) FQMN() string { + components := []string{} + components = append(components, m.Service.FQSN()) + components = append(components, m.GetName()) + return strings.Join(components, ".") +} + // Binding describes how an HTTP endpoint is bound to a gRPC method. type Binding struct { // Method is the method which the endpoint is bound to. diff --git a/protoc-gen-grpc-gateway/gengateway/generator.go b/protoc-gen-grpc-gateway/gengateway/generator.go index cb2f5e14a9f..2bd8ee41569 100644 --- a/protoc-gen-grpc-gateway/gengateway/generator.go +++ b/protoc-gen-grpc-gateway/gengateway/generator.go @@ -13,7 +13,6 @@ import ( plugin "github.com/golang/protobuf/protoc-gen-go/plugin" "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway/descriptor" gen "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway/generator" - options "google.golang.org/genproto/googleapis/api/annotations" ) var ( @@ -103,7 +102,7 @@ func (g *generator) generate(file *descriptor.File) (string, error) { for _, svc := range file.Services { for _, m := range svc.Methods { pkg := m.RequestType.File.GoPkg - if m.Options == nil || !proto.HasExtension(m.Options, options.E_Http) || + if len(m.Bindings) == 0 || pkg == file.GoPkg || pkgSeen[pkg.Path] { continue } diff --git a/protoc-gen-grpc-gateway/main.go b/protoc-gen-grpc-gateway/main.go index d4569d2079b..3037d68b8e9 100644 --- a/protoc-gen-grpc-gateway/main.go +++ b/protoc-gen-grpc-gateway/main.go @@ -22,10 +22,11 @@ import ( ) var ( - importPrefix = flag.String("import_prefix", "", "prefix to be added to go package paths for imported proto files") - importPath = flag.String("import_path", "", "used as the package if no input files declare go_package. If it contains slashes, everything up to the rightmost slash is ignored.") - useRequestContext = flag.Bool("request_context", true, "determine whether to use http.Request's context or not") - allowDeleteBody = flag.Bool("allow_delete_body", false, "unless set, HTTP DELETE methods may not have a body") + importPrefix = flag.String("import_prefix", "", "prefix to be added to go package paths for imported proto files") + importPath = flag.String("import_path", "", "used as the package if no input files declare go_package. If it contains slashes, everything up to the rightmost slash is ignored.") + useRequestContext = flag.Bool("request_context", true, "determine whether to use http.Request's context or not") + allowDeleteBody = flag.Bool("allow_delete_body", false, "unless set, HTTP DELETE methods may not have a body") + grpcAPIConfiguration = flag.String("grpc_api_configuration", "", "path to gRPC API Configuration in YAML format") ) func main() { @@ -62,6 +63,13 @@ func main() { g := gengateway.New(reg, *useRequestContext) + if *grpcAPIConfiguration != "" { + if err := reg.LoadGrpcAPIServiceFromYAML(*grpcAPIConfiguration); err != nil { + emitError(err) + return + } + } + reg.SetPrefix(*importPrefix) reg.SetImportPath(*importPath) reg.SetAllowDeleteBody(*allowDeleteBody) diff --git a/protoc-gen-swagger/main.go b/protoc-gen-swagger/main.go index 3d7f1ab7580..ad1dd8b06ae 100644 --- a/protoc-gen-swagger/main.go +++ b/protoc-gen-swagger/main.go @@ -15,9 +15,10 @@ import ( ) var ( - importPrefix = flag.String("import_prefix", "", "prefix to be added to go package paths for imported proto files") + importPrefix = flag.String("import_prefix", "", "prefix to be added to go package paths for imported proto files") file = flag.String("file", "-", "where to load data from") - allowDeleteBody = flag.Bool("allow_delete_body", false, "unless set, HTTP DELETE methods may not have a body") + allowDeleteBody = flag.Bool("allow_delete_body", false, "unless set, HTTP DELETE methods may not have a body") + grpcAPIConfiguration = flag.String("grpc_api_configuration", "", "path to gRPC API Configuration in YAML format") ) func main() { @@ -33,7 +34,7 @@ func main() { f, err = os.Open(*file) if err != nil { glog.Fatal(err) - } + } } glog.V(1).Info("Parsing code generator request") req, err := codegenerator.ParseRequest(f) @@ -54,6 +55,14 @@ func main() { for k, v := range pkgMap { reg.AddPkgMap(k, v) } + + if *grpcAPIConfiguration != "" { + if err := reg.LoadGrpcAPIServiceFromYAML(*grpcAPIConfiguration); err != nil { + emitError(err) + return + } + } + g := genswagger.New(reg) if err := reg.Load(req); err != nil {