├── .github └── workflows │ └── main.yml ├── .gitignore ├── .goreleaser.yml ├── CODEOWNERS ├── LICENSE ├── README.md ├── data ├── enum.go ├── file.go ├── message.go └── service.go ├── generator ├── generator.go └── template.go ├── go.mod ├── go.sum ├── integration_tests ├── .gitignore ├── README.md ├── empty.proto ├── google │ ├── api │ │ ├── annotations.proto │ │ └── http.proto │ ├── protobuf │ │ ├── any.proto │ │ ├── descriptor.proto │ │ ├── empty.proto │ │ ├── struct.proto │ │ └── timestamp.proto │ └── rpc │ │ └── status.proto ├── integration_test.ts ├── karma.conf.ci.js ├── karma.conf.common.js ├── karma.conf.js ├── main.go ├── msg.pb.go ├── msg.proto ├── package-lock.json ├── package.json ├── scripts │ ├── gen-protos.sh │ ├── gen-server-proto.sh │ ├── source.sh │ ├── test-ci.sh │ └── test.sh ├── service.go ├── service.pb.go ├── service.pb.gw.go ├── service.proto ├── tools.go └── tsconfig.json ├── main.go ├── options ├── gen.sh ├── ts_package.pb.go └── ts_package.proto ├── registry ├── enum.go ├── field.go ├── file.go ├── message.go ├── registry.go └── service.go ├── test └── oneof_test.go └── testdata ├── .gitignore ├── Makefile ├── datasource └── datasource.proto ├── environment.proto ├── log.proto └── tsconfig.json /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: [push, pull_request] 8 | 9 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 10 | jobs: 11 | # This workflow contains a single job called "build" 12 | test: 13 | # The type of runner that the job will run on 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [12.x] 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v3 24 | 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - name: Set up Go 1.x 31 | uses: actions/setup-go@v3 32 | with: 33 | go-version: ^1.18 34 | 35 | - name: Install Protoc 36 | uses: arduino/setup-protoc@v1 37 | 38 | - name: golangci-lint 39 | uses: golangci/golangci-lint-action@v3 40 | with: 41 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 42 | version: v1.46 43 | 44 | # Runs a single command using the runners shell 45 | - name: Get go dependencies 46 | run: | 47 | go get 48 | 49 | # Runs check dependencies for integratino tests 50 | - name: Get node dependencies 51 | run: | 52 | cd integration_tests && npm install 53 | 54 | # Runs a set of commands using the runners shell 55 | - name: Run unit test 56 | run: | 57 | cd testdata && make protos && cd .. 58 | go test ./... 59 | 60 | # Runs 61 | - name: Run integration tests 62 | run: | 63 | cd integration_tests 64 | ./scripts/test-ci.sh 65 | goreleaser: 66 | runs-on: ubuntu-latest 67 | needs: [test] 68 | steps: 69 | - name: Checkout 70 | uses: actions/checkout@v3 71 | with: 72 | fetch-depth: 0 73 | - name: Set up Go 74 | uses: actions/setup-go@v3 75 | with: 76 | go-version: 1.18 77 | - name: Run GoReleaser 78 | uses: goreleaser/goreleaser-action@v2 79 | if: startsWith(github.ref, 'refs/tags/') 80 | with: 81 | version: latest 82 | args: release --rm-dist 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | protoc-gen-grpc-gateway-ts 2 | .vscode 3 | */node_modules/* 4 | */coverage/* 5 | dist/* -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | goos: 13 | - linux 14 | - windows 15 | - darwin 16 | archives: 17 | - replacements: 18 | darwin: Darwin 19 | linux: Linux 20 | windows: Windows 21 | 386: i386 22 | checksum: 23 | name_template: 'checksums.txt' 24 | snapshot: 25 | name_template: "{{ .Tag }}-next" 26 | changelog: 27 | sort: asc 28 | filters: 29 | exclude: 30 | - '^docs:' 31 | - '^test:' 32 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | ## This defines the code owner of the repository 2 | 3 | @lyonlai @alecthomas @jvmakine @juliaogris 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # protoc-gen-grpc-gateway-ts 2 | 3 | `protoc-gen-grpc-gateway-ts` is a TypeScript client generator for the [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway/) project. It generates idiomatic TypeScript clients that connect the web frontend and golang backend fronted by grpc-gateway. 4 | 5 | ## Features: 6 | 1. Idiomatic Typescript clients and messages. 7 | 2. Supports both one way and server side streaming gRPC calls. 8 | 3. POJO request construction guarded by message type definitions, which is way easier compare to `grpc-web`. 9 | 4. No need to use swagger/open api to generate client code for the web. 10 | 11 | ## Getting Started: 12 | 13 | ### Install `protoc-gen-grpc-gateway-ts` 14 | You will need to install `protoc-gen-grpc-gateway-ts` before it could be picked up by the `protoc` command. Just run `go install github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts` 15 | 16 | ### Sample Usage: 17 | `protoc-gen-grpc-gateway-ts` should be used along with the `protoc` command. A sample invocation looks like the following: 18 | 19 | `protoc --grpc-gateway-ts_out=ts_import_roots=$(pwd),ts_import_root_aliases=base:. input.proto` 20 | 21 | As a result the generated file will be `input.pb.ts` in the same directory. 22 | 23 | ## Parameters: 24 | ### `ts_import_roots` 25 | Since protoc plugins do not get the import path information as what's specified in `protoc -I`, this parameter gives the plugin the same information to figure out where a specific type is coming from so that it can generate `import` statement at the top of the generated typescript file. Defaults to `$(pwd)` 26 | 27 | ### `ts_import_root_aliases` 28 | If a project has setup an alias for their import. This parameter can be used to keep up with the project setup. It will print out alias instead of relative path in the import statement. Default to "". 29 | 30 | `ts_import_roots` & `ts_import_root_aliases` are useful when you have setup import alias in your project with the project asset bundler, e.g. Webpack. 31 | 32 | ### `fetch_module_directory` and `fetch_module_filename` 33 | `protoc-gen-grpc-gateway-ts` generates a shared typescript file with communication functions. These two parameters together will determine where the fetch module file is located. Default to `$(pwd)/fetch.pb.ts` 34 | 35 | ### `use_proto_names` 36 | To keep the same convention with `grpc-gateway` v2 & `protojson`. The field name in message generated by this library is in lowerCamelCase by default. If you prefer to make it stick the same with what is defined in the proto file, this option needs to be set to true. 37 | 38 | ### `logtostderr` 39 | Turn on logging to stderr. Default to false. 40 | 41 | ### `loglevel` 42 | Defines the logging levels. Default to info. Valid values are: debug, info, warn, error 43 | 44 | ### Notes: 45 | Zero-value fields are omitted from the URL query parameter list for GET requests. Therefore for a request payload such as `{ a: "A", b: "" c: 1, d: 0, e: false }` will become `/path/query?a=A&c=1`. A sample implementation is present within this [proto file](https://github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/blob/master/integration_tests/service.proto) in the`integration_tests` folder. For further explanation please read the following: 46 | - 47 | - 48 | 49 | ## Examples: 50 | The following shows how to use the generated TypeScript code. 51 | 52 | Proto file: `counter.proto` 53 | 54 | ```proto 55 | // file: counter.proto 56 | message Request { 57 | int32 counter = 1; 58 | } 59 | 60 | message Response { 61 | int32 result = 1; 62 | } 63 | 64 | service CounterService { 65 | rpc Increase(Request) returns (Response); 66 | rpc Increase10X(Request) returns (stream Response); 67 | } 68 | ``` 69 | 70 | Run the following command to generate the TypeScript client: 71 | 72 | `protoc --grpc-gateway-ts_out=. counter.proto` 73 | 74 | Then a `counter.pb.ts` file will be available at the current directory. You can use it like the following example. 75 | 76 | ```typescript 77 | import {CounterService} from './counter.pb' 78 | 79 | // increase the given number once 80 | async function increase(base: number): Promise { 81 | const resp = await CounterService.Increase({counter: base}) 82 | return resp.result 83 | } 84 | 85 | // increase the base repeatedly and return all results returned back from server 86 | // the notifier after the request will be called once a result comes back from server streaming 87 | async function increaseRepeatedly(base: number): Promise { 88 | let results = [] 89 | await CounterService.Increase10X({base}, (resp: Response) => { 90 | result.push(resp.result) 91 | }) 92 | 93 | return results 94 | } 95 | 96 | ``` 97 | 98 | ## License 99 | 100 | ```text 101 | Copyright 2020 Square, Inc. 102 | 103 | Licensed under the Apache License, Version 2.0 (the "License"); 104 | you may not use this file except in compliance with the License. 105 | You may obtain a copy of the License at 106 | 107 | http://www.apache.org/licenses/LICENSE-2.0 108 | 109 | Unless required by applicable law or agreed to in writing, software 110 | distributed under the License is distributed on an "AS IS" BASIS, 111 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 112 | See the License for the specific language governing permissions and 113 | limitations under the License. 114 | ``` 115 | -------------------------------------------------------------------------------- /data/enum.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | // Enum is the data out to render Enums in a file 4 | // Enums that nested inside messages will be pulled out to the top level 5 | // Because the way it works in typescript 6 | type Enum struct { 7 | // Name will be the package level unique name 8 | // Nested names will concat with their parent messages so that it will remain unique 9 | // This also means nested type might be a bit ugly in type script but whatever 10 | Name string 11 | // Due to the fact that Protos allows alias fields which is not a feature 12 | // in Typescript, it's better to use string representation of it. 13 | // So Values here will basically be the name of the field. 14 | Values []string 15 | } 16 | 17 | // NewEnum creates an enum instance. 18 | func NewEnum() *Enum { 19 | return &Enum{ 20 | Name: "", 21 | Values: make([]string, 0), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /data/file.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "path" 5 | "path/filepath" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | // File store the information about rendering a file 11 | type File struct { 12 | // Dependencies is a list of dependencies for the file, which will be rendered at the top of the file as import statements 13 | Dependencies []*Dependency 14 | // Enums is a list of enums to render, due to the fact that there cannot be any enum defined nested in the class in Typescript. 15 | // All Enums will be rendered at the top level 16 | Enums []*Enum 17 | // Messages represents top level messages inside the file. 18 | Messages []*Message 19 | // ExternalDependingTypes stores the external dependenciees fully qualified name, 20 | ExternalDependingTypes []string 21 | // Services stores the information to render service 22 | Services Services 23 | // Name is the name of the file 24 | Name string 25 | // TSFileName is the name of the output file 26 | TSFileName string 27 | // PackageNonScalarType stores the type inside the same packages within the file, which will be used to figure out external dependencies inside the same package (different files) 28 | PackageNonScalarType []Type 29 | // EnableStylingCheck enables the styling check for the given file 30 | EnableStylingCheck bool 31 | } 32 | 33 | // StableDependencies are dependencies in a stable order. 34 | func (f *File) StableDependencies() []*Dependency { 35 | out := make([]*Dependency, len(f.Dependencies)) 36 | copy(out, f.Dependencies) 37 | sort.Slice(out, func(i, j int) bool { 38 | return out[i].SourceFile < out[j].SourceFile 39 | }) 40 | return out 41 | } 42 | 43 | // NeedsOneOfSupport indicates the file needs one of support type utilities 44 | func (f *File) NeedsOneOfSupport() bool { 45 | for _, m := range f.Messages { 46 | if m.HasOneOfFields() { 47 | return true 48 | } 49 | } 50 | 51 | return false 52 | } 53 | 54 | // TrackPackageNonScalarType tracks the supplied non scala type in the same package 55 | func (f *File) TrackPackageNonScalarType(t Type) { 56 | isNonScalarType := strings.Index(t.GetType().Type, ".") == 0 57 | if isNonScalarType { 58 | f.PackageNonScalarType = append(f.PackageNonScalarType, t) 59 | } 60 | } 61 | 62 | func (f *File) IsEmpty() bool { 63 | return len(f.Enums) == 0 && len(f.Messages) == 0 && len(f.Services) == 0 64 | } 65 | 66 | // NewFile returns an initialised new file 67 | func NewFile() *File { 68 | return &File{ 69 | Dependencies: make([]*Dependency, 0), 70 | Enums: make([]*Enum, 0), 71 | Messages: make([]*Message, 0), 72 | Services: make([]*Service, 0), 73 | ExternalDependingTypes: make([]string, 0), 74 | } 75 | 76 | } 77 | 78 | // Dependency stores the information about dependencies. 79 | type Dependency struct { 80 | // ModuleIdentifier will be a concanation of package + file base name to make it 81 | // unnique inside the file. This will act as a name space for other file and 82 | // types inside other file can be referred to using . 83 | ModuleIdentifier string 84 | // Source file will be the file at the end of the import statement. 85 | SourceFile string 86 | } 87 | 88 | // GetModuleName returns module name = package name + file name to be the unique identifier for source file in a ts file 89 | func GetModuleName(packageName, fileName string) string { 90 | baseName := filepath.Base(fileName) 91 | ext := filepath.Ext(fileName) 92 | name := baseName[0 : len(baseName)-len(ext)] 93 | packageParts := strings.Split(packageName, ".") 94 | 95 | if packageName != "" { 96 | for i, p := range packageParts { 97 | packageParts[i] = strings.ToUpper(p[:1]) + p[1:] 98 | } 99 | } 100 | 101 | return strings.Join(packageParts, "") + strings.ToUpper(name[:1]) + name[1:] 102 | } 103 | 104 | // GetTSFileName gets the typescript filename out of the proto file name 105 | func GetTSFileName(fileName string) string { 106 | baseName := filepath.Base(fileName) 107 | ext := filepath.Ext(fileName) 108 | name := baseName[0 : len(baseName)-len(ext)] 109 | return path.Join(filepath.Dir(fileName), name+".pb.ts") 110 | } 111 | 112 | // Type is an interface to get type out of field and method arguments 113 | type Type interface { 114 | // GetType returns some information of the type to aid the rendering 115 | GetType() *TypeInfo 116 | // SetExternal changes the external field inside the data structure 117 | SetExternal(bool) 118 | } 119 | 120 | // TypeInfo stores some common type information for rendering 121 | type TypeInfo struct { 122 | // Type 123 | Type string 124 | // IsRepeated indicates whether this field is a repeated field 125 | IsRepeated bool 126 | // IsExternal indicates whether this type is external 127 | IsExternal bool 128 | } 129 | -------------------------------------------------------------------------------- /data/message.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | // Message stores the rendering information about message 4 | type Message struct { 5 | // Nested shows whether this message is a nested message and needs to be exported 6 | Nested bool 7 | // Name is the name of the Message 8 | Name string 9 | //FQType is the fully qualified type name for the message itself 10 | FQType string 11 | // Enums is a list of NestedEnums inside 12 | Enums []*NestedEnum 13 | // Fields is a list of fields to render 14 | Fields []*Field 15 | // NonOneOfFields contains a subset of fields that are not in the one-of groups 16 | NonOneOfFields []*Field 17 | // Message is the nested messages defined inside the message 18 | Messages []*Message 19 | // OneOfFieldsGroups is the grouped list of one of fields with same index. so that renderer can render the clearing of other fields on set. 20 | OneOfFieldsGroups map[int32][]*Field 21 | // OneOfFieldNames is the names of one of fields with same index. so that renderer can render the clearing of other fields on set. 22 | OneOfFieldsNames map[int32]string 23 | } 24 | 25 | // HasOneOfFields returns true when the message has a one of field. 26 | func (m *Message) HasOneOfFields() bool { 27 | return len(m.OneOfFieldsGroups) > 0 28 | } 29 | 30 | // NewMessage initialises and return a Message 31 | func NewMessage() *Message { 32 | return &Message{ 33 | Nested: false, 34 | Name: "", 35 | Enums: make([]*NestedEnum, 0), 36 | Fields: make([]*Field, 0), 37 | Messages: make([]*Message, 0), 38 | OneOfFieldsGroups: make(map[int32][]*Field), 39 | OneOfFieldsNames: make(map[int32]string), 40 | } 41 | } 42 | 43 | // NestedEnum stores the information of enums defined inside a message 44 | type NestedEnum struct { 45 | // Name of the Enum inside the class, which will be identical to the name 46 | // defined inside the message 47 | Name string 48 | // Type will have two types of value, and the difference can be told by 49 | // IsExternal attribute. 50 | // For external one, because during analysis stage there might not be a full map 51 | // of the types inside Registry. So the actual translation of this will 52 | // be left in the render time 53 | // If it is only types inside the file, it will be filled with the unique type name defined 54 | // up at the top level 55 | Type string 56 | } 57 | 58 | // Field stores the information about a field inside message 59 | type Field struct { 60 | Name string 61 | // Type will be similar to NestedEnum.Type. Where scalar type and types inside 62 | // the same file will be short type 63 | // external types will have fully-qualified name and translated during render time 64 | Type string 65 | // IsExternal tells whether the type of this field is an external dependency 66 | IsExternal bool 67 | // IsOneOfField tells whether this field is part of a one of field. 68 | // one of fields will have extra method clearXXX, 69 | // and the setter accessor will clear out other fields in the group on set 70 | IsOneOfField bool 71 | // Message is the reference back to the parent message 72 | Message *Message 73 | // OneOfIndex is the index in the one of fields 74 | OneOfIndex int32 75 | // IsRepeated indicates whether the field is a repeated field 76 | IsRepeated bool 77 | } 78 | 79 | // GetType returns some information of the type to aid the rendering 80 | func (f *Field) GetType() *TypeInfo { 81 | return &TypeInfo{ 82 | Type: f.Type, 83 | IsRepeated: f.IsRepeated, 84 | IsExternal: f.IsExternal, 85 | } 86 | } 87 | 88 | // SetExternal mutate the IsExternal attribute 89 | func (f *Field) SetExternal(external bool) { 90 | f.IsExternal = external 91 | } 92 | 93 | // MapEntryType is the generic entry type for both key and value 94 | type MapEntryType struct { 95 | // Type of the map entry 96 | Type string 97 | // IsExternal indicates the field typeis external to its own package 98 | IsExternal bool 99 | } 100 | 101 | // GetType returns the type information for the type entry 102 | func (m *MapEntryType) GetType() *TypeInfo { 103 | return &TypeInfo{ 104 | Type: m.Type, 105 | IsRepeated: false, 106 | IsExternal: m.IsExternal, 107 | } 108 | } 109 | 110 | // SetExternal mutate the IsExternal attribute inside 111 | func (m *MapEntryType) SetExternal(external bool) { 112 | m.IsExternal = external 113 | } 114 | -------------------------------------------------------------------------------- /data/service.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | // Service is the data representation of Service in proto 4 | type Service struct { 5 | // Name is the name of the Service 6 | Name string 7 | // Methods is a list of methods data 8 | Methods []*Method 9 | } 10 | 11 | // Services is an alias of Service array 12 | type Services []*Service 13 | 14 | // HasServerStreamingMethod indicates whether there is server side streaming calls inside any of the services 15 | func (s Services) HasServerStreamingMethod() bool { 16 | for _, service := range s { 17 | for _, method := range service.Methods { 18 | if method.ServerStreaming { 19 | return true 20 | } 21 | } 22 | } 23 | return false 24 | } 25 | 26 | // HasUnaryCallMethod indicates whether there is unary methods inside any of the services 27 | func (s Services) HasUnaryCallMethod() bool { 28 | for _, service := range s { 29 | for _, method := range service.Methods { 30 | if !method.ServerStreaming && !method.ClientStreaming { 31 | return true 32 | } 33 | } 34 | } 35 | return false 36 | } 37 | 38 | // NeedsFetchModule returns whether the given services needs fetch module support 39 | func (s Services) NeedsFetchModule() bool { 40 | hasServices := len(s) > 0 41 | return hasServices && (s.HasUnaryCallMethod() || s.HasServerStreamingMethod()) 42 | } 43 | 44 | // NewService returns an initialised service 45 | func NewService() *Service { 46 | return &Service{ 47 | Methods: make([]*Method, 0), 48 | } 49 | } 50 | 51 | // Method represents the rpc calls in protobuf service 52 | type Method struct { 53 | // Name is the name of the method 54 | Name string 55 | // URL is the method url path to invoke from client side 56 | URL string 57 | // Input is the input argument 58 | Input *MethodArgument 59 | // Output is the output argument 60 | Output *MethodArgument 61 | // ServerStreaming indicates the RPC call is a server streaming call 62 | ServerStreaming bool 63 | // ClientStreaming indicates the RPC call is a client streaming call, which will not be supported by GRPC Gateway 64 | ClientStreaming bool 65 | // HTTPMethod indicates the http method for this function 66 | HTTPMethod string 67 | // HTTPBody is the path for request body in the body's payload 68 | HTTPRequestBody *string 69 | } 70 | 71 | // MethodArgument stores the type information about method argument 72 | type MethodArgument struct { 73 | // Type is the type of the argument 74 | Type string 75 | // IsExternal indicate if this type is an external dependency 76 | IsExternal bool 77 | // IsRepeated indicates whether the field is a repeated field 78 | IsRepeated bool 79 | } 80 | 81 | // GetType returns some information of the type to aid the rendering 82 | func (m *MethodArgument) GetType() *TypeInfo { 83 | return &TypeInfo{ 84 | Type: m.Type, 85 | IsRepeated: m.IsRepeated, 86 | IsExternal: m.IsExternal, 87 | } 88 | } 89 | 90 | // SetExternal mutates the IsExternal attribute of the type 91 | func (m *MethodArgument) SetExternal(external bool) { 92 | m.IsExternal = external 93 | } 94 | -------------------------------------------------------------------------------- /generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "path/filepath" 7 | "strings" 8 | "text/template" 9 | 10 | plugin "github.com/golang/protobuf/protoc-gen-go/plugin" 11 | log "github.com/sirupsen/logrus" // nolint: depguard 12 | 13 | "github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/data" 14 | "github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/registry" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | // TypeScriptGRPCGatewayGenerator is the protobuf generator for typescript 19 | type TypeScriptGRPCGatewayGenerator struct { 20 | Registry *registry.Registry 21 | // EnableStylingCheck enables both eslint and tsc check for the generated code 22 | // This option will only turn on in integration test to ensure the readability in 23 | // the generated code. 24 | EnableStylingCheck bool 25 | } 26 | 27 | const ( 28 | // EnableStylingCheckOption is the option name for EnableStylingCheck 29 | EnableStylingCheckOption = "enable_styling_check" 30 | ) 31 | 32 | // New returns an initialised generator 33 | func New(paramsMap map[string]string) (*TypeScriptGRPCGatewayGenerator, error) { 34 | registry, err := registry.NewRegistry(paramsMap) 35 | if err != nil { 36 | return nil, errors.Wrap(err, "error instantiating a new registry") 37 | } 38 | 39 | enableStylingCheck := false 40 | enableStylingCheckVal, ok := paramsMap[EnableStylingCheckOption] 41 | if ok { 42 | // default to true if not disabled specifi 43 | enableStylingCheck = enableStylingCheckVal == "true" 44 | } 45 | 46 | return &TypeScriptGRPCGatewayGenerator{ 47 | Registry: registry, 48 | EnableStylingCheck: enableStylingCheck, 49 | }, nil 50 | } 51 | 52 | // Generate take a code generator request and returns a response. it analyse request with registry and use the generated data to render ts files 53 | func (t *TypeScriptGRPCGatewayGenerator) Generate(req *plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorResponse, error) { 54 | resp := &plugin.CodeGeneratorResponse{} 55 | 56 | filesData, err := t.Registry.Analyse(req) 57 | if err != nil { 58 | return nil, errors.Wrap(err, "error analysing proto files") 59 | } 60 | tmpl := GetTemplate(t.Registry) 61 | log.Debugf("files to generate %v", req.GetFileToGenerate()) 62 | 63 | needToGenerateFetchModule := false 64 | // feed fileData into rendering process 65 | for _, fileData := range filesData { 66 | fileData.EnableStylingCheck = t.EnableStylingCheck 67 | if !t.Registry.IsFileToGenerate(fileData.Name) { 68 | log.Debugf("file %s is not the file to generate, skipping", fileData.Name) 69 | continue 70 | } 71 | 72 | log.Debugf("generating file for %s", fileData.TSFileName) 73 | generated, err := t.generateFile(fileData, tmpl) 74 | if err != nil { 75 | return nil, errors.Wrap(err, "error generating file") 76 | } 77 | resp.File = append(resp.File, generated) 78 | needToGenerateFetchModule = needToGenerateFetchModule || fileData.Services.NeedsFetchModule() 79 | } 80 | 81 | if needToGenerateFetchModule { 82 | // generate fetch module 83 | fetchTmpl := GetFetchModuleTemplate() 84 | log.Debugf("generate fetch template") 85 | generatedFetch, err := t.generateFetchModule(fetchTmpl) 86 | if err != nil { 87 | return nil, errors.Wrap(err, "error generating fetch module") 88 | } 89 | 90 | resp.File = append(resp.File, generatedFetch) 91 | } 92 | 93 | return resp, nil 94 | } 95 | 96 | func (t *TypeScriptGRPCGatewayGenerator) generateFile(fileData *data.File, tmpl *template.Template) (*plugin.CodeGeneratorResponse_File, error) { 97 | w := bytes.NewBufferString("") 98 | 99 | if fileData.IsEmpty() { 100 | w.Write([]byte(fmt.Sprintln("export default {}"))) 101 | } else { 102 | err := tmpl.Execute(w, fileData) 103 | if err != nil { 104 | return nil, errors.Wrapf(err, "error generating ts file for %s", fileData.Name) 105 | } 106 | } 107 | 108 | fileName := fileData.TSFileName 109 | content := strings.TrimSpace(w.String()) 110 | 111 | return &plugin.CodeGeneratorResponse_File{ 112 | Name: &fileName, 113 | InsertionPoint: nil, 114 | Content: &content, 115 | }, nil 116 | } 117 | 118 | func (t *TypeScriptGRPCGatewayGenerator) generateFetchModule(tmpl *template.Template) (*plugin.CodeGeneratorResponse_File, error) { 119 | w := bytes.NewBufferString("") 120 | fileName := filepath.Join(t.Registry.FetchModuleDirectory, t.Registry.FetchModuleFilename) 121 | err := tmpl.Execute(w, &data.File{EnableStylingCheck: t.EnableStylingCheck}) 122 | if err != nil { 123 | return nil, errors.Wrapf(err, "error generating fetch module at %s", fileName) 124 | } 125 | 126 | content := strings.TrimSpace(w.String()) 127 | return &plugin.CodeGeneratorResponse_File{ 128 | Name: &fileName, 129 | InsertionPoint: nil, 130 | Content: &content, 131 | }, nil 132 | } 133 | -------------------------------------------------------------------------------- /generator/template.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/url" 7 | "regexp" 8 | "strings" 9 | "text/template" 10 | 11 | log "github.com/sirupsen/logrus" 12 | 13 | "github.com/Masterminds/sprig" 14 | "github.com/iancoleman/strcase" 15 | 16 | "github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/data" 17 | "github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/registry" 18 | ) 19 | 20 | const tmpl = ` 21 | {{define "dependencies"}} 22 | {{range .}}import * as {{.ModuleIdentifier}} from "{{.SourceFile}}" 23 | {{end}}{{end}} 24 | 25 | {{define "enums"}} 26 | {{range .}}export enum {{.Name}} { 27 | {{- range .Values}} 28 | {{.}} = "{{.}}", 29 | {{- end}} 30 | } 31 | 32 | {{end}}{{end}} 33 | 34 | {{define "messages"}}{{range .}} 35 | {{- if .HasOneOfFields}} 36 | type Base{{.Name}} = { 37 | {{- range .NonOneOfFields}} 38 | {{fieldName .Name}}?: {{tsType .}} 39 | {{- end}} 40 | } 41 | 42 | export type {{.Name}} = Base{{.Name}} 43 | {{range $groupId, $fields := .OneOfFieldsGroups}} & OneOf<{ {{range $index, $field := $fields}}{{fieldName $field.Name}}: {{tsType $field}}{{if (lt (add $index 1) (len $fields))}}; {{end}}{{end}} }> 44 | {{end}} 45 | {{- else -}} 46 | export type {{.Name}} = { 47 | {{- range .Fields}} 48 | {{fieldName .Name}}?: {{tsType .}} 49 | {{- end}} 50 | } 51 | {{end}} 52 | {{end}}{{end}} 53 | 54 | {{define "services"}}{{range .}}export class {{.Name}} { 55 | {{- range .Methods}} 56 | {{- if .ServerStreaming }} 57 | static {{.Name}}(req: {{tsType .Input}}, entityNotifier?: fm.NotifyStreamEntityArrival<{{tsType .Output}}>, initReq?: fm.InitReq): Promise { 58 | return fm.fetchStreamingRequest<{{tsType .Input}}, {{tsType .Output}}>(` + "`{{renderURL .}}`" + `, entityNotifier, {...initReq, {{buildInitReq .}}}) 59 | } 60 | {{- else }} 61 | static {{.Name}}(req: {{tsType .Input}}, initReq?: fm.InitReq): Promise<{{tsType .Output}}> { 62 | return fm.fetchReq<{{tsType .Input}}, {{tsType .Output}}>(` + "`{{renderURL .}}`" + `, {...initReq, {{buildInitReq .}}}) 63 | } 64 | {{- end}} 65 | {{- end}} 66 | } 67 | {{end}}{{end}} 68 | 69 | {{- if not .EnableStylingCheck}} 70 | /* eslint-disable */ 71 | // @ts-nocheck 72 | {{- end}} 73 | /* 74 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 75 | */ 76 | {{if .Dependencies}}{{- include "dependencies" .StableDependencies -}}{{end}} 77 | {{- if .NeedsOneOfSupport}} 78 | type Absent = { [k in Exclude]?: undefined }; 79 | type OneOf = 80 | | { [k in keyof T]?: undefined } 81 | | ( 82 | keyof T extends infer K ? 83 | (K extends string & keyof T ? { [k in K]: T[K] } & Absent 84 | : never) 85 | : never); 86 | {{end}} 87 | {{- if .Enums}}{{include "enums" .Enums}}{{end}} 88 | {{- if .Messages}}{{include "messages" .Messages}}{{end}} 89 | {{- if .Services}}{{include "services" .Services}}{{end}} 90 | ` 91 | 92 | const fetchTmpl = ` 93 | {{- if not .EnableStylingCheck}} 94 | /* eslint-disable */ 95 | // @ts-nocheck 96 | {{- end}} 97 | /* 98 | * This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY 99 | */ 100 | 101 | /** 102 | * base64 encoder and decoder 103 | * Copied and adapted from https://github.com/protobufjs/protobuf.js/blob/master/lib/base64/index.js 104 | */ 105 | // Base64 encoding table 106 | const b64 = new Array(64); 107 | 108 | // Base64 decoding table 109 | const s64 = new Array(123); 110 | 111 | // 65..90, 97..122, 48..57, 43, 47 112 | for (let i = 0; i < 64;) 113 | s64[b64[i] = i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i - 59 | 43] = i++; 114 | 115 | export function b64Encode(buffer: Uint8Array, start: number, end: number): string { 116 | let parts: string[] = null; 117 | const chunk = []; 118 | let i = 0, // output index 119 | j = 0, // goto index 120 | t; // temporary 121 | while (start < end) { 122 | const b = buffer[start++]; 123 | switch (j) { 124 | case 0: 125 | chunk[i++] = b64[b >> 2]; 126 | t = (b & 3) << 4; 127 | j = 1; 128 | break; 129 | case 1: 130 | chunk[i++] = b64[t | b >> 4]; 131 | t = (b & 15) << 2; 132 | j = 2; 133 | break; 134 | case 2: 135 | chunk[i++] = b64[t | b >> 6]; 136 | chunk[i++] = b64[b & 63]; 137 | j = 0; 138 | break; 139 | } 140 | if (i > 8191) { 141 | (parts || (parts = [])).push(String.fromCharCode.apply(String, chunk)); 142 | i = 0; 143 | } 144 | } 145 | if (j) { 146 | chunk[i++] = b64[t]; 147 | chunk[i++] = 61; 148 | if (j === 1) 149 | chunk[i++] = 61; 150 | } 151 | if (parts) { 152 | if (i) 153 | parts.push(String.fromCharCode.apply(String, chunk.slice(0, i))); 154 | return parts.join(""); 155 | } 156 | return String.fromCharCode.apply(String, chunk.slice(0, i)); 157 | } 158 | 159 | const invalidEncoding = "invalid encoding"; 160 | 161 | export function b64Decode(s: string): Uint8Array { 162 | const buffer = []; 163 | let offset = 0; 164 | let j = 0, // goto index 165 | t; // temporary 166 | for (let i = 0; i < s.length;) { 167 | let c = s.charCodeAt(i++); 168 | if (c === 61 && j > 1) 169 | break; 170 | if ((c = s64[c]) === undefined) 171 | throw Error(invalidEncoding); 172 | switch (j) { 173 | case 0: 174 | t = c; 175 | j = 1; 176 | break; 177 | case 1: 178 | buffer[offset++] = t << 2 | (c & 48) >> 4; 179 | t = c; 180 | j = 2; 181 | break; 182 | case 2: 183 | buffer[offset++] = (t & 15) << 4 | (c & 60) >> 2; 184 | t = c; 185 | j = 3; 186 | break; 187 | case 3: 188 | buffer[offset++] = (t & 3) << 6 | c; 189 | j = 0; 190 | break; 191 | } 192 | } 193 | if (j === 1) 194 | throw Error(invalidEncoding); 195 | return new Uint8Array(buffer); 196 | } 197 | 198 | function b64Test(s: string): boolean { 199 | return /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(s); 200 | } 201 | 202 | export interface InitReq extends RequestInit { 203 | pathPrefix?: string 204 | } 205 | 206 | export function replacer(key: any, value: any): any { 207 | if(value && value.constructor === Uint8Array) { 208 | return b64Encode(value, 0, value.length); 209 | } 210 | 211 | return value; 212 | } 213 | 214 | export function fetchReq(path: string, init?: InitReq): Promise { 215 | const {pathPrefix, ...req} = init || {} 216 | 217 | const url = pathPrefix ? ` + "`${pathPrefix}${path}`" + ` : path 218 | 219 | return fetch(url, req).then(r => r.json().then((body: O) => { 220 | if (!r.ok) { throw body; } 221 | return body; 222 | })) as Promise 223 | } 224 | 225 | // NotifyStreamEntityArrival is a callback that will be called on streaming entity arrival 226 | export type NotifyStreamEntityArrival = (resp: T) => void 227 | 228 | /** 229 | * fetchStreamingRequest is able to handle grpc-gateway server side streaming call 230 | * it takes NotifyStreamEntityArrival that lets users respond to entity arrival during the call 231 | * all entities will be returned as an array after the call finishes. 232 | **/ 233 | export async function fetchStreamingRequest(path: string, callback?: NotifyStreamEntityArrival, init?: InitReq) { 234 | const {pathPrefix, ...req} = init || {} 235 | const url = pathPrefix ?` + "`${pathPrefix}${path}`" + ` : path 236 | const result = await fetch(url, req) 237 | // needs to use the .ok to check the status of HTTP status code 238 | // http other than 200 will not throw an error, instead the .ok will become false. 239 | // see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch# 240 | if (!result.ok) { 241 | const resp = await result.json() 242 | const errMsg = resp.error && resp.error.message ? resp.error.message : "" 243 | throw new Error(errMsg) 244 | } 245 | 246 | if (!result.body) { 247 | throw new Error("response doesnt have a body") 248 | } 249 | 250 | await result.body 251 | .pipeThrough(new TextDecoderStream()) 252 | .pipeThrough(getNewLineDelimitedJSONDecodingStream()) 253 | .pipeTo(getNotifyEntityArrivalSink((e: R) => { 254 | if (callback) { 255 | callback(e) 256 | } 257 | })) 258 | 259 | // wait for the streaming to finish and return the success respond 260 | return 261 | } 262 | 263 | /** 264 | * JSONStringStreamController represents the transform controller that's able to transform the incoming 265 | * new line delimited json content stream into entities and able to push the entity to the down stream 266 | */ 267 | interface JSONStringStreamController extends TransformStreamDefaultController { 268 | buf?: string 269 | pos?: number 270 | enqueue: (s: T) => void 271 | } 272 | 273 | /** 274 | * getNewLineDelimitedJSONDecodingStream returns a TransformStream that's able to handle new line delimited json stream content into parsed entities 275 | */ 276 | function getNewLineDelimitedJSONDecodingStream(): TransformStream { 277 | return new TransformStream({ 278 | start(controller: JSONStringStreamController) { 279 | controller.buf = '' 280 | controller.pos = 0 281 | }, 282 | 283 | transform(chunk: string, controller: JSONStringStreamController) { 284 | if (controller.buf === undefined) { 285 | controller.buf = '' 286 | } 287 | if (controller.pos === undefined) { 288 | controller.pos = 0 289 | } 290 | controller.buf += chunk 291 | while (controller.pos < controller.buf.length) { 292 | if (controller.buf[controller.pos] === '\n') { 293 | const line = controller.buf.substring(0, controller.pos) 294 | const response = JSON.parse(line) 295 | controller.enqueue(response.result) 296 | controller.buf = controller.buf.substring(controller.pos + 1) 297 | controller.pos = 0 298 | } else { 299 | ++controller.pos 300 | } 301 | } 302 | } 303 | }) 304 | 305 | } 306 | 307 | /** 308 | * getNotifyEntityArrivalSink takes the NotifyStreamEntityArrival callback and return 309 | * a sink that will call the callback on entity arrival 310 | * @param notifyCallback 311 | */ 312 | function getNotifyEntityArrivalSink(notifyCallback: NotifyStreamEntityArrival) { 313 | return new WritableStream({ 314 | write(entity: T) { 315 | notifyCallback(entity) 316 | } 317 | }) 318 | } 319 | 320 | type Primitive = string | boolean | number; 321 | type RequestPayload = Record; 322 | type FlattenedRequestPayload = Record>; 323 | 324 | /** 325 | * Checks if given value is a plain object 326 | * Logic copied and adapted from below source: 327 | * https://github.com/char0n/ramda-adjunct/blob/master/src/isPlainObj.js 328 | * @param {unknown} value 329 | * @return {boolean} 330 | */ 331 | function isPlainObject(value: unknown): boolean { 332 | const isObject = 333 | Object.prototype.toString.call(value).slice(8, -1) === "Object"; 334 | const isObjLike = value !== null && isObject; 335 | 336 | if (!isObjLike || !isObject) { 337 | return false; 338 | } 339 | 340 | const proto = Object.getPrototypeOf(value); 341 | 342 | const hasObjectConstructor = 343 | typeof proto === "object" && 344 | proto.constructor === Object.prototype.constructor; 345 | 346 | return hasObjectConstructor; 347 | } 348 | 349 | /** 350 | * Checks if given value is of a primitive type 351 | * @param {unknown} value 352 | * @return {boolean} 353 | */ 354 | function isPrimitive(value: unknown): boolean { 355 | return ["string", "number", "boolean"].some(t => typeof value === t); 356 | } 357 | 358 | /** 359 | * Checks if given primitive is zero-value 360 | * @param {Primitive} value 361 | * @return {boolean} 362 | */ 363 | function isZeroValuePrimitive(value: Primitive): boolean { 364 | return value === false || value === 0 || value === ""; 365 | } 366 | 367 | /** 368 | * Flattens a deeply nested request payload and returns an object 369 | * with only primitive values and non-empty array of primitive values 370 | * as per https://github.com/googleapis/googleapis/blob/master/google/api/http.proto 371 | * @param {RequestPayload} requestPayload 372 | * @param {String} path 373 | * @return {FlattenedRequestPayload>} 374 | */ 375 | function flattenRequestPayload( 376 | requestPayload: T, 377 | path: string = "" 378 | ): FlattenedRequestPayload { 379 | return Object.keys(requestPayload).reduce( 380 | (acc: T, key: string): T => { 381 | const value = requestPayload[key]; 382 | const newPath = path ? [path, key].join(".") : key; 383 | 384 | const isNonEmptyPrimitiveArray = 385 | Array.isArray(value) && 386 | value.every(v => isPrimitive(v)) && 387 | value.length > 0; 388 | 389 | const isNonZeroValuePrimitive = 390 | isPrimitive(value) && !isZeroValuePrimitive(value as Primitive); 391 | 392 | let objectToMerge = {}; 393 | 394 | if (isPlainObject(value)) { 395 | objectToMerge = flattenRequestPayload(value as RequestPayload, newPath); 396 | } else if (isNonZeroValuePrimitive || isNonEmptyPrimitiveArray) { 397 | objectToMerge = { [newPath]: value }; 398 | } 399 | 400 | return { ...acc, ...objectToMerge }; 401 | }, 402 | {} as T 403 | ) as FlattenedRequestPayload; 404 | } 405 | 406 | /** 407 | * Renders a deeply nested request payload into a string of URL search 408 | * parameters by first flattening the request payload and then removing keys 409 | * which are already present in the URL path. 410 | * @param {RequestPayload} requestPayload 411 | * @param {string[]} urlPathParams 412 | * @return {string} 413 | */ 414 | export function renderURLSearchParams( 415 | requestPayload: T, 416 | urlPathParams: string[] = [] 417 | ): string { 418 | const flattenedRequestPayload = flattenRequestPayload(requestPayload); 419 | 420 | const urlSearchParams = Object.keys(flattenedRequestPayload).reduce( 421 | (acc: string[][], key: string): string[][] => { 422 | // key should not be present in the url path as a parameter 423 | const value = flattenedRequestPayload[key]; 424 | if (urlPathParams.find(f => f === key)) { 425 | return acc; 426 | } 427 | return Array.isArray(value) 428 | ? [...acc, ...value.map(m => [key, m.toString()])] 429 | : (acc = [...acc, [key, value.toString()]]); 430 | }, 431 | [] as string[][] 432 | ); 433 | 434 | return new URLSearchParams(urlSearchParams).toString(); 435 | } 436 | ` 437 | 438 | // GetTemplate gets the templates to for the typescript file 439 | func GetTemplate(r *registry.Registry) *template.Template { 440 | t := template.New("file") 441 | t = t.Funcs(sprig.TxtFuncMap()) 442 | 443 | t = t.Funcs(template.FuncMap{ 444 | "include": include(t), 445 | "tsType": func(fieldType data.Type) string { 446 | return tsType(r, fieldType) 447 | }, 448 | "renderURL": renderURL(r), 449 | "buildInitReq": buildInitReq, 450 | "fieldName": fieldName(r), 451 | }) 452 | 453 | t = template.Must(t.Parse(tmpl)) 454 | return t 455 | } 456 | 457 | func fieldName(r *registry.Registry) func(name string) string { 458 | return func(name string) string { 459 | if r.UseProtoNames { 460 | return name 461 | } 462 | 463 | return strcase.ToLowerCamel(name) 464 | } 465 | } 466 | 467 | func renderURL(r *registry.Registry) func(method data.Method) string { 468 | fieldNameFn := fieldName(r) 469 | return func(method data.Method) string { 470 | methodURL := method.URL 471 | reg := regexp.MustCompile("{([^}]+)}") 472 | matches := reg.FindAllStringSubmatch(methodURL, -1) 473 | fieldsInPath := make([]string, 0, len(matches)) 474 | if len(matches) > 0 { 475 | log.Debugf("url matches %v", matches) 476 | for _, m := range matches { 477 | expToReplace := m[0] 478 | fieldName := fieldNameFn(m[1]) 479 | part := fmt.Sprintf(`${req["%s"]}`, fieldName) 480 | methodURL = strings.ReplaceAll(methodURL, expToReplace, part) 481 | fieldsInPath = append(fieldsInPath, fmt.Sprintf(`"%s"`, fieldName)) 482 | } 483 | } 484 | urlPathParams := fmt.Sprintf("[%s]", strings.Join(fieldsInPath, ", ")) 485 | 486 | if !method.ClientStreaming && method.HTTPMethod == "GET" { 487 | // parse the url to check for query string 488 | parsedURL, err := url.Parse(methodURL) 489 | if err != nil { 490 | return methodURL 491 | } 492 | renderURLSearchParamsFn := fmt.Sprintf("${fm.renderURLSearchParams(req, %s)}", urlPathParams) 493 | // prepend "&" if query string is present otherwise prepend "?" 494 | // trim leading "&" if present before prepending it 495 | if parsedURL.RawQuery != "" { 496 | methodURL = strings.TrimRight(methodURL, "&") + "&" + renderURLSearchParamsFn 497 | } else { 498 | methodURL += "?" + renderURLSearchParamsFn 499 | } 500 | } 501 | 502 | return methodURL 503 | } 504 | } 505 | 506 | func buildInitReq(method data.Method) string { 507 | httpMethod := method.HTTPMethod 508 | m := `method: "` + httpMethod + `"` 509 | fields := []string{m} 510 | if method.HTTPRequestBody == nil || *method.HTTPRequestBody == "*" { 511 | fields = append(fields, "body: JSON.stringify(req, fm.replacer)") 512 | } else if *method.HTTPRequestBody != "" { 513 | fields = append(fields, `body: JSON.stringify(req["`+*method.HTTPRequestBody+`"], fm.replacer)`) 514 | } 515 | 516 | return strings.Join(fields, ", ") 517 | 518 | } 519 | 520 | // GetFetchModuleTemplate returns the go template for fetch module 521 | func GetFetchModuleTemplate() *template.Template { 522 | t := template.New("fetch") 523 | return template.Must(t.Parse(fetchTmpl)) 524 | } 525 | 526 | // include is the include template functions copied from 527 | // copied from: https://github.com/helm/helm/blob/8648ccf5d35d682dcd5f7a9c2082f0aaf071e817/pkg/engine/engine.go#L147-L154 528 | func include(t *template.Template) func(name string, data interface{}) (string, error) { 529 | return func(name string, data interface{}) (string, error) { 530 | buf := bytes.NewBufferString("") 531 | if err := t.ExecuteTemplate(buf, name, data); err != nil { 532 | return "", err 533 | } 534 | return buf.String(), nil 535 | } 536 | } 537 | 538 | func tsType(r *registry.Registry, fieldType data.Type) string { 539 | info := fieldType.GetType() 540 | typeInfo, ok := r.Types[info.Type] 541 | if ok && typeInfo.IsMapEntry { 542 | keyType := tsType(r, typeInfo.KeyType) 543 | valueType := tsType(r, typeInfo.ValueType) 544 | 545 | return fmt.Sprintf("{[key: %s]: %s}", keyType, valueType) 546 | } 547 | 548 | typeStr := "" 549 | if strings.Index(info.Type, ".") != 0 { 550 | typeStr = mapScalaType(info.Type) 551 | } else if !info.IsExternal { 552 | typeStr = typeInfo.PackageIdentifier 553 | } else { 554 | typeStr = data.GetModuleName(typeInfo.Package, typeInfo.File) + "." + typeInfo.PackageIdentifier 555 | } 556 | 557 | if info.IsRepeated { 558 | typeStr += "[]" 559 | } 560 | return typeStr 561 | } 562 | 563 | func mapScalaType(protoType string) string { 564 | switch protoType { 565 | case "uint64", "sint64", "int64", "fixed64", "sfixed64", "string": 566 | return "string" 567 | case "float", "double", "int32", "sint32", "uint32", "fixed32", "sfixed32": 568 | return "number" 569 | case "bool": 570 | return "boolean" 571 | case "bytes": 572 | return "Uint8Array" 573 | } 574 | 575 | return "" 576 | 577 | } 578 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/Masterminds/goutils v1.1.0 // indirect 7 | github.com/Masterminds/semver v1.5.0 // indirect 8 | github.com/Masterminds/sprig v2.22.0+incompatible 9 | github.com/golang/protobuf v1.4.3 10 | github.com/grpc-ecosystem/grpc-gateway v1.15.2 11 | github.com/huandu/xstrings v1.3.2 // indirect 12 | github.com/iancoleman/strcase v0.1.2 13 | github.com/imdario/mergo v0.3.11 // indirect 14 | github.com/mitchellh/copystructure v1.0.0 // indirect 15 | github.com/pkg/errors v0.9.1 16 | github.com/sirupsen/logrus v1.7.0 17 | github.com/stretchr/testify v1.6.1 18 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect 19 | golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect 20 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 21 | google.golang.org/grpc v1.33.1 22 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 // indirect 23 | google.golang.org/protobuf v1.25.0 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= 5 | github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 6 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 7 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 8 | github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= 9 | github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 10 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 11 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 12 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 13 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 18 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 19 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 20 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 21 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 22 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 23 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 24 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 25 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 26 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 28 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 29 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 30 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 31 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 32 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 33 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 34 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 35 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 36 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 37 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 38 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 39 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 40 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= 42 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 43 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 44 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 45 | github.com/grpc-ecosystem/grpc-gateway v1.15.2 h1:HC+hWRWf+v5zTMPyoaYTKIJih+4sd4XRWmj0qlG87Co= 46 | github.com/grpc-ecosystem/grpc-gateway v1.15.2/go.mod h1:vO11I9oWA+KsxmfFQPhLnnIb1VDE24M+pdxZFiuZcA8= 47 | github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= 48 | github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 49 | github.com/iancoleman/strcase v0.1.2 h1:gnomlvw9tnV3ITTAxzKSgTF+8kFWcU/f+TgttpXGz1U= 50 | github.com/iancoleman/strcase v0.1.2/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= 51 | github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= 52 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 53 | github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= 54 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 55 | github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= 56 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 57 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 58 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 59 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 60 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 61 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 62 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 63 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 64 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 65 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 66 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 67 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 68 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 69 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 70 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= 71 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 72 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 73 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 74 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 75 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 76 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 77 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 78 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 79 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 80 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 81 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 82 | golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 h1:2mqDk8w/o6UmeUCu5Qiq2y7iMf6anbx+YA8d1JFoFrs= 83 | golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 84 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 85 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 86 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 87 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 88 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 89 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 90 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 91 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 92 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 94 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 95 | golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= 96 | golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 97 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 98 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 99 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 100 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 101 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 102 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 103 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 104 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 105 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 106 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 107 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 108 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 109 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 110 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= 111 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 112 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 113 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 114 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 115 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 116 | google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 117 | google.golang.org/grpc v1.33.1 h1:DGeFlSan2f+WEtCERJ4J9GJWk15TxUi8QGagfI87Xyc= 118 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 119 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= 120 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= 121 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 122 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 123 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 124 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 125 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 126 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 127 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 128 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 129 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 130 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 131 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 132 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 133 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 134 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 135 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 136 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 137 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 138 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 139 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 140 | -------------------------------------------------------------------------------- /integration_tests/.gitignore: -------------------------------------------------------------------------------- 1 | *.pb.ts 2 | -------------------------------------------------------------------------------- /integration_tests/README.md: -------------------------------------------------------------------------------- 1 | # Integration test 2 | 3 | The integration test first runs `./scripts/gen-protos.sh` again to generate Typescript file for the proto `service.proto`. 4 | 5 | Then it starts `main.go` server that loads up the protos and run tests via `Karma` to verify if the generated client works properly. 6 | 7 | The JS integration test file is `integration_test.ts`. 8 | 9 | Changes on the server side needs to run `./scripts/gen-server-proto.sh` to update the protos and the implementation is in `service.go`. 10 | 11 | Changes on the test client side is in `integration_test.ts`. 12 | 13 | CI test script starts with `test-ci.sh` will make sure the client typescript file to be regenerated before running the test. 14 | -------------------------------------------------------------------------------- /integration_tests/empty.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = ".;main"; 3 | 4 | -------------------------------------------------------------------------------- /integration_tests/google/api/annotations.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/api/http.proto"; 20 | import "google/protobuf/descriptor.proto"; 21 | 22 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 23 | option java_multiple_files = true; 24 | option java_outer_classname = "AnnotationsProto"; 25 | option java_package = "com.google.api"; 26 | option objc_class_prefix = "GAPI"; 27 | 28 | extend google.protobuf.MethodOptions { 29 | // See `HttpRule`. 30 | HttpRule http = 72295728; 31 | } 32 | -------------------------------------------------------------------------------- /integration_tests/google/api/http.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | syntax = "proto3"; 17 | 18 | package google.api; 19 | 20 | option cc_enable_arenas = true; 21 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 22 | option java_multiple_files = true; 23 | option java_outer_classname = "HttpProto"; 24 | option java_package = "com.google.api"; 25 | option objc_class_prefix = "GAPI"; 26 | 27 | // Defines the HTTP configuration for an API service. It contains a list of 28 | // [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method 29 | // to one or more HTTP REST API methods. 30 | message Http { 31 | // A list of HTTP configuration rules that apply to individual API methods. 32 | // 33 | // **NOTE:** All service configuration rules follow "last one wins" order. 34 | repeated HttpRule rules = 1; 35 | 36 | // When set to true, URL path parameters will be fully URI-decoded except in 37 | // cases of single segment matches in reserved expansion, where "%2F" will be 38 | // left encoded. 39 | // 40 | // The default behavior is to not decode RFC 6570 reserved characters in multi 41 | // segment matches. 42 | bool fully_decode_reserved_expansion = 2; 43 | } 44 | 45 | // # gRPC Transcoding 46 | // 47 | // gRPC Transcoding is a feature for mapping between a gRPC method and one or 48 | // more HTTP REST endpoints. It allows developers to build a single API service 49 | // that supports both gRPC APIs and REST APIs. Many systems, including [Google 50 | // APIs](https://github.com/googleapis/googleapis), 51 | // [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC 52 | // Gateway](https://github.com/grpc-ecosystem/grpc-gateway), 53 | // and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature 54 | // and use it for large scale production services. 55 | // 56 | // `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies 57 | // how different portions of the gRPC request message are mapped to the URL 58 | // path, URL query parameters, and HTTP request body. It also controls how the 59 | // gRPC response message is mapped to the HTTP response body. `HttpRule` is 60 | // typically specified as an `google.api.http` annotation on the gRPC method. 61 | // 62 | // Each mapping specifies a URL path template and an HTTP method. The path 63 | // template may refer to one or more fields in the gRPC request message, as long 64 | // as each field is a non-repeated field with a primitive (non-message) type. 65 | // The path template controls how fields of the request message are mapped to 66 | // the URL path. 67 | // 68 | // Example: 69 | // 70 | // service Messaging { 71 | // rpc GetMessage(GetMessageRequest) returns (Message) { 72 | // option (google.api.http) = { 73 | // get: "/v1/{name=messages/*}" 74 | // }; 75 | // } 76 | // } 77 | // message GetMessageRequest { 78 | // string name = 1; // Mapped to URL path. 79 | // } 80 | // message Message { 81 | // string text = 1; // The resource content. 82 | // } 83 | // 84 | // This enables an HTTP REST to gRPC mapping as below: 85 | // 86 | // HTTP | gRPC 87 | // -----|----- 88 | // `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` 89 | // 90 | // Any fields in the request message which are not bound by the path template 91 | // automatically become HTTP query parameters if there is no HTTP request body. 92 | // For example: 93 | // 94 | // service Messaging { 95 | // rpc GetMessage(GetMessageRequest) returns (Message) { 96 | // option (google.api.http) = { 97 | // get:"/v1/messages/{message_id}" 98 | // }; 99 | // } 100 | // } 101 | // message GetMessageRequest { 102 | // message SubMessage { 103 | // string subfield = 1; 104 | // } 105 | // string message_id = 1; // Mapped to URL path. 106 | // int64 revision = 2; // Mapped to URL query parameter `revision`. 107 | // SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. 108 | // } 109 | // 110 | // This enables a HTTP JSON to RPC mapping as below: 111 | // 112 | // HTTP | gRPC 113 | // -----|----- 114 | // `GET /v1/messages/123456?revision=2&sub.subfield=foo` | 115 | // `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: 116 | // "foo"))` 117 | // 118 | // Note that fields which are mapped to URL query parameters must have a 119 | // primitive type or a repeated primitive type or a non-repeated message type. 120 | // In the case of a repeated type, the parameter can be repeated in the URL 121 | // as `...?param=A¶m=B`. In the case of a message type, each field of the 122 | // message is mapped to a separate parameter, such as 123 | // `...?foo.a=A&foo.b=B&foo.c=C`. 124 | // 125 | // For HTTP methods that allow a request body, the `body` field 126 | // specifies the mapping. Consider a REST update method on the 127 | // message resource collection: 128 | // 129 | // service Messaging { 130 | // rpc UpdateMessage(UpdateMessageRequest) returns (Message) { 131 | // option (google.api.http) = { 132 | // patch: "/v1/messages/{message_id}" 133 | // body: "message" 134 | // }; 135 | // } 136 | // } 137 | // message UpdateMessageRequest { 138 | // string message_id = 1; // mapped to the URL 139 | // Message message = 2; // mapped to the body 140 | // } 141 | // 142 | // The following HTTP JSON to RPC mapping is enabled, where the 143 | // representation of the JSON in the request body is determined by 144 | // protos JSON encoding: 145 | // 146 | // HTTP | gRPC 147 | // -----|----- 148 | // `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: 149 | // "123456" message { text: "Hi!" })` 150 | // 151 | // The special name `*` can be used in the body mapping to define that 152 | // every field not bound by the path template should be mapped to the 153 | // request body. This enables the following alternative definition of 154 | // the update method: 155 | // 156 | // service Messaging { 157 | // rpc UpdateMessage(Message) returns (Message) { 158 | // option (google.api.http) = { 159 | // patch: "/v1/messages/{message_id}" 160 | // body: "*" 161 | // }; 162 | // } 163 | // } 164 | // message Message { 165 | // string message_id = 1; 166 | // string text = 2; 167 | // } 168 | // 169 | // 170 | // The following HTTP JSON to RPC mapping is enabled: 171 | // 172 | // HTTP | gRPC 173 | // -----|----- 174 | // `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: 175 | // "123456" text: "Hi!")` 176 | // 177 | // Note that when using `*` in the body mapping, it is not possible to 178 | // have HTTP parameters, as all fields not bound by the path end in 179 | // the body. This makes this option more rarely used in practice when 180 | // defining REST APIs. The common usage of `*` is in custom methods 181 | // which don't use the URL at all for transferring data. 182 | // 183 | // It is possible to define multiple HTTP methods for one RPC by using 184 | // the `additional_bindings` option. Example: 185 | // 186 | // service Messaging { 187 | // rpc GetMessage(GetMessageRequest) returns (Message) { 188 | // option (google.api.http) = { 189 | // get: "/v1/messages/{message_id}" 190 | // additional_bindings { 191 | // get: "/v1/users/{user_id}/messages/{message_id}" 192 | // } 193 | // }; 194 | // } 195 | // } 196 | // message GetMessageRequest { 197 | // string message_id = 1; 198 | // string user_id = 2; 199 | // } 200 | // 201 | // This enables the following two alternative HTTP JSON to RPC mappings: 202 | // 203 | // HTTP | gRPC 204 | // -----|----- 205 | // `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` 206 | // `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: 207 | // "123456")` 208 | // 209 | // ## Rules for HTTP mapping 210 | // 211 | // 1. Leaf request fields (recursive expansion nested messages in the request 212 | // message) are classified into three categories: 213 | // - Fields referred by the path template. They are passed via the URL path. 214 | // - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP 215 | // request body. 216 | // - All other fields are passed via the URL query parameters, and the 217 | // parameter name is the field path in the request message. A repeated 218 | // field can be represented as multiple query parameters under the same 219 | // name. 220 | // 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields 221 | // are passed via URL path and HTTP request body. 222 | // 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all 223 | // fields are passed via URL path and URL query parameters. 224 | // 225 | // ### Path template syntax 226 | // 227 | // Template = "/" Segments [ Verb ] ; 228 | // Segments = Segment { "/" Segment } ; 229 | // Segment = "*" | "**" | LITERAL | Variable ; 230 | // Variable = "{" FieldPath [ "=" Segments ] "}" ; 231 | // FieldPath = IDENT { "." IDENT } ; 232 | // Verb = ":" LITERAL ; 233 | // 234 | // The syntax `*` matches a single URL path segment. The syntax `**` matches 235 | // zero or more URL path segments, which must be the last part of the URL path 236 | // except the `Verb`. 237 | // 238 | // The syntax `Variable` matches part of the URL path as specified by its 239 | // template. A variable template must not contain other variables. If a variable 240 | // matches a single path segment, its template may be omitted, e.g. `{var}` 241 | // is equivalent to `{var=*}`. 242 | // 243 | // The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` 244 | // contains any reserved character, such characters should be percent-encoded 245 | // before the matching. 246 | // 247 | // If a variable contains exactly one path segment, such as `"{var}"` or 248 | // `"{var=*}"`, when such a variable is expanded into a URL path on the client 249 | // side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The 250 | // server side does the reverse decoding. Such variables show up in the 251 | // [Discovery 252 | // Document](https://developers.google.com/discovery/v1/reference/apis) as 253 | // `{var}`. 254 | // 255 | // If a variable contains multiple path segments, such as `"{var=foo/*}"` 256 | // or `"{var=**}"`, when such a variable is expanded into a URL path on the 257 | // client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. 258 | // The server side does the reverse decoding, except "%2F" and "%2f" are left 259 | // unchanged. Such variables show up in the 260 | // [Discovery 261 | // Document](https://developers.google.com/discovery/v1/reference/apis) as 262 | // `{+var}`. 263 | // 264 | // ## Using gRPC API Service Configuration 265 | // 266 | // gRPC API Service Configuration (service config) is a configuration language 267 | // for configuring a gRPC service to become a user-facing product. The 268 | // service config is simply the YAML representation of the `google.api.Service` 269 | // proto message. 270 | // 271 | // As an alternative to annotating your proto file, you can configure gRPC 272 | // transcoding in your service config YAML files. You do this by specifying a 273 | // `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same 274 | // effect as the proto annotation. This can be particularly useful if you 275 | // have a proto that is reused in multiple services. Note that any transcoding 276 | // specified in the service config will override any matching transcoding 277 | // configuration in the proto. 278 | // 279 | // Example: 280 | // 281 | // http: 282 | // rules: 283 | // # Selects a gRPC method and applies HttpRule to it. 284 | // - selector: example.v1.Messaging.GetMessage 285 | // get: /v1/messages/{message_id}/{sub.subfield} 286 | // 287 | // ## Special notes 288 | // 289 | // When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the 290 | // proto to JSON conversion must follow the [proto3 291 | // specification](https://developers.google.com/protocol-buffers/docs/proto3#json). 292 | // 293 | // While the single segment variable follows the semantics of 294 | // [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String 295 | // Expansion, the multi segment variable **does not** follow RFC 6570 Section 296 | // 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion 297 | // does not expand special characters like `?` and `#`, which would lead 298 | // to invalid URLs. As the result, gRPC Transcoding uses a custom encoding 299 | // for multi segment variables. 300 | // 301 | // The path variables **must not** refer to any repeated or mapped field, 302 | // because client libraries are not capable of handling such variable expansion. 303 | // 304 | // The path variables **must not** capture the leading "/" character. The reason 305 | // is that the most common use case "{var}" does not capture the leading "/" 306 | // character. For consistency, all path variables must share the same behavior. 307 | // 308 | // Repeated message fields must not be mapped to URL query parameters, because 309 | // no client library can support such complicated mapping. 310 | // 311 | // If an API needs to use a JSON array for request or response body, it can map 312 | // the request or response body to a repeated field. However, some gRPC 313 | // Transcoding implementations may not support this feature. 314 | message HttpRule { 315 | // Selects a method to which this rule applies. 316 | // 317 | // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. 318 | string selector = 1; 319 | 320 | // Determines the URL pattern is matched by this rules. This pattern can be 321 | // used with any of the {get|put|post|delete|patch} methods. A custom method 322 | // can be defined using the 'custom' field. 323 | oneof pattern { 324 | // Maps to HTTP GET. Used for listing and getting information about 325 | // resources. 326 | string get = 2; 327 | 328 | // Maps to HTTP PUT. Used for replacing a resource. 329 | string put = 3; 330 | 331 | // Maps to HTTP POST. Used for creating a resource or performing an action. 332 | string post = 4; 333 | 334 | // Maps to HTTP DELETE. Used for deleting a resource. 335 | string delete = 5; 336 | 337 | // Maps to HTTP PATCH. Used for updating a resource. 338 | string patch = 6; 339 | 340 | // The custom pattern is used for specifying an HTTP method that is not 341 | // included in the `pattern` field, such as HEAD, or "*" to leave the 342 | // HTTP method unspecified for this rule. The wild-card rule is useful 343 | // for services that provide content to Web (HTML) clients. 344 | CustomHttpPattern custom = 8; 345 | } 346 | 347 | // The name of the request field whose value is mapped to the HTTP request 348 | // body, or `*` for mapping all request fields not captured by the path 349 | // pattern to the HTTP body, or omitted for not having any HTTP request body. 350 | // 351 | // NOTE: the referred field must be present at the top-level of the request 352 | // message type. 353 | string body = 7; 354 | 355 | // Optional. The name of the response field whose value is mapped to the HTTP 356 | // response body. When omitted, the entire response message will be used 357 | // as the HTTP response body. 358 | // 359 | // NOTE: The referred field must be present at the top-level of the response 360 | // message type. 361 | string response_body = 12; 362 | 363 | // Additional HTTP bindings for the selector. Nested bindings must 364 | // not contain an `additional_bindings` field themselves (that is, 365 | // the nesting may only be one level deep). 366 | repeated HttpRule additional_bindings = 11; 367 | } 368 | 369 | // A custom pattern is used for defining custom HTTP verb. 370 | message CustomHttpPattern { 371 | // The name of this custom HTTP verb. 372 | string kind = 1; 373 | 374 | // The path matched by this custom verb. 375 | string path = 2; 376 | } 377 | -------------------------------------------------------------------------------- /integration_tests/google/protobuf/any.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option go_package = "github.com/golang/protobuf/ptypes/any"; 37 | option java_package = "com.google.protobuf"; 38 | option java_outer_classname = "AnyProto"; 39 | option java_multiple_files = true; 40 | option objc_class_prefix = "GPB"; 41 | 42 | // `Any` contains an arbitrary serialized protocol buffer message along with a 43 | // URL that describes the type of the serialized message. 44 | // 45 | // Protobuf library provides support to pack/unpack Any values in the form 46 | // of utility functions or additional generated methods of the Any type. 47 | // 48 | // Example 1: Pack and unpack a message in C++. 49 | // 50 | // Foo foo = ...; 51 | // Any any; 52 | // any.PackFrom(foo); 53 | // ... 54 | // if (any.UnpackTo(&foo)) { 55 | // ... 56 | // } 57 | // 58 | // Example 2: Pack and unpack a message in Java. 59 | // 60 | // Foo foo = ...; 61 | // Any any = Any.pack(foo); 62 | // ... 63 | // if (any.is(Foo.class)) { 64 | // foo = any.unpack(Foo.class); 65 | // } 66 | // 67 | // Example 3: Pack and unpack a message in Python. 68 | // 69 | // foo = Foo(...) 70 | // any = Any() 71 | // any.Pack(foo) 72 | // ... 73 | // if any.Is(Foo.DESCRIPTOR): 74 | // any.Unpack(foo) 75 | // ... 76 | // 77 | // Example 4: Pack and unpack a message in Go 78 | // 79 | // foo := &pb.Foo{...} 80 | // any, err := ptypes.MarshalAny(foo) 81 | // ... 82 | // foo := &pb.Foo{} 83 | // if err := ptypes.UnmarshalAny(any, foo); err != nil { 84 | // ... 85 | // } 86 | // 87 | // The pack methods provided by protobuf library will by default use 88 | // 'type.googleapis.com/full.type.name' as the type URL and the unpack 89 | // methods only use the fully qualified type name after the last '/' 90 | // in the type URL, for example "foo.bar.com/x/y.z" will yield type 91 | // name "y.z". 92 | // 93 | // 94 | // JSON 95 | // ==== 96 | // The JSON representation of an `Any` value uses the regular 97 | // representation of the deserialized, embedded message, with an 98 | // additional field `@type` which contains the type URL. Example: 99 | // 100 | // package google.profile; 101 | // message Person { 102 | // string first_name = 1; 103 | // string last_name = 2; 104 | // } 105 | // 106 | // { 107 | // "@type": "type.googleapis.com/google.profile.Person", 108 | // "firstName": , 109 | // "lastName": 110 | // } 111 | // 112 | // If the embedded message type is well-known and has a custom JSON 113 | // representation, that representation will be embedded adding a field 114 | // `value` which holds the custom JSON in addition to the `@type` 115 | // field. Example (for message [google.protobuf.Duration][]): 116 | // 117 | // { 118 | // "@type": "type.googleapis.com/google.protobuf.Duration", 119 | // "value": "1.212s" 120 | // } 121 | // 122 | message Any { 123 | // A URL/resource name that uniquely identifies the type of the serialized 124 | // protocol buffer message. This string must contain at least 125 | // one "/" character. The last segment of the URL's path must represent 126 | // the fully qualified name of the type (as in 127 | // `path/google.protobuf.Duration`). The name should be in a canonical form 128 | // (e.g., leading "." is not accepted). 129 | // 130 | // In practice, teams usually precompile into the binary all types that they 131 | // expect it to use in the context of Any. However, for URLs which use the 132 | // scheme `http`, `https`, or no scheme, one can optionally set up a type 133 | // server that maps type URLs to message definitions as follows: 134 | // 135 | // * If no scheme is provided, `https` is assumed. 136 | // * An HTTP GET on the URL must yield a [google.protobuf.Type][] 137 | // value in binary format, or produce an error. 138 | // * Applications are allowed to cache lookup results based on the 139 | // URL, or have them precompiled into a binary to avoid any 140 | // lookup. Therefore, binary compatibility needs to be preserved 141 | // on changes to types. (Use versioned type names to manage 142 | // breaking changes.) 143 | // 144 | // Note: this functionality is not currently available in the official 145 | // protobuf release, and it is not used for type URLs beginning with 146 | // type.googleapis.com. 147 | // 148 | // Schemes other than `http`, `https` (or the empty scheme) might be 149 | // used with implementation specific semantics. 150 | // 151 | string type_url = 1; 152 | 153 | // Must be a valid serialized protocol buffer of the above specified type. 154 | bytes value = 2; 155 | } 156 | -------------------------------------------------------------------------------- /integration_tests/google/protobuf/descriptor.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | // Author: kenton@google.com (Kenton Varda) 32 | // Based on original Protocol Buffers design by 33 | // Sanjay Ghemawat, Jeff Dean, and others. 34 | // 35 | // The messages in this file describe the definitions found in .proto files. 36 | // A valid .proto file can be translated directly to a FileDescriptorProto 37 | // without any other information (e.g. without reading its imports). 38 | 39 | 40 | syntax = "proto2"; 41 | 42 | package google.protobuf; 43 | 44 | option go_package = "github.com/golang/protobuf/protoc-gen-go/descriptor;descriptor"; 45 | option java_package = "com.google.protobuf"; 46 | option java_outer_classname = "DescriptorProtos"; 47 | option csharp_namespace = "Google.Protobuf.Reflection"; 48 | option objc_class_prefix = "GPB"; 49 | option cc_enable_arenas = true; 50 | 51 | // descriptor.proto must be optimized for speed because reflection-based 52 | // algorithms don't work during bootstrapping. 53 | option optimize_for = SPEED; 54 | 55 | // The protocol compiler can output a FileDescriptorSet containing the .proto 56 | // files it parses. 57 | message FileDescriptorSet { 58 | repeated FileDescriptorProto file = 1; 59 | } 60 | 61 | // Describes a complete .proto file. 62 | message FileDescriptorProto { 63 | optional string name = 1; // file name, relative to root of source tree 64 | optional string package = 2; // e.g. "foo", "foo.bar", etc. 65 | 66 | // Names of files imported by this file. 67 | repeated string dependency = 3; 68 | // Indexes of the public imported files in the dependency list above. 69 | repeated int32 public_dependency = 10; 70 | // Indexes of the weak imported files in the dependency list. 71 | // For Google-internal migration only. Do not use. 72 | repeated int32 weak_dependency = 11; 73 | 74 | // All top-level definitions in this file. 75 | repeated DescriptorProto message_type = 4; 76 | repeated EnumDescriptorProto enum_type = 5; 77 | repeated ServiceDescriptorProto service = 6; 78 | repeated FieldDescriptorProto extension = 7; 79 | 80 | optional FileOptions options = 8; 81 | 82 | // This field contains optional information about the original source code. 83 | // You may safely remove this entire field without harming runtime 84 | // functionality of the descriptors -- the information is needed only by 85 | // development tools. 86 | optional SourceCodeInfo source_code_info = 9; 87 | 88 | // The syntax of the proto file. 89 | // The supported values are "proto2" and "proto3". 90 | optional string syntax = 12; 91 | } 92 | 93 | // Describes a message type. 94 | message DescriptorProto { 95 | optional string name = 1; 96 | 97 | repeated FieldDescriptorProto field = 2; 98 | repeated FieldDescriptorProto extension = 6; 99 | 100 | repeated DescriptorProto nested_type = 3; 101 | repeated EnumDescriptorProto enum_type = 4; 102 | 103 | message ExtensionRange { 104 | optional int32 start = 1; // Inclusive. 105 | optional int32 end = 2; // Exclusive. 106 | 107 | optional ExtensionRangeOptions options = 3; 108 | } 109 | repeated ExtensionRange extension_range = 5; 110 | 111 | repeated OneofDescriptorProto oneof_decl = 8; 112 | 113 | optional MessageOptions options = 7; 114 | 115 | // Range of reserved tag numbers. Reserved tag numbers may not be used by 116 | // fields or extension ranges in the same message. Reserved ranges may 117 | // not overlap. 118 | message ReservedRange { 119 | optional int32 start = 1; // Inclusive. 120 | optional int32 end = 2; // Exclusive. 121 | } 122 | repeated ReservedRange reserved_range = 9; 123 | // Reserved field names, which may not be used by fields in the same message. 124 | // A given name may only be reserved once. 125 | repeated string reserved_name = 10; 126 | } 127 | 128 | message ExtensionRangeOptions { 129 | // The parser stores options it doesn't recognize here. See above. 130 | repeated UninterpretedOption uninterpreted_option = 999; 131 | 132 | // Clients can define custom options in extensions of this message. See above. 133 | extensions 1000 to max; 134 | } 135 | 136 | // Describes a field within a message. 137 | message FieldDescriptorProto { 138 | enum Type { 139 | // 0 is reserved for errors. 140 | // Order is weird for historical reasons. 141 | TYPE_DOUBLE = 1; 142 | TYPE_FLOAT = 2; 143 | // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if 144 | // negative values are likely. 145 | TYPE_INT64 = 3; 146 | TYPE_UINT64 = 4; 147 | // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if 148 | // negative values are likely. 149 | TYPE_INT32 = 5; 150 | TYPE_FIXED64 = 6; 151 | TYPE_FIXED32 = 7; 152 | TYPE_BOOL = 8; 153 | TYPE_STRING = 9; 154 | // Tag-delimited aggregate. 155 | // Group type is deprecated and not supported in proto3. However, Proto3 156 | // implementations should still be able to parse the group wire format and 157 | // treat group fields as unknown fields. 158 | TYPE_GROUP = 10; 159 | TYPE_MESSAGE = 11; // Length-delimited aggregate. 160 | 161 | // New in version 2. 162 | TYPE_BYTES = 12; 163 | TYPE_UINT32 = 13; 164 | TYPE_ENUM = 14; 165 | TYPE_SFIXED32 = 15; 166 | TYPE_SFIXED64 = 16; 167 | TYPE_SINT32 = 17; // Uses ZigZag encoding. 168 | TYPE_SINT64 = 18; // Uses ZigZag encoding. 169 | } 170 | 171 | enum Label { 172 | // 0 is reserved for errors 173 | LABEL_OPTIONAL = 1; 174 | LABEL_REQUIRED = 2; 175 | LABEL_REPEATED = 3; 176 | } 177 | 178 | optional string name = 1; 179 | optional int32 number = 3; 180 | optional Label label = 4; 181 | 182 | // If type_name is set, this need not be set. If both this and type_name 183 | // are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. 184 | optional Type type = 5; 185 | 186 | // For message and enum types, this is the name of the type. If the name 187 | // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping 188 | // rules are used to find the type (i.e. first the nested types within this 189 | // message are searched, then within the parent, on up to the root 190 | // namespace). 191 | optional string type_name = 6; 192 | 193 | // For extensions, this is the name of the type being extended. It is 194 | // resolved in the same manner as type_name. 195 | optional string extendee = 2; 196 | 197 | // For numeric types, contains the original text representation of the value. 198 | // For booleans, "true" or "false". 199 | // For strings, contains the default text contents (not escaped in any way). 200 | // For bytes, contains the C escaped value. All bytes >= 128 are escaped. 201 | // TODO(kenton): Base-64 encode? 202 | optional string default_value = 7; 203 | 204 | // If set, gives the index of a oneof in the containing type's oneof_decl 205 | // list. This field is a member of that oneof. 206 | optional int32 oneof_index = 9; 207 | 208 | // JSON name of this field. The value is set by protocol compiler. If the 209 | // user has set a "json_name" option on this field, that option's value 210 | // will be used. Otherwise, it's deduced from the field's name by converting 211 | // it to camelCase. 212 | optional string json_name = 10; 213 | 214 | optional FieldOptions options = 8; 215 | } 216 | 217 | // Describes a oneof. 218 | message OneofDescriptorProto { 219 | optional string name = 1; 220 | optional OneofOptions options = 2; 221 | } 222 | 223 | // Describes an enum type. 224 | message EnumDescriptorProto { 225 | optional string name = 1; 226 | 227 | repeated EnumValueDescriptorProto value = 2; 228 | 229 | optional EnumOptions options = 3; 230 | 231 | // Range of reserved numeric values. Reserved values may not be used by 232 | // entries in the same enum. Reserved ranges may not overlap. 233 | // 234 | // Note that this is distinct from DescriptorProto.ReservedRange in that it 235 | // is inclusive such that it can appropriately represent the entire int32 236 | // domain. 237 | message EnumReservedRange { 238 | optional int32 start = 1; // Inclusive. 239 | optional int32 end = 2; // Inclusive. 240 | } 241 | 242 | // Range of reserved numeric values. Reserved numeric values may not be used 243 | // by enum values in the same enum declaration. Reserved ranges may not 244 | // overlap. 245 | repeated EnumReservedRange reserved_range = 4; 246 | 247 | // Reserved enum value names, which may not be reused. A given name may only 248 | // be reserved once. 249 | repeated string reserved_name = 5; 250 | } 251 | 252 | // Describes a value within an enum. 253 | message EnumValueDescriptorProto { 254 | optional string name = 1; 255 | optional int32 number = 2; 256 | 257 | optional EnumValueOptions options = 3; 258 | } 259 | 260 | // Describes a service. 261 | message ServiceDescriptorProto { 262 | optional string name = 1; 263 | repeated MethodDescriptorProto method = 2; 264 | 265 | optional ServiceOptions options = 3; 266 | } 267 | 268 | // Describes a method of a service. 269 | message MethodDescriptorProto { 270 | optional string name = 1; 271 | 272 | // Input and output type names. These are resolved in the same way as 273 | // FieldDescriptorProto.type_name, but must refer to a message type. 274 | optional string input_type = 2; 275 | optional string output_type = 3; 276 | 277 | optional MethodOptions options = 4; 278 | 279 | // Identifies if client streams multiple client messages 280 | optional bool client_streaming = 5 [default = false]; 281 | // Identifies if server streams multiple server messages 282 | optional bool server_streaming = 6 [default = false]; 283 | } 284 | 285 | 286 | // =================================================================== 287 | // Options 288 | 289 | // Each of the definitions above may have "options" attached. These are 290 | // just annotations which may cause code to be generated slightly differently 291 | // or may contain hints for code that manipulates protocol messages. 292 | // 293 | // Clients may define custom options as extensions of the *Options messages. 294 | // These extensions may not yet be known at parsing time, so the parser cannot 295 | // store the values in them. Instead it stores them in a field in the *Options 296 | // message called uninterpreted_option. This field must have the same name 297 | // across all *Options messages. We then use this field to populate the 298 | // extensions when we build a descriptor, at which point all protos have been 299 | // parsed and so all extensions are known. 300 | // 301 | // Extension numbers for custom options may be chosen as follows: 302 | // * For options which will only be used within a single application or 303 | // organization, or for experimental options, use field numbers 50000 304 | // through 99999. It is up to you to ensure that you do not use the 305 | // same number for multiple options. 306 | // * For options which will be published and used publicly by multiple 307 | // independent entities, e-mail protobuf-global-extension-registry@google.com 308 | // to reserve extension numbers. Simply provide your project name (e.g. 309 | // Objective-C plugin) and your project website (if available) -- there's no 310 | // need to explain how you intend to use them. Usually you only need one 311 | // extension number. You can declare multiple options with only one extension 312 | // number by putting them in a sub-message. See the Custom Options section of 313 | // the docs for examples: 314 | // https://developers.google.com/protocol-buffers/docs/proto#options 315 | // If this turns out to be popular, a web service will be set up 316 | // to automatically assign option numbers. 317 | 318 | message FileOptions { 319 | 320 | // Sets the Java package where classes generated from this .proto will be 321 | // placed. By default, the proto package is used, but this is often 322 | // inappropriate because proto packages do not normally start with backwards 323 | // domain names. 324 | optional string java_package = 1; 325 | 326 | 327 | // If set, all the classes from the .proto file are wrapped in a single 328 | // outer class with the given name. This applies to both Proto1 329 | // (equivalent to the old "--one_java_file" option) and Proto2 (where 330 | // a .proto always translates to a single class, but you may want to 331 | // explicitly choose the class name). 332 | optional string java_outer_classname = 8; 333 | 334 | // If set true, then the Java code generator will generate a separate .java 335 | // file for each top-level message, enum, and service defined in the .proto 336 | // file. Thus, these types will *not* be nested inside the outer class 337 | // named by java_outer_classname. However, the outer class will still be 338 | // generated to contain the file's getDescriptor() method as well as any 339 | // top-level extensions defined in the file. 340 | optional bool java_multiple_files = 10 [default = false]; 341 | 342 | // This option does nothing. 343 | optional bool java_generate_equals_and_hash = 20 [deprecated=true]; 344 | 345 | // If set true, then the Java2 code generator will generate code that 346 | // throws an exception whenever an attempt is made to assign a non-UTF-8 347 | // byte sequence to a string field. 348 | // Message reflection will do the same. 349 | // However, an extension field still accepts non-UTF-8 byte sequences. 350 | // This option has no effect on when used with the lite runtime. 351 | optional bool java_string_check_utf8 = 27 [default = false]; 352 | 353 | 354 | // Generated classes can be optimized for speed or code size. 355 | enum OptimizeMode { 356 | SPEED = 1; // Generate complete code for parsing, serialization, 357 | // etc. 358 | CODE_SIZE = 2; // Use ReflectionOps to implement these methods. 359 | LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime. 360 | } 361 | optional OptimizeMode optimize_for = 9 [default = SPEED]; 362 | 363 | // Sets the Go package where structs generated from this .proto will be 364 | // placed. If omitted, the Go package will be derived from the following: 365 | // - The basename of the package import path, if provided. 366 | // - Otherwise, the package statement in the .proto file, if present. 367 | // - Otherwise, the basename of the .proto file, without extension. 368 | optional string go_package = 11; 369 | 370 | 371 | 372 | 373 | // Should generic services be generated in each language? "Generic" services 374 | // are not specific to any particular RPC system. They are generated by the 375 | // main code generators in each language (without additional plugins). 376 | // Generic services were the only kind of service generation supported by 377 | // early versions of google.protobuf. 378 | // 379 | // Generic services are now considered deprecated in favor of using plugins 380 | // that generate code specific to your particular RPC system. Therefore, 381 | // these default to false. Old code which depends on generic services should 382 | // explicitly set them to true. 383 | optional bool cc_generic_services = 16 [default = false]; 384 | optional bool java_generic_services = 17 [default = false]; 385 | optional bool py_generic_services = 18 [default = false]; 386 | optional bool php_generic_services = 42 [default = false]; 387 | 388 | // Is this file deprecated? 389 | // Depending on the target platform, this can emit Deprecated annotations 390 | // for everything in the file, or it will be completely ignored; in the very 391 | // least, this is a formalization for deprecating files. 392 | optional bool deprecated = 23 [default = false]; 393 | 394 | // Enables the use of arenas for the proto messages in this file. This applies 395 | // only to generated classes for C++. 396 | optional bool cc_enable_arenas = 31 [default = false]; 397 | 398 | 399 | // Sets the objective c class prefix which is prepended to all objective c 400 | // generated classes from this .proto. There is no default. 401 | optional string objc_class_prefix = 36; 402 | 403 | // Namespace for generated classes; defaults to the package. 404 | optional string csharp_namespace = 37; 405 | 406 | // By default Swift generators will take the proto package and CamelCase it 407 | // replacing '.' with underscore and use that to prefix the types/symbols 408 | // defined. When this options is provided, they will use this value instead 409 | // to prefix the types/symbols defined. 410 | optional string swift_prefix = 39; 411 | 412 | // Sets the php class prefix which is prepended to all php generated classes 413 | // from this .proto. Default is empty. 414 | optional string php_class_prefix = 40; 415 | 416 | // Use this option to change the namespace of php generated classes. Default 417 | // is empty. When this option is empty, the package name will be used for 418 | // determining the namespace. 419 | optional string php_namespace = 41; 420 | 421 | // Use this option to change the namespace of php generated metadata classes. 422 | // Default is empty. When this option is empty, the proto file name will be 423 | // used for determining the namespace. 424 | optional string php_metadata_namespace = 44; 425 | 426 | // Use this option to change the package of ruby generated classes. Default 427 | // is empty. When this option is not set, the package name will be used for 428 | // determining the ruby package. 429 | optional string ruby_package = 45; 430 | 431 | 432 | // The parser stores options it doesn't recognize here. 433 | // See the documentation for the "Options" section above. 434 | repeated UninterpretedOption uninterpreted_option = 999; 435 | 436 | // Clients can define custom options in extensions of this message. 437 | // See the documentation for the "Options" section above. 438 | extensions 1000 to max; 439 | 440 | reserved 38; 441 | } 442 | 443 | message MessageOptions { 444 | // Set true to use the old proto1 MessageSet wire format for extensions. 445 | // This is provided for backwards-compatibility with the MessageSet wire 446 | // format. You should not use this for any other reason: It's less 447 | // efficient, has fewer features, and is more complicated. 448 | // 449 | // The message must be defined exactly as follows: 450 | // message Foo { 451 | // option message_set_wire_format = true; 452 | // extensions 4 to max; 453 | // } 454 | // Note that the message cannot have any defined fields; MessageSets only 455 | // have extensions. 456 | // 457 | // All extensions of your type must be singular messages; e.g. they cannot 458 | // be int32s, enums, or repeated messages. 459 | // 460 | // Because this is an option, the above two restrictions are not enforced by 461 | // the protocol compiler. 462 | optional bool message_set_wire_format = 1 [default = false]; 463 | 464 | // Disables the generation of the standard "descriptor()" accessor, which can 465 | // conflict with a field of the same name. This is meant to make migration 466 | // from proto1 easier; new code should avoid fields named "descriptor". 467 | optional bool no_standard_descriptor_accessor = 2 [default = false]; 468 | 469 | // Is this message deprecated? 470 | // Depending on the target platform, this can emit Deprecated annotations 471 | // for the message, or it will be completely ignored; in the very least, 472 | // this is a formalization for deprecating messages. 473 | optional bool deprecated = 3 [default = false]; 474 | 475 | // Whether the message is an automatically generated map entry type for the 476 | // maps field. 477 | // 478 | // For maps fields: 479 | // map map_field = 1; 480 | // The parsed descriptor looks like: 481 | // message MapFieldEntry { 482 | // option map_entry = true; 483 | // optional KeyType key = 1; 484 | // optional ValueType value = 2; 485 | // } 486 | // repeated MapFieldEntry map_field = 1; 487 | // 488 | // Implementations may choose not to generate the map_entry=true message, but 489 | // use a native map in the target language to hold the keys and values. 490 | // The reflection APIs in such implementations still need to work as 491 | // if the field is a repeated message field. 492 | // 493 | // NOTE: Do not set the option in .proto files. Always use the maps syntax 494 | // instead. The option should only be implicitly set by the proto compiler 495 | // parser. 496 | optional bool map_entry = 7; 497 | 498 | reserved 8; // javalite_serializable 499 | reserved 9; // javanano_as_lite 500 | 501 | 502 | // The parser stores options it doesn't recognize here. See above. 503 | repeated UninterpretedOption uninterpreted_option = 999; 504 | 505 | // Clients can define custom options in extensions of this message. See above. 506 | extensions 1000 to max; 507 | } 508 | 509 | message FieldOptions { 510 | // The ctype option instructs the C++ code generator to use a different 511 | // representation of the field than it normally would. See the specific 512 | // options below. This option is not yet implemented in the open source 513 | // release -- sorry, we'll try to include it in a future version! 514 | optional CType ctype = 1 [default = STRING]; 515 | enum CType { 516 | // Default mode. 517 | STRING = 0; 518 | 519 | CORD = 1; 520 | 521 | STRING_PIECE = 2; 522 | } 523 | // The packed option can be enabled for repeated primitive fields to enable 524 | // a more efficient representation on the wire. Rather than repeatedly 525 | // writing the tag and type for each element, the entire array is encoded as 526 | // a single length-delimited blob. In proto3, only explicit setting it to 527 | // false will avoid using packed encoding. 528 | optional bool packed = 2; 529 | 530 | // The jstype option determines the JavaScript type used for values of the 531 | // field. The option is permitted only for 64 bit integral and fixed types 532 | // (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING 533 | // is represented as JavaScript string, which avoids loss of precision that 534 | // can happen when a large value is converted to a floating point JavaScript. 535 | // Specifying JS_NUMBER for the jstype causes the generated JavaScript code to 536 | // use the JavaScript "number" type. The behavior of the default option 537 | // JS_NORMAL is implementation dependent. 538 | // 539 | // This option is an enum to permit additional types to be added, e.g. 540 | // goog.math.Integer. 541 | optional JSType jstype = 6 [default = JS_NORMAL]; 542 | enum JSType { 543 | // Use the default type. 544 | JS_NORMAL = 0; 545 | 546 | // Use JavaScript strings. 547 | JS_STRING = 1; 548 | 549 | // Use JavaScript numbers. 550 | JS_NUMBER = 2; 551 | } 552 | 553 | // Should this field be parsed lazily? Lazy applies only to message-type 554 | // fields. It means that when the outer message is initially parsed, the 555 | // inner message's contents will not be parsed but instead stored in encoded 556 | // form. The inner message will actually be parsed when it is first accessed. 557 | // 558 | // This is only a hint. Implementations are free to choose whether to use 559 | // eager or lazy parsing regardless of the value of this option. However, 560 | // setting this option true suggests that the protocol author believes that 561 | // using lazy parsing on this field is worth the additional bookkeeping 562 | // overhead typically needed to implement it. 563 | // 564 | // This option does not affect the public interface of any generated code; 565 | // all method signatures remain the same. Furthermore, thread-safety of the 566 | // interface is not affected by this option; const methods remain safe to 567 | // call from multiple threads concurrently, while non-const methods continue 568 | // to require exclusive access. 569 | // 570 | // 571 | // Note that implementations may choose not to check required fields within 572 | // a lazy sub-message. That is, calling IsInitialized() on the outer message 573 | // may return true even if the inner message has missing required fields. 574 | // This is necessary because otherwise the inner message would have to be 575 | // parsed in order to perform the check, defeating the purpose of lazy 576 | // parsing. An implementation which chooses not to check required fields 577 | // must be consistent about it. That is, for any particular sub-message, the 578 | // implementation must either *always* check its required fields, or *never* 579 | // check its required fields, regardless of whether or not the message has 580 | // been parsed. 581 | optional bool lazy = 5 [default = false]; 582 | 583 | // Is this field deprecated? 584 | // Depending on the target platform, this can emit Deprecated annotations 585 | // for accessors, or it will be completely ignored; in the very least, this 586 | // is a formalization for deprecating fields. 587 | optional bool deprecated = 3 [default = false]; 588 | 589 | // For Google-internal migration only. Do not use. 590 | optional bool weak = 10 [default = false]; 591 | 592 | 593 | // The parser stores options it doesn't recognize here. See above. 594 | repeated UninterpretedOption uninterpreted_option = 999; 595 | 596 | // Clients can define custom options in extensions of this message. See above. 597 | extensions 1000 to max; 598 | 599 | reserved 4; // removed jtype 600 | } 601 | 602 | message OneofOptions { 603 | // The parser stores options it doesn't recognize here. See above. 604 | repeated UninterpretedOption uninterpreted_option = 999; 605 | 606 | // Clients can define custom options in extensions of this message. See above. 607 | extensions 1000 to max; 608 | } 609 | 610 | message EnumOptions { 611 | 612 | // Set this option to true to allow mapping different tag names to the same 613 | // value. 614 | optional bool allow_alias = 2; 615 | 616 | // Is this enum deprecated? 617 | // Depending on the target platform, this can emit Deprecated annotations 618 | // for the enum, or it will be completely ignored; in the very least, this 619 | // is a formalization for deprecating enums. 620 | optional bool deprecated = 3 [default = false]; 621 | 622 | reserved 5; // javanano_as_lite 623 | 624 | // The parser stores options it doesn't recognize here. See above. 625 | repeated UninterpretedOption uninterpreted_option = 999; 626 | 627 | // Clients can define custom options in extensions of this message. See above. 628 | extensions 1000 to max; 629 | } 630 | 631 | message EnumValueOptions { 632 | // Is this enum value deprecated? 633 | // Depending on the target platform, this can emit Deprecated annotations 634 | // for the enum value, or it will be completely ignored; in the very least, 635 | // this is a formalization for deprecating enum values. 636 | optional bool deprecated = 1 [default = false]; 637 | 638 | // The parser stores options it doesn't recognize here. See above. 639 | repeated UninterpretedOption uninterpreted_option = 999; 640 | 641 | // Clients can define custom options in extensions of this message. See above. 642 | extensions 1000 to max; 643 | } 644 | 645 | message ServiceOptions { 646 | 647 | // Note: Field numbers 1 through 32 are reserved for Google's internal RPC 648 | // framework. We apologize for hoarding these numbers to ourselves, but 649 | // we were already using them long before we decided to release Protocol 650 | // Buffers. 651 | 652 | // Is this service deprecated? 653 | // Depending on the target platform, this can emit Deprecated annotations 654 | // for the service, or it will be completely ignored; in the very least, 655 | // this is a formalization for deprecating services. 656 | optional bool deprecated = 33 [default = false]; 657 | 658 | // The parser stores options it doesn't recognize here. See above. 659 | repeated UninterpretedOption uninterpreted_option = 999; 660 | 661 | // Clients can define custom options in extensions of this message. See above. 662 | extensions 1000 to max; 663 | } 664 | 665 | message MethodOptions { 666 | 667 | // Note: Field numbers 1 through 32 are reserved for Google's internal RPC 668 | // framework. We apologize for hoarding these numbers to ourselves, but 669 | // we were already using them long before we decided to release Protocol 670 | // Buffers. 671 | 672 | // Is this method deprecated? 673 | // Depending on the target platform, this can emit Deprecated annotations 674 | // for the method, or it will be completely ignored; in the very least, 675 | // this is a formalization for deprecating methods. 676 | optional bool deprecated = 33 [default = false]; 677 | 678 | // Is this method side-effect-free (or safe in HTTP parlance), or idempotent, 679 | // or neither? HTTP based RPC implementation may choose GET verb for safe 680 | // methods, and PUT verb for idempotent methods instead of the default POST. 681 | enum IdempotencyLevel { 682 | IDEMPOTENCY_UNKNOWN = 0; 683 | NO_SIDE_EFFECTS = 1; // implies idempotent 684 | IDEMPOTENT = 2; // idempotent, but may have side effects 685 | } 686 | optional IdempotencyLevel idempotency_level = 34 687 | [default = IDEMPOTENCY_UNKNOWN]; 688 | 689 | // The parser stores options it doesn't recognize here. See above. 690 | repeated UninterpretedOption uninterpreted_option = 999; 691 | 692 | // Clients can define custom options in extensions of this message. See above. 693 | extensions 1000 to max; 694 | } 695 | 696 | 697 | // A message representing a option the parser does not recognize. This only 698 | // appears in options protos created by the compiler::Parser class. 699 | // DescriptorPool resolves these when building Descriptor objects. Therefore, 700 | // options protos in descriptor objects (e.g. returned by Descriptor::options(), 701 | // or produced by Descriptor::CopyTo()) will never have UninterpretedOptions 702 | // in them. 703 | message UninterpretedOption { 704 | // The name of the uninterpreted option. Each string represents a segment in 705 | // a dot-separated name. is_extension is true iff a segment represents an 706 | // extension (denoted with parentheses in options specs in .proto files). 707 | // E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents 708 | // "foo.(bar.baz).qux". 709 | message NamePart { 710 | required string name_part = 1; 711 | required bool is_extension = 2; 712 | } 713 | repeated NamePart name = 2; 714 | 715 | // The value of the uninterpreted option, in whatever type the tokenizer 716 | // identified it as during parsing. Exactly one of these should be set. 717 | optional string identifier_value = 3; 718 | optional uint64 positive_int_value = 4; 719 | optional int64 negative_int_value = 5; 720 | optional double double_value = 6; 721 | optional bytes string_value = 7; 722 | optional string aggregate_value = 8; 723 | } 724 | 725 | // =================================================================== 726 | // Optional source code info 727 | 728 | // Encapsulates information about the original source file from which a 729 | // FileDescriptorProto was generated. 730 | message SourceCodeInfo { 731 | // A Location identifies a piece of source code in a .proto file which 732 | // corresponds to a particular definition. This information is intended 733 | // to be useful to IDEs, code indexers, documentation generators, and similar 734 | // tools. 735 | // 736 | // For example, say we have a file like: 737 | // message Foo { 738 | // optional string foo = 1; 739 | // } 740 | // Let's look at just the field definition: 741 | // optional string foo = 1; 742 | // ^ ^^ ^^ ^ ^^^ 743 | // a bc de f ghi 744 | // We have the following locations: 745 | // span path represents 746 | // [a,i) [ 4, 0, 2, 0 ] The whole field definition. 747 | // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). 748 | // [c,d) [ 4, 0, 2, 0, 5 ] The type (string). 749 | // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). 750 | // [g,h) [ 4, 0, 2, 0, 3 ] The number (1). 751 | // 752 | // Notes: 753 | // - A location may refer to a repeated field itself (i.e. not to any 754 | // particular index within it). This is used whenever a set of elements are 755 | // logically enclosed in a single code segment. For example, an entire 756 | // extend block (possibly containing multiple extension definitions) will 757 | // have an outer location whose path refers to the "extensions" repeated 758 | // field without an index. 759 | // - Multiple locations may have the same path. This happens when a single 760 | // logical declaration is spread out across multiple places. The most 761 | // obvious example is the "extend" block again -- there may be multiple 762 | // extend blocks in the same scope, each of which will have the same path. 763 | // - A location's span is not always a subset of its parent's span. For 764 | // example, the "extendee" of an extension declaration appears at the 765 | // beginning of the "extend" block and is shared by all extensions within 766 | // the block. 767 | // - Just because a location's span is a subset of some other location's span 768 | // does not mean that it is a descendant. For example, a "group" defines 769 | // both a type and a field in a single declaration. Thus, the locations 770 | // corresponding to the type and field and their components will overlap. 771 | // - Code which tries to interpret locations should probably be designed to 772 | // ignore those that it doesn't understand, as more types of locations could 773 | // be recorded in the future. 774 | repeated Location location = 1; 775 | message Location { 776 | // Identifies which part of the FileDescriptorProto was defined at this 777 | // location. 778 | // 779 | // Each element is a field number or an index. They form a path from 780 | // the root FileDescriptorProto to the place where the definition. For 781 | // example, this path: 782 | // [ 4, 3, 2, 7, 1 ] 783 | // refers to: 784 | // file.message_type(3) // 4, 3 785 | // .field(7) // 2, 7 786 | // .name() // 1 787 | // This is because FileDescriptorProto.message_type has field number 4: 788 | // repeated DescriptorProto message_type = 4; 789 | // and DescriptorProto.field has field number 2: 790 | // repeated FieldDescriptorProto field = 2; 791 | // and FieldDescriptorProto.name has field number 1: 792 | // optional string name = 1; 793 | // 794 | // Thus, the above path gives the location of a field name. If we removed 795 | // the last element: 796 | // [ 4, 3, 2, 7 ] 797 | // this path refers to the whole field declaration (from the beginning 798 | // of the label to the terminating semicolon). 799 | repeated int32 path = 1 [packed = true]; 800 | 801 | // Always has exactly three or four elements: start line, start column, 802 | // end line (optional, otherwise assumed same as start line), end column. 803 | // These are packed into a single field for efficiency. Note that line 804 | // and column numbers are zero-based -- typically you will want to add 805 | // 1 to each before displaying to a user. 806 | repeated int32 span = 2 [packed = true]; 807 | 808 | // If this SourceCodeInfo represents a complete declaration, these are any 809 | // comments appearing before and after the declaration which appear to be 810 | // attached to the declaration. 811 | // 812 | // A series of line comments appearing on consecutive lines, with no other 813 | // tokens appearing on those lines, will be treated as a single comment. 814 | // 815 | // leading_detached_comments will keep paragraphs of comments that appear 816 | // before (but not connected to) the current element. Each paragraph, 817 | // separated by empty lines, will be one comment element in the repeated 818 | // field. 819 | // 820 | // Only the comment content is provided; comment markers (e.g. //) are 821 | // stripped out. For block comments, leading whitespace and an asterisk 822 | // will be stripped from the beginning of each line other than the first. 823 | // Newlines are included in the output. 824 | // 825 | // Examples: 826 | // 827 | // optional int32 foo = 1; // Comment attached to foo. 828 | // // Comment attached to bar. 829 | // optional int32 bar = 2; 830 | // 831 | // optional string baz = 3; 832 | // // Comment attached to baz. 833 | // // Another line attached to baz. 834 | // 835 | // // Comment attached to qux. 836 | // // 837 | // // Another line attached to qux. 838 | // optional double qux = 4; 839 | // 840 | // // Detached comment for corge. This is not leading or trailing comments 841 | // // to qux or corge because there are blank lines separating it from 842 | // // both. 843 | // 844 | // // Detached comment for corge paragraph 2. 845 | // 846 | // optional string corge = 5; 847 | // /* Block comment attached 848 | // * to corge. Leading asterisks 849 | // * will be removed. */ 850 | // /* Block comment attached to 851 | // * grault. */ 852 | // optional int32 grault = 6; 853 | // 854 | // // ignored detached comments. 855 | optional string leading_comments = 3; 856 | optional string trailing_comments = 4; 857 | repeated string leading_detached_comments = 6; 858 | } 859 | } 860 | 861 | // Describes the relationship between generated code and its original source 862 | // file. A GeneratedCodeInfo message is associated with only one generated 863 | // source file, but may contain references to different source .proto files. 864 | message GeneratedCodeInfo { 865 | // An Annotation connects some span of text in generated code to an element 866 | // of its generating .proto file. 867 | repeated Annotation annotation = 1; 868 | message Annotation { 869 | // Identifies the element in the original source .proto file. This field 870 | // is formatted the same as SourceCodeInfo.Location.path. 871 | repeated int32 path = 1 [packed = true]; 872 | 873 | // Identifies the filesystem path to the original source .proto. 874 | optional string source_file = 2; 875 | 876 | // Identifies the starting offset in bytes in the generated code 877 | // that relates to the identified object. 878 | optional int32 begin = 3; 879 | 880 | // Identifies the ending offset in bytes in the generated code that 881 | // relates to the identified offset. The end offset should be one past 882 | // the last relevant byte (so the length of the text = end - begin). 883 | optional int32 end = 4; 884 | } 885 | } 886 | -------------------------------------------------------------------------------- /integration_tests/google/protobuf/empty.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option go_package = "google.golang.org/protobuf/types/known/emptypb"; 37 | option java_package = "com.google.protobuf"; 38 | option java_outer_classname = "EmptyProto"; 39 | option java_multiple_files = true; 40 | option objc_class_prefix = "GPB"; 41 | option cc_enable_arenas = true; 42 | 43 | // A generic empty message that you can re-use to avoid defining duplicated 44 | // empty messages in your APIs. A typical example is to use it as the request 45 | // or the response type of an API method. For instance: 46 | // 47 | // service Foo { 48 | // rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); 49 | // } 50 | // 51 | // The JSON representation for `Empty` is empty JSON object `{}`. 52 | message Empty {} 53 | 54 | import "protoc-gen-grpc-gateway-ts/options/ts_package.proto"; 55 | option (grpc.gateway.protoc_gen_grpc_gateway_ts.options.ts_package) = "google-protobuf/google/protobuf/empty_pb"; 56 | -------------------------------------------------------------------------------- /integration_tests/google/protobuf/struct.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option cc_enable_arenas = true; 37 | option go_package = "github.com/golang/protobuf/ptypes/struct;structpb"; 38 | option java_package = "com.google.protobuf"; 39 | option java_outer_classname = "StructProto"; 40 | option java_multiple_files = true; 41 | option objc_class_prefix = "GPB"; 42 | 43 | // `Struct` represents a structured data value, consisting of fields 44 | // which map to dynamically typed values. In some languages, `Struct` 45 | // might be supported by a native representation. For example, in 46 | // scripting languages like JS a struct is represented as an 47 | // object. The details of that representation are described together 48 | // with the proto support for the language. 49 | // 50 | // The JSON representation for `Struct` is JSON object. 51 | message Struct { 52 | // Unordered map of dynamically typed values. 53 | map fields = 1; 54 | } 55 | 56 | // `Value` represents a dynamically typed value which can be either 57 | // null, a number, a string, a boolean, a recursive struct value, or a 58 | // list of values. A producer of value is expected to set one of that 59 | // variants, absence of any variant indicates an error. 60 | // 61 | // The JSON representation for `Value` is JSON value. 62 | message Value { 63 | // The kind of value. 64 | oneof kind { 65 | // Represents a null value. 66 | NullValue null_value = 1; 67 | // Represents a double value. 68 | double number_value = 2; 69 | // Represents a string value. 70 | string string_value = 3; 71 | // Represents a boolean value. 72 | bool bool_value = 4; 73 | // Represents a structured value. 74 | Struct struct_value = 5; 75 | // Represents a repeated `Value`. 76 | ListValue list_value = 6; 77 | } 78 | } 79 | 80 | // `NullValue` is a singleton enumeration to represent the null value for the 81 | // `Value` type union. 82 | // 83 | // The JSON representation for `NullValue` is JSON `null`. 84 | enum NullValue { 85 | // Null value. 86 | NULL_VALUE = 0; 87 | } 88 | 89 | // `ListValue` is a wrapper around a repeated field of values. 90 | // 91 | // The JSON representation for `ListValue` is JSON array. 92 | message ListValue { 93 | // Repeated field of dynamically typed values. 94 | repeated Value values = 1; 95 | } 96 | -------------------------------------------------------------------------------- /integration_tests/google/protobuf/timestamp.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option cc_enable_arenas = true; 37 | option go_package = "github.com/golang/protobuf/ptypes/timestamp"; 38 | option java_package = "com.google.protobuf"; 39 | option java_outer_classname = "TimestampProto"; 40 | option java_multiple_files = true; 41 | option objc_class_prefix = "GPB"; 42 | 43 | // A Timestamp represents a point in time independent of any time zone or local 44 | // calendar, encoded as a count of seconds and fractions of seconds at 45 | // nanosecond resolution. The count is relative to an epoch at UTC midnight on 46 | // January 1, 1970, in the proleptic Gregorian calendar which extends the 47 | // Gregorian calendar backwards to year one. 48 | // 49 | // All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap 50 | // second table is needed for interpretation, using a [24-hour linear 51 | // smear](https://developers.google.com/time/smear). 52 | // 53 | // The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By 54 | // restricting to that range, we ensure that we can convert to and from [RFC 55 | // 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. 56 | // 57 | // # Examples 58 | // 59 | // Example 1: Compute Timestamp from POSIX `time()`. 60 | // 61 | // Timestamp timestamp; 62 | // timestamp.set_seconds(time(NULL)); 63 | // timestamp.set_nanos(0); 64 | // 65 | // Example 2: Compute Timestamp from POSIX `gettimeofday()`. 66 | // 67 | // struct timeval tv; 68 | // gettimeofday(&tv, NULL); 69 | // 70 | // Timestamp timestamp; 71 | // timestamp.set_seconds(tv.tv_sec); 72 | // timestamp.set_nanos(tv.tv_usec * 1000); 73 | // 74 | // Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. 75 | // 76 | // FILETIME ft; 77 | // GetSystemTimeAsFileTime(&ft); 78 | // UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; 79 | // 80 | // // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z 81 | // // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. 82 | // Timestamp timestamp; 83 | // timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); 84 | // timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); 85 | // 86 | // Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. 87 | // 88 | // long millis = System.currentTimeMillis(); 89 | // 90 | // Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) 91 | // .setNanos((int) ((millis % 1000) * 1000000)).build(); 92 | // 93 | // 94 | // Example 5: Compute Timestamp from current time in Python. 95 | // 96 | // timestamp = Timestamp() 97 | // timestamp.GetCurrentTime() 98 | // 99 | // # JSON Mapping 100 | // 101 | // In JSON format, the Timestamp type is encoded as a string in the 102 | // [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the 103 | // format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" 104 | // where {year} is always expressed using four digits while {month}, {day}, 105 | // {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional 106 | // seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), 107 | // are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone 108 | // is required. A proto3 JSON serializer should always use UTC (as indicated by 109 | // "Z") when printing the Timestamp type and a proto3 JSON parser should be 110 | // able to accept both UTC and other timezones (as indicated by an offset). 111 | // 112 | // For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past 113 | // 01:30 UTC on January 15, 2017. 114 | // 115 | // In JavaScript, one can convert a Date object to this format using the 116 | // standard 117 | // [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) 118 | // method. In Python, a standard `datetime.datetime` object can be converted 119 | // to this format using 120 | // [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with 121 | // the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use 122 | // the Joda Time's [`ISODateTimeFormat.dateTime()`]( 123 | // http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D 124 | // ) to obtain a formatter capable of generating timestamps in this format. 125 | // 126 | // 127 | message Timestamp { 128 | // Represents seconds of UTC time since Unix epoch 129 | // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to 130 | // 9999-12-31T23:59:59Z inclusive. 131 | int64 seconds = 1; 132 | 133 | // Non-negative fractions of a second at nanosecond resolution. Negative 134 | // second values with fractions must still have non-negative nanos values 135 | // that count forward in time. Must be from 0 to 999,999,999 136 | // inclusive. 137 | int32 nanos = 2; 138 | } 139 | -------------------------------------------------------------------------------- /integration_tests/google/rpc/status.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.rpc; 18 | 19 | import "google/protobuf/any.proto"; 20 | 21 | option cc_enable_arenas = true; 22 | option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; 23 | option java_multiple_files = true; 24 | option java_outer_classname = "StatusProto"; 25 | option java_package = "com.google.rpc"; 26 | option objc_class_prefix = "RPC"; 27 | 28 | // The `Status` type defines a logical error model that is suitable for 29 | // different programming environments, including REST APIs and RPC APIs. It is 30 | // used by [gRPC](https://github.com/grpc). Each `Status` message contains 31 | // three pieces of data: error code, error message, and error details. 32 | // 33 | // You can find out more about this error model and how to work with it in the 34 | // [API Design Guide](https://cloud.google.com/apis/design/errors). 35 | message Status { 36 | // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. 37 | int32 code = 1; 38 | 39 | // A developer-facing error message, which should be in English. Any 40 | // user-facing error message should be localized and sent in the 41 | // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. 42 | string message = 2; 43 | 44 | // A list of messages that carry the error details. There is a common set of 45 | // message types for APIs to use. 46 | repeated google.protobuf.Any details = 3; 47 | } 48 | -------------------------------------------------------------------------------- /integration_tests/integration_test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import camelCase from 'lodash.camelcase'; 3 | import { pathOr } from 'ramda'; 4 | import { CounterService } from "./service.pb"; 5 | import { b64Decode } from './fetch.pb'; 6 | 7 | function getFieldName(name: string) { 8 | const useCamelCase = pathOr(false, ['__karma__', 'config', 'useProtoNames'], window) === false 9 | return useCamelCase ? camelCase(name) : name 10 | } 11 | 12 | function getField(obj: {[key: string]: any}, name: string) { 13 | return obj[getFieldName(name)] 14 | } 15 | 16 | describe("test grpc-gateway-ts communication", () => { 17 | it("unary request", async () => { 18 | const result = await CounterService.Increment({ counter: 199 }, { pathPrefix: "http://localhost:8081" }) 19 | 20 | expect(result.result).to.equal(200) 21 | }) 22 | 23 | it("failing unary request", async () => { 24 | try { 25 | await CounterService.FailingIncrement({ counter: 199 }, { pathPrefix: "http://localhost:8081" }); 26 | expect.fail("expected call to throw"); 27 | } catch (e) { 28 | expect(e).to.have.property("message", "this increment does not work") 29 | expect(e).to.have.property("code", 14); 30 | } 31 | }) 32 | 33 | it('streaming request', async () => { 34 | const response = [] as number[] 35 | await CounterService.StreamingIncrements({ counter: 1 }, (resp) => response.push(resp.result), { pathPrefix: "http://localhost:8081" }) 36 | 37 | expect(response).to.deep.equal([2, 3, 4, 5, 6]) 38 | }) 39 | 40 | it('binary echo', async () => { 41 | const message = "→ ping"; 42 | 43 | const resp:any = await CounterService.EchoBinary({ 44 | data: new TextEncoder().encode(message), 45 | }, { pathPrefix: "http://localhost:8081" }) 46 | 47 | const bytes = b64Decode(resp["data"]) 48 | expect(new TextDecoder().decode(bytes)).to.equal(message) 49 | }) 50 | 51 | it('http get check request', async () => { 52 | const result = await CounterService.HTTPGet({ [getFieldName('num_to_increase')]: 10 }, { pathPrefix: "http://localhost:8081" }) 53 | expect(result.result).to.equal(11) 54 | }) 55 | 56 | it('http post body check request with nested body path', async () => { 57 | const result = await CounterService.HTTPPostWithNestedBodyPath({ a: 10, req: { b: 15 } }, { pathPrefix: "http://localhost:8081" }) 58 | expect(getField(result, 'post_result')).to.equal(25) 59 | }) 60 | 61 | it('http post body check request with star in path', async () => { 62 | const result = await CounterService.HTTPPostWithStarBodyPath({ a: 10, req: { b: 15 }, c: 23 }, { pathPrefix: "http://localhost:8081" }) 63 | expect(getField(result, 'post_result')).to.equal(48) 64 | }) 65 | 66 | it('able to communicate with external message reference without package defined', async () => { 67 | const result = await CounterService.ExternalMessage({ content: "hello" }, { pathPrefix: "http://localhost:8081" }) 68 | expect(getField(result, 'result')).to.equal("hello!!") 69 | }) 70 | 71 | it('http patch request with star in path', async () => { 72 | const result = await CounterService.HTTPPatch({ a: 10, c: 23 }, { pathPrefix: "http://localhost:8081" }) 73 | expect(getField(result, 'patch_result')).to.equal(33) 74 | }) 75 | 76 | it('http delete check request', async () => { 77 | const result = await CounterService.HTTPDelete({ a: 10 }, { pathPrefix: "http://localhost:8081" }) 78 | expect(result).to.be.empty 79 | }) 80 | 81 | it('http get request with url search parameters', async () => { 82 | const result = await CounterService.HTTPGetWithURLSearchParams({ a: 10, [getFieldName('post_req')]: { b: 0 }, c: [23, 25], [getFieldName('ext_msg')]: { d: 12 } }, { pathPrefix: "http://localhost:8081" }) 83 | expect(getField(result, 'url_search_params_result')).to.equal(70) 84 | }) 85 | 86 | it('http get request with zero value url search parameters', async () => { 87 | const result = await CounterService.HTTPGetWithZeroValueURLSearchParams({ a: "A", b: "", [getFieldName('zero_value_msg')]: { c: 1, d: [1, 0, 2], e: false } }, { pathPrefix: "http://localhost:8081" }) 88 | expect(result).to.deep.equal({ a: "A", b: "hello", [getFieldName('zero_value_msg')]: { c: 2, d: [2, 1, 3], e: true } }) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /integration_tests/karma.conf.ci.js: -------------------------------------------------------------------------------- 1 | const getConf = require('./karma.conf.common') 2 | module.exports = function(config) { 3 | config.set(getConf(true, config)) 4 | } 5 | -------------------------------------------------------------------------------- /integration_tests/karma.conf.common.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ci, config){ 2 | return { 3 | frameworks: ['mocha', 'chai', 'karma-typescript'], 4 | preprocessors: { 5 | "**/*.ts": ["karma-typescript"] 6 | }, 7 | files: ['**/*_test.ts', '**/*.pb.ts'], 8 | reporters: ['progress', 'karma-typescript'], 9 | port: 9876, // karma web server port 10 | colors: true, 11 | logLevel: config.LOG_INFO, 12 | browsers: ['ChromeHeadless'], 13 | autoWatch: false, 14 | singleRun: ci, // Karma captures browsers, runs the tests and exits 15 | concurrency: Infinity, 16 | karmaTypescriptConfig: { 17 | tsconfig: './tsconfig.json' 18 | }, 19 | client: { 20 | useProtoNames: process.env['USE_PROTO_NAMES'] === "true" 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /integration_tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | const getConf = require('./karma.conf.common') 2 | module.exports = function(config) { 3 | config.set(getConf(false, config)) 4 | } 5 | -------------------------------------------------------------------------------- /integration_tests/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "net" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/grpc-ecosystem/grpc-gateway/runtime" 11 | "google.golang.org/grpc" 12 | ) 13 | 14 | // preflightHandler adds the necessary headers in order to serve 15 | // CORS from any origin using the methods "GET", "HEAD", "POST", "PUT", "DELETE" 16 | // We insist, don't do this without consideration in production systems. 17 | func preflightHandler(w http.ResponseWriter, r *http.Request) { 18 | headers := []string{"Content-Type", "Accept", "Authorization"} 19 | w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ",")) 20 | methods := []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"} 21 | w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ",")) 22 | } 23 | 24 | // allowCORS allows Cross Origin Resoruce Sharing from any origin. 25 | // Don't do this without consideration in production systems. 26 | func allowCORS(h http.Handler) http.Handler { 27 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 | if origin := r.Header.Get("Origin"); origin != "" { 29 | w.Header().Set("Access-Control-Allow-Origin", origin) 30 | if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" { 31 | preflightHandler(w, r) 32 | return 33 | } 34 | } 35 | h.ServeHTTP(w, r) 36 | }) 37 | } 38 | 39 | func main() { 40 | origName := flag.Bool("orig", false, "tell server to use origin name in jsonpb") 41 | flag.Parse() 42 | ctx := context.Background() 43 | ctx, cancel := context.WithCancel(ctx) 44 | defer cancel() 45 | 46 | l, err := net.Listen("tcp4", "localhost:9000") 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | rcs := &RealCounterService{} 52 | s := grpc.NewServer() 53 | RegisterCounterServiceServer(s, rcs) 54 | 55 | go func() { 56 | defer s.GracefulStop() 57 | <-ctx.Done() 58 | }() 59 | 60 | go func() { 61 | err := s.Serve(l) 62 | if err != nil { 63 | panic(err) 64 | } 65 | }() 66 | 67 | // Register gRPC server endpoint 68 | // Note: Make sure the gRPC server is running properly and accessible 69 | mux := runtime.NewServeMux(runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{ 70 | OrigName: *origName, 71 | })) 72 | opts := []grpc.DialOption{grpc.WithInsecure()} 73 | err = RegisterCounterServiceHandlerFromEndpoint(ctx, mux, "localhost:9000", opts) 74 | if err != nil { 75 | panic(err) 76 | } 77 | 78 | err = http.ListenAndServe(":8081", allowCORS(mux)) 79 | if err != nil { 80 | panic(err) 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /integration_tests/msg.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.25.0 4 | // protoc v3.15.8 5 | // source: msg.proto 6 | 7 | package main 8 | 9 | import ( 10 | proto "github.com/golang/protobuf/proto" 11 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 12 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | // This is a compile-time assertion that a sufficiently up-to-date version 25 | // of the legacy proto package is being used. 26 | const _ = proto.ProtoPackageIsVersion4 27 | 28 | type ExternalMessage struct { 29 | state protoimpl.MessageState 30 | sizeCache protoimpl.SizeCache 31 | unknownFields protoimpl.UnknownFields 32 | 33 | D int32 `protobuf:"varint,1,opt,name=d,proto3" json:"d,omitempty"` 34 | } 35 | 36 | func (x *ExternalMessage) Reset() { 37 | *x = ExternalMessage{} 38 | if protoimpl.UnsafeEnabled { 39 | mi := &file_msg_proto_msgTypes[0] 40 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 41 | ms.StoreMessageInfo(mi) 42 | } 43 | } 44 | 45 | func (x *ExternalMessage) String() string { 46 | return protoimpl.X.MessageStringOf(x) 47 | } 48 | 49 | func (*ExternalMessage) ProtoMessage() {} 50 | 51 | func (x *ExternalMessage) ProtoReflect() protoreflect.Message { 52 | mi := &file_msg_proto_msgTypes[0] 53 | if protoimpl.UnsafeEnabled && x != nil { 54 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 55 | if ms.LoadMessageInfo() == nil { 56 | ms.StoreMessageInfo(mi) 57 | } 58 | return ms 59 | } 60 | return mi.MessageOf(x) 61 | } 62 | 63 | // Deprecated: Use ExternalMessage.ProtoReflect.Descriptor instead. 64 | func (*ExternalMessage) Descriptor() ([]byte, []int) { 65 | return file_msg_proto_rawDescGZIP(), []int{0} 66 | } 67 | 68 | func (x *ExternalMessage) GetD() int32 { 69 | if x != nil { 70 | return x.D 71 | } 72 | return 0 73 | } 74 | 75 | type ExternalRequest struct { 76 | state protoimpl.MessageState 77 | sizeCache protoimpl.SizeCache 78 | unknownFields protoimpl.UnknownFields 79 | 80 | Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` 81 | } 82 | 83 | func (x *ExternalRequest) Reset() { 84 | *x = ExternalRequest{} 85 | if protoimpl.UnsafeEnabled { 86 | mi := &file_msg_proto_msgTypes[1] 87 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 88 | ms.StoreMessageInfo(mi) 89 | } 90 | } 91 | 92 | func (x *ExternalRequest) String() string { 93 | return protoimpl.X.MessageStringOf(x) 94 | } 95 | 96 | func (*ExternalRequest) ProtoMessage() {} 97 | 98 | func (x *ExternalRequest) ProtoReflect() protoreflect.Message { 99 | mi := &file_msg_proto_msgTypes[1] 100 | if protoimpl.UnsafeEnabled && x != nil { 101 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 102 | if ms.LoadMessageInfo() == nil { 103 | ms.StoreMessageInfo(mi) 104 | } 105 | return ms 106 | } 107 | return mi.MessageOf(x) 108 | } 109 | 110 | // Deprecated: Use ExternalRequest.ProtoReflect.Descriptor instead. 111 | func (*ExternalRequest) Descriptor() ([]byte, []int) { 112 | return file_msg_proto_rawDescGZIP(), []int{1} 113 | } 114 | 115 | func (x *ExternalRequest) GetContent() string { 116 | if x != nil { 117 | return x.Content 118 | } 119 | return "" 120 | } 121 | 122 | type ExternalResponse struct { 123 | state protoimpl.MessageState 124 | sizeCache protoimpl.SizeCache 125 | unknownFields protoimpl.UnknownFields 126 | 127 | Result string `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"` 128 | } 129 | 130 | func (x *ExternalResponse) Reset() { 131 | *x = ExternalResponse{} 132 | if protoimpl.UnsafeEnabled { 133 | mi := &file_msg_proto_msgTypes[2] 134 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 135 | ms.StoreMessageInfo(mi) 136 | } 137 | } 138 | 139 | func (x *ExternalResponse) String() string { 140 | return protoimpl.X.MessageStringOf(x) 141 | } 142 | 143 | func (*ExternalResponse) ProtoMessage() {} 144 | 145 | func (x *ExternalResponse) ProtoReflect() protoreflect.Message { 146 | mi := &file_msg_proto_msgTypes[2] 147 | if protoimpl.UnsafeEnabled && x != nil { 148 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 149 | if ms.LoadMessageInfo() == nil { 150 | ms.StoreMessageInfo(mi) 151 | } 152 | return ms 153 | } 154 | return mi.MessageOf(x) 155 | } 156 | 157 | // Deprecated: Use ExternalResponse.ProtoReflect.Descriptor instead. 158 | func (*ExternalResponse) Descriptor() ([]byte, []int) { 159 | return file_msg_proto_rawDescGZIP(), []int{2} 160 | } 161 | 162 | func (x *ExternalResponse) GetResult() string { 163 | if x != nil { 164 | return x.Result 165 | } 166 | return "" 167 | } 168 | 169 | var File_msg_proto protoreflect.FileDescriptor 170 | 171 | var file_msg_proto_rawDesc = []byte{ 172 | 0x0a, 0x09, 0x6d, 0x73, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x1f, 0x0a, 0x0f, 0x45, 173 | 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x0c, 174 | 0x0a, 0x01, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x64, 0x22, 0x2b, 0x0a, 0x0f, 175 | 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 176 | 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 177 | 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x22, 0x2a, 0x0a, 0x10, 0x45, 0x78, 0x74, 178 | 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 179 | 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 180 | 0x65, 0x73, 0x75, 0x6c, 0x74, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x3b, 0x6d, 0x61, 0x69, 0x6e, 181 | 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 182 | } 183 | 184 | var ( 185 | file_msg_proto_rawDescOnce sync.Once 186 | file_msg_proto_rawDescData = file_msg_proto_rawDesc 187 | ) 188 | 189 | func file_msg_proto_rawDescGZIP() []byte { 190 | file_msg_proto_rawDescOnce.Do(func() { 191 | file_msg_proto_rawDescData = protoimpl.X.CompressGZIP(file_msg_proto_rawDescData) 192 | }) 193 | return file_msg_proto_rawDescData 194 | } 195 | 196 | var file_msg_proto_msgTypes = make([]protoimpl.MessageInfo, 3) 197 | var file_msg_proto_goTypes = []interface{}{ 198 | (*ExternalMessage)(nil), // 0: ExternalMessage 199 | (*ExternalRequest)(nil), // 1: ExternalRequest 200 | (*ExternalResponse)(nil), // 2: ExternalResponse 201 | } 202 | var file_msg_proto_depIdxs = []int32{ 203 | 0, // [0:0] is the sub-list for method output_type 204 | 0, // [0:0] is the sub-list for method input_type 205 | 0, // [0:0] is the sub-list for extension type_name 206 | 0, // [0:0] is the sub-list for extension extendee 207 | 0, // [0:0] is the sub-list for field type_name 208 | } 209 | 210 | func init() { file_msg_proto_init() } 211 | func file_msg_proto_init() { 212 | if File_msg_proto != nil { 213 | return 214 | } 215 | if !protoimpl.UnsafeEnabled { 216 | file_msg_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 217 | switch v := v.(*ExternalMessage); i { 218 | case 0: 219 | return &v.state 220 | case 1: 221 | return &v.sizeCache 222 | case 2: 223 | return &v.unknownFields 224 | default: 225 | return nil 226 | } 227 | } 228 | file_msg_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 229 | switch v := v.(*ExternalRequest); i { 230 | case 0: 231 | return &v.state 232 | case 1: 233 | return &v.sizeCache 234 | case 2: 235 | return &v.unknownFields 236 | default: 237 | return nil 238 | } 239 | } 240 | file_msg_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 241 | switch v := v.(*ExternalResponse); i { 242 | case 0: 243 | return &v.state 244 | case 1: 245 | return &v.sizeCache 246 | case 2: 247 | return &v.unknownFields 248 | default: 249 | return nil 250 | } 251 | } 252 | } 253 | type x struct{} 254 | out := protoimpl.TypeBuilder{ 255 | File: protoimpl.DescBuilder{ 256 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 257 | RawDescriptor: file_msg_proto_rawDesc, 258 | NumEnums: 0, 259 | NumMessages: 3, 260 | NumExtensions: 0, 261 | NumServices: 0, 262 | }, 263 | GoTypes: file_msg_proto_goTypes, 264 | DependencyIndexes: file_msg_proto_depIdxs, 265 | MessageInfos: file_msg_proto_msgTypes, 266 | }.Build() 267 | File_msg_proto = out.File 268 | file_msg_proto_rawDesc = nil 269 | file_msg_proto_goTypes = nil 270 | file_msg_proto_depIdxs = nil 271 | } 272 | -------------------------------------------------------------------------------- /integration_tests/msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "./;main"; 3 | 4 | message ExternalMessage { 5 | int32 d = 1; 6 | } 7 | 8 | message ExternalRequest { 9 | string content = 1; 10 | } 11 | 12 | message ExternalResponse { 13 | string result = 2; 14 | } 15 | -------------------------------------------------------------------------------- /integration_tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration_tests", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "karma start" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@types/chai": "^4.2.14", 13 | "@types/google-protobuf": "^3.15.1", 14 | "@types/lodash.camelcase": "^4.3.6", 15 | "@types/mocha": "^8.0.3", 16 | "@types/ramda": "^0.27.32", 17 | "chai": "^4.2.0", 18 | "google-protobuf": "^3.15.8", 19 | "karma": "^5.2.3", 20 | "karma-chai": "^0.1.0", 21 | "karma-chrome-launcher": "^3.1.0", 22 | "karma-env-preprocessor": "^0.1.1", 23 | "karma-mocha": "^2.0.1", 24 | "karma-script-launcher": "^1.0.0", 25 | "karma-typescript": "^5.2.0", 26 | "lodash.camelcase": "^4.3.0", 27 | "mocha": "^8.2.0", 28 | "ramda": "^0.27.1", 29 | "typescript": "^4.0.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /integration_tests/scripts/gen-protos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | USE_PROTO_NAMES=${1:-"false"} 3 | ENABLE_STYLING_CHECK=${2:-"false"} 4 | cd .. && go install && cd integration_tests && \ 5 | protoc -I . -I ../.. \ 6 | --grpc-gateway-ts_out=logtostderr=true,use_proto_names=$USE_PROTO_NAMES,enable_styling_check=$ENABLE_STYLING_CHECK,loglevel=debug:./ \ 7 | service.proto msg.proto empty.proto -------------------------------------------------------------------------------- /integration_tests/scripts/gen-server-proto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # remove binaries to ensure that binaries present in tools.go are installed 4 | rm -f $GOBIN/protoc-gen-go $GOBIN/protoc-gen-grpc-gateway $GOBIN/protoc-gen-swagger 5 | 6 | go install \ 7 | github.com/golang/protobuf/protoc-gen-go \ 8 | github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway \ 9 | github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger 10 | 11 | protoc -I . -I ../.. --go_out ./ --go_opt plugins=grpc --go_opt paths=source_relative \ 12 | --grpc-gateway_out ./ --grpc-gateway_opt logtostderr=true \ 13 | --grpc-gateway_opt paths=source_relative \ 14 | --grpc-gateway_opt generate_unbound_methods=true \ 15 | service.proto msg.proto 16 | -------------------------------------------------------------------------------- /integration_tests/scripts/source.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function runTest { 4 | ORIG_NAME=${1:="false"} 5 | CONFIG_NAME=${2:="karma.conf.js"} 6 | ./scripts/gen-protos.sh $ORIG_NAME true 7 | go run ./ -orig=$ORIG_NAME & 8 | pid=$! 9 | 10 | USE_PROTO_NAMES=$ORIG_NAME ./node_modules/.bin/karma start $CONFIG_NAME 11 | TEST_EXIT=$? 12 | if [[ $TEST_EXIT -ne 0 ]]; then 13 | pkill -P $pid 14 | exit $TEST_EXIT 15 | fi 16 | 17 | pkill -P $pid 18 | } 19 | 20 | 21 | -------------------------------------------------------------------------------- /integration_tests/scripts/test-ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source ./scripts/source.sh 3 | 4 | CONF="karma.conf.ci.js" 5 | 6 | runTest false $CONF 7 | runTest true $CONF 8 | 9 | -------------------------------------------------------------------------------- /integration_tests/scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./scripts/source.sh 4 | 5 | CONF="karma.conf.js" 6 | 7 | runTest ${1:-false} $CONF -------------------------------------------------------------------------------- /integration_tests/service.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "google.golang.org/grpc/codes" 8 | "google.golang.org/grpc/status" 9 | "google.golang.org/protobuf/types/known/emptypb" 10 | ) 11 | 12 | type RealCounterService struct { 13 | UnimplementedCounterServiceServer 14 | } 15 | 16 | func (r *RealCounterService) ExternalMessage(ctx context.Context, request *ExternalRequest) (*ExternalResponse, error) { 17 | return &ExternalResponse{ 18 | Result: request.Content + "!!", 19 | }, nil 20 | } 21 | 22 | func (r *RealCounterService) Increment(c context.Context, req *UnaryRequest) (*UnaryResponse, error) { 23 | return &UnaryResponse{ 24 | Result: req.Counter + 1, 25 | }, nil 26 | } 27 | 28 | func (r *RealCounterService) FailingIncrement(c context.Context, req *UnaryRequest) (*UnaryResponse, error) { 29 | return nil, status.Errorf(codes.Unavailable, "this increment does not work") 30 | } 31 | 32 | func (r *RealCounterService) EchoBinary(c context.Context, req *BinaryRequest) (*BinaryResponse, error) { 33 | return &BinaryResponse{ 34 | Data: req.Data, 35 | }, nil 36 | } 37 | 38 | func (r *RealCounterService) StreamingIncrements(req *StreamingRequest, service CounterService_StreamingIncrementsServer) error { 39 | times := 5 40 | counter := req.Counter 41 | 42 | for i := 0; i < times; i++ { 43 | counter++ 44 | err := service.Send(&StreamingResponse{ 45 | Result: counter, 46 | }) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | time.Sleep(200 * time.Millisecond) 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (r *RealCounterService) HTTPGet(ctx context.Context, req *HttpGetRequest) (*HttpGetResponse, error) { 58 | return &HttpGetResponse{ 59 | Result: req.NumToIncrease + 1, 60 | }, nil 61 | } 62 | 63 | func (r *RealCounterService) HTTPPostWithNestedBodyPath(ctx context.Context, in *HttpPostRequest) (*HttpPostResponse, error) { 64 | return &HttpPostResponse{ 65 | PostResult: in.A + in.Req.B, 66 | }, nil 67 | } 68 | 69 | func (r *RealCounterService) HTTPPostWithStarBodyPath(ctx context.Context, in *HttpPostRequest) (*HttpPostResponse, error) { 70 | return &HttpPostResponse{ 71 | PostResult: in.A + in.Req.B + in.C, 72 | }, nil 73 | } 74 | 75 | func (r *RealCounterService) HTTPPatch(ctx context.Context, in *HttpPatchRequest) (*HttpPatchResponse, error) { 76 | return &HttpPatchResponse{ 77 | PatchResult: in.A + in.C, 78 | }, nil 79 | } 80 | 81 | func (r *RealCounterService) HTTPDelete(ctx context.Context, req *HttpDeleteRequest) (*emptypb.Empty, error) { 82 | return &emptypb.Empty{}, nil 83 | } 84 | 85 | func (r *RealCounterService) HTTPGetWithURLSearchParams(ctx context.Context, in *HTTPGetWithURLSearchParamsRequest) (*HTTPGetWithURLSearchParamsResponse, error) { 86 | totalC := 0 87 | for _, c := range in.GetC() { 88 | totalC += int(c) 89 | } 90 | return &HTTPGetWithURLSearchParamsResponse{ 91 | UrlSearchParamsResult: in.GetA() + in.PostReq.GetB() + in.ExtMsg.GetD() + int32(totalC), 92 | }, nil 93 | } 94 | 95 | func (r *RealCounterService) HTTPGetWithZeroValueURLSearchParams(ctx context.Context, in *HTTPGetWithZeroValueURLSearchParamsRequest) (*HTTPGetWithZeroValueURLSearchParamsResponse, error) { 96 | var incrementedD []int32 97 | for _, d := range in.ZeroValueMsg.GetD() { 98 | incrementedD = append(incrementedD, d+1) 99 | } 100 | return &HTTPGetWithZeroValueURLSearchParamsResponse{ 101 | A: in.GetA(), 102 | B: in.GetB() + "hello", 103 | ZeroValueMsg: &ZeroValueMsg{ 104 | C: in.ZeroValueMsg.GetC() + 1, 105 | D: incrementedD, 106 | E: !in.ZeroValueMsg.GetE(), 107 | }, 108 | }, nil 109 | } 110 | -------------------------------------------------------------------------------- /integration_tests/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package main; 3 | option go_package = "./;main"; 4 | 5 | import "google/api/annotations.proto"; 6 | import "google/protobuf/empty.proto"; 7 | import "msg.proto"; 8 | 9 | message UnaryRequest { 10 | int32 counter = 1; 11 | } 12 | 13 | message UnaryResponse { 14 | int32 result = 1; 15 | } 16 | 17 | message BinaryRequest { 18 | bytes data = 1; 19 | } 20 | 21 | message BinaryResponse { 22 | bytes data = 1; 23 | } 24 | 25 | message StreamingRequest { 26 | int32 counter = 1; 27 | } 28 | 29 | message StreamingResponse { 30 | int32 result = 1; 31 | } 32 | 33 | message HttpGetRequest { 34 | int32 num_to_increase = 1; 35 | } 36 | 37 | message HttpGetResponse { 38 | int32 result = 1; 39 | } 40 | 41 | message HttpPostRequest { 42 | int32 a = 1; 43 | PostRequest req = 2; 44 | int32 c = 3; 45 | } 46 | 47 | message PostRequest { 48 | int32 b = 1; 49 | } 50 | 51 | message HttpPostResponse { 52 | int32 post_result = 1; 53 | } 54 | 55 | message HttpPatchRequest { 56 | int32 a = 1; 57 | int32 c = 2; 58 | } 59 | 60 | message HttpPatchResponse { 61 | int32 patch_result = 2; 62 | } 63 | 64 | message HttpDeleteRequest { 65 | int32 a = 1; 66 | } 67 | 68 | message HTTPGetWithURLSearchParamsRequest { 69 | int32 a = 1; 70 | PostRequest post_req = 2; 71 | repeated int32 c = 3; 72 | ExternalMessage ext_msg = 4; 73 | } 74 | 75 | message HTTPGetWithURLSearchParamsResponse { 76 | int32 url_search_params_result = 1; 77 | } 78 | 79 | message ZeroValueMsg { 80 | int32 c = 1; 81 | repeated int32 d = 2; 82 | bool e = 3; 83 | } 84 | 85 | message HTTPGetWithZeroValueURLSearchParamsRequest { 86 | string a = 1; 87 | string b = 2; 88 | ZeroValueMsg zero_value_msg = 3; 89 | } 90 | 91 | message HTTPGetWithZeroValueURLSearchParamsResponse { 92 | string a = 1; 93 | string b = 2; 94 | ZeroValueMsg zero_value_msg = 3; 95 | } 96 | 97 | service CounterService { 98 | rpc Increment(UnaryRequest) returns (UnaryResponse); 99 | rpc StreamingIncrements(StreamingRequest) returns (stream StreamingResponse); 100 | rpc FailingIncrement(UnaryRequest) returns (UnaryResponse); 101 | rpc EchoBinary(BinaryRequest) returns (BinaryResponse); 102 | rpc HTTPGet(HttpGetRequest) returns (HttpGetResponse) { 103 | option (google.api.http) = { 104 | get: "/api/{num_to_increase}" 105 | }; 106 | } 107 | rpc HTTPPostWithNestedBodyPath(HttpPostRequest) returns (HttpPostResponse) { 108 | option (google.api.http) = { 109 | post: "/post/{a}" 110 | body: "req" 111 | }; 112 | } 113 | rpc HTTPPostWithStarBodyPath(HttpPostRequest) returns (HttpPostResponse) { 114 | option (google.api.http) = { 115 | post: "/post/{a}/{c}" 116 | body: "*" 117 | }; 118 | } 119 | rpc HTTPPatch(HttpPatchRequest) returns (HttpPatchResponse) { 120 | option (google.api.http) = { 121 | patch: "/patch" 122 | body: "*" 123 | }; 124 | } 125 | rpc HTTPDelete(HttpDeleteRequest) returns (google.protobuf.Empty) { 126 | option (google.api.http) = { 127 | delete: "/delete/{a}" 128 | }; 129 | } 130 | rpc ExternalMessage(ExternalRequest) returns (ExternalResponse); 131 | rpc HTTPGetWithURLSearchParams(HTTPGetWithURLSearchParamsRequest) returns (HTTPGetWithURLSearchParamsResponse) { 132 | option (google.api.http) = { 133 | get: "/api/query/{a}" 134 | }; 135 | } 136 | rpc HTTPGetWithZeroValueURLSearchParams(HTTPGetWithZeroValueURLSearchParamsRequest) returns (HTTPGetWithZeroValueURLSearchParamsResponse) { 137 | option (google.api.http) = { 138 | get: "/path/query" 139 | }; 140 | } 141 | } 142 | 143 | -------------------------------------------------------------------------------- /integration_tests/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package main 4 | 5 | import ( 6 | _ "github.com/golang/protobuf/protoc-gen-go" 7 | _ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway" 8 | _ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger" 9 | ) 10 | -------------------------------------------------------------------------------- /integration_tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext", 8 | "es2015" 9 | ] , 10 | "module": "commonjs", 11 | "noImplicitAny": true, 12 | "outDir": "tmp", 13 | "target": "ES5", 14 | "sourceMap": true, 15 | "types" : [ 16 | "mocha", 17 | "chai" 18 | ], 19 | "esModuleInterop": true 20 | }, 21 | "exclude": [ 22 | "node_modules" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "strings" 7 | 8 | plugin "github.com/golang/protobuf/protoc-gen-go/plugin" 9 | log "github.com/sirupsen/logrus" // nolint: depguard 10 | "google.golang.org/protobuf/proto" 11 | 12 | "github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/generator" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | func decodeReq() *plugin.CodeGeneratorRequest { 17 | req := &plugin.CodeGeneratorRequest{} 18 | data, err := ioutil.ReadAll(os.Stdin) 19 | if err != nil { 20 | panic(err) 21 | } 22 | err = proto.Unmarshal(data, req) 23 | if err != nil { 24 | panic(err) 25 | } 26 | return req 27 | } 28 | 29 | func encodeResponse(resp proto.Message) { 30 | data, err := proto.Marshal(resp) 31 | if err != nil { 32 | panic(err) 33 | } 34 | _, err = os.Stdout.Write(data) 35 | if err != nil { 36 | panic(err) 37 | } 38 | } 39 | 40 | func main() { 41 | req := decodeReq() 42 | paramsMap := getParamsMap(req) 43 | err := configureLogging(paramsMap) 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | g, err := generator.New(paramsMap) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | log.Debug("Starts generating file request") 54 | resp, err := g.Generate(req) 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | encodeResponse(resp) 60 | log.Debug("generation finished") 61 | } 62 | 63 | func configureLogging(paramsMap map[string]string) error { 64 | if paramsMap["logtostderr"] == "true" { // configure logging when it's in the options 65 | log.SetFormatter(&log.TextFormatter{ 66 | DisableTimestamp: true, 67 | }) 68 | log.SetOutput(os.Stderr) 69 | log.Debugf("Logging configured completed, logging has been enabled") 70 | levelStr := paramsMap["loglevel"] 71 | if levelStr != "" { 72 | level, err := log.ParseLevel(levelStr) 73 | if err != nil { 74 | return errors.Wrapf(err, "error parsing log level %s", levelStr) 75 | } 76 | 77 | log.SetLevel(level) 78 | } else { 79 | log.SetLevel(log.InfoLevel) 80 | } 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func getParamsMap(req *plugin.CodeGeneratorRequest) map[string]string { 87 | paramsMap := make(map[string]string) 88 | params := req.GetParameter() 89 | 90 | for _, p := range strings.Split(params, ",") { 91 | if i := strings.Index(p, "="); i < 0 { 92 | paramsMap[p] = "" 93 | } else { 94 | paramsMap[p[0:i]] = p[i+1:] 95 | } 96 | } 97 | 98 | return paramsMap 99 | } 100 | -------------------------------------------------------------------------------- /options/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | protoc --go_out=paths=source_relative:. *.proto 4 | -------------------------------------------------------------------------------- /options/ts_package.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.26.0 4 | // protoc v3.12.4 5 | // source: ts_package.proto 6 | 7 | package options 8 | 9 | import ( 10 | descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" 11 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 12 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 13 | reflect "reflect" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | var file_ts_package_proto_extTypes = []protoimpl.ExtensionInfo{ 24 | { 25 | ExtendedType: (*descriptor.FileOptions)(nil), 26 | ExtensionType: (*string)(nil), 27 | Field: 50000, 28 | Name: "grpc.gateway.protoc_gen_grpc_gateway_ts.options.ts_package", 29 | Tag: "bytes,50000,opt,name=ts_package", 30 | Filename: "ts_package.proto", 31 | }, 32 | } 33 | 34 | // Extension fields to descriptor.FileOptions. 35 | var ( 36 | // optional string ts_package = 50000; 37 | E_TsPackage = &file_ts_package_proto_extTypes[0] 38 | ) 39 | 40 | var File_ts_package_proto protoreflect.FileDescriptor 41 | 42 | var file_ts_package_proto_rawDesc = []byte{ 43 | 0x0a, 0x10, 0x74, 0x73, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 44 | 0x74, 0x6f, 0x12, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 45 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x5f, 0x67, 0x65, 0x6e, 0x5f, 0x67, 0x72, 0x70, 0x63, 46 | 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, 0x74, 0x73, 0x2e, 0x6f, 0x70, 0x74, 0x69, 47 | 0x6f, 0x6e, 0x73, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 48 | 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 49 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3a, 0x40, 0x0a, 0x0a, 0x74, 0x73, 0x5f, 0x70, 0x61, 0x63, 0x6b, 50 | 0x61, 0x67, 0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 51 | 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 52 | 0x73, 0x18, 0xd0, 0x86, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x73, 0x50, 0x61, 0x63, 53 | 0x6b, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 54 | 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x65, 0x63, 0x6f, 0x73, 0x79, 55 | 0x73, 0x74, 0x65, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 56 | 0x67, 0x72, 0x70, 0x63, 0x2d, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2d, 0x74, 0x73, 0x2f, 57 | 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 58 | } 59 | 60 | var file_ts_package_proto_goTypes = []interface{}{ 61 | (*descriptor.FileOptions)(nil), // 0: google.protobuf.FileOptions 62 | } 63 | var file_ts_package_proto_depIdxs = []int32{ 64 | 0, // 0: grpc.gateway.protoc_gen_grpc_gateway_ts.options.ts_package:extendee -> google.protobuf.FileOptions 65 | 1, // [1:1] is the sub-list for method output_type 66 | 1, // [1:1] is the sub-list for method input_type 67 | 1, // [1:1] is the sub-list for extension type_name 68 | 0, // [0:1] is the sub-list for extension extendee 69 | 0, // [0:0] is the sub-list for field type_name 70 | } 71 | 72 | func init() { file_ts_package_proto_init() } 73 | func file_ts_package_proto_init() { 74 | if File_ts_package_proto != nil { 75 | return 76 | } 77 | type x struct{} 78 | out := protoimpl.TypeBuilder{ 79 | File: protoimpl.DescBuilder{ 80 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 81 | RawDescriptor: file_ts_package_proto_rawDesc, 82 | NumEnums: 0, 83 | NumMessages: 0, 84 | NumExtensions: 1, 85 | NumServices: 0, 86 | }, 87 | GoTypes: file_ts_package_proto_goTypes, 88 | DependencyIndexes: file_ts_package_proto_depIdxs, 89 | ExtensionInfos: file_ts_package_proto_extTypes, 90 | }.Build() 91 | File_ts_package_proto = out.File 92 | file_ts_package_proto_rawDesc = nil 93 | file_ts_package_proto_goTypes = nil 94 | file_ts_package_proto_depIdxs = nil 95 | } 96 | -------------------------------------------------------------------------------- /options/ts_package.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package grpc.gateway.protoc_gen_grpc_gateway_ts.options; 4 | 5 | option go_package = "github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/options"; 6 | 7 | import "google/protobuf/descriptor.proto"; 8 | 9 | extend google.protobuf.FileOptions { 10 | string ts_package = 50000; 11 | } 12 | -------------------------------------------------------------------------------- /registry/enum.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | descriptorpb "github.com/golang/protobuf/protoc-gen-go/descriptor" 5 | 6 | "github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/data" 7 | ) 8 | 9 | func (r *Registry) analyseEnumType(fileData *data.File, packageName, fileName string, parents []string, enum *descriptorpb.EnumDescriptorProto) { 10 | packageIdentifier := r.getNameOfPackageLevelIdentifier(parents, enum.GetName()) 11 | fqName := r.getFullQualifiedName(packageName, parents, enum.GetName()) 12 | protoType := descriptorpb.FieldDescriptorProto_TYPE_ENUM 13 | r.Types[fqName] = &TypeInformation{ 14 | FullyQualifiedName: fqName, 15 | Package: packageName, 16 | File: fileName, 17 | PackageIdentifier: packageIdentifier, 18 | LocalIdentifier: enum.GetName(), 19 | ProtoType: protoType, 20 | } 21 | 22 | enumData := data.NewEnum() 23 | enumData.Name = packageIdentifier 24 | 25 | for _, e := range enum.GetValue() { 26 | enumData.Values = append(enumData.Values, e.GetName()) 27 | } 28 | 29 | fileData.Enums = append(fileData.Enums, enumData) 30 | 31 | } 32 | -------------------------------------------------------------------------------- /registry/field.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | descriptorpb "github.com/golang/protobuf/protoc-gen-go/descriptor" 5 | 6 | "github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/data" 7 | ) 8 | 9 | // getFieldType generates an intermediate type and leave the rendering logic to choose what to render 10 | func (r *Registry) getFieldType(f *descriptorpb.FieldDescriptorProto) string { 11 | typeName := "" 12 | if f.Type != nil { 13 | switch *f.Type { 14 | case descriptorpb.FieldDescriptorProto_TYPE_MESSAGE, descriptorpb.FieldDescriptorProto_TYPE_ENUM: 15 | typeName = f.GetTypeName() 16 | case descriptorpb.FieldDescriptorProto_TYPE_STRING: 17 | typeName = "string" 18 | case descriptorpb.FieldDescriptorProto_TYPE_BOOL: 19 | typeName = "bool" 20 | case descriptorpb.FieldDescriptorProto_TYPE_BYTES: 21 | typeName = "bytes" 22 | case descriptorpb.FieldDescriptorProto_TYPE_FLOAT: 23 | typeName = "float" 24 | case descriptorpb.FieldDescriptorProto_TYPE_DOUBLE: 25 | typeName = "double" 26 | case descriptorpb.FieldDescriptorProto_TYPE_FIXED32: 27 | typeName = "fixed32" 28 | case descriptorpb.FieldDescriptorProto_TYPE_SFIXED32: 29 | typeName = "sfixed32" 30 | case descriptorpb.FieldDescriptorProto_TYPE_INT32: 31 | typeName = "int32" 32 | case descriptorpb.FieldDescriptorProto_TYPE_SINT32: 33 | typeName = "sint32" 34 | case descriptorpb.FieldDescriptorProto_TYPE_UINT32: 35 | typeName = "uint32" 36 | case descriptorpb.FieldDescriptorProto_TYPE_FIXED64: 37 | typeName = "fixed64" 38 | case descriptorpb.FieldDescriptorProto_TYPE_SFIXED64: 39 | typeName = "sfixed64" 40 | case descriptorpb.FieldDescriptorProto_TYPE_INT64: 41 | typeName = "int64" 42 | case descriptorpb.FieldDescriptorProto_TYPE_SINT64: 43 | typeName = "sint64" 44 | case descriptorpb.FieldDescriptorProto_TYPE_UINT64: 45 | typeName = "uint64" 46 | } 47 | } 48 | 49 | return typeName 50 | } 51 | 52 | func (r *Registry) analyseField(fileData *data.File, msgData *data.Message, packageName string, f *descriptorpb.FieldDescriptorProto) { 53 | fqTypeName := r.getFieldType(f) 54 | 55 | isExternal := r.isExternalDependenciesOutsidePackage(fqTypeName, packageName) 56 | 57 | fieldData := &data.Field{ 58 | Name: f.GetName(), 59 | Type: fqTypeName, 60 | IsExternal: isExternal, 61 | IsOneOfField: f.OneofIndex != nil, 62 | Message: msgData, 63 | } 64 | 65 | if f.Label != nil { 66 | if f.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REPEATED { 67 | fieldData.IsRepeated = true 68 | } 69 | } 70 | 71 | msgData.Fields = append(msgData.Fields, fieldData) 72 | 73 | if !fieldData.IsOneOfField { 74 | msgData.NonOneOfFields = append(msgData.NonOneOfFields, fieldData) 75 | } 76 | 77 | // if it's an external dependencies. store in the file data so that they can be collected when every file's finished 78 | if isExternal { 79 | fileData.ExternalDependingTypes = append(fileData.ExternalDependingTypes, fqTypeName) 80 | } 81 | 82 | // if it's a one of field. register the field data in the group of the same one of index. 83 | if fieldData.IsOneOfField { // one of field 84 | index := f.GetOneofIndex() 85 | fieldData.OneOfIndex = index 86 | _, ok := msgData.OneOfFieldsGroups[index] 87 | if !ok { 88 | msgData.OneOfFieldsGroups[index] = make([]*data.Field, 0) 89 | } 90 | msgData.OneOfFieldsGroups[index] = append(msgData.OneOfFieldsGroups[index], fieldData) 91 | } 92 | 93 | fileData.TrackPackageNonScalarType(fieldData) 94 | } 95 | -------------------------------------------------------------------------------- /registry/file.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | descriptorpb "github.com/golang/protobuf/protoc-gen-go/descriptor" 8 | "github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/data" 9 | "github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/options" 10 | "github.com/pkg/errors" 11 | log "github.com/sirupsen/logrus" // nolint: depguard 12 | "google.golang.org/protobuf/proto" 13 | ) 14 | 15 | func (r *Registry) analyseFile(f *descriptorpb.FileDescriptorProto) (*data.File, error) { 16 | log.Debugf("analysing %s", f.GetName()) 17 | fileData := data.NewFile() 18 | fileName := f.GetName() 19 | packageName := f.GetPackage() 20 | parents := make([]string, 0) 21 | fileData.Name = fileName 22 | fileData.TSFileName = data.GetTSFileName(fileName) 23 | if proto.HasExtension(f.Options, options.E_TsPackage) { 24 | r.TSPackages[fileData.TSFileName] = proto.GetExtension(f.Options, options.E_TsPackage).(string) 25 | } 26 | 27 | // analyse enums 28 | for _, enum := range f.EnumType { 29 | r.analyseEnumType(fileData, packageName, fileName, parents, enum) 30 | } 31 | 32 | // analyse messages, each message will go recursively 33 | for _, message := range f.MessageType { 34 | r.analyseMessage(fileData, packageName, fileName, parents, message) 35 | } 36 | 37 | // analyse services 38 | for _, service := range f.Service { 39 | r.analyseService(fileData, packageName, fileName, service) 40 | } 41 | 42 | // add fetch module after analysed all services in the file. will add dependencies if there is any 43 | err := r.addFetchModuleDependencies(fileData) 44 | if err != nil { 45 | return nil, errors.Wrapf(err, "error adding fetch module for file %s", fileData.Name) 46 | } 47 | 48 | r.analyseFilePackageTypeDependencies(fileData) 49 | 50 | return fileData, nil 51 | } 52 | 53 | func (r *Registry) addFetchModuleDependencies(fileData *data.File) error { 54 | if !fileData.Services.NeedsFetchModule() { 55 | log.Debugf("no services found for %s, skipping fetch module", fileData.Name) 56 | return nil 57 | } 58 | 59 | absDir, err := filepath.Abs(r.FetchModuleDirectory) 60 | if err != nil { 61 | return errors.Wrapf(err, "error looking up absolute path for fetch module directory %s", r.FetchModuleDirectory) 62 | } 63 | 64 | foundAtRoot, alias, err := r.findRootAliasForPath(func(absRoot string) (bool, error) { 65 | return strings.HasPrefix(absDir, absRoot), nil 66 | 67 | }) 68 | if err != nil { 69 | return errors.Wrapf(err, "error looking up root alias for fetch module directory %s", r.FetchModuleDirectory) 70 | } 71 | 72 | fileName := filepath.Join(r.FetchModuleDirectory, r.FetchModuleFilename) 73 | 74 | sourceFile, err := r.getSourceFileForImport(fileData.TSFileName, fileName, foundAtRoot, alias) 75 | if err != nil { 76 | return errors.Wrapf(err, "error replacing source file with alias for %s", fileName) 77 | } 78 | 79 | log.Debugf("added fetch dependency %s for %s", sourceFile, fileData.TSFileName) 80 | fileData.Dependencies = append(fileData.Dependencies, &data.Dependency{ 81 | ModuleIdentifier: "fm", 82 | SourceFile: sourceFile, 83 | }) 84 | 85 | return nil 86 | } 87 | 88 | func (r *Registry) analyseFilePackageTypeDependencies(fileData *data.File) { 89 | for _, t := range fileData.PackageNonScalarType { 90 | // for each non scalar types try to determine if the type comes from same 91 | // package but a different file. if yes then will need to add the type to 92 | // the external dependencies for collection later 93 | // also need to change the type's IsExternal information for rendering purpose 94 | typeInfo := t.GetType() 95 | fqTypeName := typeInfo.Type 96 | log.Debugf("checking whether non scala type %s in the same message is external to the current file", fqTypeName) 97 | 98 | registryType, foundInRegistry := r.Types[fqTypeName] 99 | if !foundInRegistry || registryType.File != fileData.Name { 100 | // this means the type from same package in file has yet to be analysed (means in different file) 101 | // or the type has appeared in another file different to the current file 102 | // in this case we will put the type as external in the fileData 103 | // and also mutate the IsExternal field of the given type:w 104 | log.Debugf("type %s is external to file %s, mutating the external dependencies information", fqTypeName, fileData.Name) 105 | 106 | fileData.ExternalDependingTypes = append(fileData.ExternalDependingTypes, fqTypeName) 107 | t.SetExternal(true) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /registry/message.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | descriptorpb "github.com/golang/protobuf/protoc-gen-go/descriptor" 5 | 6 | "github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/data" 7 | ) 8 | 9 | func (r *Registry) analyseMessage(fileData *data.File, packageName, fileName string, parents []string, message *descriptorpb.DescriptorProto) { 10 | packageIdentifier := r.getNameOfPackageLevelIdentifier(parents, message.GetName()) 11 | 12 | fqName := r.getFullQualifiedName(packageName, parents, message.GetName()) // "." + packageName + "." + parentsPrefix + message.GetName() 13 | protoType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE 14 | 15 | typeInfo := &TypeInformation{ 16 | FullyQualifiedName: fqName, 17 | Package: packageName, 18 | File: fileName, 19 | PackageIdentifier: packageIdentifier, 20 | LocalIdentifier: message.GetName(), 21 | ProtoType: protoType, 22 | } 23 | 24 | // register itself in the registry map 25 | r.Types[fqName] = typeInfo 26 | 27 | if message.Options != nil { 28 | if message.GetOptions().GetMapEntry() { 29 | // is a map entry, need to find out the type for key and value 30 | typeInfo.IsMapEntry = true 31 | 32 | for _, f := range message.Field { 33 | switch f.GetName() { 34 | case "key": 35 | typeInfo.KeyType = &data.MapEntryType{ 36 | Type: r.getFieldType(f), 37 | IsExternal: r.isExternalDependenciesOutsidePackage(f.GetTypeName(), packageName), 38 | } 39 | case "value": 40 | typeInfo.ValueType = &data.MapEntryType{ 41 | Type: r.getFieldType(f), 42 | IsExternal: r.isExternalDependenciesOutsidePackage(f.GetTypeName(), packageName), 43 | } 44 | } 45 | 46 | } 47 | fileData.TrackPackageNonScalarType(typeInfo.KeyType) 48 | fileData.TrackPackageNonScalarType(typeInfo.ValueType) 49 | // no need to add a map type into 50 | return 51 | 52 | } 53 | } 54 | 55 | data := data.NewMessage() 56 | data.Name = packageIdentifier 57 | data.FQType = fqName 58 | 59 | newParents := append(parents, message.GetName()) 60 | 61 | // handle enums, by pulling the enums out to the top level 62 | for _, enum := range message.EnumType { 63 | r.analyseEnumType(fileData, packageName, fileName, newParents, enum) 64 | } 65 | 66 | // nested type also got pull out to the top level of the file 67 | for _, msg := range message.NestedType { 68 | r.analyseMessage(fileData, packageName, fileName, newParents, msg) 69 | } 70 | 71 | // store a map of one of names 72 | for idx, oneOf := range message.GetOneofDecl() { 73 | data.OneOfFieldsNames[int32(idx)] = oneOf.GetName() 74 | } 75 | 76 | // analyse fields in the messages 77 | for _, f := range message.Field { 78 | r.analyseField(fileData, data, packageName, f) 79 | } 80 | 81 | fileData.Messages = append(fileData.Messages, data) 82 | } 83 | -------------------------------------------------------------------------------- /registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "path/filepath" 7 | "strings" 8 | 9 | descriptorpb "github.com/golang/protobuf/protoc-gen-go/descriptor" 10 | plugin "github.com/golang/protobuf/protoc-gen-go/plugin" 11 | "github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/data" 12 | "github.com/pkg/errors" 13 | log "github.com/sirupsen/logrus" // nolint: depguard 14 | ) 15 | 16 | const ( 17 | // TSImportRootParamsKey contains the key for common_import_root in parameters 18 | TSImportRootParamsKey = "ts_import_roots" 19 | // TSImportRootAliasParamsKey contains the key for common_import_root_alias in parameters 20 | TSImportRootAliasParamsKey = "ts_import_root_aliases" 21 | // TSImportRootSeparator separates the ts import root inside ts_import_roots & ts_import_root_aliases 22 | TSImportRootSeparator = ";" 23 | // FetchModuleDirectory is the parameter for directory where fetch module will live 24 | FetchModuleDirectory = "fetch_module_directory" 25 | // FetchModuleFileName is the file name for the individual fetch module 26 | FetchModuleFileName = "fetch_module_filename" 27 | // UseProtoNames will make the generator to generate field name the same as defined in the proto 28 | UseProtoNames = "use_proto_names" 29 | ) 30 | 31 | // Registry analyse generation request, spits out the data the the rendering process 32 | // it also holds the information about all the types 33 | type Registry struct { 34 | // Types stores the type information keyed by the fully qualified name of a type 35 | Types map[string]*TypeInformation 36 | 37 | // FilesToGenerate contains a list of actual file to generate, different from all the files from the request, some of which are import files 38 | FilesToGenerate map[string]bool 39 | 40 | // TSImportRoots represents the ts import root for the generator to figure out required import path, will default to cwd 41 | TSImportRoots []string 42 | 43 | // TSImportRootAliases if not empty will substitutes the common import root when writing the import into the js file 44 | TSImportRootAliases []string 45 | 46 | // FetchModuleDirectory is the directory to place fetch module file 47 | FetchModuleDirectory string 48 | 49 | // FetchModuleFilename is the filename for the fetch module 50 | FetchModuleFilename string 51 | 52 | // FetchModuleR is the alias for fetch module directory 53 | FetchModuleDirectoryAlias string 54 | 55 | // UseProtoNames will cause the generator to generate field name the same as defined in the proto 56 | UseProtoNames bool 57 | 58 | // TSPackages stores the package name keyed by the TS file name 59 | TSPackages map[string]string 60 | } 61 | 62 | // NewRegistry initialise the registry and return the instance 63 | func NewRegistry(paramsMap map[string]string) (*Registry, error) { 64 | tsImportRoots, tsImportRootAliases, err := getTSImportRootInformation(paramsMap) 65 | log.Debugf("found ts import roots %v", tsImportRoots) 66 | log.Debugf("found ts import root aliases %v", tsImportRootAliases) 67 | if err != nil { 68 | return nil, errors.Wrap(err, "error getting common import root information") 69 | } 70 | 71 | fetchModuleDirectory, fetchModuleFilename, err := getFetchModuleDirectory(paramsMap) 72 | if err != nil { 73 | return nil, errors.Wrap(err, "error getting fetch module directory") 74 | } 75 | log.Debugf("found fetch module directory %s", fetchModuleDirectory) 76 | log.Debugf("found fetch module name %s", fetchModuleFilename) 77 | 78 | useProtoNames := false 79 | 80 | useProtoNamesVal, ok := paramsMap[UseProtoNames] 81 | if ok { 82 | // default to true if not disabled specifi 83 | useProtoNames = useProtoNamesVal == "true" 84 | } 85 | 86 | r := &Registry{ 87 | Types: make(map[string]*TypeInformation), 88 | TSImportRoots: tsImportRoots, 89 | TSImportRootAliases: tsImportRootAliases, 90 | FetchModuleDirectory: fetchModuleDirectory, 91 | FetchModuleFilename: fetchModuleFilename, 92 | UseProtoNames: useProtoNames, 93 | TSPackages: make(map[string]string), 94 | } 95 | 96 | return r, nil 97 | } 98 | 99 | func getFetchModuleDirectory(paramsMap map[string]string) (fetchModuleDirectory string, fetchModuleFile string, err error) { 100 | fetchModuleDirectory, ok := paramsMap[FetchModuleDirectory] 101 | 102 | if !ok { 103 | fetchModuleDirectory = "." 104 | } 105 | 106 | fetchModuleFile, ok = paramsMap[FetchModuleFileName] 107 | if !ok { 108 | fetchModuleFile = "fetch.pb.ts" 109 | } 110 | 111 | return fetchModuleDirectory, fetchModuleFile, nil 112 | } 113 | 114 | func getTSImportRootInformation(paramsMap map[string]string) ([]string, []string, error) { 115 | tsImportRootsValue, ok := paramsMap[TSImportRootParamsKey] 116 | 117 | if !ok { 118 | tsImportRootsValue = "." 119 | } 120 | 121 | splittedImportRoots := strings.Split(tsImportRootsValue, TSImportRootSeparator) 122 | numImportRoots := len(splittedImportRoots) 123 | 124 | tsImportRoots := make([]string, 0, numImportRoots) 125 | 126 | for _, r := range splittedImportRoots { 127 | tsImportRoot := r 128 | if !path.IsAbs(tsImportRoot) { 129 | absPath, err := filepath.Abs(tsImportRoot) 130 | if err != nil { 131 | return nil, nil, errors.Wrapf(err, "error turning path %s into absolute path", tsImportRoot) 132 | } 133 | 134 | tsImportRoot = absPath 135 | } 136 | 137 | tsImportRoots = append(tsImportRoots, tsImportRoot) 138 | } 139 | 140 | tsImportRootAliasValue, ok := paramsMap[TSImportRootAliasParamsKey] 141 | 142 | if !ok { 143 | tsImportRootAliasValue = "" 144 | } 145 | 146 | splittedImportRootAliases := strings.Split(tsImportRootAliasValue, TSImportRootSeparator) 147 | 148 | tsImportRootAliases := make([]string, numImportRoots) 149 | 150 | for i, ra := range splittedImportRootAliases { 151 | if i >= numImportRoots { 152 | // in case we have more root alias than root, we will just take the number matches the roots 153 | break 154 | } 155 | tsImportRootAliases[i] = ra 156 | 157 | } 158 | 159 | return tsImportRoots, tsImportRootAliases, nil 160 | } 161 | 162 | // TypeInformation store the information about a given type 163 | type TypeInformation struct { 164 | // Fully qualified name of the type, it starts with `.` and followed by packages and the nested structure path. 165 | FullyQualifiedName string 166 | // Package is the package of the type it belongs to 167 | Package string 168 | // Files is the file of the type belongs to, this is important in Typescript as modules is the namespace for types defined inside 169 | File string 170 | // ModuleIdentifier is the identifier of the type inside the package, this will be useful for enum and nested enum. 171 | PackageIdentifier string 172 | // LocalIdentifier is the identifier inside the types local scope 173 | LocalIdentifier string 174 | // ProtoType is the type inside the proto. This is used to tell whether it's an enum or a message 175 | ProtoType descriptorpb.FieldDescriptorProto_Type 176 | // IsMapEntry indicates whether this type is a Map Entry 177 | IsMapEntry bool 178 | // KeyType is the type information for the map key 179 | KeyType *data.MapEntryType 180 | // Value type is the type information for the map value 181 | ValueType *data.MapEntryType 182 | } 183 | 184 | // IsFileToGenerate contains the file to be generated in the request 185 | func (r *Registry) IsFileToGenerate(name string) bool { 186 | result, ok := r.FilesToGenerate[name] 187 | return ok && result 188 | } 189 | 190 | // Analyse analyses the the file inputs, stores types information and spits out the rendering data 191 | func (r *Registry) Analyse(req *plugin.CodeGeneratorRequest) (map[string]*data.File, error) { 192 | r.FilesToGenerate = make(map[string]bool) 193 | for _, f := range req.GetFileToGenerate() { 194 | r.FilesToGenerate[f] = true 195 | } 196 | 197 | files := req.GetProtoFile() 198 | log.Debugf("about to start anaylyse files, %d in total", len(files)) 199 | data := make(map[string]*data.File) 200 | // analyse all files in the request first 201 | for _, f := range files { 202 | fileData, err := r.analyseFile(f) 203 | if err != nil { 204 | return nil, errors.Wrapf(err, "error analysing file %s", *f.Name) 205 | } 206 | data[f.GetName()] = fileData 207 | } 208 | 209 | // when finishes we have a full map of types and where they are located 210 | // collect all the external dependencies and back fill it to the file data. 211 | err := r.collectExternalDependenciesFromData(data) 212 | if err != nil { 213 | return nil, errors.Wrap(err, "error collecting external dependency information after analysis finished") 214 | } 215 | 216 | return data, nil 217 | } 218 | 219 | // This simply just concats the parents name and the entity name. 220 | func (r *Registry) getNameOfPackageLevelIdentifier(parents []string, name string) string { 221 | return strings.Join(parents, "") + name 222 | } 223 | 224 | func (r *Registry) getFullQualifiedName(packageName string, parents []string, name string) string { 225 | namesToConcat := make([]string, 0, 2+len(parents)) 226 | 227 | if packageName != "" { 228 | namesToConcat = append(namesToConcat, packageName) 229 | } 230 | 231 | if len(parents) > 0 { 232 | namesToConcat = append(namesToConcat, parents...) 233 | } 234 | 235 | namesToConcat = append(namesToConcat, name) 236 | 237 | return "." + strings.Join(namesToConcat, ".") 238 | 239 | } 240 | 241 | func (r *Registry) isExternalDependenciesOutsidePackage(fqTypeName, packageName string) bool { 242 | return strings.Index(fqTypeName, "."+packageName) != 0 && strings.Index(fqTypeName, ".") == 0 243 | } 244 | 245 | // findRootAliasForPath iterate through all ts_import_roots and try to find an alias with the first matching the ts_import_root 246 | func (r *Registry) findRootAliasForPath(predicate func(root string) (bool, error)) (foundAtRoot, alias string, err error) { 247 | foundAtRoot = "" 248 | alias = "" 249 | for i, root := range r.TSImportRoots { 250 | absRoot, err := filepath.Abs(root) 251 | if err != nil { 252 | return "", "", errors.Wrapf(err, "error looking up absolute path for %s", err) 253 | } 254 | 255 | found, err := predicate(absRoot) 256 | if err != nil { 257 | return "", "", errors.Wrapf(err, "error verifying the root %s for", absRoot) 258 | } 259 | 260 | if found { 261 | foundAtRoot = root 262 | if i >= len(r.TSImportRootAliases) { 263 | alias = "" 264 | } else { 265 | alias = r.TSImportRootAliases[i] 266 | } 267 | 268 | break 269 | } 270 | } 271 | 272 | return foundAtRoot, alias, nil 273 | } 274 | 275 | // getSourceFileForImport will return source file for import use. 276 | // if alias is provided it will try to replace the absolute root with target's absolute path with alias 277 | // if no alias then it will try to return a relative path to the source file 278 | func (r *Registry) getSourceFileForImport(source, target, root, alias string) (string, error) { 279 | ret := "" 280 | absTarget, err := filepath.Abs(target) 281 | if err != nil { 282 | return "", errors.Wrapf(err, "error looking up absolute path for target %s", target) 283 | } 284 | 285 | if alias != "" { // if an alias has been provided, that means there's no need to get relative path 286 | absRoot, err := filepath.Abs(root) 287 | if err != nil { 288 | return "", errors.Wrapf(err, "error looking up absolute path for root %s", root) 289 | } 290 | 291 | ret = strings.ReplaceAll(absTarget, absRoot, alias) 292 | log.Debugf("replacing root alias %s for %s, result: %s", alias, target, ret) 293 | } else { // return relative path here 294 | log.Debugf("no root alias found, trying to get the relative path for %s", target) 295 | absSource, err := filepath.Abs(source) 296 | if err != nil { 297 | return "", errors.Wrapf(err, "error looking up absolute directory with base dir: %s", source) 298 | } 299 | 300 | ret, err = filepath.Rel(filepath.Dir(absSource), absTarget) 301 | if err != nil { 302 | return "", errors.Wrapf(err, "error looking up relative path for source target %s", target) 303 | } 304 | 305 | slashPath := filepath.ToSlash(ret) 306 | log.Debugf("got relative path %s for %s", target, slashPath) 307 | 308 | if !strings.HasPrefix(slashPath, "../") { // sub directory will not have relative path ./, if this happens, prepend one 309 | ret = filepath.FromSlash("./" + slashPath) 310 | } 311 | 312 | log.Debugf("no root alias found, trying to get the relative path for %s, result: %s", target, ret) 313 | } 314 | 315 | // remove .ts suffix if there's any 316 | suffixIndex := strings.LastIndex(ret, ".ts") 317 | if suffixIndex != -1 { 318 | ret = ret[0:suffixIndex] 319 | } 320 | 321 | return ret, nil 322 | 323 | } 324 | 325 | func (r *Registry) collectExternalDependenciesFromData(filesData map[string]*data.File) error { 326 | for _, fileData := range filesData { 327 | log.Debugf("collecting dependencies information for %s", fileData.TSFileName) 328 | // dependency group up the dependency by package+file 329 | dependencies := make(map[string]*data.Dependency) 330 | for _, typeName := range fileData.ExternalDependingTypes { 331 | typeInfo, ok := r.Types[typeName] 332 | if !ok { 333 | return errors.Errorf("cannot find type info for %s, $v", typeName) 334 | } 335 | identifier := typeInfo.Package + "|" + typeInfo.File 336 | 337 | if _, ok := dependencies[identifier]; !ok { 338 | // only fill in if this file has not been mentioned before. 339 | // the way import in the genrated file works is like 340 | // import * as [ModuleIdentifier] from '[Source File]' 341 | // so there only needs to be added once. 342 | // Referencing types will be [ModuleIdentifier].[PackageIdentifier] 343 | base := fileData.TSFileName 344 | target := data.GetTSFileName(typeInfo.File) 345 | sourceFile := "" 346 | if pkg, ok := r.TSPackages[target]; ok { 347 | log.Debugf("package import override %s has been found for file %s", pkg, target) 348 | sourceFile = pkg 349 | } else { 350 | foundAtRoot, alias, err := r.findRootAliasForPath(func(absRoot string) (bool, error) { 351 | completePath := filepath.Join(absRoot, typeInfo.File) 352 | _, err := os.Stat(completePath) 353 | if err != nil { 354 | if os.IsNotExist(err) { 355 | return false, nil 356 | } 357 | 358 | return false, err 359 | 360 | } else { 361 | return true, nil 362 | } 363 | 364 | }) 365 | if err != nil { 366 | return errors.WithStack(err) 367 | } 368 | 369 | if foundAtRoot != "" { 370 | target = filepath.Join(foundAtRoot, target) 371 | } 372 | 373 | sourceFile, err = r.getSourceFileForImport(base, target, foundAtRoot, alias) 374 | if err != nil { 375 | return errors.Wrap(err, "error getting source file for import") 376 | } 377 | } 378 | dependencies[identifier] = &data.Dependency{ 379 | ModuleIdentifier: data.GetModuleName(typeInfo.Package, typeInfo.File), 380 | SourceFile: sourceFile, 381 | } 382 | } 383 | } 384 | 385 | for _, dependency := range dependencies { 386 | fileData.Dependencies = append(fileData.Dependencies, dependency) 387 | } 388 | } 389 | 390 | return nil 391 | } 392 | -------------------------------------------------------------------------------- /registry/service.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "fmt" 5 | 6 | descriptorpb "github.com/golang/protobuf/protoc-gen-go/descriptor" 7 | "google.golang.org/genproto/googleapis/api/annotations" 8 | "google.golang.org/protobuf/proto" 9 | 10 | "github.com/grpc-ecosystem/protoc-gen-grpc-gateway-ts/data" 11 | ) 12 | 13 | func getHTTPAnnotation(m *descriptorpb.MethodDescriptorProto) *annotations.HttpRule { 14 | option := proto.GetExtension(m.GetOptions(), annotations.E_Http) 15 | return option.(*annotations.HttpRule) 16 | } 17 | 18 | func hasHTTPAnnotation(m *descriptorpb.MethodDescriptorProto) bool { 19 | return getHTTPAnnotation(m) != nil 20 | } 21 | 22 | func getHTTPMethodPath(m *descriptorpb.MethodDescriptorProto) (method, path string) { 23 | if !hasHTTPAnnotation(m) { 24 | return "", "" 25 | } 26 | 27 | rule := getHTTPAnnotation(m) 28 | pattern := rule.Pattern 29 | switch pattern.(type) { 30 | case *annotations.HttpRule_Get: 31 | return "GET", rule.GetGet() 32 | case *annotations.HttpRule_Post: 33 | return "POST", rule.GetPost() 34 | case *annotations.HttpRule_Put: 35 | return "PUT", rule.GetPut() 36 | case *annotations.HttpRule_Patch: 37 | return "PATCH", rule.GetPatch() 38 | case *annotations.HttpRule_Delete: 39 | return "DELETE", rule.GetDelete() 40 | default: 41 | panic(fmt.Sprintf("unsupported HTTP method %T", pattern)) 42 | } 43 | } 44 | 45 | func getHTTPBody(m *descriptorpb.MethodDescriptorProto) *string { 46 | if !hasHTTPAnnotation(m) { 47 | return nil 48 | } 49 | empty := "" 50 | rule := getHTTPAnnotation(m) 51 | pattern := rule.Pattern 52 | switch pattern.(type) { 53 | case *annotations.HttpRule_Get: 54 | return &empty 55 | default: 56 | body := rule.GetBody() 57 | return &body 58 | } 59 | } 60 | 61 | func (r *Registry) analyseService(fileData *data.File, packageName string, fileName string, service *descriptorpb.ServiceDescriptorProto) { 62 | packageIdentifier := service.GetName() 63 | fqName := "." + packageName + "." + packageIdentifier 64 | 65 | // register itself in the registry map 66 | r.Types[fqName] = &TypeInformation{ 67 | FullyQualifiedName: fqName, 68 | Package: packageName, 69 | File: fileName, 70 | PackageIdentifier: packageIdentifier, 71 | LocalIdentifier: service.GetName(), 72 | } 73 | 74 | serviceData := data.NewService() 75 | serviceData.Name = service.GetName() 76 | serviceURLPart := packageName + "." + serviceData.Name 77 | 78 | for _, method := range service.Method { 79 | // don't support client streaming, will ignore the client streaming method 80 | if method.GetClientStreaming() { 81 | continue 82 | } 83 | 84 | inputTypeFQName := *method.InputType 85 | isInputTypeExternal := r.isExternalDependenciesOutsidePackage(inputTypeFQName, packageName) 86 | 87 | if isInputTypeExternal { 88 | fileData.ExternalDependingTypes = append(fileData.ExternalDependingTypes, inputTypeFQName) 89 | } 90 | 91 | outputTypeFQName := *method.OutputType 92 | isOutputTypeExternal := r.isExternalDependenciesOutsidePackage(outputTypeFQName, packageName) 93 | 94 | if isOutputTypeExternal { 95 | fileData.ExternalDependingTypes = append(fileData.ExternalDependingTypes, outputTypeFQName) 96 | } 97 | 98 | httpMethod := "POST" 99 | url := "/" + serviceURLPart + "/" + method.GetName() 100 | if hasHTTPAnnotation(method) { 101 | hm, u := getHTTPMethodPath(method) 102 | if hm != "" && u != "" { 103 | httpMethod = hm 104 | url = u 105 | } 106 | } 107 | body := getHTTPBody(method) 108 | 109 | methodData := &data.Method{ 110 | Name: method.GetName(), 111 | URL: url, 112 | Input: &data.MethodArgument{ 113 | Type: inputTypeFQName, 114 | IsExternal: isInputTypeExternal, 115 | }, 116 | Output: &data.MethodArgument{ 117 | Type: outputTypeFQName, 118 | IsExternal: isOutputTypeExternal, 119 | }, 120 | ServerStreaming: method.GetServerStreaming(), 121 | ClientStreaming: method.GetClientStreaming(), 122 | HTTPMethod: httpMethod, 123 | HTTPRequestBody: body, 124 | } 125 | 126 | fileData.TrackPackageNonScalarType(methodData.Input) 127 | fileData.TrackPackageNonScalarType(methodData.Output) 128 | 129 | serviceData.Methods = append(serviceData.Methods, methodData) 130 | } 131 | 132 | fileData.Services = append(fileData.Services, serviceData) 133 | } 134 | -------------------------------------------------------------------------------- /test/oneof_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/stretchr/testify/assert" 6 | "os" 7 | "os/exec" 8 | "testing" 9 | ) 10 | 11 | func TestValidOneOfUseCase(t *testing.T) { 12 | f, err := createFileWithContent("valid.ts", ` 13 | import {LogEntryLevel, LogService} from "./log.pb"; 14 | import {DataSource} from "./datasource/datasource.pb" 15 | import {Environment} from "./environment.pb" 16 | 17 | (async () => { 18 | const cloudSourceResult = await LogService.FetchLog({ 19 | source: DataSource.Cloud, 20 | service: "cloudService" 21 | }) 22 | 23 | const dataCentreSourceResult = await LogService.StreamLog({ 24 | source: DataSource.DataCentre, 25 | application: "data-centre-app" 26 | }) 27 | 28 | const pushLogResult = await LogService.PushLog({ 29 | entry: { 30 | service: "test", 31 | level: LogEntryLevel.INFO, 32 | elapsed: 5, 33 | env: Environment.Production, 34 | message: "error message ", 35 | tags: ["activity1", "service1"], 36 | timestamp: 1592221950509.390, 37 | hasStackTrace: true, 38 | stackTraces: [{ 39 | exception: { 40 | type: 'network', 41 | message: "timeout connecting to xyz", 42 | }, 43 | lines: [{ 44 | identifier: "A.method1", 45 | file: "a.java", 46 | line: "233", 47 | }], 48 | }], 49 | }, 50 | source: DataSource.Cloud 51 | }) 52 | })() 53 | `) 54 | assert.Nil(t, err) 55 | defer f.Close() 56 | cmd := getTSCCommand() 57 | err = cmd.Run() 58 | assert.Nil(t, err) 59 | assert.Equal(t, 0, cmd.ProcessState.ExitCode()) 60 | 61 | err = removeTestFile("valid.ts") 62 | assert.Nil(t, err) 63 | } 64 | 65 | func getTSCCommand() *exec.Cmd { 66 | cmd := exec.Command("npx", "tsc", "--project", "../testdata/", "--noEmit") 67 | cmd.Dir = "../testdata/" 68 | cmd.Stderr = os.Stderr 69 | cmd.Stdout = os.Stdout 70 | return cmd 71 | } 72 | 73 | func TestInvalidOneOfUseCase(t *testing.T) { 74 | f, err := createFileWithContent("invalid.ts", ` 75 | import {LogService} from "./log.pb"; 76 | import {DataSource} from "./datasource/datasource.pb" 77 | 78 | (async () => { 79 | const cloudSourceResult = await LogService.FetchLog({ 80 | source: DataSource.Cloud, 81 | service: "cloudService", 82 | application: "cloudApplication" 83 | }) 84 | 85 | })() 86 | `) 87 | assert.Nil(t, err) 88 | defer f.Close() 89 | cmd := getTSCCommand() 90 | err = cmd.Run() 91 | assert.NotNil(t, err) 92 | assert.NotEqual(t, 0, cmd.ProcessState.ExitCode()) 93 | 94 | err = removeTestFile("invalid.ts") 95 | assert.Nil(t, err) 96 | } 97 | 98 | func createFileWithContent(fname, content string) (*os.File, error) { 99 | f, err := os.Create("../testdata/" + fname) 100 | if err != nil { 101 | return nil, errors.Wrapf(err, "error creating file") 102 | } 103 | defer f.Close() 104 | _, err = f.WriteString(content) 105 | if err != nil { 106 | return nil, errors.Wrapf(err, "error writing content into %s", fname) 107 | } 108 | 109 | return f, nil 110 | } 111 | 112 | func removeTestFile(fname string) error { 113 | return os.Remove("../testdata/" + fname) 114 | } 115 | -------------------------------------------------------------------------------- /testdata/.gitignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | *.log -------------------------------------------------------------------------------- /testdata/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all protos 2 | 3 | all: protos 4 | 5 | protos: 6 | cd .. && go install && cd testdata && \ 7 | protoc -I . \ 8 | --grpc-gateway-ts_out=logtostderr=true,loglevel=debug:./ \ 9 | log.proto environment.proto ./datasource/datasource.proto 10 | -------------------------------------------------------------------------------- /testdata/datasource/datasource.proto: -------------------------------------------------------------------------------- 1 | syntax = 'proto3'; 2 | 3 | package com.squareup.cash.gap.datasource; 4 | 5 | enum DataSource { 6 | DataCentre = 0; 7 | Cloud = 1; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /testdata/environment.proto: -------------------------------------------------------------------------------- 1 | syntax = 'proto3'; 2 | 3 | package com.squareup.cash.gap; 4 | 5 | enum Environment { 6 | Staging = 0; 7 | Production = 1; 8 | } -------------------------------------------------------------------------------- /testdata/log.proto: -------------------------------------------------------------------------------- 1 | syntax = 'proto3'; 2 | 3 | package com.squareup.cash.gap; 4 | import "environment.proto"; 5 | import "datasource/datasource.proto"; 6 | 7 | message LogEntry { 8 | enum Level { 9 | DEBUG = 0; 10 | INFO = 1; 11 | WARN = 2; 12 | ERROR = 3; 13 | } 14 | 15 | oneof identifier { 16 | string application = 1; 17 | string service = 2; 18 | } 19 | string hostname = 3; 20 | Level level = 4; 21 | int32 elapsed = 5; 22 | double timestamp = 6; 23 | Environment env = 7; 24 | bool hasStackTrace = 8; 25 | string message = 9; 26 | repeated string tags = 10; 27 | repeated StackTrace stackTraces = 11; 28 | 29 | message StackTrace { 30 | message Exception { 31 | string type = 1; 32 | string message = 2; 33 | } 34 | 35 | message Method { 36 | string identifier = 1; 37 | string file = 2; 38 | int64 line = 3; 39 | } 40 | 41 | Exception exception = 1; 42 | repeated Method lines = 2; 43 | 44 | } 45 | 46 | } 47 | 48 | message LogStream { 49 | oneof source { 50 | DataCentreLogEntries dataCentre = 1; 51 | CloudLogEntries cloud = 2; 52 | } 53 | } 54 | 55 | message DataCentreLogEntries { 56 | repeated LogEntry logs = 1; 57 | } 58 | 59 | message CloudLogEntries { 60 | repeated LogEntry logs = 1; 61 | } 62 | 63 | message FetchLogRequest { 64 | com.squareup.cash.gap.datasource.DataSource source = 1; 65 | oneof identifier { 66 | string application = 2; 67 | string service = 3; 68 | } 69 | } 70 | 71 | message FetchLogResponse { 72 | LogStream result = 1; 73 | } 74 | 75 | message PushLogRequest { 76 | LogEntry entry = 1; 77 | com.squareup.cash.gap.datasource.DataSource source = 2; 78 | } 79 | 80 | message PushLogResponse { 81 | bool success = 1; 82 | } 83 | 84 | service LogService { 85 | rpc FetchLog (FetchLogRequest) returns (FetchLogResponse); 86 | rpc StreamLog (FetchLogRequest) returns (stream FetchLogResponse); 87 | rpc PushLog (PushLogRequest) returns (PushLogResponse); 88 | } 89 | -------------------------------------------------------------------------------- /testdata/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "react", 20 | "baseUrl": "../../../", 21 | }, 22 | "include": [ 23 | "./*" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------