From ea9f6ac2507a6fdc964c1c5a8e27dd4f7ae8d062 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Thu, 23 Apr 2015 18:53:19 +0900 Subject: [PATCH 01/28] Import a definition of a custom option from github.com/google/googleapis This option will replace our custom option --- third_party/googleapis/LICENSE | 201 ++++++++++++++++++ third_party/googleapis/README.grcp-gateway | 14 ++ .../googleapis/google/api/annotations.proto | 29 +++ third_party/googleapis/google/api/http.proto | 127 +++++++++++ 4 files changed, 371 insertions(+) create mode 100644 third_party/googleapis/LICENSE create mode 100644 third_party/googleapis/README.grcp-gateway create mode 100644 third_party/googleapis/google/api/annotations.proto create mode 100644 third_party/googleapis/google/api/http.proto diff --git a/third_party/googleapis/LICENSE b/third_party/googleapis/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/third_party/googleapis/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third_party/googleapis/README.grcp-gateway b/third_party/googleapis/README.grcp-gateway new file mode 100644 index 00000000000..f716dfdc374 --- /dev/null +++ b/third_party/googleapis/README.grcp-gateway @@ -0,0 +1,14 @@ +Google APIs +============ + +Project: Google APIs +URL: https://github.com/google/googleapis +Revision: a9fb190cdb78ed9bb2d6bb3fb5b9ef46effa5df3 +License: Apache License 2.0 + + +Imported Files +--------------- + +- google/api/annotations.proto +- google/api/http.proto diff --git a/third_party/googleapis/google/api/annotations.proto b/third_party/googleapis/google/api/annotations.proto new file mode 100644 index 00000000000..cbd18b847f3 --- /dev/null +++ b/third_party/googleapis/google/api/annotations.proto @@ -0,0 +1,29 @@ +// Copyright (c) 2015, Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/third_party/googleapis/google/api/http.proto b/third_party/googleapis/google/api/http.proto new file mode 100644 index 00000000000..ce07aa14f54 --- /dev/null +++ b/third_party/googleapis/google/api/http.proto @@ -0,0 +1,127 @@ +// Copyright (c) 2015, Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; + + +// `HttpRule` defines the mapping of an RPC method to one or more HTTP REST API +// methods. The mapping determines what portions of the request message are +// populated from the path, query parameters, or body of the HTTP request. The +// mapping is typically specified as an `google.api.http` annotation, see +// "google/api/annotations.proto" for details. +// +// The mapping consists of a mandatory field specifying a path template and an +// optional `body` field specifying what data is represented in the HTTP request +// body. The field name for the path indicates the HTTP method. Example: +// +// ``` +// package google.storage.v2; +// +// import "google/api/annotations.proto"; +// +// service Storage { +// rpc CreateObject(CreateObjectRequest) returns (Object) { +// option (google.api.http) { +// post: "/v2/{bucket_name=buckets/*}/objects" +// body: "object" +// }; +// }; +// } +// ``` +// +// Here `bucket_name` and `object` bind to fields of the request message +// `CreateObjectRequest`. +// +// The rules for mapping HTTP path, query parameters, and body fields +// to the request message are as follows: +// +// 1. The `body` field specifies either `*` or a field path, or is +// omitted. If omitted, it assumes there is no HTTP body. +// 2. Leaf fields (recursive expansion of nested messages in the +// request) can be classified into three types: +// (a) Matched in the URL template. +// (b) Covered by body (if body is `*`, everything except (a) fields; +// else everything under the body field) +// (c) All other fields. +// 3. URL query parameters found in the HTTP request are mapped to (c) fields. +// 4. Any body sent with an HTTP request can contain only (b) fields. +// +// The syntax of the path template is as follows: +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// `*` matches a single path component, `**` zero or more path components, and +// `LITERAL` a constant. A `Variable` can match an entire path as specified +// again by a template; this nested template must not contain further variables. +// If no template is given with a variable, it matches a single path component. +// The notation `{var}` is henceforth equivalent to `{var=*}`. +// +// Use CustomHttpPattern to specify any HTTP method that is not included in the +// pattern field, such as HEAD, or "*" to leave the HTTP method unspecified for +// a given URL path rule. The wild-card rule is useful for services that provide +// content to Web (HTML) clients. +message HttpRule { + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Used for listing and getting information about resources. + string get = 2; + + // Used for updating a resource. + string put = 3; + + // Used for creating a resource. + string post = 4; + + // Used for deleting a resource. + string delete = 5; + + // Used for updating a resource. + string patch = 6; + + // Custom pattern is used for defining custom verbs. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP body, or + // `*` for mapping all fields not captured by the path pattern to the HTTP + // body. + string body = 7; + + // Additional HTTP bindings for the selector. Nested bindings must not + // specify a selector and must not contain additional bindings. + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} From 9bf40a9c03157efc59ef875bd661fd46f38b2861 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Thu, 23 Apr 2015 19:05:24 +0900 Subject: [PATCH 02/28] Generate Go files from the new annotation .proto files. --- Makefile | 21 ++- third_party/googleapis/README.grcp-gateway | 8 ++ .../googleapis/google/api/annotations.pb.go | 32 +++++ third_party/googleapis/google/api/http.pb.go | 126 ++++++++++++++++++ 4 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 third_party/googleapis/google/api/annotations.pb.go create mode 100644 third_party/googleapis/google/api/http.pb.go diff --git a/Makefile b/Makefile index 934f23b6229..799fab51649 100644 --- a/Makefile +++ b/Makefile @@ -5,8 +5,13 @@ GATEWAY_PLUGIN=bin/protoc-gen-grpc-gateway GATEWAY_PLUGIN_PKG=$(PKG)/protoc-gen-grpc-gateway GATEWAY_PLUGIN_SRC=protoc-gen-grpc-gateway/main.go \ protoc-gen-grpc-gateway/generator.go -OPTIONS_GO=options/options.pb.go -OPTIONS_PROTO=options/options.proto + +OLD_OPTIONS_PROTO=options/options.proto +OLD_OPTIONS_GO=$(OLD_OPTIONS_PROTO:.proto=.pb.go) +GOOGLEAPIS_DIR=third_party/googleapis +OPTIONS_PROTO=$(GOOGLEAPIS_DIR)/google/api/annotations.proto $(GOOGLEAPIS_DIR)/google/api/http.proto +OPTIONS_GO=$(OPTIONS_PROTO:.proto=.pb.go) + PKGMAP=Mgoogle/protobuf/descriptor.proto=$(GO_PLUGIN_PKG)/descriptor,Mexamples/sub/message.proto=$(PKG)/examples/sub EXAMPLES=examples/echo_service.proto \ examples/a_bit_of_everything.proto @@ -16,7 +21,7 @@ EXAMPLE_DEPS=examples/sub/message.proto EXAMPLE_DEPSRCS=$(EXAMPLE_DEPS:.proto=.pb.go) PROTOC_INC_PATH=$(dir $(shell which protoc))/../include -generate: $(OPTIONS_GO) +generate: $(OPTIONS_GO) $(OLD_OPTIONS_GO) .SUFFIXES: .go .proto @@ -24,10 +29,13 @@ $(GO_PLUGIN): go get $(GO_PLUGIN_PKG) go build -o $@ $(GO_PLUGIN_PKG) +$(OLD_OPTIONS_GO): $(OLD_OPTIONS_PROTO) $(GO_PLUGIN) + protoc -I $(PROTOC_INC_PATH) -I. --plugin=$(GO_PLUGIN) --go_out=$(PKGMAP):. $(OLD_OPTIONS_PROTO) + $(OPTIONS_GO): $(OPTIONS_PROTO) $(GO_PLUGIN) - protoc -I $(PROTOC_INC_PATH) -I. --plugin=$(GO_PLUGIN) --go_out=$(PKGMAP):. $(OPTIONS_PROTO) + protoc -I $(PROTOC_INC_PATH) -I$(GOOGLEAPIS_DIR) --plugin=$(GO_PLUGIN) --go_out=$(PKGMAP):$(GOOGLEAPIS_DIR) $(OPTIONS_PROTO) -$(GATEWAY_PLUGIN): $(OPTIONS_GO) $(GATEWAY_PLUGIN_SRC) +$(GATEWAY_PLUGIN): $(OLD_OPTIONS_GO) $(GATEWAY_PLUGIN_SRC) go build -o $@ $(GATEWAY_PLUGIN_PKG) $(EXAMPLE_SVCSRCS): $(GO_PLUGIN) $(EXAMPLES) @@ -40,7 +48,8 @@ $(EXAMPLE_GWSRCS): $(GATEWAY_PLUGIN) $(EXAMPLES) test: $(EXAMPLE_SVCSRCS) $(EXAMPLE_GWSRCS) $(EXAMPLE_DEPSRCS) go test $(PKG)/... +clean distclean: realclean: - rm -f $(OPTIONS_GO) + rm -f $(OLD_OPTIONS_GO) $(OPTIONS_GO) rm -f $(EXAMPLE_SVCSRCS) $(EXAMPLE_DEPSRCS) rm -f $(EXAMPLE_GWSRCS) diff --git a/third_party/googleapis/README.grcp-gateway b/third_party/googleapis/README.grcp-gateway index f716dfdc374..cd723e52079 100644 --- a/third_party/googleapis/README.grcp-gateway +++ b/third_party/googleapis/README.grcp-gateway @@ -12,3 +12,11 @@ Imported Files - google/api/annotations.proto - google/api/http.proto + + +Generated Files +---------------- + +They are generated from the .proto files by protoc-gen-go. +- google/api/annotations.pb.go +- google/api/http.pb.go diff --git a/third_party/googleapis/google/api/annotations.pb.go b/third_party/googleapis/google/api/annotations.pb.go new file mode 100644 index 00000000000..8a54dd65928 --- /dev/null +++ b/third_party/googleapis/google/api/annotations.pb.go @@ -0,0 +1,32 @@ +// Code generated by protoc-gen-go. +// source: google/api/annotations.proto +// DO NOT EDIT! + +/* +Package google_api is a generated protocol buffer package. + +It is generated from these files: + google/api/annotations.proto + google/api/http.proto + +It has these top-level messages: +*/ +package google_api + +import proto "github.com/golang/protobuf/proto" +import google_protobuf "github.com/golang/protobuf/protoc-gen-go/descriptor" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal + +var E_Http = &proto.ExtensionDesc{ + ExtendedType: (*google_protobuf.MethodOptions)(nil), + ExtensionType: (*HttpRule)(nil), + Field: 72295728, + Name: "google.api.http", + Tag: "bytes,72295728,opt,name=http", +} + +func init() { + proto.RegisterExtension(E_Http) +} diff --git a/third_party/googleapis/google/api/http.pb.go b/third_party/googleapis/google/api/http.pb.go new file mode 100644 index 00000000000..27e77746807 --- /dev/null +++ b/third_party/googleapis/google/api/http.pb.go @@ -0,0 +1,126 @@ +// Code generated by protoc-gen-go. +// source: google/api/http.proto +// DO NOT EDIT! + +package google_api + +import proto "github.com/golang/protobuf/proto" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal + +// `HttpRule` defines the mapping of an RPC method to one or more HTTP REST API +// methods. The mapping determines what portions of the request message are +// populated from the path, query parameters, or body of the HTTP request. The +// mapping is typically specified as an `google.api.http` annotation, see +// "google/api/annotations.proto" for details. +// +// The mapping consists of a mandatory field specifying a path template and an +// optional `body` field specifying what data is represented in the HTTP request +// body. The field name for the path indicates the HTTP method. Example: +// +// ``` +// package google.storage.v2; +// +// import "google/api/annotations.proto"; +// +// service Storage { +// rpc CreateObject(CreateObjectRequest) returns (Object) { +// option (google.api.http) { +// post: "/v2/{bucket_name=buckets/*}/objects" +// body: "object" +// }; +// }; +// } +// ``` +// +// Here `bucket_name` and `object` bind to fields of the request message +// `CreateObjectRequest`. +// +// The rules for mapping HTTP path, query parameters, and body fields +// to the request message are as follows: +// +// 1. The `body` field specifies either `*` or a field path, or is +// omitted. If omitted, it assumes there is no HTTP body. +// 2. Leaf fields (recursive expansion of nested messages in the +// request) can be classified into three types: +// (a) Matched in the URL template. +// (b) Covered by body (if body is `*`, everything except (a) fields; +// else everything under the body field) +// (c) All other fields. +// 3. URL query parameters found in the HTTP request are mapped to (c) fields. +// 4. Any body sent with an HTTP request can contain only (b) fields. +// +// The syntax of the path template is as follows: +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// `*` matches a single path component, `**` zero or more path components, and +// `LITERAL` a constant. A `Variable` can match an entire path as specified +// again by a template; this nested template must not contain further variables. +// If no template is given with a variable, it matches a single path component. +// The notation `{var}` is henceforth equivalent to `{var=*}`. +// +// Use CustomHttpPattern to specify any HTTP method that is not included in the +// pattern field, such as HEAD, or "*" to leave the HTTP method unspecified for +// a given URL path rule. The wild-card rule is useful for services that provide +// content to Web (HTML) clients. +type HttpRule struct { + // Used for listing and getting information about resources. + Get string `protobuf:"bytes,2,opt,name=get" json:"get,omitempty"` + // Used for updating a resource. + Put string `protobuf:"bytes,3,opt,name=put" json:"put,omitempty"` + // Used for creating a resource. + Post string `protobuf:"bytes,4,opt,name=post" json:"post,omitempty"` + // Used for deleting a resource. + Delete string `protobuf:"bytes,5,opt,name=delete" json:"delete,omitempty"` + // Used for updating a resource. + Patch string `protobuf:"bytes,6,opt,name=patch" json:"patch,omitempty"` + // Custom pattern is used for defining custom verbs. + Custom *CustomHttpPattern `protobuf:"bytes,8,opt,name=custom" json:"custom,omitempty"` + // The name of the request field whose value is mapped to the HTTP body, or + // `*` for mapping all fields not captured by the path pattern to the HTTP + // body. + Body string `protobuf:"bytes,7,opt,name=body" json:"body,omitempty"` + // Additional HTTP bindings for the selector. Nested bindings must not + // specify a selector and must not contain additional bindings. + AdditionalBindings []*HttpRule `protobuf:"bytes,11,rep,name=additional_bindings" json:"additional_bindings,omitempty"` +} + +func (m *HttpRule) Reset() { *m = HttpRule{} } +func (m *HttpRule) String() string { return proto.CompactTextString(m) } +func (*HttpRule) ProtoMessage() {} + +func (m *HttpRule) GetCustom() *CustomHttpPattern { + if m != nil { + return m.Custom + } + return nil +} + +func (m *HttpRule) GetAdditionalBindings() []*HttpRule { + if m != nil { + return m.AdditionalBindings + } + return nil +} + +// A custom pattern is used for defining custom HTTP verb. +type CustomHttpPattern struct { + // The name of this custom HTTP verb. + Kind string `protobuf:"bytes,1,opt,name=kind" json:"kind,omitempty"` + // The path matched by this custom verb. + Path string `protobuf:"bytes,2,opt,name=path" json:"path,omitempty"` +} + +func (m *CustomHttpPattern) Reset() { *m = CustomHttpPattern{} } +func (m *CustomHttpPattern) String() string { return proto.CompactTextString(m) } +func (*CustomHttpPattern) ProtoMessage() {} + +func init() { +} From d08615bd99ceaab0cc7f5d0fa1f539e621b8d2bc Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Fri, 24 Apr 2015 22:32:00 +0900 Subject: [PATCH 03/28] Implement runtime pattern matcher --- runtime/pattern.go | 178 ++++++++++++++++++ runtime/pattern_test.go | 386 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 564 insertions(+) create mode 100644 runtime/pattern.go create mode 100644 runtime/pattern_test.go diff --git a/runtime/pattern.go b/runtime/pattern.go new file mode 100644 index 00000000000..cdde6275235 --- /dev/null +++ b/runtime/pattern.go @@ -0,0 +1,178 @@ +package runtime + +import ( + "errors" + "strings" + + "github.com/golang/glog" +) + +var ( + // ErrNotMatch indicates that the given HTTP request path does not match to the pattern. + ErrNotMatch = errors.New("not match to the path pattern") + // ErrInvalidPattern indicates that the given definition of Pattern is not valid. + ErrInvalidPattern = errors.New("invalid pattern") +) + +type opcode int + +// These constants are the valid values of opcode. +const ( + // opNop does nothing + opNop = opcode(iota) + // opPush pushes a component to stack + opPush + // opLitPush pushes a component to stack if it matches to the literal + opLitPush + // opPushM concatenates the remaining components and pushes it to stack + opPushM + // opPopN pops a N items from stack, concatenates them and pushes it to stack + opConcatN + // opCapture pops an item and binds it to the variable + opCapture + // opEnd is the least postive invalid opcode. + opEnd +) + +type op struct { + code opcode + operand int +} + +// Pattern is a template pattern of http request paths defined in third_party/googleapis/google/api/http.proto. +type Pattern struct { + // ops is a list of operations + ops []op + // pool is a constant pool indexed by the operands or vars. + pool []string + // vars is a list of variables names to be bound by this pattern + vars []string + // stacksize is the max depth of the stack + stacksize int +} + +// NewPattern returns a new Pattern from the given definition values. +// "ops" is a sequence of op codes. "pool" is a constant pool. +// "version" must be 1 for now. +// It returns an error if the given definition is invalid. +func NewPattern(version int, ops []int, pool []string) (Pattern, error) { + if version != 1 { + glog.V(2).Infof("unsupported version: %d", version) + return Pattern{}, ErrInvalidPattern + } + + l := len(ops) + if l%2 != 0 { + glog.V(2).Infof("odd number of ops codes: %d", l) + return Pattern{}, ErrInvalidPattern + } + + var typedOps []op + var stack, maxstack int + var vars []string + for i := 0; i < l; i += 2 { + op := op{code: opcode(ops[i]), operand: ops[i+1]} + switch op.code { + case opNop: + continue + case opPush, opPushM: + stack++ + case opLitPush: + if op.operand < 0 || len(pool) <= op.operand { + glog.V(2).Infof("negative literal index: %d", op.operand) + return Pattern{}, ErrInvalidPattern + } + stack++ + case opConcatN: + if op.operand <= 0 { + glog.V(2).Infof("negative concat size: %d", op.operand) + return Pattern{}, ErrInvalidPattern + } + stack -= op.operand + if stack < 0 { + glog.V(2).Info("stack underflow") + return Pattern{}, ErrInvalidPattern + } + stack++ + case opCapture: + if op.operand < 0 || len(pool) <= op.operand { + glog.V(2).Infof("variable name index out of bound: %d", op.operand) + return Pattern{}, ErrInvalidPattern + } + v := pool[op.operand] + op.operand = len(vars) + vars = append(vars, v) + stack-- + if stack < 0 { + glog.V(2).Info("stack underflow") + return Pattern{}, ErrInvalidPattern + } + default: + glog.V(2).Infof("invalid opcode: %d", op.code) + return Pattern{}, ErrInvalidPattern + } + + if maxstack < stack { + maxstack = stack + } + typedOps = append(typedOps, op) + } + glog.V(3).Info("pattern successfully built") + return Pattern{ + ops: typedOps, + pool: pool, + vars: vars, + stacksize: maxstack, + }, nil +} + +// Match examines components if it matches to the Pattern. +// If it matches, the function returns a mapping from field paths to their captured values. +// If otherwise, the function returns an error. +func (p Pattern) Match(components []string) (map[string]string, error) { + glog.V(2).Infof("matching %q to %v", components, p) + var pos int + stack := make([]string, 0, p.stacksize) + captured := make([]string, len(p.vars)) + l := len(components) + for _, op := range p.ops { + switch op.code { + case opNop: + continue + case opPush, opLitPush: + if pos >= l { + glog.V(1).Infof("insufficient # of segments") + return nil, ErrNotMatch + } + c := components[pos] + if op.code == opLitPush { + if lit := p.pool[op.operand]; c != lit { + glog.V(1).Infof("literal segment mismatch: got %q; want %q", c, lit) + return nil, ErrNotMatch + } + } + stack = append(stack, c) + pos++ + case opPushM: + stack = append(stack, strings.Join(components[pos:], "/")) + pos = len(components) + case opConcatN: + n := op.operand + l := len(stack) - n + stack = append(stack[:l], strings.Join(stack[l:], "/")) + case opCapture: + n := len(stack) - 1 + captured[op.operand] = stack[n] + stack = stack[:n] + } + } + if pos < l { + glog.V(1).Infof("remaining segments: %q", components[pos:]) + return nil, ErrNotMatch + } + bindings := make(map[string]string) + for i, val := range captured { + bindings[p.vars[i]] = val + } + return bindings, nil +} diff --git a/runtime/pattern_test.go b/runtime/pattern_test.go new file mode 100644 index 00000000000..396e6354172 --- /dev/null +++ b/runtime/pattern_test.go @@ -0,0 +1,386 @@ +package runtime + +import ( + "reflect" + "strings" + "testing" +) + +const ( + validVersion = 1 + anything = 0 +) + +func TestNewPattern(t *testing.T) { + for _, spec := range []struct { + ops []int + pool []string + + stackSizeWant int + }{ + {}, + { + ops: []int{int(opNop), anything}, + stackSizeWant: 0, + }, + { + ops: []int{int(opPush), anything}, + stackSizeWant: 1, + }, + { + ops: []int{int(opLitPush), 0}, + pool: []string{"abc"}, + stackSizeWant: 1, + }, + { + ops: []int{int(opPushM), anything}, + stackSizeWant: 1, + }, + { + ops: []int{ + int(opPush), anything, + int(opConcatN), 1, + }, + stackSizeWant: 1, + }, + { + ops: []int{ + int(opPush), anything, + int(opConcatN), 1, + int(opCapture), 0, + }, + pool: []string{"abc"}, + stackSizeWant: 1, + }, + { + ops: []int{ + int(opPush), anything, + int(opLitPush), 0, + int(opLitPush), 1, + int(opPushM), anything, + int(opConcatN), 2, + int(opCapture), 2, + }, + pool: []string{"lit1", "lit2", "var1"}, + stackSizeWant: 4, + }, + } { + pat, err := NewPattern(validVersion, spec.ops, spec.pool) + if err != nil { + t.Errorf("NewPattern(%d, %v, %q) failed with %v; want success", validVersion, spec.ops, spec.pool, err) + continue + } + if got, want := pat.stacksize, spec.stackSizeWant; got != want { + t.Errorf("pat.stacksize = %d; want %d", got, want) + } + } +} + +func TestNewPatternWithWrongOp(t *testing.T) { + for _, spec := range []struct { + ops []int + pool []string + }{ + { + // op code out of bound + ops: []int{-1, anything}, + }, + { + // op code out of bound + ops: []int{int(opEnd), 0}, + }, + { + // odd number of items + ops: []int{int(opPush)}, + }, + { + // negative index + ops: []int{int(opLitPush), -1}, + pool: []string{"abc"}, + }, + { + // index out of bound + ops: []int{int(opLitPush), 1}, + pool: []string{"abc"}, + }, + { + // negative # of segments + ops: []int{int(opConcatN), -1}, + pool: []string{"abc"}, + }, + { + // negative index + ops: []int{int(opCapture), -1}, + pool: []string{"abc"}, + }, + { + // index out of bound + ops: []int{int(opCapture), 1}, + pool: []string{"abc"}, + }, + } { + _, err := NewPattern(validVersion, spec.ops, spec.pool) + if err == nil { + t.Errorf("NewPattern(%d, %v, %q) succeeded; want failure with %v", validVersion, spec.ops, spec.pool, ErrInvalidPattern) + continue + } + if err != ErrInvalidPattern { + t.Errorf("NewPattern(%d, %v, %q) failed with %v; want failure with %v", validVersion, spec.ops, spec.pool, err, ErrInvalidPattern) + continue + } + } +} + +func TestNewPatternWithStackUnderflow(t *testing.T) { + for _, spec := range []struct { + ops []int + pool []string + }{ + { + ops: []int{int(opConcatN), 1}, + }, + { + ops: []int{int(opCapture), 0}, + pool: []string{"abc"}, + }, + } { + _, err := NewPattern(validVersion, spec.ops, spec.pool) + if err == nil { + t.Errorf("NewPattern(%d, %v, %q) succeeded; want failure with %v", validVersion, spec.ops, spec.pool, ErrInvalidPattern) + continue + } + if err != ErrInvalidPattern { + t.Errorf("NewPattern(%d, %v, %q) failed with %v; want failure with %v", validVersion, spec.ops, spec.pool, err, ErrInvalidPattern) + continue + } + } +} + +func segments(path string) []string { + if path == "" { + return nil + } + return strings.Split(path, "/") +} + +func TestMatch(t *testing.T) { + for _, spec := range []struct { + ops []int + pool []string + + match []string + notMatch []string + }{ + { + match: []string{""}, + notMatch: []string{"example"}, + }, + { + ops: []int{int(opNop), anything}, + match: []string{""}, + notMatch: []string{"example", "path/to/example"}, + }, + { + ops: []int{int(opPush), anything}, + match: []string{"abc", "def"}, + notMatch: []string{"", "abc/def"}, + }, + { + ops: []int{int(opLitPush), 0}, + pool: []string{"v1"}, + match: []string{"v1"}, + notMatch: []string{"", "v2"}, + }, + { + ops: []int{int(opPushM), anything}, + match: []string{"", "abc", "abc/def", "abc/def/ghi"}, + }, + { + ops: []int{ + int(opLitPush), 0, + int(opLitPush), 1, + int(opPush), anything, + int(opConcatN), 1, + int(opCapture), 2, + }, + pool: []string{"v1", "bucket", "name"}, + match: []string{"v1/bucket/my-bucket", "v1/bucket/our-bucket"}, + notMatch: []string{ + "", + "v1", + "v1/bucket", + "v2/bucket/my-bucket", + "v1/pubsub/my-topic", + }, + }, + { + ops: []int{ + int(opLitPush), 0, + int(opLitPush), 1, + int(opPushM), anything, + int(opConcatN), 2, + int(opCapture), 2, + }, + pool: []string{"v1", "o", "name"}, + match: []string{ + "v1/o", + "v1/o/my-bucket", + "v1/o/our-bucket", + "v1/o/my-bucket/dir", + "v1/o/my-bucket/dir/dir2", + "v1/o/my-bucket/dir/dir2/obj", + }, + notMatch: []string{ + "", + "v1", + "v2/o/my-bucket", + "v1/b/my-bucket", + }, + }, + { + ops: []int{ + int(opLitPush), 0, + int(opLitPush), 1, + int(opPush), anything, + int(opConcatN), 2, + int(opCapture), 2, + int(opLitPush), 3, + int(opPush), anything, + int(opConcatN), 1, + int(opCapture), 4, + }, + pool: []string{"v2", "b", "name", "o", "oname"}, + match: []string{ + "v2/b/my-bucket/o/obj", + "v2/b/our-bucket/o/obj", + "v2/b/my-bucket/o/dir", + }, + notMatch: []string{ + "", + "v2", + "v2/b", + "v2/b/my-bucket", + "v2/b/my-bucket/o", + }, + }, + } { + pat, err := NewPattern(validVersion, spec.ops, spec.pool) + if err != nil { + t.Errorf("NewPattern(%d, %v, %q) failed with %v; want success", validVersion, spec.ops, spec.pool, err) + continue + } + + for _, path := range spec.match { + _, err = pat.Match(segments(path)) + if err != nil { + t.Errorf("pat.Match(%q) failed with %v; want success; pattern = (%v, %q)", path, err, spec.ops, spec.pool) + } + } + + for _, path := range spec.notMatch { + _, err = pat.Match(segments(path)) + if err == nil { + t.Errorf("pat.Match(%q) succeeded; want failure with %v; pattern = (%v, %q)", path, ErrNotMatch, spec.ops, spec.pool) + continue + } + if err != ErrNotMatch { + t.Errorf("pat.Match(%q) failed with %v; want failure with %v; pattern = (%v, %q)", spec.notMatch, err, ErrNotMatch, spec.ops, spec.pool) + } + } + } +} + +func TestMatchWithBinding(t *testing.T) { + for _, spec := range []struct { + ops []int + pool []string + path string + + want map[string]string + }{ + { + want: make(map[string]string), + }, + { + ops: []int{int(opNop), anything}, + want: make(map[string]string), + }, + { + ops: []int{int(opPush), anything}, + path: "abc", + want: make(map[string]string), + }, + { + ops: []int{int(opLitPush), 0}, + pool: []string{"endpoint"}, + path: "endpoint", + want: make(map[string]string), + }, + { + ops: []int{int(opPushM), anything}, + path: "abc/def/ghi", + want: make(map[string]string), + }, + { + ops: []int{ + int(opLitPush), 0, + int(opLitPush), 1, + int(opPush), anything, + int(opConcatN), 1, + int(opCapture), 2, + }, + pool: []string{"v1", "bucket", "name"}, + path: "v1/bucket/my-bucket", + want: map[string]string{ + "name": "my-bucket", + }, + }, + { + ops: []int{ + int(opLitPush), 0, + int(opLitPush), 1, + int(opPushM), anything, + int(opConcatN), 2, + int(opCapture), 2, + }, + pool: []string{"v1", "o", "name"}, + path: "v1/o/my-bucket/dir/dir2/obj", + want: map[string]string{ + "name": "o/my-bucket/dir/dir2/obj", + }, + }, + { + ops: []int{ + int(opLitPush), 0, + int(opLitPush), 1, + int(opPush), anything, + int(opConcatN), 2, + int(opCapture), 2, + int(opLitPush), 3, + int(opPush), anything, + int(opConcatN), 1, + int(opCapture), 4, + }, + pool: []string{"v2", "b", "name", "o", "oname"}, + path: "v2/b/my-bucket/o/obj", + want: map[string]string{ + "name": "b/my-bucket", + "oname": "obj", + }, + }, + } { + pat, err := NewPattern(validVersion, spec.ops, spec.pool) + if err != nil { + t.Errorf("NewPattern(%d, %v, %q) failed with %v; want success", validVersion, spec.ops, spec.pool, err) + continue + } + + got, err := pat.Match(segments(spec.path)) + if err != nil { + t.Errorf("pat.Match(%q) failed with %v; want success; pattern = (%v, %q)", spec.path, err, spec.ops, spec.pool) + } + if !reflect.DeepEqual(got, spec.want) { + t.Errorf("pat.Match(%q) = %q; want %q; pattern = (%v, %q)", spec.path, got, spec.want, spec.ops, spec.pool) + } + } +} From ae7afe3c9d4e6cdaea12f184a00d75a998869b42 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Sat, 25 Apr 2015 08:58:20 +0900 Subject: [PATCH 04/28] Support verb part of the template pattern --- runtime/pattern.go | 18 +++++++-- runtime/pattern_test.go | 83 +++++++++++++++++++++++++++++++---------- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/runtime/pattern.go b/runtime/pattern.go index cdde6275235..6589002323b 100644 --- a/runtime/pattern.go +++ b/runtime/pattern.go @@ -49,13 +49,16 @@ type Pattern struct { vars []string // stacksize is the max depth of the stack stacksize int + // verb is the VERB part of the path pattern. It is empty if the pattern does not have VERB part. + verb string } // NewPattern returns a new Pattern from the given definition values. // "ops" is a sequence of op codes. "pool" is a constant pool. +// "verb" is the verb part of the pattern. It is empty if the pattern does not have the part. // "version" must be 1 for now. // It returns an error if the given definition is invalid. -func NewPattern(version int, ops []int, pool []string) (Pattern, error) { +func NewPattern(version int, ops []int, pool []string, verb string) (Pattern, error) { if version != 1 { glog.V(2).Infof("unsupported version: %d", version) return Pattern{}, ErrInvalidPattern @@ -123,14 +126,20 @@ func NewPattern(version int, ops []int, pool []string) (Pattern, error) { pool: pool, vars: vars, stacksize: maxstack, + verb: verb, }, nil } // Match examines components if it matches to the Pattern. // If it matches, the function returns a mapping from field paths to their captured values. // If otherwise, the function returns an error. -func (p Pattern) Match(components []string) (map[string]string, error) { - glog.V(2).Infof("matching %q to %v", components, p) +func (p Pattern) Match(components []string, verb string) (map[string]string, error) { + glog.V(2).Infof("matching (%q, %q) to %v", components, verb, p) + + if p.verb != verb { + return nil, ErrNotMatch + } + var pos int stack := make([]string, 0, p.stacksize) captured := make([]string, len(p.vars)) @@ -176,3 +185,6 @@ func (p Pattern) Match(components []string) (map[string]string, error) { } return bindings, nil } + +// Verb returns the verb part of the Pattern. +func (p Pattern) Verb() string { return p.verb } diff --git a/runtime/pattern_test.go b/runtime/pattern_test.go index 396e6354172..b7818934702 100644 --- a/runtime/pattern_test.go +++ b/runtime/pattern_test.go @@ -15,6 +15,7 @@ func TestNewPattern(t *testing.T) { for _, spec := range []struct { ops []int pool []string + verb string stackSizeWant int }{ @@ -64,10 +65,16 @@ func TestNewPattern(t *testing.T) { pool: []string{"lit1", "lit2", "var1"}, stackSizeWant: 4, }, + { + ops: []int{int(opLitPush), 0}, + pool: []string{"abc"}, + stackSizeWant: 1, + verb: "LOCK", + }, } { - pat, err := NewPattern(validVersion, spec.ops, spec.pool) + pat, err := NewPattern(validVersion, spec.ops, spec.pool, spec.verb) if err != nil { - t.Errorf("NewPattern(%d, %v, %q) failed with %v; want success", validVersion, spec.ops, spec.pool, err) + t.Errorf("NewPattern(%d, %v, %q, %q) failed with %v; want success", validVersion, spec.ops, spec.pool, spec.verb, err) continue } if got, want := pat.stacksize, spec.stackSizeWant; got != want { @@ -80,6 +87,7 @@ func TestNewPatternWithWrongOp(t *testing.T) { for _, spec := range []struct { ops []int pool []string + verb string }{ { // op code out of bound @@ -119,13 +127,13 @@ func TestNewPatternWithWrongOp(t *testing.T) { pool: []string{"abc"}, }, } { - _, err := NewPattern(validVersion, spec.ops, spec.pool) + _, err := NewPattern(validVersion, spec.ops, spec.pool, spec.verb) if err == nil { - t.Errorf("NewPattern(%d, %v, %q) succeeded; want failure with %v", validVersion, spec.ops, spec.pool, ErrInvalidPattern) + t.Errorf("NewPattern(%d, %v, %q, %q) succeeded; want failure with %v", validVersion, spec.ops, spec.pool, spec.verb, ErrInvalidPattern) continue } if err != ErrInvalidPattern { - t.Errorf("NewPattern(%d, %v, %q) failed with %v; want failure with %v", validVersion, spec.ops, spec.pool, err, ErrInvalidPattern) + t.Errorf("NewPattern(%d, %v, %q, %q) failed with %v; want failure with %v", validVersion, spec.ops, spec.pool, spec.verb, err, ErrInvalidPattern) continue } } @@ -135,6 +143,7 @@ func TestNewPatternWithStackUnderflow(t *testing.T) { for _, spec := range []struct { ops []int pool []string + verb string }{ { ops: []int{int(opConcatN), 1}, @@ -144,29 +153,23 @@ func TestNewPatternWithStackUnderflow(t *testing.T) { pool: []string{"abc"}, }, } { - _, err := NewPattern(validVersion, spec.ops, spec.pool) + _, err := NewPattern(validVersion, spec.ops, spec.pool, spec.verb) if err == nil { - t.Errorf("NewPattern(%d, %v, %q) succeeded; want failure with %v", validVersion, spec.ops, spec.pool, ErrInvalidPattern) + t.Errorf("NewPattern(%d, %v, %q, %q) succeeded; want failure with %v", validVersion, spec.ops, spec.pool, spec.verb, ErrInvalidPattern) continue } if err != ErrInvalidPattern { - t.Errorf("NewPattern(%d, %v, %q) failed with %v; want failure with %v", validVersion, spec.ops, spec.pool, err, ErrInvalidPattern) + t.Errorf("NewPattern(%d, %v, %q, %q) failed with %v; want failure with %v", validVersion, spec.ops, spec.pool, spec.verb, err, ErrInvalidPattern) continue } } } -func segments(path string) []string { - if path == "" { - return nil - } - return strings.Split(path, "/") -} - func TestMatch(t *testing.T) { for _, spec := range []struct { ops []int pool []string + verb string match []string notMatch []string @@ -263,10 +266,17 @@ func TestMatch(t *testing.T) { "v2/b/my-bucket/o", }, }, + { + ops: []int{int(opLitPush), 0}, + pool: []string{"v1"}, + verb: "LOCK", + match: []string{"v1:LOCK"}, + notMatch: []string{"v1", "LOCK"}, + }, } { - pat, err := NewPattern(validVersion, spec.ops, spec.pool) + pat, err := NewPattern(validVersion, spec.ops, spec.pool, spec.verb) if err != nil { - t.Errorf("NewPattern(%d, %v, %q) failed with %v; want success", validVersion, spec.ops, spec.pool, err) + t.Errorf("NewPattern(%d, %v, %q, %q) failed with %v; want success", validVersion, spec.ops, spec.pool, spec.verb, err) continue } @@ -295,6 +305,7 @@ func TestMatchWithBinding(t *testing.T) { ops []int pool []string path string + verb string want map[string]string }{ @@ -310,6 +321,12 @@ func TestMatchWithBinding(t *testing.T) { path: "abc", want: make(map[string]string), }, + { + ops: []int{int(opPush), anything}, + verb: "LOCK", + path: "abc:LOCK", + want: make(map[string]string), + }, { ops: []int{int(opLitPush), 0}, pool: []string{"endpoint"}, @@ -335,6 +352,21 @@ func TestMatchWithBinding(t *testing.T) { "name": "my-bucket", }, }, + { + ops: []int{ + int(opLitPush), 0, + int(opLitPush), 1, + int(opPush), anything, + int(opConcatN), 1, + int(opCapture), 2, + }, + pool: []string{"v1", "bucket", "name"}, + verb: "LOCK", + path: "v1/bucket/my-bucket:LOCK", + want: map[string]string{ + "name": "my-bucket", + }, + }, { ops: []int{ int(opLitPush), 0, @@ -369,9 +401,9 @@ func TestMatchWithBinding(t *testing.T) { }, }, } { - pat, err := NewPattern(validVersion, spec.ops, spec.pool) + pat, err := NewPattern(validVersion, spec.ops, spec.pool, spec.verb) if err != nil { - t.Errorf("NewPattern(%d, %v, %q) failed with %v; want success", validVersion, spec.ops, spec.pool, err) + t.Errorf("NewPattern(%d, %v, %q, %q) failed with %v; want success", validVersion, spec.ops, spec.pool, spec.verb, err) continue } @@ -384,3 +416,16 @@ func TestMatchWithBinding(t *testing.T) { } } } + +func segments(path string) (components []string, verb string) { + if path == "" { + return nil, "" + } + components = strings.Split(path, "/") + l := len(components) + c := components[l-1] + if idx := strings.LastIndex(c, ":"); idx >= 0 { + components[l-1], verb = c[:idx], c[idx+1:] + } + return components, verb +} From 3daa1dfafa0b3ad5d2f659fa2ee7e1c64d9bef88 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Sat, 25 Apr 2015 09:46:40 +0900 Subject: [PATCH 05/28] Add String() to runtime.Pattern. --- runtime/pattern.go | 29 +++++++++++++++ runtime/pattern_test.go | 81 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/runtime/pattern.go b/runtime/pattern.go index 6589002323b..c6379efa054 100644 --- a/runtime/pattern.go +++ b/runtime/pattern.go @@ -2,6 +2,7 @@ package runtime import ( "errors" + "fmt" "strings" "github.com/golang/glog" @@ -188,3 +189,31 @@ func (p Pattern) Match(components []string, verb string) (map[string]string, err // Verb returns the verb part of the Pattern. func (p Pattern) Verb() string { return p.verb } + +func (p Pattern) String() string { + var stack []string + for _, op := range p.ops { + switch op.code { + case opNop: + continue + case opPush: + stack = append(stack, "*") + case opLitPush: + stack = append(stack, p.pool[op.operand]) + case opPushM: + stack = append(stack, "**") + case opConcatN: + n := op.operand + l := len(stack) - n + stack = append(stack[:l], strings.Join(stack[l:], "/")) + case opCapture: + n := len(stack) - 1 + stack[n] = fmt.Sprintf("{%s=%s}", p.vars[op.operand], stack[n]) + } + } + segs := strings.Join(stack, "/") + if p.verb != "" { + return fmt.Sprintf("/%s:%s", segs, p.verb) + } + return "/" + segs +} diff --git a/runtime/pattern_test.go b/runtime/pattern_test.go index b7818934702..8fcade152a1 100644 --- a/runtime/pattern_test.go +++ b/runtime/pattern_test.go @@ -1,6 +1,7 @@ package runtime import ( + "fmt" "reflect" "strings" "testing" @@ -429,3 +430,83 @@ func segments(path string) (components []string, verb string) { } return components, verb } + +func TestPatternString(t *testing.T) { + for _, spec := range []struct { + ops []int + pool []string + + want string + }{ + { + want: "/", + }, + { + ops: []int{int(opNop), anything}, + want: "/", + }, + { + ops: []int{int(opPush), anything}, + want: "/*", + }, + { + ops: []int{int(opLitPush), 0}, + pool: []string{"endpoint"}, + want: "/endpoint", + }, + { + ops: []int{int(opPushM), anything}, + want: "/**", + }, + { + ops: []int{ + int(opPush), anything, + int(opConcatN), 1, + }, + want: "/*", + }, + { + ops: []int{ + int(opPush), anything, + int(opConcatN), 1, + int(opCapture), 0, + }, + pool: []string{"name"}, + want: "/{name=*}", + }, + { + ops: []int{ + int(opLitPush), 0, + int(opLitPush), 1, + int(opPush), anything, + int(opConcatN), 2, + int(opCapture), 2, + int(opLitPush), 3, + int(opPushM), anything, + int(opConcatN), 2, + int(opCapture), 4, + }, + pool: []string{"v1", "buckets", "bucket_name", "objects", "name"}, + want: "/v1/{bucket_name=buckets/*}/{name=objects/**}", + }, + } { + p, err := NewPattern(validVersion, spec.ops, spec.pool, "") + if err != nil { + t.Errorf("NewPattern(%d, %v, %q, %q) failed with %v; want success", validVersion, spec.ops, spec.pool, "", err) + continue + } + if got, want := p.String(), spec.want; got != want { + t.Errorf("%#v.String() = %q; want %q", p, got, want) + } + + verb := "LOCK" + p, err = NewPattern(validVersion, spec.ops, spec.pool, verb) + if err != nil { + t.Errorf("NewPattern(%d, %v, %q, %q) failed with %v; want success", validVersion, spec.ops, spec.pool, verb, err) + continue + } + if got, want := p.String(), fmt.Sprintf("%s:%s", spec.want, verb); got != want { + t.Errorf("%#v.String() = %q; want %q", p, got, want) + } + } +} From a8d44919adee72d38f491722f4a5a4775771a90b Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Sun, 26 Apr 2015 15:25:32 +0900 Subject: [PATCH 06/28] Export opcodes in internal/ package. --- internal/doc.go | 2 + internal/pattern.go | 22 +++++ runtime/pattern.go | 59 +++++------- runtime/pattern_test.go | 194 ++++++++++++++++++++-------------------- 4 files changed, 142 insertions(+), 135 deletions(-) create mode 100644 internal/doc.go create mode 100644 internal/pattern.go diff --git a/internal/doc.go b/internal/doc.go new file mode 100644 index 00000000000..0860ea062cf --- /dev/null +++ b/internal/doc.go @@ -0,0 +1,2 @@ +// Package internal provides members for internal use in grpc-gateway. +package internal diff --git a/internal/pattern.go b/internal/pattern.go new file mode 100644 index 00000000000..41cd77672bb --- /dev/null +++ b/internal/pattern.go @@ -0,0 +1,22 @@ +package internal + +// An OpCode is a opcode of compiled path patterns. +type OpCode int + +// These constants are the valid values of OpCode. +const ( + // OpNop does nothing + OpNop = OpCode(iota) + // OpPush pushes a component to stack + OpPush + // OpLitPush pushes a component to stack if it matches to the literal + OpLitPush + // OpPushM concatenates the remaining components and pushes it to stack + OpPushM + // OpPopN pops a N items from stack, concatenates them and pushes it to stack + OpConcatN + // OpCapture pops an item and binds it to the variable + OpCapture + // OpEnd is the least postive invalid opcode. + OpEnd +) diff --git a/runtime/pattern.go b/runtime/pattern.go index c6379efa054..8d2eae0afe4 100644 --- a/runtime/pattern.go +++ b/runtime/pattern.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/gengo/grpc-gateway/internal" "github.com/golang/glog" ) @@ -15,28 +16,8 @@ var ( ErrInvalidPattern = errors.New("invalid pattern") ) -type opcode int - -// These constants are the valid values of opcode. -const ( - // opNop does nothing - opNop = opcode(iota) - // opPush pushes a component to stack - opPush - // opLitPush pushes a component to stack if it matches to the literal - opLitPush - // opPushM concatenates the remaining components and pushes it to stack - opPushM - // opPopN pops a N items from stack, concatenates them and pushes it to stack - opConcatN - // opCapture pops an item and binds it to the variable - opCapture - // opEnd is the least postive invalid opcode. - opEnd -) - type op struct { - code opcode + code internal.OpCode operand int } @@ -75,19 +56,19 @@ func NewPattern(version int, ops []int, pool []string, verb string) (Pattern, er var stack, maxstack int var vars []string for i := 0; i < l; i += 2 { - op := op{code: opcode(ops[i]), operand: ops[i+1]} + op := op{code: internal.OpCode(ops[i]), operand: ops[i+1]} switch op.code { - case opNop: + case internal.OpNop: continue - case opPush, opPushM: + case internal.OpPush, internal.OpPushM: stack++ - case opLitPush: + case internal.OpLitPush: if op.operand < 0 || len(pool) <= op.operand { glog.V(2).Infof("negative literal index: %d", op.operand) return Pattern{}, ErrInvalidPattern } stack++ - case opConcatN: + case internal.OpConcatN: if op.operand <= 0 { glog.V(2).Infof("negative concat size: %d", op.operand) return Pattern{}, ErrInvalidPattern @@ -98,7 +79,7 @@ func NewPattern(version int, ops []int, pool []string, verb string) (Pattern, er return Pattern{}, ErrInvalidPattern } stack++ - case opCapture: + case internal.OpCapture: if op.operand < 0 || len(pool) <= op.operand { glog.V(2).Infof("variable name index out of bound: %d", op.operand) return Pattern{}, ErrInvalidPattern @@ -147,15 +128,15 @@ func (p Pattern) Match(components []string, verb string) (map[string]string, err l := len(components) for _, op := range p.ops { switch op.code { - case opNop: + case internal.OpNop: continue - case opPush, opLitPush: + case internal.OpPush, internal.OpLitPush: if pos >= l { glog.V(1).Infof("insufficient # of segments") return nil, ErrNotMatch } c := components[pos] - if op.code == opLitPush { + if op.code == internal.OpLitPush { if lit := p.pool[op.operand]; c != lit { glog.V(1).Infof("literal segment mismatch: got %q; want %q", c, lit) return nil, ErrNotMatch @@ -163,14 +144,14 @@ func (p Pattern) Match(components []string, verb string) (map[string]string, err } stack = append(stack, c) pos++ - case opPushM: + case internal.OpPushM: stack = append(stack, strings.Join(components[pos:], "/")) pos = len(components) - case opConcatN: + case internal.OpConcatN: n := op.operand l := len(stack) - n stack = append(stack[:l], strings.Join(stack[l:], "/")) - case opCapture: + case internal.OpCapture: n := len(stack) - 1 captured[op.operand] = stack[n] stack = stack[:n] @@ -194,19 +175,19 @@ func (p Pattern) String() string { var stack []string for _, op := range p.ops { switch op.code { - case opNop: + case internal.OpNop: continue - case opPush: + case internal.OpPush: stack = append(stack, "*") - case opLitPush: + case internal.OpLitPush: stack = append(stack, p.pool[op.operand]) - case opPushM: + case internal.OpPushM: stack = append(stack, "**") - case opConcatN: + case internal.OpConcatN: n := op.operand l := len(stack) - n stack = append(stack[:l], strings.Join(stack[l:], "/")) - case opCapture: + case internal.OpCapture: n := len(stack) - 1 stack[n] = fmt.Sprintf("{%s=%s}", p.vars[op.operand], stack[n]) } diff --git a/runtime/pattern_test.go b/runtime/pattern_test.go index 8fcade152a1..878b21c9f29 100644 --- a/runtime/pattern_test.go +++ b/runtime/pattern_test.go @@ -5,6 +5,8 @@ import ( "reflect" "strings" "testing" + + "github.com/gengo/grpc-gateway/internal" ) const ( @@ -22,52 +24,52 @@ func TestNewPattern(t *testing.T) { }{ {}, { - ops: []int{int(opNop), anything}, + ops: []int{int(internal.OpNop), anything}, stackSizeWant: 0, }, { - ops: []int{int(opPush), anything}, + ops: []int{int(internal.OpPush), anything}, stackSizeWant: 1, }, { - ops: []int{int(opLitPush), 0}, + ops: []int{int(internal.OpLitPush), 0}, pool: []string{"abc"}, stackSizeWant: 1, }, { - ops: []int{int(opPushM), anything}, + ops: []int{int(internal.OpPushM), anything}, stackSizeWant: 1, }, { ops: []int{ - int(opPush), anything, - int(opConcatN), 1, + int(internal.OpPush), anything, + int(internal.OpConcatN), 1, }, stackSizeWant: 1, }, { ops: []int{ - int(opPush), anything, - int(opConcatN), 1, - int(opCapture), 0, + int(internal.OpPush), anything, + int(internal.OpConcatN), 1, + int(internal.OpCapture), 0, }, pool: []string{"abc"}, stackSizeWant: 1, }, { ops: []int{ - int(opPush), anything, - int(opLitPush), 0, - int(opLitPush), 1, - int(opPushM), anything, - int(opConcatN), 2, - int(opCapture), 2, + int(internal.OpPush), anything, + int(internal.OpLitPush), 0, + int(internal.OpLitPush), 1, + int(internal.OpPushM), anything, + int(internal.OpConcatN), 2, + int(internal.OpCapture), 2, }, pool: []string{"lit1", "lit2", "var1"}, stackSizeWant: 4, }, { - ops: []int{int(opLitPush), 0}, + ops: []int{int(internal.OpLitPush), 0}, pool: []string{"abc"}, stackSizeWant: 1, verb: "LOCK", @@ -96,35 +98,35 @@ func TestNewPatternWithWrongOp(t *testing.T) { }, { // op code out of bound - ops: []int{int(opEnd), 0}, + ops: []int{int(internal.OpEnd), 0}, }, { // odd number of items - ops: []int{int(opPush)}, + ops: []int{int(internal.OpPush)}, }, { // negative index - ops: []int{int(opLitPush), -1}, + ops: []int{int(internal.OpLitPush), -1}, pool: []string{"abc"}, }, { // index out of bound - ops: []int{int(opLitPush), 1}, + ops: []int{int(internal.OpLitPush), 1}, pool: []string{"abc"}, }, { // negative # of segments - ops: []int{int(opConcatN), -1}, + ops: []int{int(internal.OpConcatN), -1}, pool: []string{"abc"}, }, { // negative index - ops: []int{int(opCapture), -1}, + ops: []int{int(internal.OpCapture), -1}, pool: []string{"abc"}, }, { // index out of bound - ops: []int{int(opCapture), 1}, + ops: []int{int(internal.OpCapture), 1}, pool: []string{"abc"}, }, } { @@ -147,10 +149,10 @@ func TestNewPatternWithStackUnderflow(t *testing.T) { verb string }{ { - ops: []int{int(opConcatN), 1}, + ops: []int{int(internal.OpConcatN), 1}, }, { - ops: []int{int(opCapture), 0}, + ops: []int{int(internal.OpCapture), 0}, pool: []string{"abc"}, }, } { @@ -180,32 +182,32 @@ func TestMatch(t *testing.T) { notMatch: []string{"example"}, }, { - ops: []int{int(opNop), anything}, + ops: []int{int(internal.OpNop), anything}, match: []string{""}, notMatch: []string{"example", "path/to/example"}, }, { - ops: []int{int(opPush), anything}, + ops: []int{int(internal.OpPush), anything}, match: []string{"abc", "def"}, notMatch: []string{"", "abc/def"}, }, { - ops: []int{int(opLitPush), 0}, + ops: []int{int(internal.OpLitPush), 0}, pool: []string{"v1"}, match: []string{"v1"}, notMatch: []string{"", "v2"}, }, { - ops: []int{int(opPushM), anything}, + ops: []int{int(internal.OpPushM), anything}, match: []string{"", "abc", "abc/def", "abc/def/ghi"}, }, { ops: []int{ - int(opLitPush), 0, - int(opLitPush), 1, - int(opPush), anything, - int(opConcatN), 1, - int(opCapture), 2, + int(internal.OpLitPush), 0, + int(internal.OpLitPush), 1, + int(internal.OpPush), anything, + int(internal.OpConcatN), 1, + int(internal.OpCapture), 2, }, pool: []string{"v1", "bucket", "name"}, match: []string{"v1/bucket/my-bucket", "v1/bucket/our-bucket"}, @@ -219,11 +221,11 @@ func TestMatch(t *testing.T) { }, { ops: []int{ - int(opLitPush), 0, - int(opLitPush), 1, - int(opPushM), anything, - int(opConcatN), 2, - int(opCapture), 2, + int(internal.OpLitPush), 0, + int(internal.OpLitPush), 1, + int(internal.OpPushM), anything, + int(internal.OpConcatN), 2, + int(internal.OpCapture), 2, }, pool: []string{"v1", "o", "name"}, match: []string{ @@ -243,15 +245,15 @@ func TestMatch(t *testing.T) { }, { ops: []int{ - int(opLitPush), 0, - int(opLitPush), 1, - int(opPush), anything, - int(opConcatN), 2, - int(opCapture), 2, - int(opLitPush), 3, - int(opPush), anything, - int(opConcatN), 1, - int(opCapture), 4, + int(internal.OpLitPush), 0, + int(internal.OpLitPush), 1, + int(internal.OpPush), anything, + int(internal.OpConcatN), 2, + int(internal.OpCapture), 2, + int(internal.OpLitPush), 3, + int(internal.OpPush), anything, + int(internal.OpConcatN), 1, + int(internal.OpCapture), 4, }, pool: []string{"v2", "b", "name", "o", "oname"}, match: []string{ @@ -268,7 +270,7 @@ func TestMatch(t *testing.T) { }, }, { - ops: []int{int(opLitPush), 0}, + ops: []int{int(internal.OpLitPush), 0}, pool: []string{"v1"}, verb: "LOCK", match: []string{"v1:LOCK"}, @@ -314,38 +316,38 @@ func TestMatchWithBinding(t *testing.T) { want: make(map[string]string), }, { - ops: []int{int(opNop), anything}, + ops: []int{int(internal.OpNop), anything}, want: make(map[string]string), }, { - ops: []int{int(opPush), anything}, + ops: []int{int(internal.OpPush), anything}, path: "abc", want: make(map[string]string), }, { - ops: []int{int(opPush), anything}, + ops: []int{int(internal.OpPush), anything}, verb: "LOCK", path: "abc:LOCK", want: make(map[string]string), }, { - ops: []int{int(opLitPush), 0}, + ops: []int{int(internal.OpLitPush), 0}, pool: []string{"endpoint"}, path: "endpoint", want: make(map[string]string), }, { - ops: []int{int(opPushM), anything}, + ops: []int{int(internal.OpPushM), anything}, path: "abc/def/ghi", want: make(map[string]string), }, { ops: []int{ - int(opLitPush), 0, - int(opLitPush), 1, - int(opPush), anything, - int(opConcatN), 1, - int(opCapture), 2, + int(internal.OpLitPush), 0, + int(internal.OpLitPush), 1, + int(internal.OpPush), anything, + int(internal.OpConcatN), 1, + int(internal.OpCapture), 2, }, pool: []string{"v1", "bucket", "name"}, path: "v1/bucket/my-bucket", @@ -355,11 +357,11 @@ func TestMatchWithBinding(t *testing.T) { }, { ops: []int{ - int(opLitPush), 0, - int(opLitPush), 1, - int(opPush), anything, - int(opConcatN), 1, - int(opCapture), 2, + int(internal.OpLitPush), 0, + int(internal.OpLitPush), 1, + int(internal.OpPush), anything, + int(internal.OpConcatN), 1, + int(internal.OpCapture), 2, }, pool: []string{"v1", "bucket", "name"}, verb: "LOCK", @@ -370,11 +372,11 @@ func TestMatchWithBinding(t *testing.T) { }, { ops: []int{ - int(opLitPush), 0, - int(opLitPush), 1, - int(opPushM), anything, - int(opConcatN), 2, - int(opCapture), 2, + int(internal.OpLitPush), 0, + int(internal.OpLitPush), 1, + int(internal.OpPushM), anything, + int(internal.OpConcatN), 2, + int(internal.OpCapture), 2, }, pool: []string{"v1", "o", "name"}, path: "v1/o/my-bucket/dir/dir2/obj", @@ -384,15 +386,15 @@ func TestMatchWithBinding(t *testing.T) { }, { ops: []int{ - int(opLitPush), 0, - int(opLitPush), 1, - int(opPush), anything, - int(opConcatN), 2, - int(opCapture), 2, - int(opLitPush), 3, - int(opPush), anything, - int(opConcatN), 1, - int(opCapture), 4, + int(internal.OpLitPush), 0, + int(internal.OpLitPush), 1, + int(internal.OpPush), anything, + int(internal.OpConcatN), 2, + int(internal.OpCapture), 2, + int(internal.OpLitPush), 3, + int(internal.OpPush), anything, + int(internal.OpConcatN), 1, + int(internal.OpCapture), 4, }, pool: []string{"v2", "b", "name", "o", "oname"}, path: "v2/b/my-bucket/o/obj", @@ -442,49 +444,49 @@ func TestPatternString(t *testing.T) { want: "/", }, { - ops: []int{int(opNop), anything}, + ops: []int{int(internal.OpNop), anything}, want: "/", }, { - ops: []int{int(opPush), anything}, + ops: []int{int(internal.OpPush), anything}, want: "/*", }, { - ops: []int{int(opLitPush), 0}, + ops: []int{int(internal.OpLitPush), 0}, pool: []string{"endpoint"}, want: "/endpoint", }, { - ops: []int{int(opPushM), anything}, + ops: []int{int(internal.OpPushM), anything}, want: "/**", }, { ops: []int{ - int(opPush), anything, - int(opConcatN), 1, + int(internal.OpPush), anything, + int(internal.OpConcatN), 1, }, want: "/*", }, { ops: []int{ - int(opPush), anything, - int(opConcatN), 1, - int(opCapture), 0, + int(internal.OpPush), anything, + int(internal.OpConcatN), 1, + int(internal.OpCapture), 0, }, pool: []string{"name"}, want: "/{name=*}", }, { ops: []int{ - int(opLitPush), 0, - int(opLitPush), 1, - int(opPush), anything, - int(opConcatN), 2, - int(opCapture), 2, - int(opLitPush), 3, - int(opPushM), anything, - int(opConcatN), 2, - int(opCapture), 4, + int(internal.OpLitPush), 0, + int(internal.OpLitPush), 1, + int(internal.OpPush), anything, + int(internal.OpConcatN), 2, + int(internal.OpCapture), 2, + int(internal.OpLitPush), 3, + int(internal.OpPushM), anything, + int(internal.OpConcatN), 2, + int(internal.OpCapture), 4, }, pool: []string{"v1", "buckets", "bucket_name", "objects", "name"}, want: "/v1/{bucket_name=buckets/*}/{name=objects/**}", From e407fb135b50cda8e63f658289f20de616b0673b Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 27 Apr 2015 08:05:22 +0900 Subject: [PATCH 07/28] Add path template parser --- protoc-gen-grpc-gateway/httprule/parse.go | 349 ++++++++++++++++++ .../httprule/parse_test.go | 313 ++++++++++++++++ protoc-gen-grpc-gateway/httprule/types.go | 22 ++ 3 files changed, 684 insertions(+) create mode 100644 protoc-gen-grpc-gateway/httprule/parse.go create mode 100644 protoc-gen-grpc-gateway/httprule/parse_test.go create mode 100644 protoc-gen-grpc-gateway/httprule/types.go diff --git a/protoc-gen-grpc-gateway/httprule/parse.go b/protoc-gen-grpc-gateway/httprule/parse.go new file mode 100644 index 00000000000..0f4b4c53d19 --- /dev/null +++ b/protoc-gen-grpc-gateway/httprule/parse.go @@ -0,0 +1,349 @@ +package httprule + +import ( + "fmt" + "strings" + + "github.com/golang/glog" +) + +type InvalidTemplateError struct { + tmpl string + msg string +} + +func (e InvalidTemplateError) Error() string { + return fmt.Sprintf("%s: %s", e.msg, e.tmpl) +} + +func Parse(tmpl string) (template, error) { + if !strings.HasPrefix(tmpl, "/") { + return template{}, InvalidTemplateError{tmpl: tmpl, msg: "no leading /"} + } + tokens, verb := tokenize(tmpl[1:]) + + p := parser{tokens: tokens} + segs, err := p.topLevelSegments() + if err != nil { + return template{}, InvalidTemplateError{tmpl: tmpl, msg: err.Error()} + } + + return template{ + segments: segs, + verb: verb, + }, nil +} + +func tokenize(path string) (tokens []string, verb string) { + if path == "" { + return []string{eof}, "" + } + + const ( + init = iota + field + nested + ) + var ( + st = init + ) + for path != "" { + var idx int + switch st { + case init: + idx = strings.IndexAny(path, "/{") + case field: + idx = strings.IndexAny(path, ".=}") + case nested: + idx = strings.IndexAny(path, "/}") + } + if idx < 0 { + tokens = append(tokens, path) + break + } + switch r := path[idx]; r { + case '/', '.': + case '{': + st = field + case '=': + st = nested + case '}': + st = init + } + if idx == 0 { + tokens = append(tokens, path[idx:idx+1]) + } else { + tokens = append(tokens, path[:idx], path[idx:idx+1]) + } + path = path[idx+1:] + } + + l := len(tokens) + t := tokens[l-1] + if idx := strings.LastIndex(t, ":"); idx == 0 { + tokens, verb = tokens[:l-1], t[1:] + } else if idx > 0 { + tokens[l-1], verb = t[:idx], t[idx+1:] + } + tokens = append(tokens, eof) + return tokens, verb +} + +// parser is a parser of the template syntax defined in third_party/googleapis/google/api/httprule.proto. +type parser struct { + tokens []string + accepted []string +} + +// topLevelSegments is the target of this parser. +func (p *parser) topLevelSegments() ([]segment, error) { + glog.V(1).Infof("Parsing %q", p.tokens) + segs, err := p.segments() + if err != nil { + return nil, err + } + glog.V(2).Infof("accept segments: %q; %q", p.accepted, p.tokens) + if _, err := p.accept(typeEOF); err != nil { + return nil, fmt.Errorf("unexpected token %q after segments %q", p.tokens[0], strings.Join(p.accepted, "")) + } + glog.V(2).Infof("accept eof: %q; %q", p.accepted, p.tokens) + return segs, nil +} + +func (p *parser) segments() ([]segment, error) { + s, err := p.segment() + if err != nil { + return nil, err + } + glog.V(2).Infof("accept segment: %q; %q", p.accepted, p.tokens) + + segs := []segment{s} + for { + if _, err := p.accept("/"); err != nil { + return segs, nil + } + s, err := p.segment() + if err != nil { + return segs, err + } + segs = append(segs, s) + glog.V(2).Infof("accept segment: %q; %q", p.accepted, p.tokens) + } +} + +func (p *parser) segment() (segment, error) { + if _, err := p.accept("*"); err == nil { + return wildcard{}, nil + } + if _, err := p.accept("**"); err == nil { + return deepWildcard{}, nil + } + if l, err := p.literal(); err == nil { + return l, nil + } + + v, err := p.variable() + if err != nil { + return nil, fmt.Errorf("segment neither wildcards, literal or variable: %v", err) + } + return v, err +} + +func (p *parser) literal() (segment, error) { + lit, err := p.accept(typeLiteral) + if err != nil { + return nil, err + } + return literal(lit), nil +} + +func (p *parser) variable() (segment, error) { + if _, err := p.accept("{"); err != nil { + return nil, err + } + + path, err := p.fieldPath() + if err != nil { + return nil, err + } + + var segs []segment + if _, err := p.accept("="); err == nil { + segs, err = p.segments() + if err != nil { + return nil, fmt.Errorf("invalid segment in variable %q: %v", path, err) + } + } else { + segs = []segment{wildcard{}} + } + + if _, err := p.accept("}"); err != nil { + return nil, fmt.Errorf("unterminated variable segment: %s", path) + } + return variable{ + path: path, + segments: segs, + }, nil +} + +func (p *parser) fieldPath() (string, error) { + c, err := p.accept(typeIdent) + if err != nil { + return "", err + } + components := []string{c} + for { + if _, err = p.accept("."); err != nil { + return strings.Join(components, "."), nil + } + c, err := p.accept(typeIdent) + if err != nil { + return "", fmt.Errorf("invalid field path component: %v", err) + } + components = append(components, c) + } + return strings.Join(components, "."), nil +} + +// A termType is a type of terminal symbols. +type termType string + +// These constants define some of valid values of termType. +// They improve readability of parse functions. +// +// You can also use "/", "*", "**", "." or "=" as valid values. +const ( + typeIdent = termType("ident") + typeLiteral = termType("literal") + typeEOF = termType("$") +) + +const ( + // eof is the terminal symbol which always appears at the end of token sequence. + eof = "\u0000" +) + +// accept tries to accept a token in "p". +// This function consumes a token and returns it if it matches to the specified "term". +// If it doesn't match, the function does not consume any tokens and return an error. +func (p *parser) accept(term termType) (string, error) { + t := p.tokens[0] + switch term { + case "/", "*", "**", ".", "=", "{", "}": + if t != string(term) { + return "", fmt.Errorf("expected %q but got %q", term, t) + } + case typeEOF: + if t != eof { + return "", fmt.Errorf("expected EOF but got %q", t) + } + case typeIdent: + if err := expectIdent(t); err != nil { + return "", err + } + case typeLiteral: + if err := expectPChars(t); err != nil { + return "", err + } + default: + return "", fmt.Errorf("unknown termType %q", term) + } + p.tokens = p.tokens[1:] + p.accepted = append(p.accepted, t) + return t, nil +} + +// expectPChars determines if "t" consists of only pchars defined in RFC3986. +// +// https://www.ietf.org/rfc/rfc3986.txt, P.49 +// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" +// / "*" / "+" / "," / ";" / "=" +// pct-encoded = "%" HEXDIG HEXDIG +func expectPChars(t string) error { + const ( + init = iota + pct1 + pct2 + ) + st := init + for _, r := range t { + if st != init { + if !isHexDigit(r) { + return fmt.Errorf("invalid hexdigit: %c(%U)", r, r) + } + switch st { + case pct1: + st = pct2 + case pct2: + st = init + } + continue + } + + // unreserved + switch { + case 'A' <= r && r <= 'Z': + continue + case 'a' <= r && r <= 'z': + continue + case '0' <= r && r <= '9': + continue + } + switch r { + case '-', '.', '_', '~': + // unreserved + case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': + // sub-delims + case ':', '@': + // rest of pchar + case '%': + // pct-encoded + st = pct1 + default: + return fmt.Errorf("invalid character in path segment: %q(%U)", r, r) + } + } + if st != init { + return fmt.Errorf("invalid percent-encoding in %q", t) + } + return nil +} + +// expectIdent determines if "ident" is a valid identifier in .proto schema ([[:alpha:]_][[:alphanum:]_]*). +func expectIdent(ident string) error { + if ident == "" { + return fmt.Errorf("empty identifier") + } + for pos, r := range ident { + switch { + case '0' <= r && r <= '9': + if pos == 0 { + return fmt.Errorf("identifier starting with digit: %s", ident) + } + continue + case 'A' <= r && r <= 'Z': + continue + case 'a' <= r && r <= 'z': + continue + case r == '_': + continue + default: + return fmt.Errorf("invalid character %q(%U) in identifier: %s", r, r, ident) + } + } + return nil +} + +func isHexDigit(r rune) bool { + switch { + case '0' <= r && r <= '9': + return true + case 'A' <= r && r <= 'F': + return true + case 'a' <= r && r <= 'f': + return true + } + return false +} diff --git a/protoc-gen-grpc-gateway/httprule/parse_test.go b/protoc-gen-grpc-gateway/httprule/parse_test.go new file mode 100644 index 00000000000..6a49c712d62 --- /dev/null +++ b/protoc-gen-grpc-gateway/httprule/parse_test.go @@ -0,0 +1,313 @@ +package httprule + +import ( + "flag" + "fmt" + "reflect" + "testing" + + "github.com/golang/glog" +) + +func TestTokenize(t *testing.T) { + for _, spec := range []struct { + src string + tokens []string + }{ + { + src: "", + tokens: []string{eof}, + }, + { + src: "v1", + tokens: []string{"v1", eof}, + }, + { + src: "v1/b", + tokens: []string{"v1", "/", "b", eof}, + }, + { + src: "v1/endpoint/*", + tokens: []string{"v1", "/", "endpoint", "/", "*", eof}, + }, + { + src: "v1/endpoint/**", + tokens: []string{"v1", "/", "endpoint", "/", "**", eof}, + }, + { + src: "v1/b/{bucket_name=*}", + tokens: []string{ + "v1", "/", + "b", "/", + "{", "bucket_name", "=", "*", "}", + eof, + }, + }, + { + src: "v1/b/{bucket_name=buckets/*}", + tokens: []string{ + "v1", "/", + "b", "/", + "{", "bucket_name", "=", "buckets", "/", "*", "}", + eof, + }, + }, + { + src: "v1/b/{bucket_name=buckets/*}/o", + tokens: []string{ + "v1", "/", + "b", "/", + "{", "bucket_name", "=", "buckets", "/", "*", "}", "/", + "o", + eof, + }, + }, + { + src: "v1/b/{bucket_name=buckets/*}/o/{name}", + tokens: []string{ + "v1", "/", + "b", "/", + "{", "bucket_name", "=", "buckets", "/", "*", "}", "/", + "o", "/", "{", "name", "}", + eof, + }, + }, + { + src: "v1/a=b&c=d;e=f:g/endpoint.rdf", + tokens: []string{ + "v1", "/", + "a=b&c=d;e=f:g", "/", + "endpoint.rdf", + eof, + }, + }, + } { + tokens, verb := tokenize(spec.src) + if got, want := tokens, spec.tokens; !reflect.DeepEqual(got, want) { + t.Errorf("tokenize(%q) = %q, _; want %q, _", spec.src, got, want) + } + if got, want := verb, ""; got != want { + t.Errorf("tokenize(%q) = _, %q; want _, %q", spec.src, got, want) + } + + src := fmt.Sprintf("%s:%s", spec.src, "LOCK") + tokens, verb = tokenize(src) + if got, want := tokens, spec.tokens; !reflect.DeepEqual(got, want) { + t.Errorf("tokenize(%q) = %q, _; want %q, _", src, got, want) + } + if got, want := verb, "LOCK"; got != want { + t.Errorf("tokenize(%q) = _, %q; want _, %q", src, got, want) + } + } +} + +func TestParseSegments(t *testing.T) { + flag.Set("v", "3") + for _, spec := range []struct { + tokens []string + want []segment + }{ + { + tokens: []string{"v1", eof}, + want: []segment{ + literal("v1"), + }, + }, + { + tokens: []string{"-._~!$&'()*+,;=:@", eof}, + want: []segment{ + literal("-._~!$&'()*+,;=:@"), + }, + }, + { + tokens: []string{"%e7%ac%ac%e4%b8%80%e7%89%88", eof}, + want: []segment{ + literal("%e7%ac%ac%e4%b8%80%e7%89%88"), + }, + }, + { + tokens: []string{"v1", "/", "*", eof}, + want: []segment{ + literal("v1"), + wildcard{}, + }, + }, + { + tokens: []string{"v1", "/", "**", eof}, + want: []segment{ + literal("v1"), + deepWildcard{}, + }, + }, + { + tokens: []string{"{", "name", "}", eof}, + want: []segment{ + variable{ + path: "name", + segments: []segment{ + wildcard{}, + }, + }, + }, + }, + { + tokens: []string{"{", "name", "=", "*", "}", eof}, + want: []segment{ + variable{ + path: "name", + segments: []segment{ + wildcard{}, + }, + }, + }, + }, + { + tokens: []string{"{", "field", ".", "nested", ".", "nested2", "=", "*", "}", eof}, + want: []segment{ + variable{ + path: "field.nested.nested2", + segments: []segment{ + wildcard{}, + }, + }, + }, + }, + { + tokens: []string{"{", "name", "=", "a", "/", "b", "/", "*", "}", eof}, + want: []segment{ + variable{ + path: "name", + segments: []segment{ + literal("a"), + literal("b"), + wildcard{}, + }, + }, + }, + }, + { + tokens: []string{ + "v1", "/", + "{", + "name", ".", "nested", ".", "nested2", + "=", + "a", "/", "b", "/", "*", + "}", "/", + "o", "/", + "{", + "another_name", + "=", + "a", "/", "b", "/", "*", "/", "c", + "}", "/", + "**", + eof}, + want: []segment{ + literal("v1"), + variable{ + path: "name.nested.nested2", + segments: []segment{ + literal("a"), + literal("b"), + wildcard{}, + }, + }, + literal("o"), + variable{ + path: "another_name", + segments: []segment{ + literal("a"), + literal("b"), + wildcard{}, + literal("c"), + }, + }, + deepWildcard{}, + }, + }, + } { + p := parser{tokens: spec.tokens} + segs, err := p.topLevelSegments() + if err != nil { + t.Errorf("parser{%q}.segments() failed with %v; want success", spec.tokens, err) + continue + } + if got, want := segs, spec.want; !reflect.DeepEqual(got, want) { + t.Errorf("parser{%q}.segments() = %#v; want %#v", spec.tokens, got, want) + } + if got := p.tokens; len(got) > 0 { + t.Errorf("p.tokens = %q; want []; spec.tokens=%q", got, spec.tokens) + } + } +} + +func TestParseSegmentsWithErrors(t *testing.T) { + flag.Set("v", "3") + for _, spec := range []struct { + tokens []string + }{ + { + // double slash + tokens: []string{"/", eof}, + }, + { + // invalid literal + tokens: []string{"a?b", eof}, + }, + { + // invalid percent-encoding + tokens: []string{"%", eof}, + }, + { + // invalid percent-encoding + tokens: []string{"%2", eof}, + }, + { + // invalid percent-encoding + tokens: []string{"a%2z", eof}, + }, + { + // empty segments + tokens: []string{eof}, + }, + { + // unterminated variable + tokens: []string{"{", "name", eof}, + }, + { + // unterminated variable + tokens: []string{"{", "name", "=", eof}, + }, + { + // unterminated variable + tokens: []string{"{", "name", "=", "*", eof}, + }, + { + // empty component in field path + tokens: []string{"{", "name", ".", "}", eof}, + }, + { + // empty component in field path + tokens: []string{"{", "name", ".", ".", "nested", "}", eof}, + }, + { + // invalid character in identifier + tokens: []string{"{", "field-name", "}", eof}, + }, + { + // no slash between segments + tokens: []string{"v1", "endpoint", eof}, + }, + { + // no slash between segments + tokens: []string{"v1", "{", "name", "}", eof}, + }, + } { + p := parser{tokens: spec.tokens} + segs, err := p.topLevelSegments() + if err == nil { + t.Errorf("parser{%q}.segments() succeeded; want InvalidTemplateError; accepted %#v", spec.tokens, segs) + continue + } + glog.V(1).Info(err) + } +} diff --git a/protoc-gen-grpc-gateway/httprule/types.go b/protoc-gen-grpc-gateway/httprule/types.go new file mode 100644 index 00000000000..d4495b7eb2d --- /dev/null +++ b/protoc-gen-grpc-gateway/httprule/types.go @@ -0,0 +1,22 @@ +package httprule + +type template struct { + segments []segment + verb string +} + +type segment interface { + // Stringer + // compile() (ops []op) +} + +type wildcard struct{} + +type deepWildcard struct{} + +type literal string + +type variable struct { + path string + segments []segment +} From 34560be02c6ff827238458ddd59781b668acda8b Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 27 Apr 2015 19:02:41 +0900 Subject: [PATCH 08/28] Implement compiler from AST to opcodes --- protoc-gen-grpc-gateway/httprule/compile.go | 107 ++++++++++++++++ .../httprule/compile_test.go | 116 ++++++++++++++++++ protoc-gen-grpc-gateway/httprule/parse.go | 1 + protoc-gen-grpc-gateway/httprule/types.go | 41 ++++++- .../httprule/types_test.go | 91 ++++++++++++++ 5 files changed, 354 insertions(+), 2 deletions(-) create mode 100644 protoc-gen-grpc-gateway/httprule/compile.go create mode 100644 protoc-gen-grpc-gateway/httprule/compile_test.go create mode 100644 protoc-gen-grpc-gateway/httprule/types_test.go diff --git a/protoc-gen-grpc-gateway/httprule/compile.go b/protoc-gen-grpc-gateway/httprule/compile.go new file mode 100644 index 00000000000..08146dee2b5 --- /dev/null +++ b/protoc-gen-grpc-gateway/httprule/compile.go @@ -0,0 +1,107 @@ +package httprule + +import ( + "github.com/gengo/grpc-gateway/internal" +) + +const ( + opcodeVersion = 1 +) + +// Template is a compiled representation of path templates. +type Template struct { + // Version is the version number of the format. + Version int + // OpCodes is a sequence of operations. + OpCodes []int + // Pool is a constant pool + Pool []string + // Verb is a VERB part in the template. + Verb string +} + +// Compiler compiles internal representation of path templates into marshallable operations. +// They can be unmarshalled by runtime.NewPattern. +type Compiler interface { + Compile() Template +} + +type op struct { + // code is the opcode of the operation + code internal.OpCode + + // str is a string operand of the code. + // num is ignored if str is not empty. + str string + + // num is a numeric operand of the code. + num int +} + +func (w wildcard) compile() []op { + return []op{ + {code: internal.OpPush}, + } +} + +func (w deepWildcard) compile() []op { + return []op{ + {code: internal.OpPushM}, + } +} + +func (l literal) compile() []op { + return []op{ + { + code: internal.OpLitPush, + str: string(l), + }, + } +} + +func (v variable) compile() []op { + var ops []op + for _, s := range v.segments { + ops = append(ops, s.compile()...) + } + ops = append(ops, op{ + code: internal.OpConcatN, + num: len(v.segments), + }, op{ + code: internal.OpCapture, + str: v.path, + }) + + return ops +} + +func (t template) Compile() Template { + var rawOps []op + for _, s := range t.segments { + rawOps = append(rawOps, s.compile()...) + } + + var ( + ops []int + pool []string + ) + consts := make(map[string]int) + for _, op := range rawOps { + ops = append(ops, int(op.code)) + if op.str == "" { + ops = append(ops, op.num) + } else { + if _, ok := consts[op.str]; !ok { + consts[op.str] = len(pool) + pool = append(pool, op.str) + } + ops = append(ops, consts[op.str]) + } + } + return Template{ + Version: opcodeVersion, + OpCodes: ops, + Pool: pool, + Verb: t.verb, + } +} diff --git a/protoc-gen-grpc-gateway/httprule/compile_test.go b/protoc-gen-grpc-gateway/httprule/compile_test.go new file mode 100644 index 00000000000..fa05684d33c --- /dev/null +++ b/protoc-gen-grpc-gateway/httprule/compile_test.go @@ -0,0 +1,116 @@ +package httprule + +import ( + "reflect" + "testing" + + "github.com/gengo/grpc-gateway/internal" +) + +const ( + operandFiller = 0 +) + +func TestCompile(t *testing.T) { + for _, spec := range []struct { + segs []segment + verb string + + ops []int + pool []string + }{ + {}, + { + segs: []segment{ + wildcard{}, + }, + ops: []int{int(internal.OpPush), operandFiller}, + }, + { + segs: []segment{ + deepWildcard{}, + }, + ops: []int{int(internal.OpPushM), operandFiller}, + }, + { + segs: []segment{ + literal("v1"), + }, + ops: []int{int(internal.OpLitPush), 0}, + pool: []string{"v1"}, + }, + { + segs: []segment{ + literal("v1"), + }, + verb: "LOCK", + ops: []int{int(internal.OpLitPush), 0}, + pool: []string{"v1"}, + }, + { + segs: []segment{ + variable{ + path: "name.nested", + segments: []segment{ + wildcard{}, + }, + }, + }, + ops: []int{ + int(internal.OpPush), operandFiller, + int(internal.OpConcatN), 1, + int(internal.OpCapture), 0, + }, + pool: []string{"name.nested"}, + }, + { + segs: []segment{ + literal("obj"), + variable{ + path: "name.nested", + segments: []segment{ + literal("a"), + wildcard{}, + literal("b"), + }, + }, + variable{ + path: "obj", + segments: []segment{ + deepWildcard{}, + }, + }, + }, + ops: []int{ + int(internal.OpLitPush), 0, + int(internal.OpLitPush), 1, + int(internal.OpPush), operandFiller, + int(internal.OpLitPush), 2, + int(internal.OpConcatN), 3, + int(internal.OpCapture), 3, + int(internal.OpPushM), operandFiller, + int(internal.OpConcatN), 1, + int(internal.OpCapture), 0, + }, + pool: []string{"obj", "a", "b", "name.nested"}, + }, + } { + tmpl := template{ + segments: spec.segs, + verb: spec.verb, + } + compiled := tmpl.Compile() + if got, want := compiled.Version, opcodeVersion; got != want { + t.Errorf("tmpl.Compile().Version = %d; want %d; segs=%#v, verb=%q", got, want, spec.segs, spec.verb) + } + if got, want := compiled.OpCodes, spec.ops; !reflect.DeepEqual(got, want) { + t.Errorf("tmpl.Compile().OpCodes = %v; want %v; segs=%#v, verb=%q", got, want, spec.segs, spec.verb) + } + if got, want := compiled.Pool, spec.pool; !reflect.DeepEqual(got, want) { + t.Errorf("tmpl.Compile().Pool = %q; want %q; segs=%#v, verb=%q", got, want, spec.segs, spec.verb) + } + if got, want := compiled.Verb, spec.verb; got != want { + t.Errorf("tmpl.Compile().Verb = %q; want %q; segs=%#v, verb=%q", got, want, spec.segs, spec.verb) + } + } +} diff --git a/protoc-gen-grpc-gateway/httprule/parse.go b/protoc-gen-grpc-gateway/httprule/parse.go index 0f4b4c53d19..d3b0843b315 100644 --- a/protoc-gen-grpc-gateway/httprule/parse.go +++ b/protoc-gen-grpc-gateway/httprule/parse.go @@ -7,6 +7,7 @@ import ( "github.com/golang/glog" ) +// InvalidTemplateError indicates that the path template is not valid. type InvalidTemplateError struct { tmpl string msg string diff --git a/protoc-gen-grpc-gateway/httprule/types.go b/protoc-gen-grpc-gateway/httprule/types.go index d4495b7eb2d..45d733ade51 100644 --- a/protoc-gen-grpc-gateway/httprule/types.go +++ b/protoc-gen-grpc-gateway/httprule/types.go @@ -1,13 +1,18 @@ package httprule +import ( + "fmt" + "strings" +) + type template struct { segments []segment verb string } type segment interface { - // Stringer - // compile() (ops []op) + fmt.Stringer + compile() (ops []op) } type wildcard struct{} @@ -20,3 +25,35 @@ type variable struct { path string segments []segment } + +func (wildcard) String() string { + return "*" +} + +func (deepWildcard) String() string { + return "**" +} + +func (l literal) String() string { + return string(l) +} + +func (v variable) String() string { + var segs []string + for _, s := range v.segments { + segs = append(segs, s.String()) + } + return fmt.Sprintf("{%s=%s}", v.path, strings.Join(segs, "/")) +} + +func (t template) String() string { + var segs []string + for _, s := range t.segments { + segs = append(segs, s.String()) + } + str := strings.Join(segs, "/") + if t.verb != "" { + str = fmt.Sprintf("%s:%s", str, t.verb) + } + return "/" + str +} diff --git a/protoc-gen-grpc-gateway/httprule/types_test.go b/protoc-gen-grpc-gateway/httprule/types_test.go new file mode 100644 index 00000000000..7ed0c5c26a3 --- /dev/null +++ b/protoc-gen-grpc-gateway/httprule/types_test.go @@ -0,0 +1,91 @@ +package httprule + +import ( + "fmt" + "testing" +) + +func TestTemplateStringer(t *testing.T) { + for _, spec := range []struct { + segs []segment + want string + }{ + { + segs: []segment{ + literal("v1"), + }, + want: "/v1", + }, + { + segs: []segment{ + wildcard{}, + }, + want: "/*", + }, + { + segs: []segment{ + deepWildcard{}, + }, + want: "/**", + }, + { + segs: []segment{ + variable{ + path: "name", + segments: []segment{ + literal("a"), + }, + }, + }, + want: "/{name=a}", + }, + { + segs: []segment{ + variable{ + path: "name", + segments: []segment{ + literal("a"), + wildcard{}, + literal("b"), + }, + }, + }, + want: "/{name=a/*/b}", + }, + { + segs: []segment{ + literal("v1"), + variable{ + path: "name", + segments: []segment{ + literal("a"), + wildcard{}, + literal("b"), + }, + }, + literal("c"), + variable{ + path: "field.nested", + segments: []segment{ + wildcard{}, + literal("d"), + }, + }, + wildcard{}, + literal("e"), + deepWildcard{}, + }, + want: "/v1/{name=a/*/b}/c/{field.nested=*/d}/*/e/**", + }, + } { + tmpl := template{segments: spec.segs} + if got, want := tmpl.String(), spec.want; got != want { + t.Errorf("%#v.String() = %q; want %q", tmpl, got, want) + } + + tmpl.verb = "LOCK" + if got, want := tmpl.String(), fmt.Sprintf("%s:LOCK", spec.want); got != want { + t.Errorf("%#v.String() = %q; want %q", tmpl, got, want) + } + } +} From 2b416096d09d6fa52e54dd4cc59b7ae8bafa30f0 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 27 Apr 2015 19:17:09 +0900 Subject: [PATCH 09/28] Fix golint and go tool vet errors --- protoc-gen-grpc-gateway/httprule/parse.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protoc-gen-grpc-gateway/httprule/parse.go b/protoc-gen-grpc-gateway/httprule/parse.go index d3b0843b315..1213a6edefa 100644 --- a/protoc-gen-grpc-gateway/httprule/parse.go +++ b/protoc-gen-grpc-gateway/httprule/parse.go @@ -17,7 +17,8 @@ func (e InvalidTemplateError) Error() string { return fmt.Sprintf("%s: %s", e.msg, e.tmpl) } -func Parse(tmpl string) (template, error) { +// Parse parses the string representation of path template +func Parse(tmpl string) (Compiler, error) { if !strings.HasPrefix(tmpl, "/") { return template{}, InvalidTemplateError{tmpl: tmpl, msg: "no leading /"} } @@ -203,7 +204,6 @@ func (p *parser) fieldPath() (string, error) { } components = append(components, c) } - return strings.Join(components, "."), nil } // A termType is a type of terminal symbols. From 0ad57f79e2c1e6e649833bd749c91b854ba63c96 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 27 Apr 2015 20:05:55 +0900 Subject: [PATCH 10/28] Export bound fields from httprule.Template --- protoc-gen-grpc-gateway/httprule/compile.go | 11 +++++++++-- protoc-gen-grpc-gateway/httprule/compile_test.go | 14 ++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/protoc-gen-grpc-gateway/httprule/compile.go b/protoc-gen-grpc-gateway/httprule/compile.go index 08146dee2b5..8339b896201 100644 --- a/protoc-gen-grpc-gateway/httprule/compile.go +++ b/protoc-gen-grpc-gateway/httprule/compile.go @@ -18,6 +18,8 @@ type Template struct { Pool []string // Verb is a VERB part in the template. Verb string + // Fields is a list of field paths bound in this template. + Fields []string } // Compiler compiles internal representation of path templates into marshallable operations. @@ -82,8 +84,9 @@ func (t template) Compile() Template { } var ( - ops []int - pool []string + ops []int + pool []string + fields []string ) consts := make(map[string]int) for _, op := range rawOps { @@ -97,11 +100,15 @@ func (t template) Compile() Template { } ops = append(ops, consts[op.str]) } + if op.code == internal.OpCapture { + fields = append(fields, op.str) + } } return Template{ Version: opcodeVersion, OpCodes: ops, Pool: pool, Verb: t.verb, + Fields: fields, } } diff --git a/protoc-gen-grpc-gateway/httprule/compile_test.go b/protoc-gen-grpc-gateway/httprule/compile_test.go index fa05684d33c..d4e8b3e7d12 100644 --- a/protoc-gen-grpc-gateway/httprule/compile_test.go +++ b/protoc-gen-grpc-gateway/httprule/compile_test.go @@ -16,8 +16,9 @@ func TestCompile(t *testing.T) { segs []segment verb string - ops []int - pool []string + ops []int + pool []string + fields []string }{ {}, { @@ -61,7 +62,8 @@ func TestCompile(t *testing.T) { int(internal.OpConcatN), 1, int(internal.OpCapture), 0, }, - pool: []string{"name.nested"}, + pool: []string{"name.nested"}, + fields: []string{"name.nested"}, }, { segs: []segment{ @@ -92,7 +94,8 @@ func TestCompile(t *testing.T) { int(internal.OpConcatN), 1, int(internal.OpCapture), 0, }, - pool: []string{"obj", "a", "b", "name.nested"}, + pool: []string{"obj", "a", "b", "name.nested"}, + fields: []string{"name.nested", "obj"}, }, } { tmpl := template{ @@ -112,5 +115,8 @@ func TestCompile(t *testing.T) { if got, want := compiled.Verb, spec.verb; got != want { t.Errorf("tmpl.Compile().Verb = %q; want %q; segs=%#v, verb=%q", got, want, spec.segs, spec.verb) } + if got, want := compiled.Fields, spec.fields; !reflect.DeepEqual(got, want) { + t.Errorf("tmpl.Compile().Fields = %q; want %q; segs=%#v, verb=%q", got, want, spec.segs, spec.verb) + } } } From d8bcb1100e79de87f539e810c05515c67501acf4 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Tue, 28 Apr 2015 21:43:38 +0900 Subject: [PATCH 11/28] Add Pattern-based request multiplexer --- runtime/mux.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 runtime/mux.go diff --git a/runtime/mux.go b/runtime/mux.go new file mode 100644 index 00000000000..0fbd365c640 --- /dev/null +++ b/runtime/mux.go @@ -0,0 +1,79 @@ +package runtime + +import ( + "net/http" + "strings" + + "github.com/golang/glog" +) + +// A HandlerFunc handles a specific pair of path pattern and HTTP method. +type HandlerFunc func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) + +// ServeMux is a request multiplexer for grpc-gateway. +// It matches http requests to patterns and invokes the corresponding handler. +type ServeMux struct { + // handlers maps HTTP method to a list of handlers. + handlers map[string][]handler +} + +// NewServeMux returns a new MuxHandler whose internal mapping is empty. +func NewServeMux() *ServeMux { + return &ServeMux{ + handlers: make(map[string][]handler), + } +} + +// Handle associates "h" to the pair of HTTP method and path pattern. +func (s *ServeMux) Handle(meth string, pat Pattern, h HandlerFunc) { + s.handlers[meth] = append(s.handlers[meth], handler{pat: pat, h: h}) +} + +// ServeHTTP dispatches the request to the first handler whose pattern matches to r.Method and r.Path. +func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + if !strings.HasPrefix(path, "/") { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + components := strings.Split(path[1:], "/") + l := len(components) + var verb string + if idx := strings.LastIndex(components[l-1], ":"); idx == 0 { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } else if idx > 0 { + c := components[l-1] + verb, components[l-1] = c[:idx], c[idx+1:] + } + + for _, h := range s.handlers[r.Method] { + pathParams, err := h.pat.Match(components, verb) + if err != nil { + glog.V(3).Infof("path mismatch: %q to %q", path, h.pat) + continue + } + h.h(w, r, pathParams) + return + } + + // lookup other methods to determine if it is MethodNotAllowed + for m, handlers := range s.handlers { + if m == r.Method { + continue + } + for _, h := range handlers { + if _, err := h.pat.Match(components, verb); err == nil { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + } + } + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) +} + +type handler struct { + pat Pattern + h HandlerFunc +} From f709571e38c297cd1ad86b16984688fbcb236489 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 10:47:57 +0900 Subject: [PATCH 12/28] Add intermediate representation of services and messages --- protoc-gen-grpc-gateway/descriptor/name.go | 18 + .../descriptor/registry.go | 204 +++++ .../descriptor/registry_test.go | 346 ++++++++ .../descriptor/services.go | 255 ++++++ .../descriptor/services_test.go | 803 ++++++++++++++++++ protoc-gen-grpc-gateway/descriptor/types.go | 274 ++++++ .../descriptor/types_test.go | 198 +++++ 7 files changed, 2098 insertions(+) create mode 100644 protoc-gen-grpc-gateway/descriptor/name.go create mode 100644 protoc-gen-grpc-gateway/descriptor/registry.go create mode 100644 protoc-gen-grpc-gateway/descriptor/registry_test.go create mode 100644 protoc-gen-grpc-gateway/descriptor/services.go create mode 100644 protoc-gen-grpc-gateway/descriptor/services_test.go create mode 100644 protoc-gen-grpc-gateway/descriptor/types.go create mode 100644 protoc-gen-grpc-gateway/descriptor/types_test.go diff --git a/protoc-gen-grpc-gateway/descriptor/name.go b/protoc-gen-grpc-gateway/descriptor/name.go new file mode 100644 index 00000000000..78234461a73 --- /dev/null +++ b/protoc-gen-grpc-gateway/descriptor/name.go @@ -0,0 +1,18 @@ +package descriptor + +import ( + "regexp" + "strings" +) + +var ( + upperPattern = regexp.MustCompile("[A-Z]") +) + +func toCamel(str string) string { + var components []string + for _, c := range strings.Split(str, "_") { + components = append(components, strings.Title(strings.ToLower(c))) + } + return strings.Join(components, "") +} diff --git a/protoc-gen-grpc-gateway/descriptor/registry.go b/protoc-gen-grpc-gateway/descriptor/registry.go new file mode 100644 index 00000000000..27a67338934 --- /dev/null +++ b/protoc-gen-grpc-gateway/descriptor/registry.go @@ -0,0 +1,204 @@ +package descriptor + +import ( + "fmt" + "path" + "path/filepath" + "strings" + + "github.com/golang/glog" + descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" + plugin "github.com/golang/protobuf/protoc-gen-go/plugin" +) + +// Registry is a registry of information extracted from plugin.CodeGeneratorRequest. +type Registry struct { + // msgs is a mapping from fully-qualified message name to descriptor + msgs map[string]*Message + + // files is a mapping from file path to descriptor + files map[string]*File + + // prefix is a prefix to be inserted to golang pacakge paths generated from proto package names. + prefix string + + // pkgMap is a user-specified mapping from file path to proto package. + pkgMap map[string]string + + // pkgAliases is a mapping from package aliases to package paths in go which are already taken. + pkgAliases map[string]string +} + +// NewRegistry returns a new Registry. +func NewRegistry() *Registry { + return &Registry{ + msgs: make(map[string]*Message), + files: make(map[string]*File), + pkgMap: make(map[string]string), + pkgAliases: map[string]string{ + // TODO(yugui) Move this initialization to generators. + "json": "encoding/json", + "io": "io", + "http": "net/http", + "runtime": "runtime", + "glog": "github.com/golang/glog", + "proto": "github.com/golang/protobuf/proto", + "context": "golang.org/x/net/context", + "grpc": "google.golang.org/grpc", + "codes": "google.golang.org/grpc/codes", + }, + } +} + +// Load loads definitions of services, methods, messages and fields from "req". +func (r *Registry) Load(req *plugin.CodeGeneratorRequest) error { + for _, file := range req.GetProtoFile() { + r.loadFile(file) + } + for _, target := range req.FileToGenerate { + if err := r.loadServices(target); err != nil { + return err + } + } + return nil +} + +// loadFile loads messages and fiels from "file". +// It does not loads services and methods in "file". You need to call +// loadServices after loadFiles is called for all files to load services and methods. +func (r *Registry) loadFile(file *descriptor.FileDescriptorProto) { + pkg := GoPackage{ + Path: r.goPackagePath(file), + Name: defaultGoPackageName(file), + } + if err := r.ReserveGoPackageAlias(pkg.Name, pkg.Path); err != nil { + for i := 0; ; i++ { + alias := fmt.Sprintf("%s_%d", pkg.Name, i) + if err := r.ReserveGoPackageAlias(alias, pkg.Path); err == nil { + pkg.Alias = alias + break + } + } + } + f := &File{ + FileDescriptorProto: file, + GoPkg: pkg, + } + + r.files[file.GetName()] = f + r.registerMsg(f, nil, file.GetMessageType()) +} + +func (r *Registry) registerMsg(file *File, outerPath []string, msgs []*descriptor.DescriptorProto) { + for _, md := range msgs { + m := &Message{ + File: file, + Outers: outerPath, + DescriptorProto: md, + } + for _, fd := range md.GetField() { + m.Fields = append(m.Fields, &Field{ + Message: m, + FieldDescriptorProto: fd, + }) + } + file.Messages = append(file.Messages, m) + r.msgs[m.FQMN()] = m + glog.Infof("register name: %s", m.FQMN()) + + var outers []string + outers = append(outers, outerPath...) + outers = append(outers, m.GetName()) + r.registerMsg(file, outers, m.GetNestedType()) + } +} + +// LookupMsg looks up a message type by "name". +// It tries to resolve "name" from "location" if "name" is a relative message name. +func (r *Registry) LookupMsg(location, name string) (*Message, error) { + glog.Infof("lookup %s from %s", name, location) + if strings.HasPrefix(name, ".") { + m, ok := r.msgs[name] + if !ok { + return nil, fmt.Errorf("no message found: %s", name) + } + return m, nil + } + + if !strings.HasPrefix(location, ".") { + location = fmt.Sprintf(".%s", location) + } + components := strings.Split(location, ".") + for len(components) > 0 { + fqmn := strings.Join(append(components, name), ".") + if m, ok := r.msgs[fqmn]; ok { + return m, nil + } + components = components[:len(components)-1] + } + return nil, fmt.Errorf("no message found: %s", name) +} + +func (r *Registry) LookupFile(name string) (*File, error) { + f, ok := r.files[name] + if !ok { + return nil, fmt.Errorf("no such file given: %s", name) + } + return f, nil +} + +// AddPkgMap adds a mapping from a .proto file to proto package name. +func (r *Registry) AddPkgMap(file, protoPkg string) { + r.pkgMap[file] = protoPkg +} + +// SetPrefix registeres the perfix to be added to go package paths generated from proto package names. +func (r *Registry) SetPrefix(prefix string) { + r.prefix = prefix +} + +// ReserveGoPackageAlias reserves the unique alias of go package. +// If succeeded, the alias will be never used for other packages in generated go files. +// If failed, the alias is already taken by another package, so you need to use another +// alias for the package in your go files. +func (r *Registry) ReserveGoPackageAlias(alias, pkgpath string) error { + if taken, ok := r.pkgAliases[alias]; ok { + if taken == pkgpath { + return nil + } + return fmt.Errorf("package name %s is already taken. Use another alias", alias) + } + r.pkgAliases[alias] = pkgpath + return nil +} + +// goPackagePath returns the go package path which go files generated from "f" should have. +// It respects the mapping registered by AddPkgMap if exists. Or it generates a path from +// the file name of "f" if otherwise. +func (r *Registry) goPackagePath(f *descriptor.FileDescriptorProto) string { + name := f.GetName() + if pkg, ok := r.pkgMap[name]; ok { + return path.Join(r.prefix, pkg) + } + + ext := filepath.Ext(name) + if ext == ".protodevel" || ext == ".proto" { + name = strings.TrimSuffix(name, ext) + } + return path.Join(r.prefix, fmt.Sprintf("%s.pb", name)) +} + +// defaultGoPackageName returns the default go package name to be used for go files generated from "f". +// You might need to use an unique alias for the package when you import it. Use ReserveGoPackageAlias to get a unique alias. +func defaultGoPackageName(f *descriptor.FileDescriptorProto) string { + if f.Options != nil && f.Options.GoPackage != nil { + return f.Options.GetGoPackage() + } + + if f.Package == nil { + base := filepath.Base(f.GetName()) + ext := filepath.Ext(base) + return strings.TrimSuffix(base, ext) + } + return strings.Replace(f.GetPackage(), ".", "_", -1) +} diff --git a/protoc-gen-grpc-gateway/descriptor/registry_test.go b/protoc-gen-grpc-gateway/descriptor/registry_test.go new file mode 100644 index 00000000000..5261f53f2d6 --- /dev/null +++ b/protoc-gen-grpc-gateway/descriptor/registry_test.go @@ -0,0 +1,346 @@ +package descriptor + +import ( + "testing" + + "github.com/golang/protobuf/proto" + descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" +) + +func load(t *testing.T, reg *Registry, src string) *descriptor.FileDescriptorProto { + var file descriptor.FileDescriptorProto + if err := proto.UnmarshalText(src, &file); err != nil { + t.Fatalf("proto.UnmarshalText(%s, &file) failed with %v; want success", err) + } + reg.loadFile(&file) + return &file +} + +func TestLoadFile(t *testing.T) { + reg := NewRegistry() + fd := load(t, reg, ` + name: 'example.proto' + package: 'example' + message_type < + name: 'ExampleMessage' + field < + name: 'str' + label: LABEL_OPTIONAL + type: TYPE_STRING + number: 1 + > + > + `) + + file := reg.files["example.proto"] + if file == nil { + t.Errorf("reg.files[%q] = nil; want non-nil", "example.proto") + return + } + wantPkg := GoPackage{Path: "example.pb", Name: "example"} + if got, want := file.GoPkg, wantPkg; got != want { + t.Errorf("file.GoPkg = %#v; want %#v", got, want) + } + + msg, err := reg.LookupMsg("", ".example.ExampleMessage") + if err != nil { + t.Errorf("reg.LookupMsg(%q, %q)) failed with %v; want success", "", ".example.ExampleMessage") + return + } + if got, want := msg.DescriptorProto, fd.MessageType[0]; got != want { + t.Errorf("reg.lookupMsg(%q, %q).DescriptorProto = %#v; want %#v", got, want) + } + if got, want := msg.File, file; got != want { + t.Errorf("msg.File = %v; want %v", got, want) + } + if got := msg.Outers; got != nil { + t.Errorf("msg.Outers = %v; want %v", got, nil) + } + if got, want := len(msg.Fields), 1; got != want { + t.Errorf("len(msg.Fields) = %d; want %d", got, want) + } else if got, want := msg.Fields[0].FieldDescriptorProto, fd.MessageType[0].Field[0]; got != want { + t.Errorf("msg.Fields[0].FieldDescriptorProto = %v; want %v", got, want) + } else if got, want := msg.Fields[0].Message, msg; got != want { + t.Errorf("msg.Fields[0].Message = %v; want %v", got, want) + } + + if got, want := len(file.Messages), 1; got != want { + t.Errorf("file.Meeesages = %#v; want %#v", file.Messages, []*Message{msg}) + } + if got, want := file.Messages[0], msg; got != want { + t.Errorf("file.Meeesages[0] = %v; want %v", got, want) + } +} + +func TestLoadFileNestedPackage(t *testing.T) { + reg := NewRegistry() + load(t, reg, ` + name: 'example.proto' + package: 'example.nested.nested2' + `) + + file := reg.files["example.proto"] + if file == nil { + t.Errorf("reg.files[%q] = nil; want non-nil", "example.proto") + return + } + wantPkg := GoPackage{Path: "example.pb", Name: "example_nested_nested2"} + if got, want := file.GoPkg, wantPkg; got != want { + t.Errorf("file.GoPkg = %#v; want %#v", got, want) + } +} + +func TestLoadFileWithDir(t *testing.T) { + reg := NewRegistry() + load(t, reg, ` + name: 'path/to/example.proto' + package: 'example' + `) + + file := reg.files["path/to/example.proto"] + if file == nil { + t.Errorf("reg.files[%q] = nil; want non-nil", "example.proto") + return + } + wantPkg := GoPackage{Path: "path/to/example.pb", Name: "example"} + if got, want := file.GoPkg, wantPkg; got != want { + t.Errorf("file.GoPkg = %#v; want %#v", got, want) + } +} + +func TestLoadFileWithoutPackage(t *testing.T) { + reg := NewRegistry() + load(t, reg, ` + name: 'path/to/example_file.proto' + `) + + file := reg.files["path/to/example_file.proto"] + if file == nil { + t.Errorf("reg.files[%q] = nil; want non-nil", "example.proto") + return + } + wantPkg := GoPackage{Path: "path/to/example_file.pb", Name: "example_file"} + if got, want := file.GoPkg, wantPkg; got != want { + t.Errorf("file.GoPkg = %#v; want %#v", got, want) + } +} + +func TestLoadFileWithMapping(t *testing.T) { + reg := NewRegistry() + reg.AddPkgMap("path/to/example.proto", "example.com/proj/example/proto") + load(t, reg, ` + name: 'path/to/example.proto' + package: 'example' + `) + + file := reg.files["path/to/example.proto"] + if file == nil { + t.Errorf("reg.files[%q] = nil; want non-nil", "example.proto") + return + } + wantPkg := GoPackage{Path: "example.com/proj/example/proto", Name: "example"} + if got, want := file.GoPkg, wantPkg; got != want { + t.Errorf("file.GoPkg = %#v; want %#v", got, want) + } +} + +func TestLoadFileWithPackageNameCollision(t *testing.T) { + reg := NewRegistry() + load(t, reg, ` + name: 'path/to/another.proto' + package: 'example' + `) + load(t, reg, ` + name: 'path/to/example.proto' + package: 'example' + `) + if err := reg.ReserveGoPackageAlias("ioutil", "io/ioutil"); err != nil { + t.Fatalf("reg.ReserveGoPackageAlias(%q) failed with %v; want success", "ioutil", err) + } + load(t, reg, ` + name: 'path/to/ioutil.proto' + package: 'ioutil' + `) + + file := reg.files["path/to/another.proto"] + if file == nil { + t.Errorf("reg.files[%q] = nil; want non-nil", "path/to/another.proto") + return + } + wantPkg := GoPackage{Path: "path/to/another.pb", Name: "example"} + if got, want := file.GoPkg, wantPkg; got != want { + t.Errorf("file.GoPkg = %#v; want %#v", got, want) + } + + file = reg.files["path/to/example.proto"] + if file == nil { + t.Errorf("reg.files[%q] = nil; want non-nil", "path/to/example.proto") + return + } + wantPkg = GoPackage{Path: "path/to/example.pb", Name: "example", Alias: "example_0"} + if got, want := file.GoPkg, wantPkg; got != want { + t.Errorf("file.GoPkg = %#v; want %#v", got, want) + } + + file = reg.files["path/to/ioutil.proto"] + if file == nil { + t.Errorf("reg.files[%q] = nil; want non-nil", "path/to/ioutil.proto") + return + } + wantPkg = GoPackage{Path: "path/to/ioutil.pb", Name: "ioutil", Alias: "ioutil_0"} + if got, want := file.GoPkg, wantPkg; got != want { + t.Errorf("file.GoPkg = %#v; want %#v", got, want) + } +} + +func TestLoadFileWithIdenticalGoPkg(t *testing.T) { + reg := NewRegistry() + reg.AddPkgMap("path/to/another.proto", "example.com/example") + reg.AddPkgMap("path/to/example.proto", "example.com/example") + load(t, reg, ` + name: 'path/to/another.proto' + package: 'example' + `) + load(t, reg, ` + name: 'path/to/example.proto' + package: 'example' + `) + + file := reg.files["path/to/example.proto"] + if file == nil { + t.Errorf("reg.files[%q] = nil; want non-nil", "example.proto") + return + } + wantPkg := GoPackage{Path: "example.com/example", Name: "example"} + if got, want := file.GoPkg, wantPkg; got != want { + t.Errorf("file.GoPkg = %#v; want %#v", got, want) + } + + file = reg.files["path/to/another.proto"] + if file == nil { + t.Errorf("reg.files[%q] = nil; want non-nil", "example.proto") + return + } + wantPkg = GoPackage{Path: "example.com/example", Name: "example"} + if got, want := file.GoPkg, wantPkg; got != want { + t.Errorf("file.GoPkg = %#v; want %#v", got, want) + } +} + +func TestLoadFileWithPrefix(t *testing.T) { + reg := NewRegistry() + reg.SetPrefix("third_party") + load(t, reg, ` + name: 'path/to/example.proto' + package: 'example' + `) + + file := reg.files["path/to/example.proto"] + if file == nil { + t.Errorf("reg.files[%q] = nil; want non-nil", "example.proto") + return + } + wantPkg := GoPackage{Path: "third_party/path/to/example.pb", Name: "example"} + if got, want := file.GoPkg, wantPkg; got != want { + t.Errorf("file.GoPkg = %#v; want %#v", got, want) + } +} + +func TestLookupMsgWithoutPackage(t *testing.T) { + reg := NewRegistry() + fd := load(t, reg, ` + name: 'example.proto' + message_type < + name: 'ExampleMessage' + field < + name: 'str' + label: LABEL_OPTIONAL + type: TYPE_STRING + number: 1 + > + > + `) + + msg, err := reg.LookupMsg("", ".ExampleMessage") + if err != nil { + t.Errorf("reg.LookupMsg(%q, %q)) failed with %v; want success", "", ".ExampleMessage") + return + } + if got, want := msg.DescriptorProto, fd.MessageType[0]; got != want { + t.Errorf("reg.lookupMsg(%q, %q).DescriptorProto = %#v; want %#v", got, want) + } +} + +func TestLookupMsgWithNestedPackage(t *testing.T) { + reg := NewRegistry() + fd := load(t, reg, ` + name: 'example.proto' + package: 'nested.nested2.mypackage' + message_type < + name: 'ExampleMessage' + field < + name: 'str' + label: LABEL_OPTIONAL + type: TYPE_STRING + number: 1 + > + > + `) + + for _, name := range []string{ + "nested.nested2.mypackage.ExampleMessage", + "nested2.mypackage.ExampleMessage", + "mypackage.ExampleMessage", + "ExampleMessage", + } { + msg, err := reg.LookupMsg("nested.nested2.mypackage", name) + if err != nil { + t.Errorf("reg.LookupMsg(%q, %q)) failed with %v; want success", ".nested.nested2.mypackage", name, err) + return + } + if got, want := msg.DescriptorProto, fd.MessageType[0]; got != want { + t.Errorf("reg.lookupMsg(%q, %q).DescriptorProto = %#v; want %#v", ".nested.nested2.mypackage", name, got, want) + } + } + + for _, loc := range []string{ + ".nested.nested2.mypackage", + "nested.nested2.mypackage", + ".nested.nested2", + "nested.nested2", + ".nested", + "nested", + ".", + "", + "somewhere.else", + } { + name := "nested.nested2.mypackage.ExampleMessage" + msg, err := reg.LookupMsg(loc, name) + if err != nil { + t.Errorf("reg.LookupMsg(%q, %q)) failed with %v; want success", loc, name, err) + return + } + if got, want := msg.DescriptorProto, fd.MessageType[0]; got != want { + t.Errorf("reg.lookupMsg(%q, %q).DescriptorProto = %#v; want %#v", loc, name, got, want) + } + } + + for _, loc := range []string{ + ".nested.nested2.mypackage", + "nested.nested2.mypackage", + ".nested.nested2", + "nested.nested2", + ".nested", + "nested", + } { + name := "nested2.mypackage.ExampleMessage" + msg, err := reg.LookupMsg(loc, name) + if err != nil { + t.Errorf("reg.LookupMsg(%q, %q)) failed with %v; want success", loc, name, err) + return + } + if got, want := msg.DescriptorProto, fd.MessageType[0]; got != want { + t.Errorf("reg.lookupMsg(%q, %q).DescriptorProto = %#v; want %#v", loc, name, got, want) + } + } +} diff --git a/protoc-gen-grpc-gateway/descriptor/services.go b/protoc-gen-grpc-gateway/descriptor/services.go new file mode 100644 index 00000000000..76cf2f39efc --- /dev/null +++ b/protoc-gen-grpc-gateway/descriptor/services.go @@ -0,0 +1,255 @@ +package descriptor + +import ( + "fmt" + "strings" + + "github.com/gengo/grpc-gateway/protoc-gen-grpc-gateway/httprule" + options "github.com/gengo/grpc-gateway/third_party/googleapis/google/api" + "github.com/golang/glog" + "github.com/golang/protobuf/proto" + descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" +) + +// loadServices registers services and their methods from "targetFile" to "r". +// It must be called after loadFile is called for all files so that loadServices +// can resolve names of message types and their fields. +func (r *Registry) loadServices(targetFile string) error { + file := r.files[targetFile] + if file == nil { + return fmt.Errorf("no such file: %s", targetFile) + } + var svcs []*Service + for _, sd := range file.GetService() { + svc := &Service{ + File: file, + ServiceDescriptorProto: sd, + } + for _, md := range sd.GetMethod() { + opts, err := extractAPIOptions(md) + if err != nil { + glog.Errorf("Failed to extract ApiMethodOptions from %s.%s: %v", svc.GetName(), md.GetName(), err) + return err + } + if opts == nil { + glog.V(1).Infof("Skip non-target method: %s.%s", svc.GetName(), md.GetName()) + continue + } + meth, err := r.newMethod(svc, md, opts) + if err != nil { + return err + } + svc.Methods = append(svc.Methods, meth) + } + if len(svc.Methods) == 0 { + continue + } + svcs = append(svcs, svc) + } + file.Services = svcs + return nil +} + +func (r *Registry) newMethod(svc *Service, md *descriptor.MethodDescriptorProto, opts *options.HttpRule) (*Method, error) { + var ( + httpMethod string + pathTemplate string + ) + switch { + case opts.Get != "": + httpMethod = "GET" + pathTemplate = opts.Get + if opts.Body != "" { + return nil, fmt.Errorf("needs request body even though http method is GET: %s", md.GetName()) + } + + case opts.Put != "": + httpMethod = "PUT" + pathTemplate = opts.Put + + case opts.Post != "": + httpMethod = "POST" + pathTemplate = opts.Post + + case opts.Delete != "": + httpMethod = "DELETE" + pathTemplate = opts.Delete + if opts.Body != "" { + return nil, fmt.Errorf("needs request body even though http method is DELETE: %s", md.GetName()) + } + + case opts.Patch != "": + httpMethod = "PATCH" + pathTemplate = opts.Patch + + case opts.Custom != nil: + httpMethod = opts.Custom.Kind + pathTemplate = opts.Custom.Path + + default: + glog.Errorf("No pattern specified in google.api.HttpRule: %s", md.GetName()) + return nil, fmt.Errorf("none of pattern specified") + } + + parsed, err := httprule.Parse(pathTemplate) + if err != nil { + return nil, err + } + tmpl := parsed.Compile() + + if md.GetClientStreaming() && len(tmpl.Fields) > 0 { + return nil, fmt.Errorf("cannot use path parameter in client streaming") + } + + requestType, err := r.LookupMsg(svc.File.GetPackage(), md.GetInputType()) + if err != nil { + return nil, err + } + responseType, err := r.LookupMsg(svc.File.GetPackage(), md.GetOutputType()) + if err != nil { + return nil, err + } + + meth := &Method{ + Service: svc, + MethodDescriptorProto: md, + PathTmpl: tmpl, + HTTPMethod: httpMethod, + RequestType: requestType, + ResponseType: responseType, + } + + for _, f := range tmpl.Fields { + param, err := r.newParam(meth, f) + if err != nil { + return nil, err + } + meth.PathParams = append(meth.PathParams, param) + } + + // TODO(yugui) Handle query params + + meth.Body, err = r.newBody(meth, opts.Body) + if err != nil { + return nil, err + } + + return meth, nil +} + +func extractAPIOptions(meth *descriptor.MethodDescriptorProto) (*options.HttpRule, error) { + if meth.Options == nil { + return nil, nil + } + if !proto.HasExtension(meth.Options, options.E_Http) { + return nil, nil + } + ext, err := proto.GetExtension(meth.Options, options.E_Http) + if err != nil { + return nil, err + } + opts, ok := ext.(*options.HttpRule) + if !ok { + return nil, fmt.Errorf("extension is %T; want an HttpRule", ext) + } + return opts, nil +} + +func (r *Registry) newParam(meth *Method, path string) (Parameter, error) { + msg := meth.RequestType + fields, err := r.resolveFiledPath(msg, path) + if err != nil { + return Parameter{}, err + } + l := len(fields) + if l == 0 { + return Parameter{}, fmt.Errorf("invalid field access list for %s", path) + } + target := fields[l-1].Target + switch target.GetType() { + case descriptor.FieldDescriptorProto_TYPE_MESSAGE, descriptor.FieldDescriptorProto_TYPE_GROUP: + return Parameter{}, fmt.Errorf("aggregate type %s in parameter of %s.%s: %s", target.Type, meth.Service.GetName(), meth.GetName(), path) + } + return Parameter{ + FieldPath: FieldPath(fields), + Method: meth, + Target: fields[l-1].Target, + }, nil +} + +func (r *Registry) newBody(meth *Method, path string) (*Body, error) { + msg := meth.RequestType + switch path { + case "": + return nil, nil + case "*": + return &Body{ + DecoderFactoryExpr: "json.NewDecoder", + DecoderImports: []GoPackage{ + { + Path: "encoding/json", + Name: "json", + }, + }, + FieldPath: nil, + }, nil + } + fields, err := r.resolveFiledPath(msg, path) + if err != nil { + return nil, err + } + return &Body{ + DecoderFactoryExpr: "json.NewDecoder", + DecoderImports: []GoPackage{ + { + Path: "encoding/json", + Name: "json", + }, + }, + FieldPath: FieldPath(fields), + }, nil +} + +// lookupField looks up a field named "name" within "msg". +// It returns nil if no such field found. +func lookupField(msg *Message, name string) *Field { + for _, f := range msg.Fields { + if f.GetName() == name { + return f + } + } + return nil +} + +// resolveFieldPath resolves "path" into a list of fieldDescriptor, starting from "msg". +func (r *Registry) resolveFiledPath(msg *Message, path string) ([]FieldPathComponent, error) { + if path == "" { + return nil, nil + } + + root := msg + var result []FieldPathComponent + for i, c := range strings.Split(path, ".") { + if i > 0 { + f := result[i-1].Target + switch f.GetType() { + case descriptor.FieldDescriptorProto_TYPE_MESSAGE, descriptor.FieldDescriptorProto_TYPE_GROUP: + var err error + msg, err = r.LookupMsg(msg.FQMN(), f.GetTypeName()) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("not an aggregate type: %s in %s", f.GetName(), path) + } + } + + glog.Infof("Lookup %s in %s", c, msg.FQMN()) + f := lookupField(msg, c) + if f == nil { + return nil, fmt.Errorf("no field %q found in %s", path, root.GetName()) + } + result = append(result, FieldPathComponent{Name: c, Target: f}) + } + return result, nil +} diff --git a/protoc-gen-grpc-gateway/descriptor/services_test.go b/protoc-gen-grpc-gateway/descriptor/services_test.go new file mode 100644 index 00000000000..e06ae61e22e --- /dev/null +++ b/protoc-gen-grpc-gateway/descriptor/services_test.go @@ -0,0 +1,803 @@ +package descriptor + +import ( + "reflect" + "testing" + + "github.com/gengo/grpc-gateway/protoc-gen-grpc-gateway/httprule" + "github.com/golang/protobuf/proto" + descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" +) + +func compilePath(t *testing.T, path string) httprule.Template { + parsed, err := httprule.Parse(path) + if err != nil { + t.Fatalf("httprule.Parse(%q) failed with %v; want success", path, err) + } + return parsed.Compile() +} + +func testExtractServices(t *testing.T, input []*descriptor.FileDescriptorProto, target string, wantSvcs []*Service) { + reg := NewRegistry() + for _, file := range input { + reg.loadFile(file) + } + err := reg.loadServices(target) + if err != nil { + t.Errorf("loadServices(%q) failed with %v; want success; files=%v", target, err, input) + } + + file := reg.files[target] + svcs := file.Services + var i int + for i = 0; i < len(svcs) && i < len(wantSvcs); i++ { + svc, wantSvc := svcs[i], wantSvcs[i] + if got, want := svc.ServiceDescriptorProto, wantSvc.ServiceDescriptorProto; !proto.Equal(got, want) { + t.Errorf("svcs[%d].ServiceDescriptorProto = %v; want %v; input = %v", i, got, want, input) + continue + } + var j int + for j = 0; j < len(svc.Methods) && j < len(wantSvc.Methods); j++ { + meth, wantMeth := svc.Methods[j], wantSvc.Methods[j] + if got, want := meth.MethodDescriptorProto, wantMeth.MethodDescriptorProto; !proto.Equal(got, want) { + t.Errorf("svcs[%d].Methods[%d].MethodDescriptorProto = %v; want %v; input = %v", i, j, got, want, input) + continue + } + if got, want := meth.PathTmpl, wantMeth.PathTmpl; !reflect.DeepEqual(got, want) { + t.Errorf("svcs[%d].Methods[%d].PathTmpl = %#v; want %#v; input = %v", i, j, got, want, input) + } + if got, want := meth.HTTPMethod, wantMeth.HTTPMethod; got != want { + t.Errorf("svcs[%d].Methods[%d].HTTPMethod = %q; want %q; input = %v", i, j, got, want, input) + } + if got, want := meth.RequestType, wantMeth.RequestType; got.FQMN() != want.FQMN() { + t.Errorf("svcs[%d].Methods[%d].RequestType = %s; want %s; input = %v", i, j, got.FQMN(), want.FQMN(), input) + } + if got, want := meth.ResponseType, wantMeth.ResponseType; got.FQMN() != want.FQMN() { + t.Errorf("svcs[%d].Methods[%d].ResponseType = %s; want %s; input = %v", i, j, got.FQMN(), want.FQMN(), input) + } + + var k int + for k = 0; k < len(meth.PathParams) && k < len(wantMeth.PathParams); k++ { + param, wantParam := meth.PathParams[k], wantMeth.PathParams[k] + if got, want := param.FieldPath.String(), wantParam.FieldPath.String(); got != want { + t.Errorf("svcs[%d].Methods[%d].PathParams[%d].FieldPath.String() = %q; want %q; input = %v", i, j, k, got, want, input) + continue + } + for l := 0; l < len(param.FieldPath) && l < len(wantParam.FieldPath); l++ { + field, wantField := param.FieldPath[l].Target, wantParam.FieldPath[l].Target + if got, want := field.FieldDescriptorProto, wantField.FieldDescriptorProto; !proto.Equal(got, want) { + t.Errorf("svcs[%d].Methods[%d].PathParams[%d].FieldPath[%d].Target.FieldDescriptorProto = %v; want %v; input = %v", i, j, k, l, got, want, input) + } + } + } + for ; k < len(meth.PathParams); k++ { + got := meth.PathParams[k].FieldPath.String() + t.Errorf("svcs[%d].Methods[%d].PathParams[%d] = %q; want it to be missing; input = %v", i, j, k, got, input) + } + for ; k < len(wantMeth.PathParams); k++ { + want := wantMeth.PathParams[k].FieldPath.String() + t.Errorf("svcs[%d].Methods[%d].PathParams[%d] missing; want %q; input = %v", i, j, k, want, input) + } + + if got, want := (meth.Body != nil), (wantMeth.Body != nil); got != want { + if got { + t.Errorf("svcs[%d].Methods[%d].Body = %q; want it to be missing; input = %v", i, j, meth.Body.FieldPath.String(), input) + } else { + t.Errorf("svcs[%d].Methods[%d].Body missing; want %q; input = %v", i, j, wantMeth.Body.FieldPath.String(), input) + } + } + } + for ; j < len(svc.Methods); j++ { + got := svc.Methods[j].MethodDescriptorProto + t.Errorf("svcs[%d].Methods[%d] = %v; want it to be missing; input = %v", i, j, got, input) + } + for ; j < len(wantSvc.Methods); j++ { + want := wantSvc.Methods[j].MethodDescriptorProto + t.Errorf("svcs[%d].Methods[%d] missing; want %v; input = %v", i, j, want, input) + } + } + for ; i < len(svcs); i++ { + got := svcs[i].ServiceDescriptorProto + t.Errorf("svcs[%d] = %v; want it to be missing; input = %v", i, got, input) + } + for ; i < len(wantSvcs); i++ { + want := wantSvcs[i].ServiceDescriptorProto + t.Errorf("svcs[%d] missing; want %v; input = %v", i, want, input) + } +} + +func crossLinkFixture(f *File) *File { + for _, m := range f.Messages { + m.File = f + for _, f := range m.Fields { + f.Message = m + } + } + for _, svc := range f.Services { + svc.File = f + for _, m := range svc.Methods { + m.Service = svc + for _, param := range m.PathParams { + param.Method = m + } + for _, param := range m.QueryParams { + param.Method = m + } + } + } + return f +} + +func TestExtractServicesSimple(t *testing.T) { + src := ` + name: "path/to/example.proto", + package: "example" + message_type < + name: "StringMessage" + field < + name: "string" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + > + > + service < + name: "ExampleService" + method < + name: "Echo" + input_type: "StringMessage" + output_type: "StringMessage" + options < + [google.api.http] < + post: "/v1/example/echo" + body: "*" + > + > + > + > + ` + var fd descriptor.FileDescriptorProto + if err := proto.UnmarshalText(src, &fd); err != nil { + t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err) + } + msg := &Message{ + DescriptorProto: fd.MessageType[0], + Fields: []*Field{ + { + FieldDescriptorProto: fd.MessageType[0].Field[0], + }, + }, + } + file := &File{ + FileDescriptorProto: &fd, + GoPkg: GoPackage{ + Path: "path/to/example.pb", + Name: "example_pb", + }, + Messages: []*Message{msg}, + Services: []*Service{ + { + ServiceDescriptorProto: fd.Service[0], + Methods: []*Method{ + { + MethodDescriptorProto: fd.Service[0].Method[0], + PathTmpl: compilePath(t, "/v1/example/echo"), + HTTPMethod: "POST", + RequestType: msg, + ResponseType: msg, + Body: &Body{ + DecoderFactoryExpr: "json.NewDecoder", + DecoderImports: []GoPackage{ + { + Path: "encoding/json", + Name: "json", + }, + }, + FieldPath: nil, + }, + }, + }, + }, + }, + } + + crossLinkFixture(file) + testExtractServices(t, []*descriptor.FileDescriptorProto{&fd}, "path/to/example.proto", file.Services) +} + +func TestExtractServicesCrossPackage(t *testing.T) { + srcs := []string{ + ` + name: "path/to/example.proto", + package: "example" + message_type < + name: "StringMessage" + field < + name: "string" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + > + > + service < + name: "ExampleService" + method < + name: "ToString" + input_type: ".another.example.BoolMessage" + output_type: "StringMessage" + options < + [google.api.http] < + post: "/v1/example/to_s" + body: "*" + > + > + > + > + `, ` + name: "path/to/another/example.proto", + package: "another.example" + message_type < + name: "BoolMessage" + field < + name: "bool" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_BOOL + > + > + `, + } + var fds []*descriptor.FileDescriptorProto + for _, src := range srcs { + var fd descriptor.FileDescriptorProto + if err := proto.UnmarshalText(src, &fd); err != nil { + t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err) + } + fds = append(fds, &fd) + } + stringMsg := &Message{ + DescriptorProto: fds[0].MessageType[0], + Fields: []*Field{ + { + FieldDescriptorProto: fds[0].MessageType[0].Field[0], + }, + }, + } + boolMsg := &Message{ + DescriptorProto: fds[1].MessageType[0], + Fields: []*Field{ + { + FieldDescriptorProto: fds[1].MessageType[0].Field[0], + }, + }, + } + files := []*File{ + { + FileDescriptorProto: fds[0], + GoPkg: GoPackage{ + Path: "path/to/example.pb", + Name: "example_pb", + }, + Messages: []*Message{stringMsg}, + Services: []*Service{ + { + ServiceDescriptorProto: fds[0].Service[0], + Methods: []*Method{ + { + MethodDescriptorProto: fds[0].Service[0].Method[0], + PathTmpl: compilePath(t, "/v1/example/to_s"), + HTTPMethod: "POST", + RequestType: boolMsg, + ResponseType: stringMsg, + Body: &Body{ + DecoderFactoryExpr: "json.NewDecoder", + DecoderImports: []GoPackage{ + { + Path: "encoding/json", + Name: "json", + }, + }, + FieldPath: nil, + }, + }, + }, + }, + }, + }, + { + FileDescriptorProto: fds[1], + GoPkg: GoPackage{ + Path: "path/to/another/example.pb", + Name: "example_pb", + }, + Messages: []*Message{boolMsg}, + }, + } + + for _, file := range files { + crossLinkFixture(file) + } + testExtractServices(t, fds, "path/to/example.proto", files[0].Services) +} + +func TestExtractServicesWithBodyPath(t *testing.T) { + src := ` + name: "path/to/example.proto", + package: "example" + message_type < + name: "OuterMessage" + nested_type < + name: "StringMessage" + field < + name: "string" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + > + > + field < + name: "nested" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: "StringMessage" + > + > + service < + name: "ExampleService" + method < + name: "Echo" + input_type: "OuterMessage" + output_type: "OuterMessage" + options < + [google.api.http] < + post: "/v1/example/echo" + body: "nested" + > + > + > + > + ` + var fd descriptor.FileDescriptorProto + if err := proto.UnmarshalText(src, &fd); err != nil { + t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err) + } + msg := &Message{ + DescriptorProto: fd.MessageType[0], + Fields: []*Field{ + { + FieldDescriptorProto: fd.MessageType[0].Field[0], + }, + }, + } + file := &File{ + FileDescriptorProto: &fd, + GoPkg: GoPackage{ + Path: "path/to/example.pb", + Name: "example_pb", + }, + Messages: []*Message{msg}, + Services: []*Service{ + { + ServiceDescriptorProto: fd.Service[0], + Methods: []*Method{ + { + MethodDescriptorProto: fd.Service[0].Method[0], + PathTmpl: compilePath(t, "/v1/example/echo"), + HTTPMethod: "POST", + RequestType: msg, + ResponseType: msg, + Body: &Body{ + DecoderFactoryExpr: "json.NewDecoder", + DecoderImports: []GoPackage{ + { + Path: "encoding/json", + Name: "json", + }, + }, + FieldPath: FieldPath{ + { + Name: "nested", + Target: msg.Fields[0], + }, + }, + }, + }, + }, + }, + }, + } + + crossLinkFixture(file) + testExtractServices(t, []*descriptor.FileDescriptorProto{&fd}, "path/to/example.proto", file.Services) +} + +func TestExtractServicesWithPathParam(t *testing.T) { + src := ` + name: "path/to/example.proto", + package: "example" + message_type < + name: "StringMessage" + field < + name: "string" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + > + > + service < + name: "ExampleService" + method < + name: "Echo" + input_type: "StringMessage" + output_type: "StringMessage" + options < + [google.api.http] < + get: "/v1/example/echo/{string=*}" + > + > + > + > + ` + var fd descriptor.FileDescriptorProto + if err := proto.UnmarshalText(src, &fd); err != nil { + t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err) + } + msg := &Message{ + DescriptorProto: fd.MessageType[0], + Fields: []*Field{ + { + FieldDescriptorProto: fd.MessageType[0].Field[0], + }, + }, + } + file := &File{ + FileDescriptorProto: &fd, + GoPkg: GoPackage{ + Path: "path/to/example.pb", + Name: "example_pb", + }, + Messages: []*Message{msg}, + Services: []*Service{ + { + ServiceDescriptorProto: fd.Service[0], + Methods: []*Method{ + { + MethodDescriptorProto: fd.Service[0].Method[0], + PathTmpl: compilePath(t, "/v1/example/echo/{string=*}"), + HTTPMethod: "GET", + RequestType: msg, + ResponseType: msg, + PathParams: []Parameter{ + { + FieldPath: FieldPath{ + { + Name: "string", + Target: msg.Fields[0], + }, + }, + Target: msg.Fields[0], + }, + }, + }, + }, + }, + }, + } + + crossLinkFixture(file) + testExtractServices(t, []*descriptor.FileDescriptorProto{&fd}, "path/to/example.proto", file.Services) +} + +func TestExtractServicesWithError(t *testing.T) { + for _, spec := range []struct { + target string + srcs []string + }{ + { + target: "path/to/example.proto", + srcs: []string{ + // message not found + ` + name: "path/to/example.proto", + package: "example" + service < + name: "ExampleService" + method < + name: "Echo" + input_type: "StringMessage" + output_type: "StringMessage" + options < + [google.api.http] < + post: "/v1/example/echo" + body: "*" + > + > + > + > + `, + }, + }, + // body field path not resolved + { + target: "path/to/example.proto", + srcs: []string{` + name: "path/to/example.proto", + package: "example" + message_type < + name: "StringMessage" + field < + name: "string" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + > + > + service < + name: "ExampleService" + method < + name: "Echo" + input_type: "StringMessage" + output_type: "StringMessage" + options < + [google.api.http] < + post: "/v1/example/echo" + body: "bool" + > + > + > + >`, + }, + }, + // param field path not resolved + { + target: "path/to/example.proto", + srcs: []string{ + ` + name: "path/to/example.proto", + package: "example" + message_type < + name: "StringMessage" + field < + name: "string" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + > + > + service < + name: "ExampleService" + method < + name: "Echo" + input_type: "StringMessage" + output_type: "StringMessage" + options < + [google.api.http] < + post: "/v1/example/echo/{bool=*}" + > + > + > + > + `, + }, + }, + // non aggregate type on field path + { + target: "path/to/example.proto", + srcs: []string{ + ` + name: "path/to/example.proto", + package: "example" + message_type < + name: "OuterMessage" + field < + name: "mid" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + > + field < + name: "bool" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_BOOL + > + > + service < + name: "ExampleService" + method < + name: "Echo" + input_type: "OuterMessage" + output_type: "OuterMessage" + options < + [google.api.http] < + post: "/v1/example/echo/{mid.bool=*}" + > + > + > + > + `, + }, + }, + // path param in client streaming + { + target: "path/to/example.proto", + srcs: []string{ + ` + name: "path/to/example.proto", + package: "example" + message_type < + name: "StringMessage" + field < + name: "string" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + > + > + service < + name: "ExampleService" + method < + name: "Echo" + input_type: "StringMessage" + output_type: "StringMessage" + options < + [google.api.http] < + post: "/v1/example/echo/{bool=*}" + > + > + client_streaming: true + > + > + `, + }, + }, + // body for GET + { + target: "path/to/example.proto", + srcs: []string{ + ` + name: "path/to/example.proto", + package: "example" + message_type < + name: "StringMessage" + field < + name: "string" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + > + > + service < + name: "ExampleService" + method < + name: "Echo" + input_type: "StringMessage" + output_type: "StringMessage" + options < + [google.api.http] < + get: "/v1/example/echo" + body: "string" + > + > + > + > + `, + }, + }, + // body for DELETE + { + target: "path/to/example.proto", + srcs: []string{ + ` + name: "path/to/example.proto", + package: "example" + message_type < + name: "StringMessage" + field < + name: "string" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + > + > + service < + name: "ExampleService" + method < + name: "RemoveResource" + input_type: "StringMessage" + output_type: "StringMessage" + options < + [google.api.http] < + delete: "/v1/example/resource" + body: "string" + > + > + > + > + `, + }, + }, + // no pattern specified + { + target: "path/to/example.proto", + srcs: []string{ + ` + name: "path/to/example.proto", + package: "example" + service < + name: "ExampleService" + method < + name: "RemoveResource" + input_type: "StringMessage" + output_type: "StringMessage" + options < + [google.api.http] < + body: "string" + > + > + > + > + `, + }, + }, + // unsupported path parameter type + { + target: "path/to/example.proto", + srcs: []string{` + name: "path/to/example.proto", + package: "example" + message_type < + name: "OuterMessage" + nested_type < + name: "StringMessage" + field < + name: "value" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + > + > + field < + name: "string" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: "StringMessage" + > + > + service < + name: "ExampleService" + method < + name: "Echo" + input_type: "OuterMessage" + output_type: "OuterMessage" + options < + [google.api.http] < + get: "/v1/example/echo/{string=*}" + > + > + > + > + `, + }, + }, + } { + reg := NewRegistry() + + var fds []*descriptor.FileDescriptorProto + for _, src := range spec.srcs { + var fd descriptor.FileDescriptorProto + if err := proto.UnmarshalText(src, &fd); err != nil { + t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err) + } + reg.loadFile(&fd) + fds = append(fds, &fd) + } + err := reg.loadServices(spec.target) + if err == nil { + t.Errorf("loadServices(%q) succeeded; want an error; files=%v", spec.target, spec.srcs) + } + t.Log(err) + } +} diff --git a/protoc-gen-grpc-gateway/descriptor/types.go b/protoc-gen-grpc-gateway/descriptor/types.go new file mode 100644 index 00000000000..e44f821fa42 --- /dev/null +++ b/protoc-gen-grpc-gateway/descriptor/types.go @@ -0,0 +1,274 @@ +package descriptor + +import ( + "fmt" + "strings" + + "github.com/gengo/grpc-gateway/protoc-gen-grpc-gateway/httprule" + descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" +) + +// GoPackage represents a golang package +type GoPackage struct { + // Path is the package path to the package. + Path string + // Name is the package name of the package + Name string + // Alias is an alias of the package unique within the current invokation of grpc-gateway generator. + Alias string +} + +// Standard returns whether the import is a golang standard pacakge. +func (p GoPackage) Standard() bool { + return !strings.Contains(p.Path, ".") +} + +// String returns a string representation of this package in the form of import line in golang. +func (p GoPackage) String() string { + if p.Alias == "" { + return fmt.Sprintf("%q", p.Path) + } + return fmt.Sprintf("%s %q", p.Alias, p.Path) +} + +// File wraps descriptor.FileDescriptorProto for richer features. +type File struct { + *descriptor.FileDescriptorProto + // GoPkg is the go package of the go file generated from this file.. + GoPkg GoPackage + // Messages is the list of messages defined in this file. + Messages []*Message + // Services is the list of services defined in this file. + Services []*Service +} + +// proto2 determines if the syntax of the file is proto2. +func (f *File) proto2() bool { + return f.Syntax == nil || f.GetSyntax() == "proto2" +} + +// Message describes a protocol buffer message types +type Message struct { + // File is the file where the message is defined + File *File + // Outers is a list of outer messages if this message is a nested type. + Outers []string + *descriptor.DescriptorProto + Fields []*Field +} + +// FQMN returns a fully qualified message name of this message. +func (m *Message) FQMN() string { + components := []string{""} + if m.File.Package != nil { + components = append(components, m.File.GetPackage()) + } + components = append(components, m.Outers...) + components = append(components, m.GetName()) + return strings.Join(components, ".") +} + +// GoType returns a go type name for the message type. +// It prefixes the type name with the package alias if +// its belonging package is not "currentPackage". +func (m *Message) GoType(currentPackage string) string { + var components []string + components = append(components, m.Outers...) + components = append(components, m.GetName()) + + name := strings.Join(components, "_") + if m.File.GoPkg.Path == currentPackage { + return name + } + pkg := m.File.GoPkg.Name + if alias := m.File.GoPkg.Alias; alias != "" { + pkg = alias + } + return fmt.Sprintf("%s.%s", pkg, name) +} + +// Service wraps descriptor.ServiceDescriptorProto for richer features. +type Service struct { + // File is the file where this service is defined. + File *File + *descriptor.ServiceDescriptorProto + // Methods is the list of methods defined in this service. + Methods []*Method +} + +// Method wraps descriptor.MethodDescriptorProto for richer features. +type Method struct { + // Service is the service which this method belongs to. + Service *Service + *descriptor.MethodDescriptorProto + + // PathTmpl is path template where this method is mapped to. + PathTmpl httprule.Template + // HTTPMethod is the HTTP method which this method is mapped to. + HTTPMethod string + // RequestType is the message type of requests to this method. + RequestType *Message + // ResponseType is the message type of responses from this method. + ResponseType *Message + // PathParams is the list of parameters provided in HTTP request paths. + PathParams []Parameter + // QueryParam is the list of parameters provided in HTTP query strings. + QueryParams []Parameter + // Body describes parameters provided in HTTP request body. + Body *Body +} + +// Field wraps descriptor.FieldDescriptorProto for richer features. +type Field struct { + // Message is the message type which this field belongs to. + Message *Message + *descriptor.FieldDescriptorProto +} + +// Parameter is a parameter provided in http requests +type Parameter struct { + // FieldPath is a path to a proto field which this parameter is mapped to. + FieldPath + // Target is the proto field which this parameter is mapped to. + Target *Field + // Method is the method which this parameter is used for. + Method *Method +} + +// ConvertFuncExpr returns a go expression of a converter function. +// The converter function converts a string into a value for the parameter. +func (p Parameter) ConvertFuncExpr() (string, error) { + tbl := proto3ConvertFuncs + if p.Target.Message.File.proto2() { + tbl = proto2ConvertFuncs + } + typ := p.Target.GetType() + conv, ok := tbl[typ] + if !ok { + return "", fmt.Errorf("unsupported field type %s of parameter %s in %s.%s", typ, p.FieldPath, p.Method.Service.GetName(), p.Method.GetName()) + } + return conv, nil +} + +// Body describes a http requtest body to be sent to the method. +type Body struct { + // DecoderFactoryExpr is a go expression of a factory function + // which takes a io.Reader and returns a Decoder (unmarshaller). + // TODO(yugui) Extract this to a flag. + DecoderFactoryExpr string + + // DecoderImports is a list of packages to be imported from the + // generated go files so that DecoderFactoryExpr is valid. + // TODO(yugui) Extract this to a flag. + DecoderImports []GoPackage + + // FieldPath is a path to a proto field which the request body is mapped to. + // The request body is mapped to the request type itself if FieldPath is empty. + FieldPath FieldPath +} + +// RHS returns a right-hand-side expression in go to be used to initialize method request object. +// It starts with "msgExpr", which is the go expression of the method request object. +func (b Body) RHS(msgExpr string) string { + return b.FieldPath.RHS(msgExpr) +} + +// FieldPath is a path to a field from a request message. +type FieldPath []FieldPathComponent + +// String returns a string representation of the field path. +func (p FieldPath) String() string { + var components []string + for _, c := range p { + components = append(components, c.Name) + } + return strings.Join(components, ".") +} + +// RHS is a right-hand-side expression in go to be used to assign a value to the target field. +// It starts with "msgExpr", which is the go expression of the method request object. +func (p FieldPath) RHS(msgExpr string) string { + l := len(p) + if l == 0 { + return msgExpr + } + components := []string{msgExpr} + for i, c := range p { + if i == l-1 { + components = append(components, c.RHS()) + continue + } + components = append(components, c.LHS()) + } + return strings.Join(components, ".") +} + +// FieldPathComponent is a path component in FieldPath +type FieldPathComponent struct { + // Name is a name of the proto field which this component corresponds to. + // TODO(yugui) is this necessary? + Name string + // Target is the proto field which this component corresponds to. + Target *Field +} + +// RHS returns a right-hand-side expression in go for this field. +func (c FieldPathComponent) RHS() string { + return toCamel(c.Name) +} + +// LHS returns a left-hand-side expression in go for this field. +func (c FieldPathComponent) LHS() string { + if c.Target.Message.File.proto2() { + return fmt.Sprintf("Get%s()", toCamel(c.Name)) + } + return toCamel(c.Name) +} + +var ( + proto3ConvertFuncs = map[descriptor.FieldDescriptorProto_Type]string{ + descriptor.FieldDescriptorProto_TYPE_DOUBLE: "runtime.Float64", + descriptor.FieldDescriptorProto_TYPE_FLOAT: "runtime.Float32", + descriptor.FieldDescriptorProto_TYPE_INT64: "runtime.Int64", + descriptor.FieldDescriptorProto_TYPE_UINT64: "runtime.Uint64", + descriptor.FieldDescriptorProto_TYPE_INT32: "runtime.Int32", + descriptor.FieldDescriptorProto_TYPE_FIXED64: "runtime.Uint64", + descriptor.FieldDescriptorProto_TYPE_FIXED32: "runtime.Uint32", + descriptor.FieldDescriptorProto_TYPE_BOOL: "runtime.Bool", + descriptor.FieldDescriptorProto_TYPE_STRING: "runtime.String", + // FieldDescriptorProto_TYPE_GROUP + // FieldDescriptorProto_TYPE_MESSAGE + // FieldDescriptorProto_TYPE_BYTES + // TODO(yugui) Handle bytes + descriptor.FieldDescriptorProto_TYPE_UINT32: "runtime.Uint32", + // FieldDescriptorProto_TYPE_ENUM + // TODO(yugui) Handle Enum + descriptor.FieldDescriptorProto_TYPE_SFIXED32: "runtime.Int32", + descriptor.FieldDescriptorProto_TYPE_SFIXED64: "runtime.Int64", + descriptor.FieldDescriptorProto_TYPE_SINT32: "runtime.Int32", + descriptor.FieldDescriptorProto_TYPE_SINT64: "runtime.Int64", + } + + proto2ConvertFuncs = map[descriptor.FieldDescriptorProto_Type]string{ + descriptor.FieldDescriptorProto_TYPE_DOUBLE: "runtime.Float64P", + descriptor.FieldDescriptorProto_TYPE_FLOAT: "runtime.Float32P", + descriptor.FieldDescriptorProto_TYPE_INT64: "runtime.Int64P", + descriptor.FieldDescriptorProto_TYPE_UINT64: "runtime.Uint64P", + descriptor.FieldDescriptorProto_TYPE_INT32: "runtime.Int32P", + descriptor.FieldDescriptorProto_TYPE_FIXED64: "runtime.Uint64P", + descriptor.FieldDescriptorProto_TYPE_FIXED32: "runtime.Uint32P", + descriptor.FieldDescriptorProto_TYPE_BOOL: "runtime.BoolP", + descriptor.FieldDescriptorProto_TYPE_STRING: "runtime.StringP", + // FieldDescriptorProto_TYPE_GROUP + // FieldDescriptorProto_TYPE_MESSAGE + // FieldDescriptorProto_TYPE_BYTES + // TODO(yugui) Handle bytes + descriptor.FieldDescriptorProto_TYPE_UINT32: "runtime.Uint32P", + // FieldDescriptorProto_TYPE_ENUM + // TODO(yugui) Handle Enum + descriptor.FieldDescriptorProto_TYPE_SFIXED32: "runtime.Int32P", + descriptor.FieldDescriptorProto_TYPE_SFIXED64: "runtime.Int64P", + descriptor.FieldDescriptorProto_TYPE_SINT32: "runtime.Int32P", + descriptor.FieldDescriptorProto_TYPE_SINT64: "runtime.Int64P", + } +) diff --git a/protoc-gen-grpc-gateway/descriptor/types_test.go b/protoc-gen-grpc-gateway/descriptor/types_test.go new file mode 100644 index 00000000000..8642ff23b19 --- /dev/null +++ b/protoc-gen-grpc-gateway/descriptor/types_test.go @@ -0,0 +1,198 @@ +package descriptor + +import ( + "testing" + + "github.com/golang/protobuf/proto" + descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" +) + +func TestGoPackageStandard(t *testing.T) { + for _, spec := range []struct { + pkg GoPackage + want bool + }{ + { + pkg: GoPackage{Path: "fmt", Name: "fmt"}, + want: true, + }, + { + pkg: GoPackage{Path: "encoding/json", Name: "json"}, + want: true, + }, + { + pkg: GoPackage{Path: "golang.org/x/net/context", Name: "context"}, + want: false, + }, + { + pkg: GoPackage{Path: "github.com/gengo/grpc-gateway", Name: "main"}, + want: false, + }, + { + pkg: GoPackage{Path: "github.com/google/googleapis/google/api/http.pb", Name: "http_pb", Alias: "htpb"}, + want: false, + }, + } { + if got, want := spec.pkg.Standard(), spec.want; got != want { + t.Errorf("%#v.Standard() = %v; want %v", spec.pkg, got, want) + } + } +} + +func TestGoPackageString(t *testing.T) { + for _, spec := range []struct { + pkg GoPackage + want string + }{ + { + pkg: GoPackage{Path: "fmt", Name: "fmt"}, + want: `"fmt"`, + }, + { + pkg: GoPackage{Path: "encoding/json", Name: "json"}, + want: `"encoding/json"`, + }, + { + pkg: GoPackage{Path: "golang.org/x/net/context", Name: "context"}, + want: `"golang.org/x/net/context"`, + }, + { + pkg: GoPackage{Path: "github.com/gengo/grpc-gateway", Name: "main"}, + want: `"github.com/gengo/grpc-gateway"`, + }, + { + pkg: GoPackage{Path: "github.com/google/googleapis/google/api/http.pb", Name: "http_pb", Alias: "htpb"}, + want: `htpb "github.com/google/googleapis/google/api/http.pb"`, + }, + } { + if got, want := spec.pkg.String(), spec.want; got != want { + t.Errorf("%#v.String() = %q; want %q", spec.pkg, got, want) + } + } +} + +func TestFieldPath(t *testing.T) { + var fds []*descriptor.FileDescriptorProto + for _, src := range []string{ + ` + name: 'example.proto' + package: 'example' + message_type < + name: 'Nest' + field < + name: 'nest2_field' + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: 'Nest2' + number: 1 + > + field < + name: 'terminal_field' + label: LABEL_OPTIONAL + type: TYPE_STRING + number: 2 + > + > + syntax: "proto3" + `, ` + name: 'another.proto' + package: 'example' + message_type < + name: 'Nest2' + field < + name: 'nest_field' + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: 'Nest' + number: 1 + > + field < + name: 'terminal_field' + label: LABEL_OPTIONAL + type: TYPE_STRING + number: 2 + > + > + syntax: "proto2" + `, + } { + var fd descriptor.FileDescriptorProto + if err := proto.UnmarshalText(src, &fd); err != nil { + t.Fatalf("proto.UnmarshalText(%s, &fd) failed with %v; want success", src, err) + } + fds = append(fds, &fd) + } + nest := &Message{ + DescriptorProto: fds[0].MessageType[0], + Fields: []*Field{ + {FieldDescriptorProto: fds[0].MessageType[0].Field[0]}, + {FieldDescriptorProto: fds[0].MessageType[0].Field[1]}, + }, + } + nest2 := &Message{ + DescriptorProto: fds[1].MessageType[0], + Fields: []*Field{ + {FieldDescriptorProto: fds[1].MessageType[0].Field[0]}, + {FieldDescriptorProto: fds[1].MessageType[0].Field[1]}, + }, + } + file1 := &File{ + FileDescriptorProto: fds[0], + GoPkg: GoPackage{Path: "example", Name: "example"}, + Messages: []*Message{nest}, + } + file2 := &File{ + FileDescriptorProto: fds[1], + GoPkg: GoPackage{Path: "example", Name: "example"}, + Messages: []*Message{nest2}, + } + crossLinkFixture(file1) + crossLinkFixture(file2) + + c1 := FieldPathComponent{ + Name: "nest_field", + Target: nest2.Fields[0], + } + if got, want := c1.LHS(), "GetNestField()"; got != want { + t.Errorf("c1.LHS() = %q; want %q", got, want) + } + if got, want := c1.RHS(), "NestField"; got != want { + t.Errorf("c1.RHS() = %q; want %q", got, want) + } + + c2 := FieldPathComponent{ + Name: "nest2_field", + Target: nest.Fields[0], + } + if got, want := c2.LHS(), "Nest2Field"; got != want { + t.Errorf("c2.LHS() = %q; want %q", got, want) + } + if got, want := c2.LHS(), "Nest2Field"; got != want { + t.Errorf("c2.LHS() = %q; want %q", got, want) + } + + fp := FieldPath{ + c1, c2, c1, FieldPathComponent{ + Name: "terminal_field", + Target: nest.Fields[1], + }, + } + if got, want := fp.RHS("resp"), "resp.GetNestField().Nest2Field.GetNestField().TerminalField"; got != want { + t.Errorf("fp.RHS(%q) = %q; want %q", "resp", got, want) + } + + fp2 := FieldPath{ + c2, c1, c2, FieldPathComponent{ + Name: "terminal_field", + Target: nest2.Fields[1], + }, + } + if got, want := fp2.RHS("resp"), "resp.Nest2Field.GetNestField().Nest2Field.TerminalField"; got != want { + t.Errorf("fp2.RHS(%q) = %q; want %q", "resp", got, want) + } + + var fpEmpty FieldPath + if got, want := fpEmpty.RHS("resp"), "resp"; got != want { + t.Errorf("fpEmpty.RHS(%q) = %q; want %q", "resp", got, want) + } +} From af1c4823eacc8ae6003714ba80b08c8a30438deb Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 10:49:32 +0900 Subject: [PATCH 13/28] Reimplement code generator with httprule and descriptor packages. --- protoc-gen-grpc-gateway/generator.go | 546 ------------------ protoc-gen-grpc-gateway/generator_test.go | 280 --------- .../gengateway/generator.go | 59 ++ .../gengateway/template.go | 229 ++++++++ .../gengateway/template_test.go | 396 +++++++++++++ protoc-gen-grpc-gateway/main.go | 33 +- runtime/pattern.go | 8 + 7 files changed, 721 insertions(+), 830 deletions(-) delete mode 100644 protoc-gen-grpc-gateway/generator.go delete mode 100644 protoc-gen-grpc-gateway/generator_test.go create mode 100644 protoc-gen-grpc-gateway/gengateway/generator.go create mode 100644 protoc-gen-grpc-gateway/gengateway/template.go create mode 100644 protoc-gen-grpc-gateway/gengateway/template_test.go diff --git a/protoc-gen-grpc-gateway/generator.go b/protoc-gen-grpc-gateway/generator.go deleted file mode 100644 index f5fde3ac8c7..00000000000 --- a/protoc-gen-grpc-gateway/generator.go +++ /dev/null @@ -1,546 +0,0 @@ -package main - -import ( - "bytes" - "errors" - "fmt" - "go/format" - "path/filepath" - "regexp" - "strings" - "text/template" - - "github.com/gengo/grpc-gateway/options" - "github.com/golang/glog" - "github.com/golang/protobuf/proto" - descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" - plugin "github.com/golang/protobuf/protoc-gen-go/plugin" -) - -var ( - // msgTbl maps a fully-qualified message name to its DescriptorProto. - msgTbl = make(map[string]*descriptor.DescriptorProto) - // proto2MsgTbl keeps a set of fully-qualified message names which came from files with proto2 syntax. - proto2MsgTbl = make(map[string]bool) - // filePkgTbl maps a file name to its proto package name. - filePkgTbl = make(map[string]string) - // importMap maps a path to a .proto file to a go pacakge path. - importMap = make(map[string]string) - - errNoTargetService = errors.New("no target service defined in the file") -) - -func registerMsg(location string, msgs []*descriptor.DescriptorProto, isProto2 bool) { - for _, m := range msgs { - name := fmt.Sprintf("%s.%s", location, m.GetName()) - msgTbl[name] = m - if isProto2 { - proto2MsgTbl[name] = true - } - glog.V(1).Infof("register name: %s", name) - registerMsg(name, m.GetNestedType(), isProto2) - } -} - -func lookupMsg(name string) (*descriptor.DescriptorProto, error) { - m, ok := msgTbl[name] - if !ok { - return nil, fmt.Errorf("no such message: %s", name) - } - return m, nil -} - -func generate(req *plugin.CodeGeneratorRequest) *plugin.CodeGeneratorResponse { - targets := make(map[string]bool) - for _, fname := range req.GetFileToGenerate() { - targets[fname] = true - } - for _, file := range req.GetProtoFile() { - pkg := file.GetPackage() - if !strings.HasPrefix(pkg, ".") { - pkg = fmt.Sprintf(".%s", pkg) - } - filePkgTbl[file.GetName()] = pkg - isProto2 := file.GetSyntax() == "" || file.GetSyntax() == "proto2" - registerMsg(pkg, file.GetMessageType(), isProto2) - } - - var files []*plugin.CodeGeneratorResponse_File - for _, file := range req.GetProtoFile() { - if !targets[file.GetName()] { - glog.V(1).Infof("Skip non-target file: %s", file.GetName()) - continue - } - glog.V(1).Infof("Processing %s", file.GetName()) - code, err := generateSingleFile(file) - if err == errNoTargetService { - glog.V(1).Info("%s: %v", file.GetName(), err) - continue - } - if err != nil { - return &plugin.CodeGeneratorResponse{ - Error: proto.String(err.Error()), - } - } - name := file.GetName() - ext := filepath.Ext(name) - base := strings.TrimSuffix(name, ext) - files = append(files, &plugin.CodeGeneratorResponse_File{ - Name: proto.String(fmt.Sprintf("%s.pb.gw.go", base)), - Content: proto.String(code), - }) - } - return &plugin.CodeGeneratorResponse{ - File: files, - } -} - -func goPackage(d *descriptor.FileDescriptorProto) string { - if d.Options != nil && d.Options.GoPackage != nil { - return d.Options.GetGoPackage() - } - if d.Package == nil { - base := filepath.Base(d.GetName()) - ext := filepath.Ext(base) - return strings.TrimSuffix(base, ext) - } - return goPkgFromProtoPkg(d.GetPackage()) -} - -func goPkgFromProtoPkg(protoPkg string) string { - return strings.NewReplacer("-", "_", ".", "_").Replace(protoPkg) -} - -func goTypeFromProtoType(protoType, currentPkg string) string { - components := strings.Split(protoType, ".") - if len(components) < 2 { - return protoType - } - msg := components[len(components)-1] - pkg := strings.Join(components[:len(components)-1], ".") - if pkg == currentPkg { - return msg - } - return fmt.Sprintf("%s.%s", goPkgFromProtoPkg(pkg), msg) -} - -var ( - upperPattern = regexp.MustCompile("[A-Z]") -) - -func toCamel(str string) string { - var components []string - for _, c := range strings.Split(str, "_") { - components = append(components, strings.Title(strings.ToLower(c))) - } - return strings.Join(components, "") -} - -func getAPIOptions(meth *descriptor.MethodDescriptorProto) (*options.ApiMethodOptions, error) { - if meth.Options == nil { - return nil, nil - } - if !proto.HasExtension(meth.Options, options.E_ApiMethodOptions_ApiOptions) { - return nil, nil - } - ext, err := proto.GetExtension(meth.Options, options.E_ApiMethodOptions_ApiOptions) - if err != nil { - return nil, err - } - opts, ok := ext.(*options.ApiMethodOptions) - if !ok { - return nil, fmt.Errorf("extension is %T; want an ApiMethodOptions", ext) - } - return opts, nil -} - -var ( - proto3ConvertFuncs = map[descriptor.FieldDescriptorProto_Type]string{ - descriptor.FieldDescriptorProto_TYPE_DOUBLE: "runtime.Float64", - descriptor.FieldDescriptorProto_TYPE_FLOAT: "runtime.Float32", - descriptor.FieldDescriptorProto_TYPE_INT64: "runtime.Int64", - descriptor.FieldDescriptorProto_TYPE_UINT64: "runtime.Uint64", - descriptor.FieldDescriptorProto_TYPE_INT32: "runtime.Int32", - descriptor.FieldDescriptorProto_TYPE_FIXED64: "runtime.Uint64", - descriptor.FieldDescriptorProto_TYPE_FIXED32: "runtime.Uint32", - descriptor.FieldDescriptorProto_TYPE_BOOL: "runtime.Bool", - descriptor.FieldDescriptorProto_TYPE_STRING: "runtime.String", - // FieldDescriptorProto_TYPE_GROUP - // FieldDescriptorProto_TYPE_MESSAGE - // FieldDescriptorProto_TYPE_BYTES - // TODO(yugui) Handle bytes - descriptor.FieldDescriptorProto_TYPE_UINT32: "runtime.Uint32", - // FieldDescriptorProto_TYPE_ENUM - // TODO(yugui) Handle Enum - descriptor.FieldDescriptorProto_TYPE_SFIXED32: "runtime.Int32", - descriptor.FieldDescriptorProto_TYPE_SFIXED64: "runtime.Int64", - descriptor.FieldDescriptorProto_TYPE_SINT32: "runtime.Int32", - descriptor.FieldDescriptorProto_TYPE_SINT64: "runtime.Int64", - } - proto2ConvertFuncs = map[descriptor.FieldDescriptorProto_Type]string{ - descriptor.FieldDescriptorProto_TYPE_DOUBLE: "runtime.Float64P", - descriptor.FieldDescriptorProto_TYPE_FLOAT: "runtime.Float32P", - descriptor.FieldDescriptorProto_TYPE_INT64: "runtime.Int64P", - descriptor.FieldDescriptorProto_TYPE_UINT64: "runtime.Uint64P", - descriptor.FieldDescriptorProto_TYPE_INT32: "runtime.Int32P", - descriptor.FieldDescriptorProto_TYPE_FIXED64: "runtime.Uint64P", - descriptor.FieldDescriptorProto_TYPE_FIXED32: "runtime.Uint32P", - descriptor.FieldDescriptorProto_TYPE_BOOL: "runtime.BoolP", - descriptor.FieldDescriptorProto_TYPE_STRING: "runtime.StringP", - // FieldDescriptorProto_TYPE_GROUP - // FieldDescriptorProto_TYPE_MESSAGE - // FieldDescriptorProto_TYPE_BYTES - // TODO(yugui) Handle bytes - descriptor.FieldDescriptorProto_TYPE_UINT32: "runtime.Uint32P", - // FieldDescriptorProto_TYPE_ENUM - // TODO(yugui) Handle Enum - descriptor.FieldDescriptorProto_TYPE_SFIXED32: "runtime.Int32P", - descriptor.FieldDescriptorProto_TYPE_SFIXED64: "runtime.Int64P", - descriptor.FieldDescriptorProto_TYPE_SINT32: "runtime.Int32P", - descriptor.FieldDescriptorProto_TYPE_SINT64: "runtime.Int64P", - } -) - -func pathParams(msg *descriptor.DescriptorProto, opts *options.ApiMethodOptions, isProto2 bool) ([]paramDesc, error) { - convertFuncs := proto3ConvertFuncs - if isProto2 { - convertFuncs = proto2ConvertFuncs - } - - var params []paramDesc - components := strings.Split(opts.GetPath(), "/") - for _, c := range components { - if !strings.HasPrefix(c, ":") { - continue - } - name := strings.TrimPrefix(c, ":") - fd := lookupField(msg, name) - if fd == nil { - return nil, fmt.Errorf("field %q not found in %s", name, msg.GetName()) - } - conv, ok := convertFuncs[fd.GetType()] - if !ok { - return nil, fmt.Errorf("unsupported path parameter type %s in %s", fd.GetType(), msg.GetName()) - } - params = append(params, paramDesc{ - ProtoName: name, - ConvertFunc: conv, - }) - } - return params, nil -} - -func lookupField(msg *descriptor.DescriptorProto, name string) *descriptor.FieldDescriptorProto { - for _, f := range msg.GetField() { - if f.GetName() == name { - return f - } - } - return nil -} - -func generateSingleFile(file *descriptor.FileDescriptorProto) (string, error) { - usedImports := make(map[string]bool) - buf := bytes.NewBuffer(nil) - var svcDescs []serviceDesc - for _, svc := range file.GetService() { - sd := serviceDesc{ - Name: svc.GetName(), - } - for _, meth := range svc.GetMethod() { - opts, err := getAPIOptions(meth) - if err != nil { - glog.Errorf("Failed to extract ApiMethodOptions: %v", err) - return "", err - } - input, err := lookupMsg(meth.GetInputType()) - if err != nil { - return "", err - } - fields := make(map[string]bool) - for _, f := range input.Field { - fields[f.GetName()] = true - } - params, err := pathParams(input, opts, proto2MsgTbl[meth.GetInputType()]) - if err != nil { - return "", err - } - for _, p := range params { - delete(fields, p.ProtoName) - } - needsBody := len(fields) != 0 - if needsBody && (opts.GetMethod() == "GET" || opts.GetMethod() == "DELETE") { - return "", fmt.Errorf("needs request body even though http method is %s: %s", opts.GetMethod(), meth.GetName()) - } - if meth.GetClientStreaming() && (len(params) > 0 || !needsBody) { - return "", fmt.Errorf("cannot use path parameter in client streaming") - } - requestGoType := goTypeFromProtoType(meth.GetInputType()[1:], file.GetPackage()) - if idx := strings.Index(requestGoType, "."); idx >= 0 { - usedImports[requestGoType[:idx]] = true - } - md := methodDesc{ - ServiceName: svc.GetName(), - Name: meth.GetName(), - Method: opts.GetMethod(), - Path: opts.GetPath(), - RequestType: requestGoType, - PathParams: params, - NeedsBody: needsBody, - ServerStreaming: meth.GetServerStreaming(), - ClientStreaming: meth.GetClientStreaming(), - } - sd.Methods = append(sd.Methods, md) - } - if len(sd.Methods) == 0 { - continue - } - svcDescs = append(svcDescs, sd) - } - if len(svcDescs) == 0 { - return "", errNoTargetService - } - - var imports []string - for _, dep := range file.GetDependency() { - protoPkg, ok := filePkgTbl[dep] - if !ok { - glog.Fatalf("unknown dependency in %s: %s", file.GetName(), dep) - } - pkg := filepath.Join(*importPrefix, filepath.Dir(dep)) - alias := goPkgFromProtoPkg(protoPkg[1:]) - if usedImports[alias] { - imports = append(imports, fmt.Sprintf("%s %q\n", alias, pkg)) - } - } - err := headerTemplate.Execute(buf, headerParams{ - Src: file.GetName(), - Pkg: goPackage(file), - Imports: imports, - }) - if err != nil { - return "", err - } - for _, sd := range svcDescs { - for _, md := range sd.Methods { - if err = handlerTemplate.Execute(buf, md); err != nil { - return "", err - } - } - } - if err := trailerTemplate.Execute(buf, svcDescs); err != nil { - return "", err - } - - code, err := format.Source(buf.Bytes()) - if err != nil { - glog.Errorf("Failed to gofmt: %s: %v", buf.String(), err) - return "", err - } - return string(code), nil -} - -type headerParams struct { - Src, Pkg string - Imports []string -} - -type paramDesc struct { - ProtoName string - ConvertFunc string -} - -func (d paramDesc) GoName() string { - return toCamel(d.ProtoName) -} - -type methodDesc struct { - ServiceName string - Name string - Method string - Path string - RequestType string - QueryParams []paramDesc - PathParams []paramDesc - NeedsBody bool - ClientStreaming bool - ServerStreaming bool -} - -func (d methodDesc) MuxRegistererName() string { - return toCamel(d.Method) -} - -func (d methodDesc) NeedsPathParam() bool { - return len(d.PathParams) != 0 -} - -type serviceDesc struct { - Name string - Methods []methodDesc -} - -var ( - headerTemplate = template.Must(template.New("header").Parse(` -// Code generated by protoc-gen-grpc-gateway -// source: {{.Src}} -// DO NOT EDIT! - -/* -Package {{.Pkg}} is a reverse proxy. - -It translates gRPC into RESTful JSON APIs. -*/ -package {{.Pkg}} -import ( - "encoding/json" - "io" - "net/http" - - "github.com/gengo/grpc-gateway/runtime" - "github.com/golang/glog" - "github.com/golang/protobuf/proto" - "github.com/zenazn/goji/web" - "golang.org/x/net/context" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - -{{range $line := .Imports}}{{$line}}{{end}} -) - -var _ codes.Code -var _ io.Reader -var _ = runtime.String -`)) - - handlerTemplate = template.Must(template.New("handler").Parse(` -{{if .ClientStreaming}} -{{template "client-streaming-request-func" .}} -{{else}} -{{template "client-rpc-request-func" .}} -{{end}} -`)) - - _ = template.Must(handlerTemplate.New("request-func-signature").Parse(strings.Replace(` -{{if .ServerStreaming}} -func request_{{.ServiceName}}_{{.Name}}(ctx context.Context, c web.C, client {{.ServiceName}}Client, req *http.Request) ({{.ServiceName}}_{{.Name}}Client, error) -{{else}} -func request_{{.ServiceName}}_{{.Name}}(ctx context.Context, c web.C, client {{.ServiceName}}Client, req *http.Request) (msg proto.Message, err error) -{{end}}`, "\n", "", -1))) - - _ = template.Must(handlerTemplate.New("client-streaming-request-func").Parse(` -{{template "request-func-signature" .}} { - stream, err := client.{{.Name}}(ctx) - if err != nil { - glog.Errorf("Failed to start streaming: %v", err) - return nil, err - } - dec := json.NewDecoder(req.Body) - for { - var protoReq {{.RequestType}} - err = dec.Decode(&protoReq) - if err == io.EOF { - break - } - if err != nil { - glog.Errorf("Failed to decode request: %v", err) - return nil, grpc.Errorf(codes.InvalidArgument, "%v", err) - } - if err = stream.Send(&protoReq); err != nil { - glog.Errorf("Failed to send request: %v", err) - return nil, err - } - } -{{if .ServerStreaming}} - if err = stream.CloseSend(); err != nil { - glog.Errorf("Failed to terminate client stream: %v", err) - return nil, err - } - return stream, nil -{{else}} - return stream.CloseAndRecv() -{{end}} -} -`)) - - _ = template.Must(handlerTemplate.New("client-rpc-request-func").Parse(` -{{template "request-func-signature" .}} { - var protoReq {{.RequestType}} - {{range $desc := .QueryParams}} - protoReq.{{$desc.ProtoName}}, err = {{$desc.ConvertFunc}}(req.FormValue({{$desc.ProtoName | printf "%q"}})) - if err != nil { - return nil, err - } - {{end}} -{{if .NeedsBody}} - if err = json.NewDecoder(req.Body).Decode(&protoReq); err != nil { - return nil, err - } -{{end}} -{{if .NeedsPathParam}} - var val string - var ok bool - {{range $desc := .PathParams}} - val, ok = c.URLParams[{{$desc.ProtoName | printf "%q"}}] - if !ok { - return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", {{$desc.ProtoName | printf "%q"}}) - } - protoReq.{{$desc.GoName}}, err = {{$desc.ConvertFunc}}(val) - if err != nil { - return nil, err - } - {{end}} -{{end}} - - return client.{{.Name}}(ctx, &protoReq) -}`)) - - trailerTemplate = template.Must(template.New("trailer").Parse(` -{{range $svc := .}} -// Register{{$svc.Name}}HandlerFromEndpoint is same as Register{{$svc.Name}}Handler but -// automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func Register{{$svc.Name}}HandlerFromEndpoint(ctx context.Context, mux *web.Mux, endpoint string) (err error) { - conn, err := grpc.Dial(endpoint) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - glog.Error("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - glog.Error("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return Register{{$svc.Name}}Handler(ctx, mux, conn) -} - -// Register{{$svc.Name}}Handler registers the http handlers for service {{$svc.Name}} to "mux". -// The handlers forward requests to the grpc endpoint over "conn". -func Register{{$svc.Name}}Handler(ctx context.Context, mux *web.Mux, conn *grpc.ClientConn) error { - client := New{{$svc.Name}}Client(conn) - {{range $m := $svc.Methods}} - mux.{{$m.MuxRegistererName}}({{$m.Path | printf "%q"}}, func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_{{.ServiceName}}_{{.Name}}(ctx, c, client, req) - if err != nil { - runtime.HTTPError(w, err) - return - } - {{if .ServerStreaming}} - runtime.ForwardResponseStream(w, func() (proto.Message, error) { return resp.Recv() }) - {{else}} - runtime.ForwardResponseMessage(w, resp) - {{end}} - }) - {{end}} - return nil -} -{{end}}`)) -) diff --git a/protoc-gen-grpc-gateway/generator_test.go b/protoc-gen-grpc-gateway/generator_test.go deleted file mode 100644 index 8e422ef46ad..00000000000 --- a/protoc-gen-grpc-gateway/generator_test.go +++ /dev/null @@ -1,280 +0,0 @@ -package main - -import ( - "strings" - "testing" - - "github.com/golang/protobuf/proto" - descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" - plugin "github.com/golang/protobuf/protoc-gen-go/plugin" -) - -func generateFromText(t *testing.T, input string) *plugin.CodeGeneratorResponse { - msgTbl = make(map[string]*descriptor.DescriptorProto) - var req plugin.CodeGeneratorRequest - if err := proto.UnmarshalText(input, &req); err != nil { - t.Fatalf("proto.Unmarshal(%q, &req) failed with %v; want success", input, err) - } - return generate(&req) -} - -func mustGenerateFromText(t *testing.T, input string) []*plugin.CodeGeneratorResponse_File { - resp := generateFromText(t, input) - if resp.Error != nil { - t.Fatalf("generate(%s) failed with %s", input, resp.GetError()) - } - return resp.File -} - -func testGenerate(t *testing.T, input string, outputs map[string]string) { - var expected plugin.CodeGeneratorResponse - for fname, content := range outputs { - expected.File = append(expected.File, &plugin.CodeGeneratorResponse_File{ - Name: proto.String(fname), - Content: proto.String(content), - }) - } - resp := generateFromText(t, input) - if !proto.Equal(resp, &expected) { - t.Errorf("generate(%s) = %s; want %s", input, proto.MarshalTextString(resp), proto.MarshalTextString(&expected)) - } -} - -func TestGenerateEmtpy(t *testing.T) { - testGenerate(t, "", nil) -} - -func TestGenerate(t *testing.T) { - testGenerate(t, ` -file_to_generate: "example.proto" -proto_file < - name: "example.proto" - package: "example" - syntax: "proto3" - message_type < - name: "SimpleMessage" - field < - name: "id" - number: 1 - label: LABEL_REQUIRED, - type: TYPE_STRING - > - > - service < - name: "EchoService" - method < - name: "Echo" - input_type: ".example.SimpleMessage" - output_type: ".example.SimpleMessage" - options < - [gengo.grpc.gateway.ApiMethodOptions.api_options] < - path: "/v1/example/echo/:id" - method: "POST" - > - > - > - method < - name: "EchoBody" - input_type: ".example.SimpleMessage" - output_type: ".example.SimpleMessage" - options < - [gengo.grpc.gateway.ApiMethodOptions.api_options] < - path: "/v1/example/echo_body" - method: "POST" - > - > - > - > ->`, map[string]string{ - "example.pb.gw.go": `// Code generated by protoc-gen-grpc-gateway -// source: example.proto -// DO NOT EDIT! - -/* -Package example is a reverse proxy. - -It translates gRPC into RESTful JSON APIs. -*/ -package example - -import ( - "encoding/json" - "io" - "net/http" - - "github.com/gengo/grpc-gateway/runtime" - "github.com/golang/glog" - "github.com/golang/protobuf/proto" - "github.com/zenazn/goji/web" - "golang.org/x/net/context" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" -) - -var _ codes.Code -var _ io.Reader -var _ = runtime.String - -func request_EchoService_Echo(ctx context.Context, c web.C, client EchoServiceClient, req *http.Request) (msg proto.Message, err error) { - var protoReq SimpleMessage - - var val string - var ok bool - - val, ok = c.URLParams["id"] - if !ok { - return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "id") - } - protoReq.Id, err = runtime.String(val) - if err != nil { - return nil, err - } - - return client.Echo(ctx, &protoReq) -} - -func request_EchoService_EchoBody(ctx context.Context, c web.C, client EchoServiceClient, req *http.Request) (msg proto.Message, err error) { - var protoReq SimpleMessage - - if err = json.NewDecoder(req.Body).Decode(&protoReq); err != nil { - return nil, err - } - - return client.EchoBody(ctx, &protoReq) -} - -// RegisterEchoServiceHandlerFromEndpoint is same as RegisterEchoServiceHandler but -// automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterEchoServiceHandlerFromEndpoint(ctx context.Context, mux *web.Mux, endpoint string) (err error) { - conn, err := grpc.Dial(endpoint) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - glog.Error("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - glog.Error("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return RegisterEchoServiceHandler(ctx, mux, conn) -} - -// RegisterEchoServiceHandler registers the http handlers for service EchoService to "mux". -// The handlers forward requests to the grpc endpoint over "conn". -func RegisterEchoServiceHandler(ctx context.Context, mux *web.Mux, conn *grpc.ClientConn) error { - client := NewEchoServiceClient(conn) - - mux.Post("/v1/example/echo/:id", func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_EchoService_Echo(ctx, c, client, req) - if err != nil { - runtime.HTTPError(w, err) - return - } - - runtime.ForwardResponseMessage(w, resp) - - }) - - mux.Post("/v1/example/echo_body", func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_EchoService_EchoBody(ctx, c, client, req) - if err != nil { - runtime.HTTPError(w, err) - return - } - - runtime.ForwardResponseMessage(w, resp) - - }) - - return nil -} -`, - }) -} - -func TestGenerateWithExternalMessage(t *testing.T) { - input := ` -file_to_generate: "example.proto" -proto_file < - name: "github.com/example/proto/message.proto" - package: "com.example.proto" - message_type < - name: "SimpleMessage" - field < - name: "id" - number: 1 - label: LABEL_REQUIRED, - type: TYPE_STRING - > - > -> -proto_file < - name: "github.com/example/sub/proto/another.proto" - package: "com.example.sub.proto" - message_type < - name: "AnotherMessage" - field < - name: "id" - number: 1 - label: LABEL_REQUIRED, - type: TYPE_STRING - > - > -> -proto_file < - name: "example.proto" - package: "example" - dependency: "github.com/example/proto/message.proto" - dependency: "github.com/example/sub/proto/another.proto" - public_dependency: 0 - public_dependency: 1 - syntax: "proto3" - service < - name: "EchoService" - method < - name: "SimpleToAnother" - input_type: ".com.example.proto.SimpleMessage" - output_type: ".com.example.sub.proto.AnotherMessage" - options < - [gengo.grpc.gateway.ApiMethodOptions.api_options] < - path: "/v1/example/conv/:id" - method: "POST" - > - > - > - method < - name: "AnotherToSimple" - input_type: ".com.example.sub.proto.AnotherMessage" - output_type: ".com.example.proto.SimpleMessage" - options < - [gengo.grpc.gateway.ApiMethodOptions.api_options] < - path: "/v1/example/rconv/:id" - method: "POST" - > - > - > - > ->` - files := mustGenerateFromText(t, input) - if got, want := len(files), 1; got != want { - t.Errorf("len(generate(%s).File) = %d; want %d", got, want) - return - } - content := files[0].GetContent() - - if want := `com_example_proto "github.com/example/proto"`; !strings.Contains(content, want) { - t.Errorf("content = %s; want it to contain %s", content, want) - } - if want := `com_example_sub_proto "github.com/example/sub/proto"`; !strings.Contains(content, want) { - t.Errorf("content = %s; want it to contain %s", content, want) - } -} diff --git a/protoc-gen-grpc-gateway/gengateway/generator.go b/protoc-gen-grpc-gateway/gengateway/generator.go new file mode 100644 index 00000000000..38084d2f688 --- /dev/null +++ b/protoc-gen-grpc-gateway/gengateway/generator.go @@ -0,0 +1,59 @@ +package gengateway + +import ( + "errors" + "fmt" + "go/format" + "path/filepath" + "strings" + + "github.com/gengo/grpc-gateway/protoc-gen-grpc-gateway/descriptor" + "github.com/golang/glog" + "github.com/golang/protobuf/proto" + plugin "github.com/golang/protobuf/protoc-gen-go/plugin" +) + +var ( + errNoTargetService = errors.New("no target service defined in the file") +) + +type generator struct { + reg *descriptor.Registry +} + +func New(reg *descriptor.Registry) *generator { + if err := reg.ReserveGoPackageAlias("json", "encoding/json"); err != nil { + glog.Fatal("package name json collides to another") + } + return &generator{reg: reg} +} + +func (g *generator) Generate(targets []*descriptor.File) ([]*plugin.CodeGeneratorResponse_File, error) { + var files []*plugin.CodeGeneratorResponse_File + for _, file := range targets { + glog.V(1).Infof("Processing %s", file.GetName()) + code, err := applyTemplate(file) + if err == errNoTargetService { + glog.V(1).Infof("%s: %v", file.GetName(), err) + continue + } + if err != nil { + return nil, err + } + formatted, err := format.Source([]byte(code)) + if err != nil { + glog.Errorf("%v: %s", err, code) + return nil, err + } + name := file.GetName() + ext := filepath.Ext(name) + base := strings.TrimSuffix(name, ext) + output := fmt.Sprintf("%s.pb.gw.go", base) + files = append(files, &plugin.CodeGeneratorResponse_File{ + Name: proto.String(output), + Content: proto.String(string(formatted)), + }) + glog.Infof("Will emit %s", output) + } + return files, nil +} diff --git a/protoc-gen-grpc-gateway/gengateway/template.go b/protoc-gen-grpc-gateway/gengateway/template.go new file mode 100644 index 00000000000..1cab1a0379f --- /dev/null +++ b/protoc-gen-grpc-gateway/gengateway/template.go @@ -0,0 +1,229 @@ +package gengateway + +import ( + "bytes" + "path" + "strings" + "text/template" + + "github.com/gengo/grpc-gateway/protoc-gen-grpc-gateway/descriptor" +) + +type param struct { + *descriptor.File + Imports []descriptor.GoPackage +} + +func applyTemplate(file *descriptor.File) (string, error) { + pkgSeen := make(map[string]bool) + var imports []descriptor.GoPackage + for _, pkg := range []string{ + "io", + "net/http", + "encoding/json", + "github.com/gengo/grpc-gateway/runtime", + "github.com/golang/glog", + "github.com/golang/protobuf/proto", + "golang.org/x/net/context", + "google.golang.org/grpc", + "google.golang.org/grpc/codes", + } { + pkgSeen[pkg] = true + imports = append(imports, descriptor.GoPackage{Path: pkg, Name: path.Base(pkg)}) + } + for _, svc := range file.Services { + for _, m := range svc.Methods { + pkg := m.RequestType.File.GoPkg + if pkg == file.GoPkg { + continue + } + if pkgSeen[pkg.Path] { + continue + } + pkgSeen[pkg.Path] = true + imports = append(imports, pkg) + } + } + + w := bytes.NewBuffer(nil) + if err := headerTemplate.Execute(w, param{File: file, Imports: imports}); err != nil { + return "", err + } + var methodSeen bool + for _, svc := range file.Services { + for _, meth := range svc.Methods { + methodSeen = true + if err := handlerTemplate.Execute(w, meth); err != nil { + return "", err + } + } + } + if !methodSeen { + return "", errNoTargetService + } + if err := trailerTemplate.Execute(w, file.Services); err != nil { + return "", err + } + return w.String(), nil +} + +var ( + headerTemplate = template.Must(template.New("header").Parse(` +// Code generated by protoc-gen-grpc-gateway +// source: {{.GetName}} +// DO NOT EDIT! + +/* +Package {{.GoPkg.Name}} is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package {{.GoPkg.Name}} +import ( + {{range $i := .Imports}}{{if $i.Standard}}{{$i | printf "%s\n"}}{{end}}{{end}} + + {{range $i := .Imports}}{{if not $i.Standard}}{{$i | printf "%s\n"}}{{end}}{{end}} +) + +var _ codes.Code +var _ io.Reader +var _ = runtime.String +`)) + + handlerTemplate = template.Must(template.New("handler").Parse(` +{{if .GetClientStreaming}} +{{template "client-streaming-request-func" .}} +{{else}} +{{template "client-rpc-request-func" .}} +{{end}} +`)) + + _ = template.Must(handlerTemplate.New("request-func-signature").Parse(strings.Replace(` +{{if .GetServerStreaming}} +func request_{{.Service.GetName}}_{{.GetName}}(ctx context.Context, client {{.Service.GetName}}Client, req *http.Request, pathParams map[string]string) ({{.Service.GetName}}_{{.GetName}}Client, error) +{{else}} +func request_{{.Service.GetName}}_{{.GetName}}(ctx context.Context, client {{.Service.GetName}}Client, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) +{{end}}`, "\n", "", -1))) + + _ = template.Must(handlerTemplate.New("client-streaming-request-func").Parse(` +{{template "request-func-signature" .}} { + stream, err := client.{{.GetName}}(ctx) + if err != nil { + glog.Errorf("Failed to start streaming: %v", err) + return nil, err + } + dec := {{.Body.DecoderFactoryExpr}}(req.Body) + for { + var protoReq {{.RequestType.GoType .Service.File.GoPkg.Path}} + err = dec.Decode(&protoReq) + if err == io.EOF { + break + } + if err != nil { + glog.Errorf("Failed to decode request: %v", err) + return nil, grpc.Errorf(codes.InvalidArgument, "%v", err) + } + if err = stream.Send(&protoReq); err != nil { + glog.Errorf("Failed to send request: %v", err) + return nil, err + } + } +{{if .GetServerStreaming}} + if err = stream.CloseSend(); err != nil { + glog.Errorf("Failed to terminate client stream: %v", err) + return nil, err + } + return stream, nil +{{else}} + return stream.CloseAndRecv() +{{end}} +} +`)) + + _ = template.Must(handlerTemplate.New("client-rpc-request-func").Parse(` +{{template "request-func-signature" .}} { + var protoReq {{.RequestType.GoType .Service.File.GoPkg.Path}} + {{range $param := .QueryParams}} + protoReq.{{$param.RHS "protoReq"}}, err = {{$param.ConvertFuncExpr}}(req.FormValue({{$param | printf "%q"}})) + if err != nil { + return nil, err + } + {{end}} +{{if .Body}} + if err = {{.Body.DecoderFactoryExpr}}(req.Body).Decode(&{{.Body.RHS "protoReq"}}); err != nil { + return nil, grpc.Errorf(codes.InvalidArgument, "%v", err) + } +{{end}} +{{if .PathParams}} + var val string + var ok bool + {{range $param := .PathParams}} + val, ok = pathParams[{{$param | printf "%q"}}] + if !ok { + return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", {{$param | printf "%q"}}) + } + {{$param.RHS "protoReq"}}, err = {{$param.ConvertFuncExpr}}(val) + if err != nil { + return nil, err + } + {{end}} +{{end}} + + return client.{{.GetName}}(ctx, &protoReq) +}`)) + + trailerTemplate = template.Must(template.New("trailer").Parse(` +{{range $svc := .}} +// Register{{$svc.GetName}}HandlerFromEndpoint is same as Register{{$svc.GetName}}Handler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func Register{{$svc.GetName}}HandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string) (err error) { + conn, err := grpc.Dial(endpoint) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + glog.Error("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + glog.Error("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return Register{{$svc.GetName}}Handler(ctx, mux, conn) +} + +// Register{{$svc.GetName}}Handler registers the http handlers for service {{$svc.GetName}} to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func Register{{$svc.GetName}}Handler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + client := New{{$svc.GetName}}Client(conn) + {{range $m := $svc.Methods}} + mux.Handle({{$m.HTTPMethod | printf "%q"}}, pattern_{{$svc.GetName}}_{{$m.GetName}}, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + resp, err := request_{{$svc.GetName}}_{{$m.GetName}}(ctx, client, req, pathParams) + if err != nil { + runtime.HTTPError(w, err) + return + } + {{if $m.GetServerStreaming}} + runtime.ForwardResponseStream(w, func() (proto.Message, error) { return resp.Recv() }) + {{else}} + runtime.ForwardResponseMessage(w, resp) + {{end}} + }) + {{end}} + return nil +} + +var ( + {{range $m := $svc.Methods}} + pattern_{{$svc.GetName}}_{{$m.GetName}} = runtime.MustPattern(runtime.NewPattern({{$m.PathTmpl.Version}}, {{$m.PathTmpl.OpCodes | printf "%#v"}}, {{$m.PathTmpl.Pool | printf "%#v"}}, {{$m.PathTmpl.Verb | printf "%q"}})) + {{end}} +) +{{end}}`)) +) diff --git a/protoc-gen-grpc-gateway/gengateway/template_test.go b/protoc-gen-grpc-gateway/gengateway/template_test.go new file mode 100644 index 00000000000..7bbbc459389 --- /dev/null +++ b/protoc-gen-grpc-gateway/gengateway/template_test.go @@ -0,0 +1,396 @@ +package gengateway + +import ( + "strings" + "testing" + + "github.com/gengo/grpc-gateway/protoc-gen-grpc-gateway/descriptor" + "github.com/gengo/grpc-gateway/protoc-gen-grpc-gateway/httprule" + "github.com/golang/protobuf/proto" + protodescriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" +) + +func crossLinkFixture(f *descriptor.File) *descriptor.File { + for _, m := range f.Messages { + m.File = f + } + for _, svc := range f.Services { + svc.File = f + for _, m := range svc.Methods { + m.Service = svc + for _, param := range m.PathParams { + param.Method = m + } + for _, param := range m.QueryParams { + param.Method = m + } + } + } + return f +} + +func TestApplyTemplateHeader(t *testing.T) { + msgdesc := &protodescriptor.DescriptorProto{ + Name: proto.String("ExampleMessage"), + } + meth := &protodescriptor.MethodDescriptorProto{ + Name: proto.String("Example"), + InputType: proto.String("ExampleMessage"), + OutputType: proto.String("ExampleMessage"), + } + svc := &protodescriptor.ServiceDescriptorProto{ + Name: proto.String("ExampleService"), + Method: []*protodescriptor.MethodDescriptorProto{meth}, + } + msg := &descriptor.Message{ + DescriptorProto: msgdesc, + } + file := descriptor.File{ + FileDescriptorProto: &protodescriptor.FileDescriptorProto{ + Name: proto.String("example.proto"), + Package: proto.String("example"), + Dependency: []string{"a.example/b/c.proto", "a.example/d/e.proto"}, + MessageType: []*protodescriptor.DescriptorProto{msgdesc}, + Service: []*protodescriptor.ServiceDescriptorProto{svc}, + }, + GoPkg: descriptor.GoPackage{ + Path: "example.com/path/to/example/example.pb", + Name: "example_pb", + }, + Messages: []*descriptor.Message{msg}, + Services: []*descriptor.Service{ + { + ServiceDescriptorProto: svc, + Methods: []*descriptor.Method{ + { + MethodDescriptorProto: meth, + HTTPMethod: "GET", + RequestType: msg, + ResponseType: msg, + Body: &descriptor.Body{ + DecoderFactoryExpr: "json.NewDecoder", + }, + }, + }, + }, + }, + } + got, err := applyTemplate(crossLinkFixture(&file)) + if err != nil { + t.Errorf("applyTemplate(%#v) failed with %v; want success", file, err) + return + } + if want := "package example_pb\n"; !strings.Contains(got, want) { + t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) + } +} + +func TestApplyTemplateRequestWithoutClientStreaming(t *testing.T) { + msgdesc := &protodescriptor.DescriptorProto{ + Name: proto.String("ExampleMessage"), + Field: []*protodescriptor.FieldDescriptorProto{ + { + Name: proto.String("nested"), + Label: protodescriptor.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + Type: protodescriptor.FieldDescriptorProto_TYPE_MESSAGE.Enum(), + TypeName: proto.String("NestedMessage"), + Number: proto.Int32(1), + }, + }, + } + nesteddesc := &protodescriptor.DescriptorProto{ + Name: proto.String("NestedMessage"), + Field: []*protodescriptor.FieldDescriptorProto{ + { + Name: proto.String("int32"), + Label: protodescriptor.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + Type: protodescriptor.FieldDescriptorProto_TYPE_INT32.Enum(), + Number: proto.Int32(1), + }, + { + Name: proto.String("bool"), + Label: protodescriptor.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + Type: protodescriptor.FieldDescriptorProto_TYPE_BOOL.Enum(), + Number: proto.Int32(2), + }, + }, + } + meth := &protodescriptor.MethodDescriptorProto{ + Name: proto.String("Echo"), + InputType: proto.String("ExampleMessage"), + OutputType: proto.String("ExampleMessage"), + ClientStreaming: proto.Bool(false), + } + svc := &protodescriptor.ServiceDescriptorProto{ + Name: proto.String("ExampleService"), + Method: []*protodescriptor.MethodDescriptorProto{meth}, + } + for _, spec := range []struct { + serverStreaming bool + sigWant string + }{ + { + serverStreaming: false, + sigWant: `func request_ExampleService_Echo(ctx context.Context, client ExampleServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) {`, + }, + { + serverStreaming: true, + sigWant: `func request_ExampleService_Echo(ctx context.Context, client ExampleServiceClient, req *http.Request, pathParams map[string]string) (ExampleService_EchoClient, error) {`, + }, + } { + meth.ServerStreaming = proto.Bool(spec.serverStreaming) + + msg := &descriptor.Message{ + DescriptorProto: msgdesc, + } + nested := &descriptor.Message{ + DescriptorProto: nesteddesc, + } + + nestedField := &descriptor.Field{ + Message: msg, + FieldDescriptorProto: msg.GetField()[0], + } + intField := &descriptor.Field{ + Message: nested, + FieldDescriptorProto: nested.GetField()[0], + } + boolField := &descriptor.Field{ + Message: nested, + FieldDescriptorProto: nested.GetField()[1], + } + file := descriptor.File{ + FileDescriptorProto: &protodescriptor.FileDescriptorProto{ + Name: proto.String("example.proto"), + Package: proto.String("example"), + MessageType: []*protodescriptor.DescriptorProto{msgdesc, nesteddesc}, + Service: []*protodescriptor.ServiceDescriptorProto{svc}, + }, + GoPkg: descriptor.GoPackage{ + Path: "example.com/path/to/example/example.pb", + Name: "example_pb", + }, + Messages: []*descriptor.Message{msg, nested}, + Services: []*descriptor.Service{ + { + ServiceDescriptorProto: svc, + Methods: []*descriptor.Method{ + { + MethodDescriptorProto: meth, + HTTPMethod: "POST", + PathTmpl: httprule.Template{ + Version: 1, + OpCodes: []int{0, 0}, + }, + RequestType: msg, + ResponseType: msg, + PathParams: []descriptor.Parameter{ + { + FieldPath: descriptor.FieldPath{ + { + Name: "nested", + Target: nestedField, + }, + { + Name: "int32", + Target: intField, + }, + }, + Target: intField, + }, + }, + Body: &descriptor.Body{ + DecoderFactoryExpr: "NewExampleDecoder", + FieldPath: descriptor.FieldPath{ + { + Name: "nested", + Target: nestedField, + }, + { + Name: "bool", + Target: boolField, + }, + }, + }, + }, + }, + }, + }, + } + got, err := applyTemplate(crossLinkFixture(&file)) + if err != nil { + t.Errorf("applyTemplate(%#v) failed with %v; want success", file, err) + return + } + if want := spec.sigWant; !strings.Contains(got, want) { + t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) + } + if want := `NewExampleDecoder(req.Body).Decode(&protoReq.GetNested().Bool)`; !strings.Contains(got, want) { + t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) + } + if want := `val, ok = pathParams["nested.int32"]`; !strings.Contains(got, want) { + t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) + } + if want := `protoReq.GetNested().Int32, err = runtime.Int32P(val)`; !strings.Contains(got, want) { + t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) + } + if want := `func RegisterExampleServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {`; !strings.Contains(got, want) { + t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) + } + if want := `pattern_ExampleService_Echo = runtime.MustPattern(runtime.NewPattern(1, []int{0, 0}, []string(nil), ""))`; !strings.Contains(got, want) { + t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) + } + } +} + +func TestApplyTemplateRequestWithClientStreaming(t *testing.T) { + msgdesc := &protodescriptor.DescriptorProto{ + Name: proto.String("ExampleMessage"), + Field: []*protodescriptor.FieldDescriptorProto{ + { + Name: proto.String("nested"), + Label: protodescriptor.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + Type: protodescriptor.FieldDescriptorProto_TYPE_MESSAGE.Enum(), + TypeName: proto.String("NestedMessage"), + Number: proto.Int32(1), + }, + }, + } + nesteddesc := &protodescriptor.DescriptorProto{ + Name: proto.String("NestedMessage"), + Field: []*protodescriptor.FieldDescriptorProto{ + { + Name: proto.String("int32"), + Label: protodescriptor.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + Type: protodescriptor.FieldDescriptorProto_TYPE_INT32.Enum(), + Number: proto.Int32(1), + }, + { + Name: proto.String("bool"), + Label: protodescriptor.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), + Type: protodescriptor.FieldDescriptorProto_TYPE_BOOL.Enum(), + Number: proto.Int32(2), + }, + }, + } + meth := &protodescriptor.MethodDescriptorProto{ + Name: proto.String("Echo"), + InputType: proto.String("ExampleMessage"), + OutputType: proto.String("ExampleMessage"), + ClientStreaming: proto.Bool(true), + } + svc := &protodescriptor.ServiceDescriptorProto{ + Name: proto.String("ExampleService"), + Method: []*protodescriptor.MethodDescriptorProto{meth}, + } + for _, spec := range []struct { + serverStreaming bool + sigWant string + }{ + { + serverStreaming: false, + sigWant: `func request_ExampleService_Echo(ctx context.Context, client ExampleServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) {`, + }, + { + serverStreaming: true, + sigWant: `func request_ExampleService_Echo(ctx context.Context, client ExampleServiceClient, req *http.Request, pathParams map[string]string) (ExampleService_EchoClient, error) {`, + }, + } { + meth.ServerStreaming = proto.Bool(spec.serverStreaming) + + msg := &descriptor.Message{ + DescriptorProto: msgdesc, + } + nested := &descriptor.Message{ + DescriptorProto: nesteddesc, + } + + nestedField := &descriptor.Field{ + Message: msg, + FieldDescriptorProto: msg.GetField()[0], + } + intField := &descriptor.Field{ + Message: nested, + FieldDescriptorProto: nested.GetField()[0], + } + boolField := &descriptor.Field{ + Message: nested, + FieldDescriptorProto: nested.GetField()[1], + } + file := descriptor.File{ + FileDescriptorProto: &protodescriptor.FileDescriptorProto{ + Name: proto.String("example.proto"), + Package: proto.String("example"), + MessageType: []*protodescriptor.DescriptorProto{msgdesc, nesteddesc}, + Service: []*protodescriptor.ServiceDescriptorProto{svc}, + }, + GoPkg: descriptor.GoPackage{ + Path: "example.com/path/to/example/example.pb", + Name: "example_pb", + }, + Messages: []*descriptor.Message{msg, nested}, + Services: []*descriptor.Service{ + { + ServiceDescriptorProto: svc, + Methods: []*descriptor.Method{ + { + MethodDescriptorProto: meth, + HTTPMethod: "POST", + PathTmpl: httprule.Template{ + Version: 1, + OpCodes: []int{0, 0}, + }, + RequestType: msg, + ResponseType: msg, + PathParams: []descriptor.Parameter{ + { + FieldPath: descriptor.FieldPath{ + { + Name: "nested", + Target: nestedField, + }, + { + Name: "int32", + Target: intField, + }, + }, + Target: intField, + }, + }, + Body: &descriptor.Body{ + DecoderFactoryExpr: "NewExampleDecoder", + FieldPath: descriptor.FieldPath{ + { + Name: "nested", + Target: nestedField, + }, + { + Name: "bool", + Target: boolField, + }, + }, + }, + }, + }, + }, + }, + } + got, err := applyTemplate(crossLinkFixture(&file)) + if err != nil { + t.Errorf("applyTemplate(%#v) failed with %v; want success", file, err) + return + } + if want := spec.sigWant; !strings.Contains(got, want) { + t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) + } + if want := `NewExampleDecoder(req.Body)`; !strings.Contains(got, want) { + t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) + } + if want := `func RegisterExampleServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {`; !strings.Contains(got, want) { + t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) + } + if want := `pattern_ExampleService_Echo = runtime.MustPattern(runtime.NewPattern(1, []int{0, 0}, []string(nil), ""))`; !strings.Contains(got, want) { + t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) + } + } +} diff --git a/protoc-gen-grpc-gateway/main.go b/protoc-gen-grpc-gateway/main.go index 52e93d878c1..9455009a14a 100644 --- a/protoc-gen-grpc-gateway/main.go +++ b/protoc-gen-grpc-gateway/main.go @@ -7,6 +7,8 @@ import ( "os" "strings" + "github.com/gengo/grpc-gateway/protoc-gen-grpc-gateway/descriptor" + "github.com/gengo/grpc-gateway/protoc-gen-grpc-gateway/gengateway" "github.com/golang/glog" "github.com/golang/protobuf/proto" plugin "github.com/golang/protobuf/protoc-gen-go/plugin" @@ -36,6 +38,8 @@ func main() { flag.Parse() defer glog.Flush() + reg := descriptor.NewRegistry() + glog.Info("Processing code generator request") req, err := parseReq(os.Stdin) if err != nil { @@ -51,8 +55,8 @@ func main() { continue } name, value := spec[0], spec[1] - if strings.HasSuffix(name, "M") { - importMap[name[1:]] = value + if strings.HasPrefix(name, "M") { + reg.AddPkgMap(name[1:], value) continue } if err := flag.CommandLine.Set(name, value); err != nil { @@ -60,10 +64,31 @@ func main() { } } } - resp := generate(req) + + reg.SetPrefix(*importPrefix) + reg.Load(req) + + g := gengateway.New(reg) + + var targets []*descriptor.File + for _, target := range req.FileToGenerate { + f, err := reg.LookupFile(target) + if err != nil { + glog.Fatal(err) + } + targets = append(targets, f) + } + + var resp plugin.CodeGeneratorResponse + out, err := g.Generate(targets) + if err != nil { + resp.Error = proto.String(err.Error()) + } else { + resp.File = out + } glog.Info("Processed code generator request") - buf, err := proto.Marshal(resp) + buf, err := proto.Marshal(&resp) if err != nil { glog.Fatal(err) } diff --git a/runtime/pattern.go b/runtime/pattern.go index 8d2eae0afe4..3bb1451707c 100644 --- a/runtime/pattern.go +++ b/runtime/pattern.go @@ -112,6 +112,14 @@ func NewPattern(version int, ops []int, pool []string, verb string) (Pattern, er }, nil } +// MustPattern is a helper function which makes it easier to call NewPattern in variable initialization. +func MustPattern(p Pattern, err error) Pattern { + if err != nil { + glog.Fatalf("Pattern initialization failed: %v", err) + } + return p +} + // Match examines components if it matches to the Pattern. // If it matches, the function returns a mapping from field paths to their captured values. // If otherwise, the function returns an error. From 57837298b9f51a8ac5843a940aae714df9dc097f Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 10:50:09 +0900 Subject: [PATCH 14/28] Update examples with the new protoc plugin Add an integration test --- Makefile | 25 +- examples/a_bit_of_everything.pb.go | 2 +- examples/a_bit_of_everything.pb.gw.go | 130 +++++---- examples/a_bit_of_everything.proto | 51 ++-- examples/echo_service.pb.go | 2 +- examples/echo_service.pb.gw.go | 31 ++- examples/echo_service.proto | 14 +- examples/integration_test.go | 378 ++++++++++++++++++++++++++ examples/main.go | 8 +- examples/server/main.go | 5 +- 10 files changed, 529 insertions(+), 117 deletions(-) create mode 100644 examples/integration_test.go diff --git a/Makefile b/Makefile index 799fab51649..d665ee4b8a4 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,23 @@ GO_PLUGIN=bin/protoc-gen-go GO_PLUGIN_PKG=github.com/golang/protobuf/protoc-gen-go GATEWAY_PLUGIN=bin/protoc-gen-grpc-gateway GATEWAY_PLUGIN_PKG=$(PKG)/protoc-gen-grpc-gateway -GATEWAY_PLUGIN_SRC=protoc-gen-grpc-gateway/main.go \ - protoc-gen-grpc-gateway/generator.go +GATEWAY_PLUGIN_SRC= protoc-gen-grpc-gateway/descriptor/name.go \ + protoc-gen-grpc-gateway/descriptor/registry.go \ + protoc-gen-grpc-gateway/descriptor/registry_test.go \ + protoc-gen-grpc-gateway/descriptor/services.go \ + protoc-gen-grpc-gateway/descriptor/services_test.go \ + protoc-gen-grpc-gateway/descriptor/types.go \ + protoc-gen-grpc-gateway/descriptor/types_test.go \ + protoc-gen-grpc-gateway/gengateway/generator.go \ + protoc-gen-grpc-gateway/gengateway/template.go \ + protoc-gen-grpc-gateway/gengateway/template_test.go \ + protoc-gen-grpc-gateway/httprule/compile.go \ + protoc-gen-grpc-gateway/httprule/compile_test.go \ + protoc-gen-grpc-gateway/httprule/parse.go \ + protoc-gen-grpc-gateway/httprule/parse_test.go \ + protoc-gen-grpc-gateway/httprule/types.go \ + protoc-gen-grpc-gateway/httprule/types_test.go \ + protoc-gen-grpc-gateway/main.go OLD_OPTIONS_PROTO=options/options.proto OLD_OPTIONS_GO=$(OLD_OPTIONS_PROTO:.proto=.pb.go) @@ -35,15 +50,15 @@ $(OLD_OPTIONS_GO): $(OLD_OPTIONS_PROTO) $(GO_PLUGIN) $(OPTIONS_GO): $(OPTIONS_PROTO) $(GO_PLUGIN) protoc -I $(PROTOC_INC_PATH) -I$(GOOGLEAPIS_DIR) --plugin=$(GO_PLUGIN) --go_out=$(PKGMAP):$(GOOGLEAPIS_DIR) $(OPTIONS_PROTO) -$(GATEWAY_PLUGIN): $(OLD_OPTIONS_GO) $(GATEWAY_PLUGIN_SRC) +$(GATEWAY_PLUGIN): $(OPTIONS_GO) $(GATEWAY_PLUGIN_SRC) go build -o $@ $(GATEWAY_PLUGIN_PKG) $(EXAMPLE_SVCSRCS): $(GO_PLUGIN) $(EXAMPLES) - protoc -I $(PROTOC_INC_PATH) -I. --plugin=$(GO_PLUGIN) --go_out=$(PKGMAP),plugins=grpc:. $(EXAMPLES) + protoc -I $(PROTOC_INC_PATH) -I. -I$(GOOGLEAPIS_DIR) --plugin=$(GO_PLUGIN) --go_out=$(PKGMAP),plugins=grpc:. $(EXAMPLES) $(EXAMPLE_DEPSRCS): $(GO_PLUGIN) $(EXAMPLE_DEPS) protoc -I $(PROTOC_INC_PATH) -I. --plugin=$(GO_PLUGIN) --go_out=$(PKGMAP),plugins=grpc:. $(EXAMPLE_DEPS) $(EXAMPLE_GWSRCS): $(GATEWAY_PLUGIN) $(EXAMPLES) - protoc -I $(PROTOC_INC_PATH) -I. --plugin=$(GATEWAY_PLUGIN) --grpc-gateway_out=logtostderr=true,import_prefix=$(PKG):. $(EXAMPLES) + protoc -I $(PROTOC_INC_PATH) -I. -I$(GOOGLEAPIS_DIR) --plugin=$(GATEWAY_PLUGIN) --grpc-gateway_out=logtostderr=true,$(PKGMAP):. $(EXAMPLES) test: $(EXAMPLE_SVCSRCS) $(EXAMPLE_GWSRCS) $(EXAMPLE_DEPSRCS) go test $(PKG)/... diff --git a/examples/a_bit_of_everything.pb.go b/examples/a_bit_of_everything.pb.go index 905560190ea..f4b1da3c0f6 100644 --- a/examples/a_bit_of_everything.pb.go +++ b/examples/a_bit_of_everything.pb.go @@ -6,7 +6,7 @@ package main import proto "github.com/golang/protobuf/proto" -// discarding unused import gengo_grpc_gateway "options/options.pb" +// discarding unused import google_api1 "google/api/annotations.pb" import gengo_grpc_gateway_examples_sub "github.com/gengo/grpc-gateway/examples/sub" import ( diff --git a/examples/a_bit_of_everything.pb.gw.go b/examples/a_bit_of_everything.pb.gw.go index a3a12810987..ddc843cbf8f 100644 --- a/examples/a_bit_of_everything.pb.gw.go +++ b/examples/a_bit_of_everything.pb.gw.go @@ -14,32 +14,26 @@ import ( "io" "net/http" + "github.com/gengo/grpc-gateway/examples/sub" "github.com/gengo/grpc-gateway/runtime" "github.com/golang/glog" "github.com/golang/protobuf/proto" - "github.com/zenazn/goji/web" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/codes" - - gengo_grpc_gateway_examples_sub "github.com/gengo/grpc-gateway/examples/sub" ) var _ codes.Code var _ io.Reader var _ = runtime.String -func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client ABitOfEverythingServiceClient, req *http.Request) (msg proto.Message, err error) { +func request_ABitOfEverythingService_Create(ctx context.Context, client ABitOfEverythingServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) { var protoReq ABitOfEverything - if err = json.NewDecoder(req.Body).Decode(&protoReq); err != nil { - return nil, err - } - var val string var ok bool - val, ok = c.URLParams["float_value"] + val, ok = pathParams["float_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "float_value") } @@ -48,7 +42,7 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return nil, err } - val, ok = c.URLParams["double_value"] + val, ok = pathParams["double_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "double_value") } @@ -57,7 +51,7 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return nil, err } - val, ok = c.URLParams["int64_value"] + val, ok = pathParams["int64_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "int64_value") } @@ -66,7 +60,7 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return nil, err } - val, ok = c.URLParams["uint64_value"] + val, ok = pathParams["uint64_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "uint64_value") } @@ -75,7 +69,7 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return nil, err } - val, ok = c.URLParams["int32_value"] + val, ok = pathParams["int32_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "int32_value") } @@ -84,7 +78,7 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return nil, err } - val, ok = c.URLParams["fixed64_value"] + val, ok = pathParams["fixed64_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "fixed64_value") } @@ -93,7 +87,7 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return nil, err } - val, ok = c.URLParams["fixed32_value"] + val, ok = pathParams["fixed32_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "fixed32_value") } @@ -102,7 +96,7 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return nil, err } - val, ok = c.URLParams["bool_value"] + val, ok = pathParams["bool_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "bool_value") } @@ -111,7 +105,7 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return nil, err } - val, ok = c.URLParams["string_value"] + val, ok = pathParams["string_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "string_value") } @@ -120,7 +114,7 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return nil, err } - val, ok = c.URLParams["uint32_value"] + val, ok = pathParams["uint32_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "uint32_value") } @@ -129,7 +123,7 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return nil, err } - val, ok = c.URLParams["sfixed32_value"] + val, ok = pathParams["sfixed32_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "sfixed32_value") } @@ -138,7 +132,7 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return nil, err } - val, ok = c.URLParams["sfixed64_value"] + val, ok = pathParams["sfixed64_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "sfixed64_value") } @@ -147,7 +141,7 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return nil, err } - val, ok = c.URLParams["sint32_value"] + val, ok = pathParams["sint32_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "sint32_value") } @@ -156,7 +150,7 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return nil, err } - val, ok = c.URLParams["sint64_value"] + val, ok = pathParams["sint64_value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "sint64_value") } @@ -168,17 +162,17 @@ func request_ABitOfEverythingService_Create(ctx context.Context, c web.C, client return client.Create(ctx, &protoReq) } -func request_ABitOfEverythingService_CreateBody(ctx context.Context, c web.C, client ABitOfEverythingServiceClient, req *http.Request) (msg proto.Message, err error) { +func request_ABitOfEverythingService_CreateBody(ctx context.Context, client ABitOfEverythingServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) { var protoReq ABitOfEverything if err = json.NewDecoder(req.Body).Decode(&protoReq); err != nil { - return nil, err + return nil, grpc.Errorf(codes.InvalidArgument, "%v", err) } return client.CreateBody(ctx, &protoReq) } -func request_ABitOfEverythingService_BulkCreate(ctx context.Context, c web.C, client ABitOfEverythingServiceClient, req *http.Request) (msg proto.Message, err error) { +func request_ABitOfEverythingService_BulkCreate(ctx context.Context, client ABitOfEverythingServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) { stream, err := client.BulkCreate(ctx) if err != nil { glog.Errorf("Failed to start streaming: %v", err) @@ -205,13 +199,13 @@ func request_ABitOfEverythingService_BulkCreate(ctx context.Context, c web.C, cl } -func request_ABitOfEverythingService_Lookup(ctx context.Context, c web.C, client ABitOfEverythingServiceClient, req *http.Request) (msg proto.Message, err error) { +func request_ABitOfEverythingService_Lookup(ctx context.Context, client ABitOfEverythingServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) { var protoReq IdMessage var val string var ok bool - val, ok = c.URLParams["uuid"] + val, ok = pathParams["uuid"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "uuid") } @@ -223,23 +217,23 @@ func request_ABitOfEverythingService_Lookup(ctx context.Context, c web.C, client return client.Lookup(ctx, &protoReq) } -func request_ABitOfEverythingService_List(ctx context.Context, c web.C, client ABitOfEverythingServiceClient, req *http.Request) (ABitOfEverythingService_ListClient, error) { +func request_ABitOfEverythingService_List(ctx context.Context, client ABitOfEverythingServiceClient, req *http.Request, pathParams map[string]string) (ABitOfEverythingService_ListClient, error) { var protoReq EmptyMessage return client.List(ctx, &protoReq) } -func request_ABitOfEverythingService_Update(ctx context.Context, c web.C, client ABitOfEverythingServiceClient, req *http.Request) (msg proto.Message, err error) { +func request_ABitOfEverythingService_Update(ctx context.Context, client ABitOfEverythingServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) { var protoReq ABitOfEverything if err = json.NewDecoder(req.Body).Decode(&protoReq); err != nil { - return nil, err + return nil, grpc.Errorf(codes.InvalidArgument, "%v", err) } var val string var ok bool - val, ok = c.URLParams["uuid"] + val, ok = pathParams["uuid"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "uuid") } @@ -251,13 +245,13 @@ func request_ABitOfEverythingService_Update(ctx context.Context, c web.C, client return client.Update(ctx, &protoReq) } -func request_ABitOfEverythingService_Delete(ctx context.Context, c web.C, client ABitOfEverythingServiceClient, req *http.Request) (msg proto.Message, err error) { +func request_ABitOfEverythingService_Delete(ctx context.Context, client ABitOfEverythingServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) { var protoReq IdMessage var val string var ok bool - val, ok = c.URLParams["uuid"] + val, ok = pathParams["uuid"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "uuid") } @@ -269,13 +263,13 @@ func request_ABitOfEverythingService_Delete(ctx context.Context, c web.C, client return client.Delete(ctx, &protoReq) } -func request_ABitOfEverythingService_Echo(ctx context.Context, c web.C, client ABitOfEverythingServiceClient, req *http.Request) (msg proto.Message, err error) { - var protoReq gengo_grpc_gateway_examples_sub.StringMessage +func request_ABitOfEverythingService_Echo(ctx context.Context, client ABitOfEverythingServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) { + var protoReq sub.StringMessage var val string var ok bool - val, ok = c.URLParams["value"] + val, ok = pathParams["value"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "value") } @@ -287,7 +281,7 @@ func request_ABitOfEverythingService_Echo(ctx context.Context, c web.C, client A return client.Echo(ctx, &protoReq) } -func request_ABitOfEverythingService_BulkEcho(ctx context.Context, c web.C, client ABitOfEverythingServiceClient, req *http.Request) (ABitOfEverythingService_BulkEchoClient, error) { +func request_ABitOfEverythingService_BulkEcho(ctx context.Context, client ABitOfEverythingServiceClient, req *http.Request, pathParams map[string]string) (ABitOfEverythingService_BulkEchoClient, error) { stream, err := client.BulkEcho(ctx) if err != nil { glog.Errorf("Failed to start streaming: %v", err) @@ -295,7 +289,7 @@ func request_ABitOfEverythingService_BulkEcho(ctx context.Context, c web.C, clie } dec := json.NewDecoder(req.Body) for { - var protoReq gengo_grpc_gateway_examples_sub.StringMessage + var protoReq sub.StringMessage err = dec.Decode(&protoReq) if err == io.EOF { break @@ -320,7 +314,7 @@ func request_ABitOfEverythingService_BulkEcho(ctx context.Context, c web.C, clie // RegisterABitOfEverythingServiceHandlerFromEndpoint is same as RegisterABitOfEverythingServiceHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterABitOfEverythingServiceHandlerFromEndpoint(ctx context.Context, mux *web.Mux, endpoint string) (err error) { +func RegisterABitOfEverythingServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string) (err error) { conn, err := grpc.Dial(endpoint) if err != nil { return err @@ -345,11 +339,11 @@ func RegisterABitOfEverythingServiceHandlerFromEndpoint(ctx context.Context, mux // RegisterABitOfEverythingServiceHandler registers the http handlers for service ABitOfEverythingService to "mux". // The handlers forward requests to the grpc endpoint over "conn". -func RegisterABitOfEverythingServiceHandler(ctx context.Context, mux *web.Mux, conn *grpc.ClientConn) error { +func RegisterABitOfEverythingServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { client := NewABitOfEverythingServiceClient(conn) - mux.Post("/v1/example/a_bit_of_everything/:float_value/:double_value/:int64_value/separator/:uint64_value/:int32_value/:fixed64_value/:fixed32_value/:bool_value/:string_value/:uint32_value/:sfixed32_value/:sfixed64_value/:sint32_value/:sint64_value", func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_ABitOfEverythingService_Create(ctx, c, client, req) + mux.Handle("POST", pattern_ABitOfEverythingService_Create, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + resp, err := request_ABitOfEverythingService_Create(ctx, client, req, pathParams) if err != nil { runtime.HTTPError(w, err) return @@ -359,8 +353,8 @@ func RegisterABitOfEverythingServiceHandler(ctx context.Context, mux *web.Mux, c }) - mux.Post("/v1/example/a_bit_of_everything", func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_ABitOfEverythingService_CreateBody(ctx, c, client, req) + mux.Handle("POST", pattern_ABitOfEverythingService_CreateBody, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + resp, err := request_ABitOfEverythingService_CreateBody(ctx, client, req, pathParams) if err != nil { runtime.HTTPError(w, err) return @@ -370,8 +364,8 @@ func RegisterABitOfEverythingServiceHandler(ctx context.Context, mux *web.Mux, c }) - mux.Post("/v1/example/a_bit_of_everything/bulk", func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_ABitOfEverythingService_BulkCreate(ctx, c, client, req) + mux.Handle("POST", pattern_ABitOfEverythingService_BulkCreate, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + resp, err := request_ABitOfEverythingService_BulkCreate(ctx, client, req, pathParams) if err != nil { runtime.HTTPError(w, err) return @@ -381,8 +375,8 @@ func RegisterABitOfEverythingServiceHandler(ctx context.Context, mux *web.Mux, c }) - mux.Get("/v1/example/a_bit_of_everything/:uuid", func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_ABitOfEverythingService_Lookup(ctx, c, client, req) + mux.Handle("GET", pattern_ABitOfEverythingService_Lookup, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + resp, err := request_ABitOfEverythingService_Lookup(ctx, client, req, pathParams) if err != nil { runtime.HTTPError(w, err) return @@ -392,8 +386,8 @@ func RegisterABitOfEverythingServiceHandler(ctx context.Context, mux *web.Mux, c }) - mux.Get("/v1/example/a_bit_of_everything", func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_ABitOfEverythingService_List(ctx, c, client, req) + mux.Handle("GET", pattern_ABitOfEverythingService_List, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + resp, err := request_ABitOfEverythingService_List(ctx, client, req, pathParams) if err != nil { runtime.HTTPError(w, err) return @@ -403,8 +397,8 @@ func RegisterABitOfEverythingServiceHandler(ctx context.Context, mux *web.Mux, c }) - mux.Put("/v1/example/a_bit_of_everything/:uuid", func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_ABitOfEverythingService_Update(ctx, c, client, req) + mux.Handle("PUT", pattern_ABitOfEverythingService_Update, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + resp, err := request_ABitOfEverythingService_Update(ctx, client, req, pathParams) if err != nil { runtime.HTTPError(w, err) return @@ -414,8 +408,8 @@ func RegisterABitOfEverythingServiceHandler(ctx context.Context, mux *web.Mux, c }) - mux.Delete("/v1/example/a_bit_of_everything/:uuid", func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_ABitOfEverythingService_Delete(ctx, c, client, req) + mux.Handle("DELETE", pattern_ABitOfEverythingService_Delete, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + resp, err := request_ABitOfEverythingService_Delete(ctx, client, req, pathParams) if err != nil { runtime.HTTPError(w, err) return @@ -425,8 +419,8 @@ func RegisterABitOfEverythingServiceHandler(ctx context.Context, mux *web.Mux, c }) - mux.Get("/v1/example/a_bit_of_everything/echo/:value", func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_ABitOfEverythingService_Echo(ctx, c, client, req) + mux.Handle("GET", pattern_ABitOfEverythingService_Echo, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + resp, err := request_ABitOfEverythingService_Echo(ctx, client, req, pathParams) if err != nil { runtime.HTTPError(w, err) return @@ -436,8 +430,8 @@ func RegisterABitOfEverythingServiceHandler(ctx context.Context, mux *web.Mux, c }) - mux.Post("/v1/example/a_bit_of_everything/echo", func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_ABitOfEverythingService_BulkEcho(ctx, c, client, req) + mux.Handle("POST", pattern_ABitOfEverythingService_BulkEcho, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + resp, err := request_ABitOfEverythingService_BulkEcho(ctx, client, req, pathParams) if err != nil { runtime.HTTPError(w, err) return @@ -449,3 +443,23 @@ func RegisterABitOfEverythingServiceHandler(ctx context.Context, mux *web.Mux, c return nil } + +var ( + pattern_ABitOfEverythingService_Create = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 1, 0, 4, 1, 5, 4, 1, 0, 4, 1, 5, 5, 2, 6, 1, 0, 4, 1, 5, 7, 1, 0, 4, 1, 5, 8, 1, 0, 4, 1, 5, 9, 1, 0, 4, 1, 5, 10, 1, 0, 4, 1, 5, 11, 2, 12, 1, 0, 4, 2, 5, 13, 1, 0, 4, 1, 5, 14, 1, 0, 4, 1, 5, 15, 1, 0, 4, 1, 5, 16, 1, 0, 4, 1, 5, 17, 1, 0, 4, 1, 5, 18}, []string{"v1", "example", "a_bit_of_everything", "float_value", "double_value", "int64_value", "separator", "uint64_value", "int32_value", "fixed64_value", "fixed32_value", "bool_value", "strprefix", "string_value", "uint32_value", "sfixed32_value", "sfixed64_value", "sint32_value", "sint64_value"}, "")) + + pattern_ABitOfEverythingService_CreateBody = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "example", "a_bit_of_everything"}, "")) + + pattern_ABitOfEverythingService_BulkCreate = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "example", "a_bit_of_everything", "bulk"}, "")) + + pattern_ABitOfEverythingService_Lookup = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "example", "a_bit_of_everything", "uuid"}, "")) + + pattern_ABitOfEverythingService_List = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "example", "a_bit_of_everything"}, "")) + + pattern_ABitOfEverythingService_Update = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "example", "a_bit_of_everything", "uuid"}, "")) + + pattern_ABitOfEverythingService_Delete = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "example", "a_bit_of_everything", "uuid"}, "")) + + pattern_ABitOfEverythingService_Echo = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"v1", "example", "a_bit_of_everything", "echo", "value"}, "")) + + pattern_ABitOfEverythingService_BulkEcho = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "example", "a_bit_of_everything", "echo"}, "")) +) diff --git a/examples/a_bit_of_everything.proto b/examples/a_bit_of_everything.proto index 0333fc181b3..434ff2b4e25 100644 --- a/examples/a_bit_of_everything.proto +++ b/examples/a_bit_of_everything.proto @@ -2,7 +2,7 @@ syntax = "proto3"; option go_package = "main"; package gengo.grpc.gateway.examples; -import "options/options.proto"; +import "google/api/annotations.proto"; import "examples/sub/message.proto"; message ABitOfEverything { @@ -40,57 +40,52 @@ message IdMessage { service ABitOfEverythingService { rpc Create(ABitOfEverything) returns (ABitOfEverything) { - option (gengo.grpc.gateway.ApiMethodOptions.api_options) = { - path: "/v1/example/a_bit_of_everything/:float_value/:double_value/:int64_value/separator/:uint64_value/:int32_value/:fixed64_value/:fixed32_value/:bool_value/:string_value/:uint32_value/:sfixed32_value/:sfixed64_value/:sint32_value/:sint64_value" - method: "POST" + option (google.api.http) = { + post: "/v1/example/a_bit_of_everything/{float_value}/{double_value}/{int64_value}/separator/{uint64_value}/{int32_value}/{fixed64_value}/{fixed32_value}/{bool_value}/{string_value=strprefix/*}/{uint32_value}/{sfixed32_value}/{sfixed64_value}/{sint32_value}/{sint64_value}" }; } rpc CreateBody(ABitOfEverything) returns (ABitOfEverything) { - option (gengo.grpc.gateway.ApiMethodOptions.api_options) = { - path: "/v1/example/a_bit_of_everything" - method: "POST" + option (google.api.http) = { + post: "/v1/example/a_bit_of_everything" + body: "*" }; } rpc BulkCreate(stream ABitOfEverything) returns (EmptyMessage) { - option (gengo.grpc.gateway.ApiMethodOptions.api_options) = { - path: "/v1/example/a_bit_of_everything/bulk" - method: "POST" + option (google.api.http) = { + post: "/v1/example/a_bit_of_everything/bulk" + body: "*" }; } rpc Lookup(IdMessage) returns (ABitOfEverything) { - option (gengo.grpc.gateway.ApiMethodOptions.api_options) = { - path: "/v1/example/a_bit_of_everything/:uuid" - method: "GET" + option (google.api.http) = { + get: "/v1/example/a_bit_of_everything/{uuid}" }; } rpc List(EmptyMessage) returns (stream ABitOfEverything) { - option (gengo.grpc.gateway.ApiMethodOptions.api_options) = { - path: "/v1/example/a_bit_of_everything" - method: "GET" + option (google.api.http) = { + get: "/v1/example/a_bit_of_everything" }; } rpc Update(ABitOfEverything) returns (EmptyMessage) { - option (gengo.grpc.gateway.ApiMethodOptions.api_options) = { - path: "/v1/example/a_bit_of_everything/:uuid" - method: "PUT" + option (google.api.http) = { + put: "/v1/example/a_bit_of_everything/{uuid}" + body: "*" }; } rpc Delete(IdMessage) returns (EmptyMessage) { - option (gengo.grpc.gateway.ApiMethodOptions.api_options) = { - path: "/v1/example/a_bit_of_everything/:uuid" - method: "DELETE" + option (google.api.http) = { + delete: "/v1/example/a_bit_of_everything/{uuid}" }; } rpc Echo(gengo.grpc.gateway.examples.sub.StringMessage) returns (gengo.grpc.gateway.examples.sub.StringMessage) { - option (gengo.grpc.gateway.ApiMethodOptions.api_options) = { - path: "/v1/example/a_bit_of_everything/echo/:value" - method: "GET" + option (google.api.http) = { + get: "/v1/example/a_bit_of_everything/echo/{value}" }; } rpc BulkEcho(stream gengo.grpc.gateway.examples.sub.StringMessage) returns (stream gengo.grpc.gateway.examples.sub.StringMessage) { - option (gengo.grpc.gateway.ApiMethodOptions.api_options) = { - path: "/v1/example/a_bit_of_everything/echo" - method: "POST" + option (google.api.http) = { + post: "/v1/example/a_bit_of_everything/echo" + body: "*" }; } } diff --git a/examples/echo_service.pb.go b/examples/echo_service.pb.go index 37926cc1a38..39b3d530fef 100644 --- a/examples/echo_service.pb.go +++ b/examples/echo_service.pb.go @@ -16,7 +16,7 @@ package main import proto "github.com/golang/protobuf/proto" -// discarding unused import gengo_grpc_gateway "options/options.pb" +// discarding unused import google_api1 "google/api/annotations.pb" import ( context "golang.org/x/net/context" diff --git a/examples/echo_service.pb.gw.go b/examples/echo_service.pb.gw.go index 510044bcf5b..3f9e05566a5 100644 --- a/examples/echo_service.pb.gw.go +++ b/examples/echo_service.pb.gw.go @@ -17,7 +17,6 @@ import ( "github.com/gengo/grpc-gateway/runtime" "github.com/golang/glog" "github.com/golang/protobuf/proto" - "github.com/zenazn/goji/web" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -27,13 +26,17 @@ var _ codes.Code var _ io.Reader var _ = runtime.String -func request_EchoService_Echo(ctx context.Context, c web.C, client EchoServiceClient, req *http.Request) (msg proto.Message, err error) { +func request_EchoService_Echo(ctx context.Context, client EchoServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) { var protoReq SimpleMessage + if err = json.NewDecoder(req.Body).Decode(&protoReq); err != nil { + return nil, grpc.Errorf(codes.InvalidArgument, "%v", err) + } + var val string var ok bool - val, ok = c.URLParams["id"] + val, ok = pathParams["id"] if !ok { return nil, grpc.Errorf(codes.InvalidArgument, "missing parameter %s", "id") } @@ -45,11 +48,11 @@ func request_EchoService_Echo(ctx context.Context, c web.C, client EchoServiceCl return client.Echo(ctx, &protoReq) } -func request_EchoService_EchoBody(ctx context.Context, c web.C, client EchoServiceClient, req *http.Request) (msg proto.Message, err error) { +func request_EchoService_EchoBody(ctx context.Context, client EchoServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) { var protoReq SimpleMessage if err = json.NewDecoder(req.Body).Decode(&protoReq); err != nil { - return nil, err + return nil, grpc.Errorf(codes.InvalidArgument, "%v", err) } return client.EchoBody(ctx, &protoReq) @@ -57,7 +60,7 @@ func request_EchoService_EchoBody(ctx context.Context, c web.C, client EchoServi // RegisterEchoServiceHandlerFromEndpoint is same as RegisterEchoServiceHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterEchoServiceHandlerFromEndpoint(ctx context.Context, mux *web.Mux, endpoint string) (err error) { +func RegisterEchoServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string) (err error) { conn, err := grpc.Dial(endpoint) if err != nil { return err @@ -82,11 +85,11 @@ func RegisterEchoServiceHandlerFromEndpoint(ctx context.Context, mux *web.Mux, e // RegisterEchoServiceHandler registers the http handlers for service EchoService to "mux". // The handlers forward requests to the grpc endpoint over "conn". -func RegisterEchoServiceHandler(ctx context.Context, mux *web.Mux, conn *grpc.ClientConn) error { +func RegisterEchoServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { client := NewEchoServiceClient(conn) - mux.Post("/v1/example/echo/:id", func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_EchoService_Echo(ctx, c, client, req) + mux.Handle("POST", pattern_EchoService_Echo, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + resp, err := request_EchoService_Echo(ctx, client, req, pathParams) if err != nil { runtime.HTTPError(w, err) return @@ -96,8 +99,8 @@ func RegisterEchoServiceHandler(ctx context.Context, mux *web.Mux, conn *grpc.Cl }) - mux.Post("/v1/example/echo_body", func(c web.C, w http.ResponseWriter, req *http.Request) { - resp, err := request_EchoService_EchoBody(ctx, c, client, req) + mux.Handle("POST", pattern_EchoService_EchoBody, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + resp, err := request_EchoService_EchoBody(ctx, client, req, pathParams) if err != nil { runtime.HTTPError(w, err) return @@ -109,3 +112,9 @@ func RegisterEchoServiceHandler(ctx context.Context, mux *web.Mux, conn *grpc.Cl return nil } + +var ( + pattern_EchoService_Echo = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "example", "echo", "id"}, "")) + + pattern_EchoService_EchoBody = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "example", "echo_body"}, "")) +) diff --git a/examples/echo_service.proto b/examples/echo_service.proto index 6fd35ab8ea4..fbbdeb245fb 100644 --- a/examples/echo_service.proto +++ b/examples/echo_service.proto @@ -2,7 +2,7 @@ syntax = "proto3"; option go_package = "main"; package gengo.grpc.gateway.example; -import "options/options.proto"; +import "google/api/annotations.proto"; message SimpleMessage { string id = 1; @@ -10,15 +10,15 @@ message SimpleMessage { service EchoService { rpc Echo(SimpleMessage) returns (SimpleMessage) { - option (gengo.grpc.gateway.ApiMethodOptions.api_options) = { - path: "/v1/example/echo/:id" - method: "POST" + option (google.api.http) = { + post: "/v1/example/echo/{id}" + body: "*" }; } rpc EchoBody(SimpleMessage) returns (SimpleMessage) { - option (gengo.grpc.gateway.ApiMethodOptions.api_options) = { - path: "/v1/example/echo_body" - method: "POST" + option (google.api.http) = { + post: "/v1/example/echo_body" + body: "*" }; } } diff --git a/examples/integration_test.go b/examples/integration_test.go new file mode 100644 index 00000000000..8bb3191f5d9 --- /dev/null +++ b/examples/integration_test.go @@ -0,0 +1,378 @@ +package main_test + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "reflect" + "strings" + "testing" + "time" + + gw "github.com/gengo/grpc-gateway/examples" + server "github.com/gengo/grpc-gateway/examples/server" +) + +func TestIntegration(t *testing.T) { + if testing.Short() { + t.Skip() + return + } + + go func() { + if err := server.Run(); err != nil { + t.Errorf("server.Run() failed with %v; want success", err) + return + } + }() + go func() { + if err := gw.Run(); err != nil { + t.Errorf("gw.Run() failed with %v; want success", err) + return + } + }() + + time.Sleep(100 * time.Millisecond) + testEcho(t) + testEchoBody(t) + testABECreate(t) + testABECreateBody(t) + testABEBulkCreate(t) + testABELookup(t) + testABEList(t) +} + +func testEcho(t *testing.T) { + url := "http://localhost:8080/v1/example/echo/myid" + resp, err := http.Post(url, "application/json", strings.NewReader("{}")) + if err != nil { + t.Errorf("http.Post(%q) failed with %v; want success", url, err) + return + } + defer resp.Body.Close() + buf, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("iotuil.ReadAll(resp.Body) failed with %v; want success", err) + return + } + + if got, want := resp.StatusCode, http.StatusOK; got != want { + t.Errorf("resp.StatusCode = %d; want %d", got, want) + t.Logf("%s", buf) + } + + var msg gw.SimpleMessage + if err := json.Unmarshal(buf, &msg); err != nil { + t.Errorf("json.Unmarshal(%s, &msg) failed with %v; want success", buf, err) + return + } + if got, want := msg.Id, "myid"; got != want { + t.Errorf("msg.Id = %q; want %q", got, want) + } +} + +func testEchoBody(t *testing.T) { + sent := gw.SimpleMessage{Id: "example"} + buf, err := json.Marshal(sent) + if err != nil { + t.Fatalf("json.Marshal(%#v) failed with %v; want success", sent, err) + } + + url := "http://localhost:8080/v1/example/echo_body" + resp, err := http.Post(url, "", bytes.NewReader(buf)) + if err != nil { + t.Errorf("http.Post(%q) failed with %v; want success", url, err) + return + } + defer resp.Body.Close() + buf, err = ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("iotuil.ReadAll(resp.Body) failed with %v; want success", err) + return + } + + if got, want := resp.StatusCode, http.StatusOK; got != want { + t.Errorf("resp.StatusCode = %d; want %d", got, want) + t.Logf("%s", buf) + } + + var received gw.SimpleMessage + if err := json.Unmarshal(buf, &received); err != nil { + t.Errorf("json.Unmarshal(%s, &msg) failed with %v; want success", buf, err) + return + } + if got, want := received, sent; !reflect.DeepEqual(got, want) { + t.Errorf("msg.Id = %q; want %q", got, want) + } +} + +func testABECreate(t *testing.T) { + want := gw.ABitOfEverything{ + FloatValue: 1.5, + DoubleValue: 2.5, + Int64Value: 4294967296, + Uint64Value: 9223372036854775807, + Int32Value: -2147483648, + Fixed64Value: 9223372036854775807, + Fixed32Value: 4294967295, + BoolValue: true, + StringValue: "strprefix/foo", + Uint32Value: 4294967295, + Sfixed32Value: 2147483647, + Sfixed64Value: -4611686018427387904, + Sint32Value: 2147483647, + Sint64Value: 4611686018427387903, + } + url := fmt.Sprintf("http://localhost:8080/v1/example/a_bit_of_everything/%f/%f/%d/separator/%d/%d/%d/%d/%v/%s/%d/%d/%d/%d/%d", want.FloatValue, want.DoubleValue, want.Int64Value, want.Uint64Value, want.Int32Value, want.Fixed64Value, want.Fixed32Value, want.BoolValue, want.StringValue, want.Uint32Value, want.Sfixed32Value, want.Sfixed64Value, want.Sint32Value, want.Sint64Value) + + resp, err := http.Post(url, "application/json", strings.NewReader("{}")) + if err != nil { + t.Errorf("http.Post(%q) failed with %v; want success", url, err) + return + } + defer resp.Body.Close() + buf, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("iotuil.ReadAll(resp.Body) failed with %v; want success", err) + return + } + + if got, want := resp.StatusCode, http.StatusOK; got != want { + t.Errorf("resp.StatusCode = %d; want %d", got, want) + t.Logf("%s", buf) + } + + var msg gw.ABitOfEverything + if err := json.Unmarshal(buf, &msg); err != nil { + t.Errorf("json.Unmarshal(%s, &msg) failed with %v; want success", buf, err) + return + } + if msg.Uuid == "" { + t.Error("msg.Uuid is empty; want not empty") + } + msg.Uuid = "" + if got := msg; !reflect.DeepEqual(got, want) { + t.Errorf("msg= %v; want %v", &got, &want) + } +} + +func testABECreateBody(t *testing.T) { + want := gw.ABitOfEverything{ + FloatValue: 1.5, + DoubleValue: 2.5, + Int64Value: 4294967296, + Uint64Value: 9223372036854775807, + Int32Value: -2147483648, + Fixed64Value: 9223372036854775807, + Fixed32Value: 4294967295, + BoolValue: true, + StringValue: "strprefix/foo", + Uint32Value: 4294967295, + Sfixed32Value: 2147483647, + Sfixed64Value: -4611686018427387904, + Sint32Value: 2147483647, + Sint64Value: 4611686018427387903, + + Nested: []*gw.ABitOfEverything_Nested{ + { + Name: "bar", + Amount: 10, + }, + { + Name: "baz", + Amount: 20, + }, + }, + } + url := "http://localhost:8080/v1/example/a_bit_of_everything" + buf, err := json.Marshal(want) + if err != nil { + t.Fatalf("json.Marshal(%#v) failed with %v; want success", want, err) + } + + resp, err := http.Post(url, "application/json", bytes.NewReader(buf)) + if err != nil { + t.Errorf("http.Post(%q) failed with %v; want success", url, err) + return + } + defer resp.Body.Close() + buf, err = ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("iotuil.ReadAll(resp.Body) failed with %v; want success", err) + return + } + + if got, want := resp.StatusCode, http.StatusOK; got != want { + t.Errorf("resp.StatusCode = %d; want %d", got, want) + t.Logf("%s", buf) + } + + var msg gw.ABitOfEverything + if err := json.Unmarshal(buf, &msg); err != nil { + t.Errorf("json.Unmarshal(%s, &msg) failed with %v; want success", buf, err) + return + } + if msg.Uuid == "" { + t.Error("msg.Uuid is empty; want not empty") + } + msg.Uuid = "" + if got := msg; !reflect.DeepEqual(got, want) { + t.Errorf("msg= %v; want %v", &got, &want) + } +} + +func testABEBulkCreate(t *testing.T) { + r, w := io.Pipe() + go func(w io.WriteCloser) { + defer func() { + if cerr := w.Close(); cerr != nil { + t.Errorf("w.Close() failed with %v; want success", cerr) + } + }() + for _, val := range []string{ + "foo", "bar", "baz", "qux", "quux", + } { + want := gw.ABitOfEverything{ + FloatValue: 1.5, + DoubleValue: 2.5, + Int64Value: 4294967296, + Uint64Value: 9223372036854775807, + Int32Value: -2147483648, + Fixed64Value: 9223372036854775807, + Fixed32Value: 4294967295, + BoolValue: true, + StringValue: fmt.Sprintf("strprefix/%s", val), + Uint32Value: 4294967295, + Sfixed32Value: 2147483647, + Sfixed64Value: -4611686018427387904, + Sint32Value: 2147483647, + Sint64Value: 4611686018427387903, + + Nested: []*gw.ABitOfEverything_Nested{ + { + Name: "hoge", + Amount: 10, + }, + { + Name: "fuga", + Amount: 20, + }, + }, + } + buf, err := json.Marshal(want) + if err != nil { + t.Fatalf("json.Marshal(%#v) failed with %v; want success", want, err) + } + if _, err := w.Write(buf); err != nil { + t.Errorf("w.Write(%s) failed with %v; want success", buf, err) + return + } + if _, err := io.WriteString(w, "\n"); err != nil { + t.Errorf("w.Write(%s) failed with %v; want success", buf, err) + return + } + } + }(w) + url := "http://localhost:8080/v1/example/a_bit_of_everything/bulk" + resp, err := http.Post(url, "application/json", r) + if err != nil { + t.Errorf("http.Post(%q) failed with %v; want success", url, err) + return + } + defer resp.Body.Close() + buf, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("iotuil.ReadAll(resp.Body) failed with %v; want success", err) + return + } + + if got, want := resp.StatusCode, http.StatusOK; got != want { + t.Errorf("resp.StatusCode = %d; want %d", got, want) + t.Logf("%s", buf) + } + + var msg gw.EmptyMessage + if err := json.Unmarshal(buf, &msg); err != nil { + t.Errorf("json.Unmarshal(%s, &msg) failed with %v; want success", buf, err) + return + } +} + +func testABELookup(t *testing.T) { + url := "http://localhost:8080/v1/example/a_bit_of_everything" + cresp, err := http.Post(url, "application/json", strings.NewReader(` + {"bool_value": true, "string_value": "strprefix/example"} + `)) + if err != nil { + t.Errorf("http.Post(%q) failed with %v; want success", url, err) + return + } + defer cresp.Body.Close() + buf, err := ioutil.ReadAll(cresp.Body) + if err != nil { + t.Errorf("iotuil.ReadAll(cresp.Body) failed with %v; want success", err) + return + } + if got, want := cresp.StatusCode, http.StatusOK; got != want { + t.Errorf("resp.StatusCode = %d; want %d", got, want) + t.Logf("%s", buf) + return + } + + var want gw.ABitOfEverything + if err := json.Unmarshal(buf, &want); err != nil { + t.Errorf("json.Unmarshal(%s, &want) failed with %v; want success", buf, err) + return + } + + resp, err := http.Get(fmt.Sprintf("%s/%s", url, want.Uuid)) + if err != nil { + t.Errorf("http.Get(%q) failed with %v; want success", err) + return + } + defer resp.Body.Close() + + buf, err = ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("ioutil.ReadAll(resp.Body) failed with %v; want success", err) + return + } + + var msg gw.ABitOfEverything + if err := json.Unmarshal(buf, &msg); err != nil { + t.Errorf("json.Unmarshal(%s, &msg) failed with %v; want success", buf, err) + return + } + if got := msg; !reflect.DeepEqual(got, want) { + t.Errorf("msg= %v; want %v", &got, &want) + } +} + +func testABEList(t *testing.T) { + url := "http://localhost:8080/v1/example/a_bit_of_everything" + resp, err := http.Get(url) + if err != nil { + t.Errorf("http.Get(%q) failed with %v; want success", err) + return + } + defer resp.Body.Close() + + dec := json.NewDecoder(resp.Body) + var i int + for i = 0; ; i++ { + var msg gw.ABitOfEverything + err := dec.Decode(&msg) + if err == io.EOF { + break + } + if err != nil { + t.Errorf("dec.Decode(&msg) failed with %v; want success; i = %d", err, i) + } + } + if i <= 0 { + t.Errorf("i == %d; want > 0", i) + } +} diff --git a/examples/main.go b/examples/main.go index 3e626723395..6836c9b21be 100644 --- a/examples/main.go +++ b/examples/main.go @@ -4,8 +4,8 @@ import ( "flag" "net/http" + "github.com/gengo/grpc-gateway/runtime" "github.com/golang/glog" - "github.com/zenazn/goji/web" "golang.org/x/net/context" ) @@ -14,12 +14,12 @@ var ( abeEndpoint = flag.String("more_endpoint", "localhost:9090", "endpoint of ABitOfEverythingService") ) -func run() error { +func Run() error { ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() - mux := web.New() + mux := runtime.NewServeMux() err := RegisterEchoServiceHandlerFromEndpoint(ctx, mux, *echoEndpoint) if err != nil { return err @@ -37,7 +37,7 @@ func main() { flag.Parse() defer glog.Flush() - if err := run(); err != nil { + if err := Run(); err != nil { glog.Fatal(err) } } diff --git a/examples/server/main.go b/examples/server/main.go index 2eaec00a73c..052d6336f42 100644 --- a/examples/server/main.go +++ b/examples/server/main.go @@ -63,6 +63,7 @@ func (s *_ABitOfEverythingServer) Create(ctx context.Context, msg *examples.ABit } s.v[uuid] = msg s.v[uuid].Uuid = uuid + glog.Infof("%v", s.v[uuid]) return s.v[uuid], nil } @@ -165,7 +166,7 @@ func (s *_ABitOfEverythingServer) BulkEcho(stream examples.ABitOfEverythingServi return nil } -func run() error { +func Run() error { ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -185,7 +186,7 @@ func main() { flag.Parse() defer glog.Flush() - if err := run(); err != nil { + if err := Run(); err != nil { glog.Fatal(err) } } From 85ef5cfa2160c60db7f27279808660a68cb60d3f Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 10:44:03 +0900 Subject: [PATCH 15/28] Deprecate the old custom option --- Makefile | 9 ++---- options/options.pb.go | 70 ------------------------------------------- options/options.proto | 3 ++ 3 files changed, 5 insertions(+), 77 deletions(-) delete mode 100644 options/options.pb.go diff --git a/Makefile b/Makefile index d665ee4b8a4..725cf374623 100644 --- a/Makefile +++ b/Makefile @@ -21,8 +21,6 @@ GATEWAY_PLUGIN_SRC= protoc-gen-grpc-gateway/descriptor/name.go \ protoc-gen-grpc-gateway/httprule/types_test.go \ protoc-gen-grpc-gateway/main.go -OLD_OPTIONS_PROTO=options/options.proto -OLD_OPTIONS_GO=$(OLD_OPTIONS_PROTO:.proto=.pb.go) GOOGLEAPIS_DIR=third_party/googleapis OPTIONS_PROTO=$(GOOGLEAPIS_DIR)/google/api/annotations.proto $(GOOGLEAPIS_DIR)/google/api/http.proto OPTIONS_GO=$(OPTIONS_PROTO:.proto=.pb.go) @@ -36,7 +34,7 @@ EXAMPLE_DEPS=examples/sub/message.proto EXAMPLE_DEPSRCS=$(EXAMPLE_DEPS:.proto=.pb.go) PROTOC_INC_PATH=$(dir $(shell which protoc))/../include -generate: $(OPTIONS_GO) $(OLD_OPTIONS_GO) +generate: $(OPTIONS_GO) .SUFFIXES: .go .proto @@ -44,9 +42,6 @@ $(GO_PLUGIN): go get $(GO_PLUGIN_PKG) go build -o $@ $(GO_PLUGIN_PKG) -$(OLD_OPTIONS_GO): $(OLD_OPTIONS_PROTO) $(GO_PLUGIN) - protoc -I $(PROTOC_INC_PATH) -I. --plugin=$(GO_PLUGIN) --go_out=$(PKGMAP):. $(OLD_OPTIONS_PROTO) - $(OPTIONS_GO): $(OPTIONS_PROTO) $(GO_PLUGIN) protoc -I $(PROTOC_INC_PATH) -I$(GOOGLEAPIS_DIR) --plugin=$(GO_PLUGIN) --go_out=$(PKGMAP):$(GOOGLEAPIS_DIR) $(OPTIONS_PROTO) @@ -65,6 +60,6 @@ test: $(EXAMPLE_SVCSRCS) $(EXAMPLE_GWSRCS) $(EXAMPLE_DEPSRCS) clean distclean: realclean: - rm -f $(OLD_OPTIONS_GO) $(OPTIONS_GO) + rm -f $(OPTIONS_GO) rm -f $(EXAMPLE_SVCSRCS) $(EXAMPLE_DEPSRCS) rm -f $(EXAMPLE_GWSRCS) diff --git a/options/options.pb.go b/options/options.pb.go deleted file mode 100644 index c6d9e58f03f..00000000000 --- a/options/options.pb.go +++ /dev/null @@ -1,70 +0,0 @@ -// Code generated by protoc-gen-go. -// source: options/options.proto -// DO NOT EDIT! - -/* -Package options is a generated protocol buffer package. - -It is generated from these files: - options/options.proto - -It has these top-level messages: - ApiMethodOptions -*/ -package options - -import proto "github.com/golang/protobuf/proto" -import math "math" -import google_protobuf "github.com/golang/protobuf/protoc-gen-go/descriptor" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = math.Inf - -type ApiMethodOptions struct { - // Path of the RESTful API method. - // Path components which start with colon is mapped to the corresponding fields in the request message. - Path *string `protobuf:"bytes,1,req,name=path" json:"path,omitempty"` - // HTTP method of the RESTful API method - Method *string `protobuf:"bytes,2,req,name=method" json:"method,omitempty"` - // Human-readable description of the method. - Description *string `protobuf:"bytes,3,opt,name=description" json:"description,omitempty"` - XXX_unrecognized []byte `json:"-"` -} - -func (m *ApiMethodOptions) Reset() { *m = ApiMethodOptions{} } -func (m *ApiMethodOptions) String() string { return proto.CompactTextString(m) } -func (*ApiMethodOptions) ProtoMessage() {} - -func (m *ApiMethodOptions) GetPath() string { - if m != nil && m.Path != nil { - return *m.Path - } - return "" -} - -func (m *ApiMethodOptions) GetMethod() string { - if m != nil && m.Method != nil { - return *m.Method - } - return "" -} - -func (m *ApiMethodOptions) GetDescription() string { - if m != nil && m.Description != nil { - return *m.Description - } - return "" -} - -var E_ApiMethodOptions_ApiOptions = &proto.ExtensionDesc{ - ExtendedType: (*google_protobuf.MethodOptions)(nil), - ExtensionType: (*ApiMethodOptions)(nil), - Field: 1022, - Name: "gengo.grpc.gateway.ApiMethodOptions.api_options", - Tag: "bytes,1022,opt,name=api_options", -} - -func init() { - proto.RegisterExtension(E_ApiMethodOptions_ApiOptions) -} diff --git a/options/options.proto b/options/options.proto index 33c6b44019f..be81c963db5 100644 --- a/options/options.proto +++ b/options/options.proto @@ -5,6 +5,9 @@ package gengo.grpc.gateway; import "google/protobuf/descriptor.proto"; message ApiMethodOptions { + // Use HttpRule instead. + option deprecated = true; + extend google.protobuf.MethodOptions { // Describes how the gRPC method should be exported as a RESTful API. // From 415c655b4e0ccebbcef840f21530024e45c66f5e Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 11:36:54 +0900 Subject: [PATCH 16/28] Update README.md --- README.md | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e339edc3da7..9b2b13eb420 100644 --- a/README.md +++ b/README.md @@ -60,9 +60,9 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. ```diff syntax = "proto3"; package example; - - +import "github.com/gengo/grpc-gateway/options/options.proto"; - + + + +import "github.com/gengo/grpc-gateway/third_party/googleapis/google/api/annnotations.proto"; + + message StringMessage { string value = 1; } @@ -70,7 +70,7 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. service YourService { - rpc Echo(StringMessage) returns (StringMessage) {} + rpc Echo(StringMessage) returns (StringMessage) { - + option (gengo.grpc.gateway.ApiMethodOptions.api_options) = { + + option (google.api.http) = { + path: "/v1/example/echo" + method: "POST" + }; @@ -86,7 +86,20 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. ``` It will generate a stub file `path/to/your_service.pb.go`. - Now you can implement your service on top of the stub. +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 --ruby_out=. \ + path/to/your/service_proto + + protoc -I/usr/local/include -I. -I$GOPATH/src \ + --plugin=protoc-gen-grpc-ruby=grpc_ruby_plugin \ + --grpc-ruby_out=. \ + path/to/your/service.proto + ``` + 2. Implement your service 4. Generate reverse-proxy ```sh @@ -106,8 +119,8 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. "net/http" "github.com/golang/glog" - "github.com/zenazn/goji/web" "golang.org/x/net/context" + "github.com/gengo/grpc-gateway/runtime" gw "path/to/your_service_package" ) @@ -121,7 +134,7 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. ctx, cancel := context.WithCancel(ctx) defer cancel() - mux := web.New() + mux := runtime.NewServeMux() err := gw.RegisterYourServiceHandlerFromEndpoint(ctx, mux, *echoEndpoint) if err != nil { return err From 9b048b1346860a9daa912366152e01c9735d4e18 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 11:55:39 +0900 Subject: [PATCH 17/28] Extract go package reservation from descriptor loader to code generator --- .../descriptor/registry.go | 19 ++---- .../gengateway/generator.go | 60 +++++++++++++++++-- .../gengateway/template.go | 39 ++---------- .../gengateway/template_test.go | 6 +- 4 files changed, 66 insertions(+), 58 deletions(-) diff --git a/protoc-gen-grpc-gateway/descriptor/registry.go b/protoc-gen-grpc-gateway/descriptor/registry.go index 27a67338934..8112174a06a 100644 --- a/protoc-gen-grpc-gateway/descriptor/registry.go +++ b/protoc-gen-grpc-gateway/descriptor/registry.go @@ -32,21 +32,10 @@ type Registry struct { // NewRegistry returns a new Registry. func NewRegistry() *Registry { return &Registry{ - msgs: make(map[string]*Message), - files: make(map[string]*File), - pkgMap: make(map[string]string), - pkgAliases: map[string]string{ - // TODO(yugui) Move this initialization to generators. - "json": "encoding/json", - "io": "io", - "http": "net/http", - "runtime": "runtime", - "glog": "github.com/golang/glog", - "proto": "github.com/golang/protobuf/proto", - "context": "golang.org/x/net/context", - "grpc": "google.golang.org/grpc", - "codes": "google.golang.org/grpc/codes", - }, + msgs: make(map[string]*Message), + files: make(map[string]*File), + pkgMap: make(map[string]string), + pkgAliases: make(map[string]string), } } diff --git a/protoc-gen-grpc-gateway/gengateway/generator.go b/protoc-gen-grpc-gateway/gengateway/generator.go index 38084d2f688..2f37421e838 100644 --- a/protoc-gen-grpc-gateway/gengateway/generator.go +++ b/protoc-gen-grpc-gateway/gengateway/generator.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "go/format" + "path" "path/filepath" "strings" @@ -18,21 +19,47 @@ var ( ) type generator struct { - reg *descriptor.Registry + reg *descriptor.Registry + baseImports []descriptor.GoPackage } func New(reg *descriptor.Registry) *generator { - if err := reg.ReserveGoPackageAlias("json", "encoding/json"); err != nil { - glog.Fatal("package name json collides to another") + var imports []descriptor.GoPackage + for _, pkgpath := range []string{ + "encoding/json", + "io", + "net/http", + "github.com/gengo/grpc-gateway/runtime", + "github.com/golang/glog", + "github.com/golang/protobuf/proto", + "golang.org/x/net/context", + "google.golang.org/grpc", + "google.golang.org/grpc/codes", + } { + pkg := descriptor.GoPackage{ + Path: pkgpath, + Name: path.Base(pkgpath), + } + if err := reg.ReserveGoPackageAlias(pkg.Name, pkg.Path); err != nil { + for i := 0; ; i++ { + alias := fmt.Sprintf("%s_%d", pkg.Name, i) + if err := reg.ReserveGoPackageAlias(pkg.Name, pkg.Path); err != nil { + continue + } + pkg.Alias = alias + break + } + } + imports = append(imports, pkg) } - return &generator{reg: reg} + return &generator{reg: reg, baseImports: imports} } func (g *generator) Generate(targets []*descriptor.File) ([]*plugin.CodeGeneratorResponse_File, error) { var files []*plugin.CodeGeneratorResponse_File for _, file := range targets { glog.V(1).Infof("Processing %s", file.GetName()) - code, err := applyTemplate(file) + code, err := g.generate(file) if err == errNoTargetService { glog.V(1).Infof("%s: %v", file.GetName(), err) continue @@ -57,3 +84,26 @@ func (g *generator) Generate(targets []*descriptor.File) ([]*plugin.CodeGenerato } return files, nil } + +func (g *generator) generate(file *descriptor.File) (string, error) { + pkgSeen := make(map[string]bool) + var imports []descriptor.GoPackage + for _, pkg := range g.baseImports { + pkgSeen[pkg.Path] = true + imports = append(imports, pkg) + } + for _, svc := range file.Services { + for _, m := range svc.Methods { + pkg := m.RequestType.File.GoPkg + if pkg == file.GoPkg { + continue + } + if pkgSeen[pkg.Path] { + continue + } + pkgSeen[pkg.Path] = true + imports = append(imports, pkg) + } + } + return applyTemplate(param{File: file, Imports: imports}) +} diff --git a/protoc-gen-grpc-gateway/gengateway/template.go b/protoc-gen-grpc-gateway/gengateway/template.go index 1cab1a0379f..36b271cd63d 100644 --- a/protoc-gen-grpc-gateway/gengateway/template.go +++ b/protoc-gen-grpc-gateway/gengateway/template.go @@ -2,7 +2,6 @@ package gengateway import ( "bytes" - "path" "strings" "text/template" @@ -14,43 +13,13 @@ type param struct { Imports []descriptor.GoPackage } -func applyTemplate(file *descriptor.File) (string, error) { - pkgSeen := make(map[string]bool) - var imports []descriptor.GoPackage - for _, pkg := range []string{ - "io", - "net/http", - "encoding/json", - "github.com/gengo/grpc-gateway/runtime", - "github.com/golang/glog", - "github.com/golang/protobuf/proto", - "golang.org/x/net/context", - "google.golang.org/grpc", - "google.golang.org/grpc/codes", - } { - pkgSeen[pkg] = true - imports = append(imports, descriptor.GoPackage{Path: pkg, Name: path.Base(pkg)}) - } - for _, svc := range file.Services { - for _, m := range svc.Methods { - pkg := m.RequestType.File.GoPkg - if pkg == file.GoPkg { - continue - } - if pkgSeen[pkg.Path] { - continue - } - pkgSeen[pkg.Path] = true - imports = append(imports, pkg) - } - } - +func applyTemplate(p param) (string, error) { w := bytes.NewBuffer(nil) - if err := headerTemplate.Execute(w, param{File: file, Imports: imports}); err != nil { + if err := headerTemplate.Execute(w, p); err != nil { return "", err } var methodSeen bool - for _, svc := range file.Services { + for _, svc := range p.Services { for _, meth := range svc.Methods { methodSeen = true if err := handlerTemplate.Execute(w, meth); err != nil { @@ -61,7 +30,7 @@ func applyTemplate(file *descriptor.File) (string, error) { if !methodSeen { return "", errNoTargetService } - if err := trailerTemplate.Execute(w, file.Services); err != nil { + if err := trailerTemplate.Execute(w, p.Services); err != nil { return "", err } return w.String(), nil diff --git a/protoc-gen-grpc-gateway/gengateway/template_test.go b/protoc-gen-grpc-gateway/gengateway/template_test.go index 7bbbc459389..5476f4c33b6 100644 --- a/protoc-gen-grpc-gateway/gengateway/template_test.go +++ b/protoc-gen-grpc-gateway/gengateway/template_test.go @@ -75,7 +75,7 @@ func TestApplyTemplateHeader(t *testing.T) { }, }, } - got, err := applyTemplate(crossLinkFixture(&file)) + got, err := applyTemplate(param{File: crossLinkFixture(&file)}) if err != nil { t.Errorf("applyTemplate(%#v) failed with %v; want success", file, err) return @@ -217,7 +217,7 @@ func TestApplyTemplateRequestWithoutClientStreaming(t *testing.T) { }, }, } - got, err := applyTemplate(crossLinkFixture(&file)) + got, err := applyTemplate(param{File: crossLinkFixture(&file)}) if err != nil { t.Errorf("applyTemplate(%#v) failed with %v; want success", file, err) return @@ -375,7 +375,7 @@ func TestApplyTemplateRequestWithClientStreaming(t *testing.T) { }, }, } - got, err := applyTemplate(crossLinkFixture(&file)) + got, err := applyTemplate(param{File: crossLinkFixture(&file)}) if err != nil { t.Errorf("applyTemplate(%#v) failed with %v; want success", file, err) return From 8cfb28f95d21a57542bd1b50223ec1b3524a64ab Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 19:07:57 +0900 Subject: [PATCH 18/28] Remove test files from the list of plugin sources --- Makefile | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Makefile b/Makefile index 725cf374623..6e8fb59f667 100644 --- a/Makefile +++ b/Makefile @@ -5,20 +5,13 @@ GATEWAY_PLUGIN=bin/protoc-gen-grpc-gateway GATEWAY_PLUGIN_PKG=$(PKG)/protoc-gen-grpc-gateway GATEWAY_PLUGIN_SRC= protoc-gen-grpc-gateway/descriptor/name.go \ protoc-gen-grpc-gateway/descriptor/registry.go \ - protoc-gen-grpc-gateway/descriptor/registry_test.go \ protoc-gen-grpc-gateway/descriptor/services.go \ - protoc-gen-grpc-gateway/descriptor/services_test.go \ protoc-gen-grpc-gateway/descriptor/types.go \ - protoc-gen-grpc-gateway/descriptor/types_test.go \ protoc-gen-grpc-gateway/gengateway/generator.go \ protoc-gen-grpc-gateway/gengateway/template.go \ - protoc-gen-grpc-gateway/gengateway/template_test.go \ protoc-gen-grpc-gateway/httprule/compile.go \ - protoc-gen-grpc-gateway/httprule/compile_test.go \ protoc-gen-grpc-gateway/httprule/parse.go \ - protoc-gen-grpc-gateway/httprule/parse_test.go \ protoc-gen-grpc-gateway/httprule/types.go \ - protoc-gen-grpc-gateway/httprule/types_test.go \ protoc-gen-grpc-gateway/main.go GOOGLEAPIS_DIR=third_party/googleapis From b807c973696d3e796a1a07e47e30af16eac6c1cd Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 19:09:44 +0900 Subject: [PATCH 19/28] Renumber an ordered list --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9b2b13eb420..aac87356b8c 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. path/to/your/service.proto ``` 2. Implement your service -4. Generate reverse-proxy +5. Generate reverse-proxy ```sh protoc -I/usr/local/include -I. -I$GOPATH/src \ @@ -109,7 +109,7 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. ``` It will generate a reverse proxy `path/to/your_service.pb.gw.go`. -5. Write an entrypoint +6. Write an entrypoint Now you need to write an entrypoint of the proxy server. ```go From d8399af770e20e3ed2d8a166591d30f259210c45 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 19:24:59 +0900 Subject: [PATCH 20/28] Fix golint and go vet errors in runtime/ --- runtime/convert.go | 9 +++++++++ runtime/doc.go | 2 +- runtime/handler.go | 10 +++++----- runtime/proto2_convert.go | 15 +++++++++++++++ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/runtime/convert.go b/runtime/convert.go index cc6b0da0366..1af5cc4ebdd 100644 --- a/runtime/convert.go +++ b/runtime/convert.go @@ -4,18 +4,23 @@ import ( "strconv" ) +// String just returns the given string. +// It is just for compatibility to other types. func String(val string) (string, error) { return val, nil } +// Bool converts the given string representation of a boolean value into bool. func Bool(val string) (bool, error) { return strconv.ParseBool(val) } +// Float64 converts the given string representation into representation of a floating point number into float64. func Float64(val string) (float64, error) { return strconv.ParseFloat(val, 64) } +// Float32 converts the given string representation of a floating point number into float32. func Float32(val string) (float32, error) { f, err := strconv.ParseFloat(val, 32) if err != nil { @@ -24,10 +29,12 @@ func Float32(val string) (float32, error) { return float32(f), nil } +// Int64 converts the given string representation of an integer into int64. func Int64(val string) (int64, error) { return strconv.ParseInt(val, 0, 64) } +// Int32 converts the given string representation of an integer into int32. func Int32(val string) (int32, error) { i, err := strconv.ParseInt(val, 0, 32) if err != nil { @@ -36,10 +43,12 @@ func Int32(val string) (int32, error) { return int32(i), nil } +// Uint64 converts the given string representation of an integer into uint64. func Uint64(val string) (uint64, error) { return strconv.ParseUint(val, 0, 64) } +// Uint32 converts the given string representation of an integer into uint32. func Uint32(val string) (uint32, error) { i, err := strconv.ParseUint(val, 0, 32) if err != nil { diff --git a/runtime/doc.go b/runtime/doc.go index 3efbaadda20..b6e5ddf7a9f 100644 --- a/runtime/doc.go +++ b/runtime/doc.go @@ -1,5 +1,5 @@ /* -package runtime contains runtime helper functions used by +Package runtime contains runtime helper functions used by servers which protoc-gen-grpc-gateway generates. */ package runtime diff --git a/runtime/handler.go b/runtime/handler.go index 928638c9364..69884d608a3 100644 --- a/runtime/handler.go +++ b/runtime/handler.go @@ -36,28 +36,28 @@ func ForwardResponseStream(w http.ResponseWriter, recv func() (proto.Message, er if err != nil { buf, merr := json.Marshal(responseStreamChunk{Error: err.Error()}) if merr != nil { - glog.Error("Failed to marshal an error: %v", merr) + glog.Errorf("Failed to marshal an error: %v", merr) return } if _, werr := fmt.Fprintf(w, "%s\n", buf); werr != nil { - glog.Error("Failed to notify error to client: %v", werr) + glog.Errorf("Failed to notify error to client: %v", werr) return } return } buf, err := json.Marshal(responseStreamChunk{Result: resp}) if err != nil { - glog.Error("Failed to marshal response chunk: %v", err) + glog.Errorf("Failed to marshal response chunk: %v", err) return } if _, err = fmt.Fprintf(w, "%s\n", buf); err != nil { - glog.Error("Failed to send response chunk: %v", err) + glog.Errorf("Failed to send response chunk: %v", err) return } } } -// ForwardResponseStream forwards the message from gRPC server to REST client. +// ForwardResponseMessage forwards the message from gRPC server to REST client. func ForwardResponseMessage(w http.ResponseWriter, resp proto.Message) { buf, err := json.Marshal(resp) if err != nil { diff --git a/runtime/proto2_convert.go b/runtime/proto2_convert.go index c6e40680c03..a3151e2a552 100644 --- a/runtime/proto2_convert.go +++ b/runtime/proto2_convert.go @@ -4,10 +4,13 @@ import ( "github.com/golang/protobuf/proto" ) +// StringP returns a pointer to a string whose pointee is same as the given string value. func StringP(val string) (*string, error) { return proto.String(val), nil } +// BoolP parses the given string representation of a boolean value, +// and returns a pointer to a bool whose value is same as the parsed value. func BoolP(val string) (*bool, error) { b, err := Bool(val) if err != nil { @@ -16,6 +19,8 @@ func BoolP(val string) (*bool, error) { return proto.Bool(b), nil } +// Float64P parses the given string representation of a floating point number, +// and returns a pointer to a float64 whose value is same as the parsed number. func Float64P(val string) (*float64, error) { f, err := Float64(val) if err != nil { @@ -24,6 +29,8 @@ func Float64P(val string) (*float64, error) { return proto.Float64(f), nil } +// Float32P parses the given string representation of a floating point number, +// and returns a pointer to a float32 whose value is same as the parsed number. func Float32P(val string) (*float32, error) { f, err := Float32(val) if err != nil { @@ -32,6 +39,8 @@ func Float32P(val string) (*float32, error) { return proto.Float32(f), nil } +// Int64P parses the given string representation of an integer +// and returns a pointer to a int64 whose value is same as the parsed integer. func Int64P(val string) (*int64, error) { i, err := Int64(val) if err != nil { @@ -40,6 +49,8 @@ func Int64P(val string) (*int64, error) { return proto.Int64(i), nil } +// Int32P parses the given string representation of an integer +// and returns a pointer to a int32 whose value is same as the parsed integer. func Int32P(val string) (*int32, error) { i, err := Int32(val) if err != nil { @@ -48,6 +59,8 @@ func Int32P(val string) (*int32, error) { return proto.Int32(i), err } +// Uint64P parses the given string representation of an integer +// and returns a pointer to a uint64 whose value is same as the parsed integer. func Uint64P(val string) (*uint64, error) { i, err := Uint64(val) if err != nil { @@ -56,6 +69,8 @@ func Uint64P(val string) (*uint64, error) { return proto.Uint64(i), err } +// Uint32P parses the given string representation of an integer +// and returns a pointer to a uint32 whose value is same as the parsed integer. func Uint32P(val string) (*uint32, error) { i, err := Uint32(val) if err != nil { From a584b490b411dffda91ce797e32aff68842577c7 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 19:26:34 +0900 Subject: [PATCH 21/28] Fix a golint error in internal/ --- internal/pattern.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pattern.go b/internal/pattern.go index 41cd77672bb..99680fc3851 100644 --- a/internal/pattern.go +++ b/internal/pattern.go @@ -13,7 +13,7 @@ const ( OpLitPush // OpPushM concatenates the remaining components and pushes it to stack OpPushM - // OpPopN pops a N items from stack, concatenates them and pushes it to stack + // OpConcatN pops N items from stack, concatenates them and pushes it back to stack OpConcatN // OpCapture pops an item and binds it to the variable OpCapture From adba95ae244eb28f11b25ef97890c771fc555483 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 19:30:38 +0900 Subject: [PATCH 22/28] Fix golint and go vet errrors in descriptor/ --- protoc-gen-grpc-gateway/descriptor/registry.go | 1 + protoc-gen-grpc-gateway/descriptor/registry_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/protoc-gen-grpc-gateway/descriptor/registry.go b/protoc-gen-grpc-gateway/descriptor/registry.go index 8112174a06a..fca57c4b6d2 100644 --- a/protoc-gen-grpc-gateway/descriptor/registry.go +++ b/protoc-gen-grpc-gateway/descriptor/registry.go @@ -128,6 +128,7 @@ func (r *Registry) LookupMsg(location, name string) (*Message, error) { return nil, fmt.Errorf("no message found: %s", name) } +// LookupFile looks up a file by name. func (r *Registry) LookupFile(name string) (*File, error) { f, ok := r.files[name] if !ok { diff --git a/protoc-gen-grpc-gateway/descriptor/registry_test.go b/protoc-gen-grpc-gateway/descriptor/registry_test.go index 5261f53f2d6..c4007010e7e 100644 --- a/protoc-gen-grpc-gateway/descriptor/registry_test.go +++ b/protoc-gen-grpc-gateway/descriptor/registry_test.go @@ -10,7 +10,7 @@ import ( func load(t *testing.T, reg *Registry, src string) *descriptor.FileDescriptorProto { var file descriptor.FileDescriptorProto if err := proto.UnmarshalText(src, &file); err != nil { - t.Fatalf("proto.UnmarshalText(%s, &file) failed with %v; want success", err) + t.Fatalf("proto.UnmarshalText(%s, &file) failed with %v; want success", src, err) } reg.loadFile(&file) return &file @@ -44,11 +44,11 @@ func TestLoadFile(t *testing.T) { msg, err := reg.LookupMsg("", ".example.ExampleMessage") if err != nil { - t.Errorf("reg.LookupMsg(%q, %q)) failed with %v; want success", "", ".example.ExampleMessage") + t.Errorf("reg.LookupMsg(%q, %q)) failed with %v; want success", "", ".example.ExampleMessage", "", ".example.ExampleMessage", err) return } if got, want := msg.DescriptorProto, fd.MessageType[0]; got != want { - t.Errorf("reg.lookupMsg(%q, %q).DescriptorProto = %#v; want %#v", got, want) + t.Errorf("reg.lookupMsg(%q, %q).DescriptorProto = %#v; want %#v", "", ".example.ExampleMessage", got, want) } if got, want := msg.File, file; got != want { t.Errorf("msg.File = %v; want %v", got, want) @@ -267,7 +267,7 @@ func TestLookupMsgWithoutPackage(t *testing.T) { return } if got, want := msg.DescriptorProto, fd.MessageType[0]; got != want { - t.Errorf("reg.lookupMsg(%q, %q).DescriptorProto = %#v; want %#v", got, want) + t.Errorf("reg.lookupMsg(%q, %q).DescriptorProto = %#v; want %#v", "", ".ExampleMessage", got, want) } } From 8308b8f10cb66997b3b9fba038073dc2fe101dda Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 19:36:41 +0900 Subject: [PATCH 23/28] Fix golint and go vet errors in gengateway --- protoc-gen-grpc-gateway/gengateway/doc.go | 2 ++ protoc-gen-grpc-gateway/gengateway/generator.go | 1 + .../gengateway/template_test.go | 16 ++++++++-------- 3 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 protoc-gen-grpc-gateway/gengateway/doc.go diff --git a/protoc-gen-grpc-gateway/gengateway/doc.go b/protoc-gen-grpc-gateway/gengateway/doc.go new file mode 100644 index 00000000000..223d8108267 --- /dev/null +++ b/protoc-gen-grpc-gateway/gengateway/doc.go @@ -0,0 +1,2 @@ +// Package gengateway provides a code generator for grpc gateway files. +package gengateway diff --git a/protoc-gen-grpc-gateway/gengateway/generator.go b/protoc-gen-grpc-gateway/gengateway/generator.go index 2f37421e838..f0d1706cbaf 100644 --- a/protoc-gen-grpc-gateway/gengateway/generator.go +++ b/protoc-gen-grpc-gateway/gengateway/generator.go @@ -23,6 +23,7 @@ type generator struct { baseImports []descriptor.GoPackage } +// New returns a new generator which generates grpc gateway files. func New(reg *descriptor.Registry) *generator { var imports []descriptor.GoPackage for _, pkgpath := range []string{ diff --git a/protoc-gen-grpc-gateway/gengateway/template_test.go b/protoc-gen-grpc-gateway/gengateway/template_test.go index 5476f4c33b6..af7c8aa56ab 100644 --- a/protoc-gen-grpc-gateway/gengateway/template_test.go +++ b/protoc-gen-grpc-gateway/gengateway/template_test.go @@ -186,7 +186,7 @@ func TestApplyTemplateRequestWithoutClientStreaming(t *testing.T) { ResponseType: msg, PathParams: []descriptor.Parameter{ { - FieldPath: descriptor.FieldPath{ + FieldPath: descriptor.FieldPath([]descriptor.FieldPathComponent{ { Name: "nested", Target: nestedField, @@ -195,13 +195,13 @@ func TestApplyTemplateRequestWithoutClientStreaming(t *testing.T) { Name: "int32", Target: intField, }, - }, + }), Target: intField, }, }, Body: &descriptor.Body{ DecoderFactoryExpr: "NewExampleDecoder", - FieldPath: descriptor.FieldPath{ + FieldPath: descriptor.FieldPath([]descriptor.FieldPathComponent{ { Name: "nested", Target: nestedField, @@ -210,7 +210,7 @@ func TestApplyTemplateRequestWithoutClientStreaming(t *testing.T) { Name: "bool", Target: boolField, }, - }, + }), }, }, }, @@ -344,7 +344,7 @@ func TestApplyTemplateRequestWithClientStreaming(t *testing.T) { ResponseType: msg, PathParams: []descriptor.Parameter{ { - FieldPath: descriptor.FieldPath{ + FieldPath: descriptor.FieldPath([]descriptor.FieldPathComponent{ { Name: "nested", Target: nestedField, @@ -353,13 +353,13 @@ func TestApplyTemplateRequestWithClientStreaming(t *testing.T) { Name: "int32", Target: intField, }, - }, + }), Target: intField, }, }, Body: &descriptor.Body{ DecoderFactoryExpr: "NewExampleDecoder", - FieldPath: descriptor.FieldPath{ + FieldPath: descriptor.FieldPath([]descriptor.FieldPathComponent{ { Name: "nested", Target: nestedField, @@ -368,7 +368,7 @@ func TestApplyTemplateRequestWithClientStreaming(t *testing.T) { Name: "bool", Target: boolField, }, - }, + }), }, }, }, From 169d51e0b8cc22e3c41a7937d63ca1cf9ee68739 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 19:43:40 +0900 Subject: [PATCH 24/28] Fix go vet errors in generated codes --- examples/a_bit_of_everything.pb.gw.go | 4 ++-- examples/echo_service.pb.gw.go | 4 ++-- examples/integration_test.go | 7 ++++--- protoc-gen-grpc-gateway/gengateway/template.go | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/examples/a_bit_of_everything.pb.gw.go b/examples/a_bit_of_everything.pb.gw.go index ddc843cbf8f..3d63c615381 100644 --- a/examples/a_bit_of_everything.pb.gw.go +++ b/examples/a_bit_of_everything.pb.gw.go @@ -322,14 +322,14 @@ func RegisterABitOfEverythingServiceHandlerFromEndpoint(ctx context.Context, mux defer func() { if err != nil { if cerr := conn.Close(); cerr != nil { - glog.Error("Failed to close conn to %s: %v", endpoint, cerr) + glog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) } return } go func() { <-ctx.Done() if cerr := conn.Close(); cerr != nil { - glog.Error("Failed to close conn to %s: %v", endpoint, cerr) + glog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) } }() }() diff --git a/examples/echo_service.pb.gw.go b/examples/echo_service.pb.gw.go index 3f9e05566a5..af4c523be74 100644 --- a/examples/echo_service.pb.gw.go +++ b/examples/echo_service.pb.gw.go @@ -68,14 +68,14 @@ func RegisterEchoServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.Se defer func() { if err != nil { if cerr := conn.Close(); cerr != nil { - glog.Error("Failed to close conn to %s: %v", endpoint, cerr) + glog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) } return } go func() { <-ctx.Done() if cerr := conn.Close(); cerr != nil { - glog.Error("Failed to close conn to %s: %v", endpoint, cerr) + glog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) } }() }() diff --git a/examples/integration_test.go b/examples/integration_test.go index 8bb3191f5d9..7dfac5315de 100644 --- a/examples/integration_test.go +++ b/examples/integration_test.go @@ -328,9 +328,10 @@ func testABELookup(t *testing.T) { return } - resp, err := http.Get(fmt.Sprintf("%s/%s", url, want.Uuid)) + url = fmt.Sprintf("%s/%s", url, want.Uuid) + resp, err := http.Get(url) if err != nil { - t.Errorf("http.Get(%q) failed with %v; want success", err) + t.Errorf("http.Get(%q) failed with %v; want success", url, err) return } defer resp.Body.Close() @@ -355,7 +356,7 @@ func testABEList(t *testing.T) { url := "http://localhost:8080/v1/example/a_bit_of_everything" resp, err := http.Get(url) if err != nil { - t.Errorf("http.Get(%q) failed with %v; want success", err) + t.Errorf("http.Get(%q) failed with %v; want success", url, err) return } defer resp.Body.Close() diff --git a/protoc-gen-grpc-gateway/gengateway/template.go b/protoc-gen-grpc-gateway/gengateway/template.go index 36b271cd63d..b96fcc0f117 100644 --- a/protoc-gen-grpc-gateway/gengateway/template.go +++ b/protoc-gen-grpc-gateway/gengateway/template.go @@ -153,14 +153,14 @@ func Register{{$svc.GetName}}HandlerFromEndpoint(ctx context.Context, mux *runti defer func() { if err != nil { if cerr := conn.Close(); cerr != nil { - glog.Error("Failed to close conn to %s: %v", endpoint, cerr) + glog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) } return } go func() { <-ctx.Done() if cerr := conn.Close(); cerr != nil { - glog.Error("Failed to close conn to %s: %v", endpoint, cerr) + glog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) } }() }() From 593bfccb64069a873a37a637260d2fb084d290e1 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Mon, 4 May 2015 19:52:19 +0900 Subject: [PATCH 25/28] Adjust log verbosity --- protoc-gen-grpc-gateway/descriptor/registry.go | 4 ++-- protoc-gen-grpc-gateway/descriptor/services.go | 2 +- protoc-gen-grpc-gateway/gengateway/generator.go | 2 +- protoc-gen-grpc-gateway/main.go | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/protoc-gen-grpc-gateway/descriptor/registry.go b/protoc-gen-grpc-gateway/descriptor/registry.go index fca57c4b6d2..091c41e3019 100644 --- a/protoc-gen-grpc-gateway/descriptor/registry.go +++ b/protoc-gen-grpc-gateway/descriptor/registry.go @@ -93,7 +93,7 @@ func (r *Registry) registerMsg(file *File, outerPath []string, msgs []*descripto } file.Messages = append(file.Messages, m) r.msgs[m.FQMN()] = m - glog.Infof("register name: %s", m.FQMN()) + glog.V(1).Infof("register name: %s", m.FQMN()) var outers []string outers = append(outers, outerPath...) @@ -105,7 +105,7 @@ func (r *Registry) registerMsg(file *File, outerPath []string, msgs []*descripto // LookupMsg looks up a message type by "name". // It tries to resolve "name" from "location" if "name" is a relative message name. func (r *Registry) LookupMsg(location, name string) (*Message, error) { - glog.Infof("lookup %s from %s", name, location) + glog.V(1).Infof("lookup %s from %s", name, location) if strings.HasPrefix(name, ".") { m, ok := r.msgs[name] if !ok { diff --git a/protoc-gen-grpc-gateway/descriptor/services.go b/protoc-gen-grpc-gateway/descriptor/services.go index 76cf2f39efc..03c4bc5b7b9 100644 --- a/protoc-gen-grpc-gateway/descriptor/services.go +++ b/protoc-gen-grpc-gateway/descriptor/services.go @@ -244,7 +244,7 @@ func (r *Registry) resolveFiledPath(msg *Message, path string) ([]FieldPathCompo } } - glog.Infof("Lookup %s in %s", c, msg.FQMN()) + glog.V(2).Infof("Lookup %s in %s", c, msg.FQMN()) f := lookupField(msg, c) if f == nil { return nil, fmt.Errorf("no field %q found in %s", path, root.GetName()) diff --git a/protoc-gen-grpc-gateway/gengateway/generator.go b/protoc-gen-grpc-gateway/gengateway/generator.go index f0d1706cbaf..78b8ba5fd05 100644 --- a/protoc-gen-grpc-gateway/gengateway/generator.go +++ b/protoc-gen-grpc-gateway/gengateway/generator.go @@ -81,7 +81,7 @@ func (g *generator) Generate(targets []*descriptor.File) ([]*plugin.CodeGenerato Name: proto.String(output), Content: proto.String(string(formatted)), }) - glog.Infof("Will emit %s", output) + glog.V(1).Infof("Will emit %s", output) } return files, nil } diff --git a/protoc-gen-grpc-gateway/main.go b/protoc-gen-grpc-gateway/main.go index 9455009a14a..80f02e7b120 100644 --- a/protoc-gen-grpc-gateway/main.go +++ b/protoc-gen-grpc-gateway/main.go @@ -40,7 +40,7 @@ func main() { reg := descriptor.NewRegistry() - glog.Info("Processing code generator request") + glog.V(1).Info("Processing code generator request") req, err := parseReq(os.Stdin) if err != nil { glog.Fatal(err) @@ -86,7 +86,7 @@ func main() { } else { resp.File = out } - glog.Info("Processed code generator request") + glog.V(1).Info("Processed code generator request") buf, err := proto.Marshal(&resp) if err != nil { From 0691d055fae466b440221b501253121d849521c7 Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Tue, 5 May 2015 12:29:19 +0900 Subject: [PATCH 26/28] Correct example codes --- README.md | 21 +++++++++++++++------ examples/echo_service.pb.gw.go | 4 ---- examples/echo_service.proto | 1 - 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index aac87356b8c..c31758561a5 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,8 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. - rpc Echo(StringMessage) returns (StringMessage) {} + rpc Echo(StringMessage) returns (StringMessage) { + option (google.api.http) = { - + path: "/v1/example/echo" - + method: "POST" + + post: "/v1/example/echo" + + body: "*" + }; + } } @@ -80,7 +80,9 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. 3. Generate gRPC stub ```sh - protoc -I/usr/local/include -I. -I$GOPATH/src \ + protoc -I/usr/local/include -I. \ + -I$GOPATH/src \ + -I$GOPATH/src/github.com/gengo/grpc-gateway/third_party/googleapis \ --go_out=plugins=grpc:. \ path/to/your_service.proto ``` @@ -91,10 +93,15 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. e.g. ```sh - protoc -I/usr/local/include -I. -I$GOPATH/src --ruby_out=. \ + protoc -I/usr/local/include -I. \ + -I$GOPATH/src \ + -I$GOPATH/src/github.com/gengo/grpc-gateway/third_party/googleapis \ + --ruby_out=. \ path/to/your/service_proto - protoc -I/usr/local/include -I. -I$GOPATH/src \ + protoc -I/usr/local/include -I. \ + -I$GOPATH/src \ + -I$GOPATH/src/github.com/gengo/grpc-gateway/third_party/googleapis \ --plugin=protoc-gen-grpc-ruby=grpc_ruby_plugin \ --grpc-ruby_out=. \ path/to/your/service.proto @@ -103,7 +110,9 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. 5. Generate reverse-proxy ```sh - protoc -I/usr/local/include -I. -I$GOPATH/src \ + protoc -I/usr/local/include -I. \ + -I$GOPATH/src \ + -I$GOPATH/src/github.com/gengo/grpc-gateway/third_party/googleapis \ --grpc-gateway_out=logtostderr=true:. \ path/to/your_service.proto ``` diff --git a/examples/echo_service.pb.gw.go b/examples/echo_service.pb.gw.go index af4c523be74..c2ec625c5d7 100644 --- a/examples/echo_service.pb.gw.go +++ b/examples/echo_service.pb.gw.go @@ -29,10 +29,6 @@ var _ = runtime.String func request_EchoService_Echo(ctx context.Context, client EchoServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) { var protoReq SimpleMessage - if err = json.NewDecoder(req.Body).Decode(&protoReq); err != nil { - return nil, grpc.Errorf(codes.InvalidArgument, "%v", err) - } - var val string var ok bool diff --git a/examples/echo_service.proto b/examples/echo_service.proto index fbbdeb245fb..f9d5d762847 100644 --- a/examples/echo_service.proto +++ b/examples/echo_service.proto @@ -12,7 +12,6 @@ service EchoService { rpc Echo(SimpleMessage) returns (SimpleMessage) { option (google.api.http) = { post: "/v1/example/echo/{id}" - body: "*" }; } rpc EchoBody(SimpleMessage) returns (SimpleMessage) { From 9b03271eca572e5dae593a5b864dc5f4ece08dfe Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Tue, 5 May 2015 12:49:02 +0900 Subject: [PATCH 27/28] Remove decoder configurtion from descriptor.Body This is because all methods will share the same set of decoders. https://groups.google.com/d/msg/grpc-io/Xqx80hG0D44/1gwmwBcnNScJ Tentatively json.Decoder is hard-coded in the code template. We are also going to support application/x-www-form-urlencoded and eventually it will get pluggable by commandline flag. --- examples/a_bit_of_everything.pb.gw.go | 1 + examples/echo_service.pb.gw.go | 1 + .../descriptor/services.go | 22 ++------------ .../descriptor/services_test.go | 29 ++----------------- protoc-gen-grpc-gateway/descriptor/types.go | 10 ------- .../gengateway/template.go | 5 ++-- .../gengateway/template_test.go | 10 ++----- 7 files changed, 12 insertions(+), 66 deletions(-) diff --git a/examples/a_bit_of_everything.pb.gw.go b/examples/a_bit_of_everything.pb.gw.go index 3d63c615381..27873818077 100644 --- a/examples/a_bit_of_everything.pb.gw.go +++ b/examples/a_bit_of_everything.pb.gw.go @@ -26,6 +26,7 @@ import ( var _ codes.Code var _ io.Reader var _ = runtime.String +var _ = json.Marshal func request_ABitOfEverythingService_Create(ctx context.Context, client ABitOfEverythingServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) { var protoReq ABitOfEverything diff --git a/examples/echo_service.pb.gw.go b/examples/echo_service.pb.gw.go index c2ec625c5d7..6e47decf18b 100644 --- a/examples/echo_service.pb.gw.go +++ b/examples/echo_service.pb.gw.go @@ -25,6 +25,7 @@ import ( var _ codes.Code var _ io.Reader var _ = runtime.String +var _ = json.Marshal func request_EchoService_Echo(ctx context.Context, client EchoServiceClient, req *http.Request, pathParams map[string]string) (msg proto.Message, err error) { var protoReq SimpleMessage diff --git a/protoc-gen-grpc-gateway/descriptor/services.go b/protoc-gen-grpc-gateway/descriptor/services.go index 03c4bc5b7b9..32e38111946 100644 --- a/protoc-gen-grpc-gateway/descriptor/services.go +++ b/protoc-gen-grpc-gateway/descriptor/services.go @@ -183,31 +183,13 @@ func (r *Registry) newBody(meth *Method, path string) (*Body, error) { case "": return nil, nil case "*": - return &Body{ - DecoderFactoryExpr: "json.NewDecoder", - DecoderImports: []GoPackage{ - { - Path: "encoding/json", - Name: "json", - }, - }, - FieldPath: nil, - }, nil + return &Body{FieldPath: nil}, nil } fields, err := r.resolveFiledPath(msg, path) if err != nil { return nil, err } - return &Body{ - DecoderFactoryExpr: "json.NewDecoder", - DecoderImports: []GoPackage{ - { - Path: "encoding/json", - Name: "json", - }, - }, - FieldPath: FieldPath(fields), - }, nil + return &Body{FieldPath: FieldPath(fields)}, nil } // lookupField looks up a field named "name" within "msg". diff --git a/protoc-gen-grpc-gateway/descriptor/services_test.go b/protoc-gen-grpc-gateway/descriptor/services_test.go index e06ae61e22e..cfed7dab86b 100644 --- a/protoc-gen-grpc-gateway/descriptor/services_test.go +++ b/protoc-gen-grpc-gateway/descriptor/services_test.go @@ -185,16 +185,7 @@ func TestExtractServicesSimple(t *testing.T) { HTTPMethod: "POST", RequestType: msg, ResponseType: msg, - Body: &Body{ - DecoderFactoryExpr: "json.NewDecoder", - DecoderImports: []GoPackage{ - { - Path: "encoding/json", - Name: "json", - }, - }, - FieldPath: nil, - }, + Body: &Body{FieldPath: nil}, }, }, }, @@ -289,16 +280,7 @@ func TestExtractServicesCrossPackage(t *testing.T) { HTTPMethod: "POST", RequestType: boolMsg, ResponseType: stringMsg, - Body: &Body{ - DecoderFactoryExpr: "json.NewDecoder", - DecoderImports: []GoPackage{ - { - Path: "encoding/json", - Name: "json", - }, - }, - FieldPath: nil, - }, + Body: &Body{FieldPath: nil}, }, }, }, @@ -388,13 +370,6 @@ func TestExtractServicesWithBodyPath(t *testing.T) { RequestType: msg, ResponseType: msg, Body: &Body{ - DecoderFactoryExpr: "json.NewDecoder", - DecoderImports: []GoPackage{ - { - Path: "encoding/json", - Name: "json", - }, - }, FieldPath: FieldPath{ { Name: "nested", diff --git a/protoc-gen-grpc-gateway/descriptor/types.go b/protoc-gen-grpc-gateway/descriptor/types.go index e44f821fa42..6d8eeade127 100644 --- a/protoc-gen-grpc-gateway/descriptor/types.go +++ b/protoc-gen-grpc-gateway/descriptor/types.go @@ -152,16 +152,6 @@ func (p Parameter) ConvertFuncExpr() (string, error) { // Body describes a http requtest body to be sent to the method. type Body struct { - // DecoderFactoryExpr is a go expression of a factory function - // which takes a io.Reader and returns a Decoder (unmarshaller). - // TODO(yugui) Extract this to a flag. - DecoderFactoryExpr string - - // DecoderImports is a list of packages to be imported from the - // generated go files so that DecoderFactoryExpr is valid. - // TODO(yugui) Extract this to a flag. - DecoderImports []GoPackage - // FieldPath is a path to a proto field which the request body is mapped to. // The request body is mapped to the request type itself if FieldPath is empty. FieldPath FieldPath diff --git a/protoc-gen-grpc-gateway/gengateway/template.go b/protoc-gen-grpc-gateway/gengateway/template.go index b96fcc0f117..387039a57e3 100644 --- a/protoc-gen-grpc-gateway/gengateway/template.go +++ b/protoc-gen-grpc-gateway/gengateway/template.go @@ -57,6 +57,7 @@ import ( var _ codes.Code var _ io.Reader var _ = runtime.String +var _ = json.Marshal `)) handlerTemplate = template.Must(template.New("handler").Parse(` @@ -81,7 +82,7 @@ func request_{{.Service.GetName}}_{{.GetName}}(ctx context.Context, client {{.Se glog.Errorf("Failed to start streaming: %v", err) return nil, err } - dec := {{.Body.DecoderFactoryExpr}}(req.Body) + dec := json.NewDecoder(req.Body) for { var protoReq {{.RequestType.GoType .Service.File.GoPkg.Path}} err = dec.Decode(&protoReq) @@ -119,7 +120,7 @@ func request_{{.Service.GetName}}_{{.GetName}}(ctx context.Context, client {{.Se } {{end}} {{if .Body}} - if err = {{.Body.DecoderFactoryExpr}}(req.Body).Decode(&{{.Body.RHS "protoReq"}}); err != nil { + if err = json.NewDecoder(req.Body).Decode(&{{.Body.RHS "protoReq"}}); err != nil { return nil, grpc.Errorf(codes.InvalidArgument, "%v", err) } {{end}} diff --git a/protoc-gen-grpc-gateway/gengateway/template_test.go b/protoc-gen-grpc-gateway/gengateway/template_test.go index af7c8aa56ab..3f0ecfe9313 100644 --- a/protoc-gen-grpc-gateway/gengateway/template_test.go +++ b/protoc-gen-grpc-gateway/gengateway/template_test.go @@ -67,9 +67,7 @@ func TestApplyTemplateHeader(t *testing.T) { HTTPMethod: "GET", RequestType: msg, ResponseType: msg, - Body: &descriptor.Body{ - DecoderFactoryExpr: "json.NewDecoder", - }, + Body: &descriptor.Body{FieldPath: nil}, }, }, }, @@ -200,7 +198,6 @@ func TestApplyTemplateRequestWithoutClientStreaming(t *testing.T) { }, }, Body: &descriptor.Body{ - DecoderFactoryExpr: "NewExampleDecoder", FieldPath: descriptor.FieldPath([]descriptor.FieldPathComponent{ { Name: "nested", @@ -225,7 +222,7 @@ func TestApplyTemplateRequestWithoutClientStreaming(t *testing.T) { if want := spec.sigWant; !strings.Contains(got, want) { t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) } - if want := `NewExampleDecoder(req.Body).Decode(&protoReq.GetNested().Bool)`; !strings.Contains(got, want) { + if want := `json.NewDecoder(req.Body).Decode(&protoReq.GetNested().Bool)`; !strings.Contains(got, want) { t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) } if want := `val, ok = pathParams["nested.int32"]`; !strings.Contains(got, want) { @@ -358,7 +355,6 @@ func TestApplyTemplateRequestWithClientStreaming(t *testing.T) { }, }, Body: &descriptor.Body{ - DecoderFactoryExpr: "NewExampleDecoder", FieldPath: descriptor.FieldPath([]descriptor.FieldPathComponent{ { Name: "nested", @@ -383,7 +379,7 @@ func TestApplyTemplateRequestWithClientStreaming(t *testing.T) { if want := spec.sigWant; !strings.Contains(got, want) { t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) } - if want := `NewExampleDecoder(req.Body)`; !strings.Contains(got, want) { + if want := `json.NewDecoder(req.Body)`; !strings.Contains(got, want) { t.Errorf("applyTemplate(%#v) = %s; want to contain %s", file, got, want) } if want := `func RegisterExampleServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {`; !strings.Contains(got, want) { From 334e625e64081de2844c0189418c28af7e46593f Mon Sep 17 00:00:00 2001 From: Yuki Yugui Sonoda Date: Thu, 7 May 2015 09:05:26 +0900 Subject: [PATCH 28/28] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c31758561a5..56b14eead99 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Make sure that your `$GOPATH/bin` is in your `$PATH`. syntax = "proto3"; package example; + - +import "github.com/gengo/grpc-gateway/third_party/googleapis/google/api/annnotations.proto"; + +import "google/api/annotations.proto"; + message StringMessage { string value = 1;