├── .gitignore ├── LICENSE ├── README.md ├── e2e ├── e2e_test.go ├── gen.go ├── painters │ ├── gen.go │ ├── painters.pb.go │ └── painters.proto ├── service.pb.go ├── service.proto ├── service.twirp.go └── twirpql │ ├── enums.gen.go │ ├── generated.go │ ├── gqlgen.yml │ ├── resolver.go │ ├── scalars.go │ ├── schema.graphql │ ├── server.go │ └── unions.gen.go ├── go.mod ├── go.sum ├── internal ├── genenums │ ├── genenums.go │ ├── goenums_test.go │ ├── template.go │ └── testdata │ │ └── enums.golden ├── genresolver │ ├── genresolver.go │ └── template.go ├── genscalar │ ├── genscalar.go │ ├── genscalar_test.go │ └── testdata │ │ └── scalars.golden ├── genserver │ ├── genserver.go │ └── template.go ├── genunions │ └── genunions.go ├── gocopy │ ├── lazyregexp │ │ └── lazyre.go │ ├── modfile │ │ ├── gopkgin.go │ │ ├── print.go │ │ ├── printhtml.go │ │ ├── read.go │ │ └── rule.go │ ├── module │ │ └── module.go │ └── semver │ │ └── semver.go └── gqlfmt │ ├── gqlfmt.go │ ├── gqlfmt_test.go │ └── testdata │ ├── expected.graphql │ └── given.graphql ├── main.go ├── options ├── gen.go ├── options.pb.go └── options.proto └── twirpql ├── testdata ├── mixed │ ├── code_generator_request.pb.bin │ ├── gen.go │ ├── gqlgen.yml.golden │ ├── mixed.proto │ └── schema.graphql.golden ├── multifile │ ├── code_generator_request.pb.bin │ ├── gen.go │ ├── gqlgen.yml.golden │ ├── multifile.proto │ ├── schema.graphql.golden │ └── second.proto ├── multitypes │ ├── code_generator_request.pb.bin │ ├── gen.go │ ├── gqlgen.yml.golden │ ├── multitypes.proto │ └── schema.graphql.golden ├── mutations │ ├── code_generator_request.pb.bin │ ├── gen.go │ ├── gqlgen.yml.golden │ ├── mutations.proto │ └── schema.graphql.golden ├── noinput │ ├── code_generator_request.pb.bin │ ├── gen.go │ ├── gqlgen.yml.golden │ ├── noinput.proto │ └── schema.graphql.golden └── simple │ ├── code_generator_request.pb.bin │ ├── gen.go │ ├── gqlgen.yml.golden │ ├── schema.graphql.golden │ └── simple.proto ├── tmpl.go ├── twirpql.go ├── twirpql_test.go └── types.go /.gitignore: -------------------------------------------------------------------------------- 1 | .notes 2 | .DS_Store -------------------------------------------------------------------------------- /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. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #

2 | 3 | A Protoc plugin that generates a GraphQL layer on top of Twirp servers. 4 | 5 | ## Status 6 | 7 | There have been a few breaking changes in the upstream dependencies (protobufs and gqlgen) which makes this plugin a bit outdated if not flat out broken. So the go.mod file to pin the versions that would work with this library or feel free to open a PR upgrading to the latest of the two versions. See https://github.com/marwan-at-work/protoc-gen-twirpql/issues/9 8 | 9 | ## Features 10 | 11 | - [x] Generates a full GraphQL server implementation based on [gqlgen](https://github.com/99designs/gqlgen) 12 | 13 | - [x] Marshals/Unmarshals ProtoBuf enums (ints) to GraphQL enums (strings). 14 | 15 | - [x] Generates GraphQL Scalars for Protobuf map types that can be used as encoded json strings. 16 | 17 | - [x] Exposes a GraphQL handler as well as a Graph(i)QL interface to make Twirp servers interactive. 18 | 19 | - [x] Unit tests and end to end tests. 20 | 21 | - [x] Twirp Server Hooks -> GraphQL Middleware 22 | 23 | 24 | ## Why 25 | 26 | RPC Frameworks such as Twirp have two big benefits: 27 | 28 | 1. Automatic client generation 29 | 2. Documentation-first APIs ensure your contract is not out of date. 30 | 31 | This makes it really easy for multiple teams to start using your program right away. But before they use it, they need to learn how it works. This is where GraphQL does a better job. 32 | 33 | Twirp and many other frameworks do not provide a friendly UI to discover and interact with their services. 34 | GraphQL comes with a very friendly UI that makes discovering an API quick, easy and fun. You get expressiveness and auto-completion out of the box. 35 | 36 | By combining both technologies, my hope is that you get the best of both worlds. 37 | 38 | ## Install 39 | 40 | ```bash 41 | GO111MODULE=on go install marwan.io/protoc-gen-twirpql 42 | ``` 43 | 44 | ## Usage 45 | 46 | ### For a full tutorial, [click here](https://twirpql.dev/docs/install). 47 | 48 | ```bash 49 | protoc --go_out=. --twirp_out=. service.proto 50 | protoc --twirpql_out=. service.proto 51 | ``` 52 | 53 | This will generate a subpackage that you can import and use as such: 54 | 55 | ```golang 56 | package main 57 | 58 | import ( 59 | "net/http" 60 | 61 | "./twirpql" 62 | "./mytwirpserver" 63 | ) 64 | 65 | func main() { 66 | s := mytwirpserver.New() 67 | http.Handle("/query", twirpql.Handler(s, nil)) 68 | http.Handle("/play", twirpql.Playground("my service", "/query")) 69 | } 70 | ``` 71 | 72 | ### Server Hooks 73 | 74 | TwirpQL supports mapping the Error callback of `twirp.ServerHooks` to a GraphQL middleware. 75 | 76 | All you have to do is the following: 77 | 78 | ```golang 79 | hooks := &twirpql.Handler{Error: myErrorHook} 80 | http.Handle("/query", twirpql.Handler(s, hooks)) 81 | ``` 82 | 83 | And TwirpQL will call your hook if the underlying service implementation returned an error. 84 | 85 | 86 | ### Workflow 87 | 88 | TwirpQL expects the service.proto to have already been used to generate a `.pb.go`, and `.twirp.go` files 89 | in the same directory as the `.proto` file. 90 | 91 | The plugin will generate a `twirpql` sub-package that contains the GraphQL layer with all types mapped to 92 | the original `.pb.go` file generated by your `.proto` file. 93 | 94 | 95 | ### Multiple Services 96 | 97 | Currently you can only specify one target file such as `service.proto`. In other words, the following is not allowed: 98 | 99 | ```bash 100 | protoc --twirpql_out=. service.proto otherservice.proto 101 | ``` 102 | 103 | That said, `service.proto` can import multiple protofiles so breaking out your proto files should not be problem. 104 | 105 | However, each TwirpQL generation is meant for one `service` declaration in a protocol buffer file. If a Protobuf file has one service declaration, that will be the one chosen. If the Protobuf file has multiple service declarations, then you must explicitly specify which service you'd like to generate a GraphQL layer for as such: 106 | 107 | ```bash 108 | protoc --twirpql_out=service=SomeService:. service.proto 109 | ``` 110 | 111 | If you want to generate a GraphQL layer over multiple services, see [here](https://twirpql.dev/docs/multiple-services) 112 | -------------------------------------------------------------------------------- /e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "context" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | "marwan.io/protoc-gen-twirpql/e2e" 11 | "marwan.io/protoc-gen-twirpql/e2e/painters" 12 | "marwan.io/protoc-gen-twirpql/e2e/twirpql" 13 | ) 14 | 15 | func TestHello(t *testing.T) { 16 | s := &service{helloResp: &e2e.HelloResp{Text: "hello"}} 17 | h := twirpql.Handler(s, nil) 18 | w := httptest.NewRecorder() 19 | req := httptest.NewRequest("POST", "/", strings.NewReader(`{ 20 | "operationName": "q", 21 | "variables": { 22 | "req": { 23 | "name": "twirpql" 24 | } 25 | }, 26 | "query": "query q($req: HelloReq) {\n hello(req: $req) {\n text }\n}\n" 27 | }`)) 28 | req.Header.Add("Content-Type", "application/json") 29 | h.ServeHTTP(w, req) 30 | 31 | require.Equal(t, "twirpql", s.helloReq.GetName(), "Expected GraphQL request to populate Twirp Object") 32 | 33 | expected := `{"data":{"hello":{"text":"hello"}}}` 34 | 35 | require.Equal(t, expected, w.Body.String(), "Expected GraphQL query to return valid json") 36 | } 37 | 38 | func TestTrafficJam(t *testing.T) { 39 | s := &service{trafficJamResp: &e2e.TrafficJamResp{Next: e2e.TrafficLight_YELLOW}} 40 | h := twirpql.Handler(s, nil) 41 | w := httptest.NewRecorder() 42 | req := httptest.NewRequest("POST", "/", strings.NewReader(`{ 43 | "operationName": "q", 44 | "variables": { 45 | "req": { 46 | "color": "GREEN", 47 | "trafficLights": ["YELLOW", "RED"] 48 | } 49 | }, 50 | "query": "query q($req: TrafficJamReq) {\n trafficJam(req: $req) {\n next }\n}\n" 51 | }`)) 52 | req.Header.Add("Content-Type", "application/json") 53 | h.ServeHTTP(w, req) 54 | 55 | require.Equal( 56 | t, 57 | e2e.TrafficLight_GREEN, 58 | s.trafficJamReq.GetColor(), 59 | "Expected GraphQL request to populate Twirp Object", 60 | ) 61 | require.Equal( 62 | t, 63 | []e2e.TrafficLight{e2e.TrafficLight_YELLOW, e2e.TrafficLight_RED}, 64 | s.trafficJamReq.GetTrafficLights(), 65 | "Expected repeated enums to be equal", 66 | ) 67 | 68 | expected := `{"data":{"trafficJam":{"next":"YELLOW"}}}` 69 | 70 | require.Equal(t, expected, w.Body.String(), "Expected GraphQL query to return valid json") 71 | } 72 | 73 | func TestPainters(t *testing.T) { 74 | s := &service{paintersResp: &e2e.PaintersResp{ 75 | BestPainter: &painters.Painter{Name: "picasso"}, 76 | AllPainters: []string{"one", "two"}, 77 | }} 78 | h := twirpql.Handler(s, nil) 79 | w := httptest.NewRecorder() 80 | req := httptest.NewRequest("POST", "/", strings.NewReader(`{ 81 | "operationName": "q", 82 | "variables": {}, 83 | "query": "query q {\n getPainters {\n bestPainter {\n name }\n allPainters }\n}\n" 84 | }`)) 85 | req.Header.Add("Content-Type", "application/json") 86 | h.ServeHTTP(w, req) 87 | 88 | expected := `{"data":{"getPainters":{"bestPainter":{"name":"picasso"},"allPainters":["one","two"]}}}` 89 | 90 | require.Equal(t, expected, w.Body.String(), "Expected GraphQL query to return valid json") 91 | } 92 | 93 | func TestTranslate(t *testing.T) { 94 | s := &service{translateResp: &e2e.TranslateResp{ 95 | Translations: map[string]*e2e.Word{ 96 | "english": &e2e.Word{ 97 | Word: "hello", 98 | }, 99 | }, 100 | }} 101 | h := twirpql.Handler(s, nil) 102 | w := httptest.NewRecorder() 103 | req := httptest.NewRequest("POST", "/", strings.NewReader(`{ 104 | "operationName": "q", 105 | "variables": { 106 | "req": { 107 | "words": "{\"english\": {\"word\": \"hello\"}}" 108 | } 109 | }, 110 | "query": "query q($req: TranslateReq) {\n translate(req: $req) {\n translations }\n}\n" 111 | }`)) 112 | req.Header.Add("Content-Type", "application/json") 113 | h.ServeHTTP(w, req) 114 | 115 | require.Equal(t, s.translateReq.GetWords(), map[string]*e2e.Word{"english": {Word: "hello"}}) 116 | 117 | expected := `{"data":{"translate":{"translations":{"english":{"word":"hello"}}}}}` 118 | 119 | require.Equal(t, expected, w.Body.String(), "Expected GraphQL query to return valid json") 120 | } 121 | 122 | func TestBread(t *testing.T) { 123 | s := &service{breadResp: &e2e.BreadResp{ 124 | Answer: &e2e.BreadResp_Toasted{Toasted: true}, 125 | }} 126 | h := twirpql.Handler(s, nil) 127 | w := httptest.NewRecorder() 128 | req := httptest.NewRequest("POST", "/", strings.NewReader(`{ 129 | "operationName": "q", 130 | "variables": { 131 | "req": { 132 | "count": 3 133 | } 134 | }, 135 | "query": "query q($req: BreadReq) {\n bread(req: $req) {\n answer\n {\n __typename } \n }\n}\n" 136 | }`)) 137 | req.Header.Add("Content-Type", "application/json") 138 | h.ServeHTTP(w, req) 139 | 140 | expected := `{"data":{"bread":{"answer":{"__typename":"BreadRespAnswerToasted"}}}}` 141 | require.Equal(t, expected, w.Body.String(), "Expected GraphQL query to return valid json") 142 | 143 | require.Equal(t, s.breadReq.GetCount(), int64(3)) 144 | } 145 | 146 | func TestMutations(t *testing.T) { 147 | s := &service{changeResp: &e2e.ChangeMeResp{ 148 | Name: "james", 149 | Previous: map[string]*e2e.ChangeMeResp{ 150 | "john": &e2e.ChangeMeResp{ 151 | Name: "john", 152 | Previous: map[string]*e2e.ChangeMeResp{ 153 | "jack": &e2e.ChangeMeResp{Name: "jack"}, 154 | }, 155 | }, 156 | }, 157 | Answer: &e2e.ChangeMeResp_Changed{ 158 | Changed: true, 159 | }, 160 | }} 161 | h := twirpql.Handler(s, nil) 162 | w := httptest.NewRecorder() 163 | req := httptest.NewRequest("POST", "/", strings.NewReader(`{ 164 | "operationName": "m", 165 | "variables": { 166 | "req": { 167 | "name": "john", 168 | "previous": "{\"jack\": {\"name\": \"jack\"}}" 169 | } 170 | }, 171 | "query": "mutation m($req: ChangeMeReq) {\n changeMe(req: $req) {\n name\n previous\n answer {\n __typename\n ... on ChangeMeRespAnswerChanged {\n changed\n }\n ... on ChangeMeRespAnswerNewName {\n newName\n }\n }\n }\n}\n" 172 | }`)) 173 | req.Header.Add("Content-Type", "application/json") 174 | h.ServeHTTP(w, req) 175 | 176 | expected := `{"data":{"changeMe":{"name":"james","previous":{"john":{"name":"john","Answer":null,"previous":{"jack":{"name":"jack","Answer":null}}}},"answer":{"__typename":"ChangeMeRespAnswerChanged","changed":true}}}}` 177 | require.Equal(t, expected, w.Body.String(), "Expected GraphQL query to return valid json") 178 | require.Equal(t, s.changeReq.GetName(), "john") 179 | require.Equal(t, s.changeReq.GetPrevious()["jack"].GetName(), "jack") 180 | } 181 | 182 | type service struct { 183 | e2e.Service 184 | helloReq *e2e.HelloReq 185 | helloResp *e2e.HelloResp 186 | trafficJamReq *e2e.TrafficJamReq 187 | trafficJamResp *e2e.TrafficJamResp 188 | paintersReq *e2e.PaintersReq 189 | paintersResp *e2e.PaintersResp 190 | translateReq *e2e.TranslateReq 191 | translateResp *e2e.TranslateResp 192 | breadReq *e2e.BreadReq 193 | breadResp *e2e.BreadResp 194 | changeReq *e2e.ChangeMeReq 195 | changeResp *e2e.ChangeMeResp 196 | err error 197 | } 198 | 199 | func (s *service) Hello(ctx context.Context, req *e2e.HelloReq) (*e2e.HelloResp, error) { 200 | s.helloReq = req 201 | return s.helloResp, s.err 202 | } 203 | 204 | func (s *service) TrafficJam(ctx context.Context, req *e2e.TrafficJamReq) (*e2e.TrafficJamResp, error) { 205 | s.trafficJamReq = req 206 | return s.trafficJamResp, s.err 207 | } 208 | 209 | func (s *service) GetPainters(ctx context.Context, req *e2e.PaintersReq) (*e2e.PaintersResp, error) { 210 | s.paintersReq = req 211 | return s.paintersResp, s.err 212 | } 213 | 214 | func (s *service) Translate(ctx context.Context, req *e2e.TranslateReq) (*e2e.TranslateResp, error) { 215 | s.translateReq = req 216 | return s.translateResp, s.err 217 | } 218 | 219 | func (s *service) Bread(ctx context.Context, req *e2e.BreadReq) (*e2e.BreadResp, error) { 220 | s.breadReq = req 221 | return s.breadResp, s.err 222 | } 223 | 224 | func (s *service) ChangeMe(ctx context.Context, req *e2e.ChangeMeReq) (*e2e.ChangeMeResp, error) { 225 | s.changeReq = req 226 | return s.changeResp, s.err 227 | } 228 | -------------------------------------------------------------------------------- /e2e/gen.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | //go:generate protoc -I . -I /usr/local/include -I ../options --go_out=. --twirp_out=. service.proto 4 | //go:generate protoc -I . -I /usr/local/include -I ../options --twirpql_out=. service.proto 5 | -------------------------------------------------------------------------------- /e2e/painters/gen.go: -------------------------------------------------------------------------------- 1 | package painters 2 | 3 | //go:generate protoc --go_out=paths=source_relative:. painters.proto 4 | -------------------------------------------------------------------------------- /e2e/painters/painters.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: painters.proto 3 | 4 | package painters 5 | 6 | import ( 7 | fmt "fmt" 8 | proto "github.com/golang/protobuf/proto" 9 | math "math" 10 | ) 11 | 12 | // Reference imports to suppress errors if they are not otherwise used. 13 | var _ = proto.Marshal 14 | var _ = fmt.Errorf 15 | var _ = math.Inf 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the proto package it is being compiled against. 19 | // A compilation error at this line likely means your copy of the 20 | // proto package needs to be updated. 21 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 22 | 23 | type Painter struct { 24 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 25 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 26 | XXX_unrecognized []byte `json:"-"` 27 | XXX_sizecache int32 `json:"-"` 28 | } 29 | 30 | func (m *Painter) Reset() { *m = Painter{} } 31 | func (m *Painter) String() string { return proto.CompactTextString(m) } 32 | func (*Painter) ProtoMessage() {} 33 | func (*Painter) Descriptor() ([]byte, []int) { 34 | return fileDescriptor_b32ba4ba83eb3540, []int{0} 35 | } 36 | 37 | func (m *Painter) XXX_Unmarshal(b []byte) error { 38 | return xxx_messageInfo_Painter.Unmarshal(m, b) 39 | } 40 | func (m *Painter) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 41 | return xxx_messageInfo_Painter.Marshal(b, m, deterministic) 42 | } 43 | func (m *Painter) XXX_Merge(src proto.Message) { 44 | xxx_messageInfo_Painter.Merge(m, src) 45 | } 46 | func (m *Painter) XXX_Size() int { 47 | return xxx_messageInfo_Painter.Size(m) 48 | } 49 | func (m *Painter) XXX_DiscardUnknown() { 50 | xxx_messageInfo_Painter.DiscardUnknown(m) 51 | } 52 | 53 | var xxx_messageInfo_Painter proto.InternalMessageInfo 54 | 55 | func (m *Painter) GetName() string { 56 | if m != nil { 57 | return m.Name 58 | } 59 | return "" 60 | } 61 | 62 | func init() { 63 | proto.RegisterType((*Painter)(nil), "painters.Painter") 64 | } 65 | 66 | func init() { proto.RegisterFile("painters.proto", fileDescriptor_b32ba4ba83eb3540) } 67 | 68 | var fileDescriptor_b32ba4ba83eb3540 = []byte{ 69 | // 108 bytes of a gzipped FileDescriptorProto 70 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2b, 0x48, 0xcc, 0xcc, 71 | 0x2b, 0x49, 0x2d, 0x2a, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x80, 0xf1, 0x95, 0x64, 72 | 0xb9, 0xd8, 0x03, 0x20, 0x6c, 0x21, 0x21, 0x2e, 0x96, 0xbc, 0xc4, 0xdc, 0x54, 0x09, 0x46, 0x05, 73 | 0x46, 0x0d, 0xce, 0x20, 0x30, 0xdb, 0x49, 0x3b, 0x4a, 0x33, 0x37, 0xb1, 0xa8, 0x3c, 0x31, 0x4f, 74 | 0x2f, 0x33, 0x5f, 0x1f, 0xac, 0x37, 0x59, 0x37, 0x3d, 0x35, 0x4f, 0xb7, 0xa4, 0x3c, 0xb3, 0xa8, 75 | 0xa0, 0x30, 0x47, 0x3f, 0xd5, 0x28, 0x55, 0x1f, 0x66, 0x56, 0x12, 0x1b, 0x58, 0x81, 0x31, 0x20, 76 | 0x00, 0x00, 0xff, 0xff, 0x72, 0x73, 0x21, 0x89, 0x6e, 0x00, 0x00, 0x00, 77 | } 78 | -------------------------------------------------------------------------------- /e2e/painters/painters.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package painters; 3 | option go_package = "marwan.io/protoc-gen-twirpql/e2e/painters"; 4 | 5 | message Painter { 6 | string name = 1; 7 | // TODO: add painter's style as an enum 8 | } 9 | -------------------------------------------------------------------------------- /e2e/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package e2e; 3 | option go_package = "e2e"; 4 | 5 | import "painters/painters.proto"; 6 | import "options.proto"; 7 | 8 | service Service { 9 | rpc Hello(HelloReq) returns (HelloResp); 10 | rpc TrafficJam(TrafficJamReq) returns (TrafficJamResp); 11 | rpc GetPainters(PaintersReq) returns (PaintersResp); 12 | rpc Translate(TranslateReq) returns (TranslateResp); 13 | rpc Bread(BreadReq) returns (BreadResp); 14 | rpc ChangeMe(ChangeMeReq) returns (ChangeMeResp) { 15 | option (twirpql.options.rpc) = { 16 | mutation: true 17 | }; 18 | }; 19 | } 20 | 21 | message HelloReq { 22 | string name = 1; 23 | } 24 | 25 | message HelloResp { 26 | string text = 1; 27 | } 28 | 29 | message TrafficJamReq { 30 | TrafficLight color = 1; 31 | repeated TrafficLight trafficLights = 2; 32 | } 33 | 34 | enum TrafficLight { 35 | RED = 0; 36 | YELLOW = 1; 37 | GREEN = 2; 38 | } 39 | 40 | message TrafficJamResp { 41 | TrafficLight next = 1; 42 | } 43 | 44 | message PaintersReq { 45 | 46 | } 47 | 48 | message PaintersResp { 49 | painters.Painter bestPainter = 1; 50 | repeated string allPainters = 2; 51 | } 52 | 53 | message TranslateResp { 54 | map translations = 1; 55 | } 56 | 57 | message Word { 58 | string word = 1; 59 | string language = 2; 60 | } 61 | 62 | message TranslateReq { 63 | map words = 1; 64 | } 65 | 66 | message BreadReq { 67 | int64 count = 1; 68 | } 69 | 70 | message BreadResp { 71 | oneof answer { 72 | string name = 1; 73 | bool toasted = 2; 74 | } 75 | } 76 | 77 | message ChangeMeReq { 78 | string name = 1; 79 | map previous = 2; 80 | } 81 | 82 | message ChangeMeResp { 83 | string name = 1; 84 | oneof answer { 85 | string newName = 2; 86 | bool changed = 3; 87 | } 88 | map previous = 4; 89 | } 90 | -------------------------------------------------------------------------------- /e2e/twirpql/enums.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by marwan.io/protoc-gen-twirpql, DO NOT EDIT. 2 | 3 | package twirpql 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | 9 | "github.com/99designs/gqlgen/graphql" 10 | "github.com/vektah/gqlparser/v2/ast" 11 | "marwan.io/protoc-gen-twirpql/e2e" 12 | ) 13 | 14 | func (ec *executionContext) _TrafficLight(ctx context.Context, sel ast.SelectionSet, v *e2e.TrafficLight) graphql.Marshaler { 15 | return graphql.MarshalString((*v).String()) 16 | } 17 | 18 | func (ec *executionContext) unmarshalInputTrafficLight(ctx context.Context, v interface{}) (e2e.TrafficLight, error) { 19 | switch v := v.(type) { 20 | case string: 21 | intValue, ok := e2e.TrafficLight_value[v] 22 | if !ok { 23 | return 0, errors.New("unknown value: " + v) 24 | } 25 | return e2e.TrafficLight(intValue), nil 26 | } 27 | return 0, errors.New("wrong type") 28 | } 29 | -------------------------------------------------------------------------------- /e2e/twirpql/gqlgen.yml: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | schema: 4 | - twirpql/schema.graphql 5 | exec: 6 | filename: twirpql/generated.go 7 | model: 8 | filename: twirpql/models_gen.go 9 | resolver: 10 | filename: twirpql/resolver.go 11 | type: Resolver 12 | dir: "" 13 | autobind: [] 14 | models: 15 | BreadReq: 16 | model: 17 | - marwan.io/protoc-gen-twirpql/e2e.BreadReq 18 | BreadResp: 19 | model: 20 | - marwan.io/protoc-gen-twirpql/e2e.BreadResp 21 | BreadRespAnswer: 22 | model: 23 | - marwan.io/protoc-gen-twirpql/e2e/twirpql.unionMask 24 | BreadRespAnswerName: 25 | model: 26 | - marwan.io/protoc-gen-twirpql/e2e.BreadResp_Name 27 | BreadRespAnswerToasted: 28 | model: 29 | - marwan.io/protoc-gen-twirpql/e2e.BreadResp_Toasted 30 | ChangeMeReq: 31 | model: 32 | - marwan.io/protoc-gen-twirpql/e2e.ChangeMeReq 33 | ChangeMeResp: 34 | model: 35 | - marwan.io/protoc-gen-twirpql/e2e.ChangeMeResp 36 | ChangeMeRespAnswer: 37 | model: 38 | - marwan.io/protoc-gen-twirpql/e2e/twirpql.unionMask 39 | ChangeMeRespAnswerChanged: 40 | model: 41 | - marwan.io/protoc-gen-twirpql/e2e.ChangeMeResp_Changed 42 | ChangeMeRespAnswerNewName: 43 | model: 44 | - marwan.io/protoc-gen-twirpql/e2e.ChangeMeResp_NewName 45 | HelloReq: 46 | model: 47 | - marwan.io/protoc-gen-twirpql/e2e.HelloReq 48 | HelloResp: 49 | model: 50 | - marwan.io/protoc-gen-twirpql/e2e.HelloResp 51 | Painters_Painter: 52 | model: 53 | - marwan.io/protoc-gen-twirpql/e2e/painters.Painter 54 | PaintersResp: 55 | model: 56 | - marwan.io/protoc-gen-twirpql/e2e.PaintersResp 57 | Previous: 58 | model: 59 | - marwan.io/protoc-gen-twirpql/e2e/twirpql.Previous 60 | TrafficJamReq: 61 | model: 62 | - marwan.io/protoc-gen-twirpql/e2e.TrafficJamReq 63 | TrafficJamResp: 64 | model: 65 | - marwan.io/protoc-gen-twirpql/e2e.TrafficJamResp 66 | TrafficLight: 67 | model: 68 | - marwan.io/protoc-gen-twirpql/e2e.TrafficLight 69 | TranslateReq: 70 | model: 71 | - marwan.io/protoc-gen-twirpql/e2e.TranslateReq 72 | TranslateResp: 73 | model: 74 | - marwan.io/protoc-gen-twirpql/e2e.TranslateResp 75 | Translations: 76 | model: 77 | - marwan.io/protoc-gen-twirpql/e2e/twirpql.Translations 78 | Words: 79 | model: 80 | - marwan.io/protoc-gen-twirpql/e2e/twirpql.Words 81 | -------------------------------------------------------------------------------- /e2e/twirpql/resolver.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package twirpql 4 | 5 | import ( 6 | "context" 7 | 8 | "marwan.io/protoc-gen-twirpql/e2e" 9 | ) 10 | 11 | type Resolver struct { 12 | e2e.Service 13 | } 14 | 15 | func (r *Resolver) BreadResp() BreadRespResolver { 16 | return &breadRespResolver{r} 17 | } 18 | func (r *Resolver) ChangeMeResp() ChangeMeRespResolver { 19 | return &changeMeRespResolver{r} 20 | } 21 | func (r *Resolver) Mutation() MutationResolver { 22 | return &mutationResolver{r} 23 | } 24 | func (r *Resolver) Query() QueryResolver { 25 | return &queryResolver{r} 26 | } 27 | func (r *Resolver) TranslateResp() TranslateRespResolver { 28 | return &translateRespResolver{r} 29 | } 30 | 31 | type breadRespResolver struct{ *Resolver } 32 | 33 | func (r *breadRespResolver) Answer(ctx context.Context, obj *e2e.BreadResp) (unionMask, error) { 34 | return obj.GetAnswer(), nil 35 | } 36 | 37 | type changeMeRespResolver struct{ *Resolver } 38 | 39 | func (r *changeMeRespResolver) Previous(ctx context.Context, obj *e2e.ChangeMeResp) (Previous, error) { 40 | return obj.GetPrevious(), nil 41 | } 42 | 43 | func (r *changeMeRespResolver) Answer(ctx context.Context, obj *e2e.ChangeMeResp) (unionMask, error) { 44 | return obj.GetAnswer(), nil 45 | } 46 | 47 | type mutationResolver struct{ *Resolver } 48 | 49 | func (r *mutationResolver) ChangeMe(ctx context.Context, req *e2e.ChangeMeReq) (*e2e.ChangeMeResp, error) { 50 | return r.Service.ChangeMe(ctx, req) 51 | } 52 | 53 | type queryResolver struct{ *Resolver } 54 | 55 | func (r *queryResolver) Hello(ctx context.Context, req *e2e.HelloReq) (*e2e.HelloResp, error) { 56 | return r.Service.Hello(ctx, req) 57 | } 58 | 59 | func (r *queryResolver) TrafficJam(ctx context.Context, req *e2e.TrafficJamReq) (*e2e.TrafficJamResp, error) { 60 | return r.Service.TrafficJam(ctx, req) 61 | } 62 | 63 | func (r *queryResolver) GetPainters(ctx context.Context) (*e2e.PaintersResp, error) { 64 | return r.Service.GetPainters(ctx, nil) 65 | } 66 | 67 | func (r *queryResolver) Translate(ctx context.Context, req *e2e.TranslateReq) (*e2e.TranslateResp, error) { 68 | return r.Service.Translate(ctx, req) 69 | } 70 | 71 | func (r *queryResolver) Bread(ctx context.Context, req *e2e.BreadReq) (*e2e.BreadResp, error) { 72 | return r.Service.Bread(ctx, req) 73 | } 74 | 75 | type translateRespResolver struct{ *Resolver } 76 | 77 | func (r *translateRespResolver) Translations(ctx context.Context, obj *e2e.TranslateResp) (Translations, error) { 78 | return obj.GetTranslations(), nil 79 | } 80 | -------------------------------------------------------------------------------- /e2e/twirpql/scalars.go: -------------------------------------------------------------------------------- 1 | // Code generated by marwan.io/protoc-gen-twirpql, DO NOT EDIT. 2 | 3 | package twirpql 4 | 5 | import ( 6 | "encoding/json" 7 | "io" 8 | 9 | "marwan.io/protoc-gen-twirpql/e2e" 10 | ) 11 | 12 | type Previous map[string]*e2e.ChangeMeResp 13 | 14 | func (scalar *Previous) UnmarshalGQL(v interface{}) error { 15 | str, ok := v.(string) 16 | if !ok { 17 | return nil 18 | } 19 | return json.Unmarshal([]byte(str), scalar) 20 | } 21 | 22 | func (scalar Previous) MarshalGQL(w io.Writer) { 23 | json.NewEncoder(w).Encode(scalar) 24 | } 25 | 26 | type Translations map[string]*e2e.Word 27 | 28 | func (scalar *Translations) UnmarshalGQL(v interface{}) error { 29 | str, ok := v.(string) 30 | if !ok { 31 | return nil 32 | } 33 | return json.Unmarshal([]byte(str), scalar) 34 | } 35 | 36 | func (scalar Translations) MarshalGQL(w io.Writer) { 37 | json.NewEncoder(w).Encode(scalar) 38 | } 39 | 40 | type Words map[string]*e2e.Word 41 | 42 | func (scalar *Words) UnmarshalGQL(v interface{}) error { 43 | str, ok := v.(string) 44 | if !ok { 45 | return nil 46 | } 47 | return json.Unmarshal([]byte(str), scalar) 48 | } 49 | 50 | func (scalar Words) MarshalGQL(w io.Writer) { 51 | json.NewEncoder(w).Encode(scalar) 52 | } 53 | -------------------------------------------------------------------------------- /e2e/twirpql/schema.graphql: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | type Query { 4 | hello(req: HelloReq): HelloResp! 5 | trafficJam(req: TrafficJamReq): TrafficJamResp! 6 | getPainters: PaintersResp! 7 | translate(req: TranslateReq): TranslateResp! 8 | bread(req: BreadReq): BreadResp! 9 | } 10 | 11 | type Mutation { 12 | changeMe(req: ChangeMeReq): ChangeMeResp! 13 | } 14 | 15 | type BreadResp { 16 | answer: BreadRespAnswer! 17 | } 18 | 19 | type BreadRespAnswerName { 20 | name: String! 21 | } 22 | 23 | type BreadRespAnswerToasted { 24 | toasted: Boolean! 25 | } 26 | 27 | type ChangeMeResp { 28 | name: String! 29 | previous: Previous! 30 | answer: ChangeMeRespAnswer! 31 | } 32 | 33 | type ChangeMeRespAnswerChanged { 34 | changed: Boolean! 35 | } 36 | 37 | type ChangeMeRespAnswerNewName { 38 | newName: String! 39 | } 40 | 41 | type HelloResp { 42 | text: String! 43 | } 44 | 45 | type PaintersResp { 46 | bestPainter: Painters_Painter! 47 | allPainters: [String]! 48 | } 49 | 50 | type Painters_Painter { 51 | name: String! 52 | } 53 | 54 | type TrafficJamResp { 55 | next: TrafficLight! 56 | } 57 | 58 | type TranslateResp { 59 | translations: Translations! 60 | } 61 | 62 | input BreadReq { 63 | count: Int! 64 | } 65 | 66 | input ChangeMeReq { 67 | name: String! 68 | previous: Previous! 69 | } 70 | 71 | input HelloReq { 72 | name: String! 73 | } 74 | 75 | input TrafficJamReq { 76 | color: TrafficLight! 77 | trafficLights: [TrafficLight]! 78 | } 79 | 80 | input TranslateReq { 81 | words: Words! 82 | } 83 | 84 | enum TrafficLight { 85 | RED 86 | YELLOW 87 | GREEN 88 | } 89 | 90 | scalar Previous 91 | 92 | scalar Translations 93 | 94 | scalar Words 95 | 96 | union BreadRespAnswer = BreadRespAnswerName | BreadRespAnswerToasted 97 | union ChangeMeRespAnswer = ChangeMeRespAnswerChanged | ChangeMeRespAnswerNewName 98 | -------------------------------------------------------------------------------- /e2e/twirpql/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package twirpql 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | 9 | "github.com/99designs/gqlgen/graphql" 10 | "github.com/99designs/gqlgen/graphql/handler" 11 | "github.com/99designs/gqlgen/graphql/handler/extension" 12 | "github.com/99designs/gqlgen/graphql/handler/transport" 13 | "github.com/99designs/gqlgen/graphql/playground" 14 | "github.com/twitchtv/twirp" 15 | "github.com/twitchtv/twirp/ctxsetters" 16 | "marwan.io/protoc-gen-twirpql/e2e" 17 | ) 18 | 19 | // Playground is a proxy to github.com/99designs/gqlgen/handler.Playground 20 | // All you need to do is provide a title and the URL Path to the GraphQL handler 21 | func Playground(title, endpoint string) http.Handler { 22 | return playground.Handler(title, endpoint) 23 | } 24 | 25 | // Handler returns a handler to the GraphQL API. 26 | // Server Hooks are optional but if present, they will 27 | // be injected as GraphQL middleware. 28 | func Handler(service e2e.Service, hooks *twirp.ServerHooks) *handler.Server { 29 | es := NewExecutableSchema(Config{Resolvers: &Resolver{service}}) 30 | srv := handler.New(es) 31 | srv.AddTransport(transport.POST{}) 32 | srv.Use(extension.Introspection{}) 33 | if hooks == nil { 34 | return srv 35 | } 36 | srv.AroundFields(func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) { 37 | f := graphql.GetFieldContext(ctx) 38 | parent := f.Parent.Path().String() 39 | if parent != "" { 40 | return next(ctx) 41 | } 42 | ctx = ctxsetters.WithMethodName(ctx, f.Field.Name) 43 | if hooks.RequestRouted != nil { 44 | ctx, err = hooks.RequestRouted(ctx) 45 | if err != nil { 46 | if terr, ok := err.(twirp.Error); ok && hooks.Error != nil { 47 | ctx = hooks.Error(ctx, terr) 48 | } 49 | return nil, err 50 | } 51 | } 52 | res, err = next(ctx) 53 | if terr, ok := err.(twirp.Error); ok && hooks.Error != nil { 54 | ctx = hooks.Error(ctx, terr) 55 | } 56 | return res, err 57 | }) 58 | return srv 59 | } 60 | -------------------------------------------------------------------------------- /e2e/twirpql/unions.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by marwan.io/protoc-gen-twirpql, DO NOT EDIT. 2 | 3 | package twirpql 4 | 5 | type unionMask interface{} 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module marwan.io/protoc-gen-twirpql 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.11.2 7 | github.com/golang/protobuf v1.3.4 8 | github.com/lyft/protoc-gen-star v0.4.14 9 | github.com/spf13/afero v1.2.2 // indirect 10 | github.com/stretchr/testify v1.5.1 11 | github.com/twitchtv/twirp v5.10.1+incompatible 12 | github.com/vektah/gqlparser/v2 v2.0.1 13 | gopkg.in/yaml.v2 v2.2.8 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/gqlgen v0.11.2 h1:qatIx2DY7YyaUIBd47ORY3Aj/+pJsPLoL7tyuuISJR0= 2 | github.com/99designs/gqlgen v0.11.2/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ= 5 | github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= 6 | github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0= 7 | github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs= 8 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= 9 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 10 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= 11 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 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/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c h1:TUuUh0Xgj97tLMNtWtNvI9mIV6isjEb9lBMNv+77IGM= 18 | github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= 19 | github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 20 | github.com/gogo/protobuf v1.0.0 h1:2jyBKDKU/8v3v2xVR2PtiWQviFUyiaGk2rpfyFT8rTM= 21 | github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 22 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 23 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 24 | github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= 25 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 26 | github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 27 | github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 28 | github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= 29 | github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 30 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 31 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 32 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 33 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 34 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 35 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 36 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 37 | github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 38 | github.com/lyft/protoc-gen-star v0.4.9 h1:dm1l/IvkdW3IzdwrqPu2ywcBdqZ7hp+ppzN/AJZg0Z0= 39 | github.com/lyft/protoc-gen-star v0.4.9/go.mod h1:mE8fbna26u7aEA2QCVvvfBU/ZrPgocG1206xAFPcs94= 40 | github.com/lyft/protoc-gen-star v0.4.14 h1:HUkD4H4dYFIgu3Bns/3N6J5GmKHCEGnhYBwNu3fvXgA= 41 | github.com/lyft/protoc-gen-star v0.4.14/go.mod h1:mE8fbna26u7aEA2QCVvvfBU/ZrPgocG1206xAFPcs94= 42 | github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg= 43 | github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= 44 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 45 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 46 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 47 | github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8= 48 | github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 49 | github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= 50 | github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 51 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 52 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 53 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 54 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 55 | github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 56 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 57 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 58 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 59 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 60 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 61 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 62 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 63 | github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= 64 | github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= 65 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 66 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 67 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 68 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 69 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 70 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 71 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 72 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 73 | github.com/twitchtv/twirp v5.10.1+incompatible h1:35js8ID9rYPKkZ0qWnuZw+q+OuCWM1GIibu1F1YImjA= 74 | github.com/twitchtv/twirp v5.10.1+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= 75 | github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= 76 | github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 77 | github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg= 78 | github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= 79 | github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o= 80 | github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms= 81 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 82 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 83 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 84 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 85 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 86 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 87 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 88 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 89 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 90 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 93 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 94 | golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 95 | golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw= 96 | golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 97 | golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM= 98 | golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 99 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 100 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 101 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 102 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 103 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 104 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 105 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 106 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 107 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 108 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 109 | sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= 110 | sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= 111 | -------------------------------------------------------------------------------- /internal/genenums/genenums.go: -------------------------------------------------------------------------------- 1 | package genenums 2 | 3 | import ( 4 | "bytes" 5 | "go/format" 6 | "io" 7 | ) 8 | 9 | // Data is the Data that's needed 10 | // to bridge protocol buffer enums 11 | // and gql enums. 12 | type Data struct { 13 | ImportPath string 14 | Pkg string 15 | Name string 16 | GoName string 17 | } 18 | 19 | type final struct { 20 | Imports []string 21 | Enums []*Data 22 | } 23 | 24 | // Render extends gqlgen's exectuionContext 25 | // to map protobuf enums to gql enums 26 | func Render(data []*Data, out io.Writer) error { 27 | var b bytes.Buffer 28 | final := &final{} 29 | mp := map[string]struct{}{} 30 | for _, d := range data { 31 | mp[d.ImportPath] = struct{}{} 32 | final.Enums = append(final.Enums, d) 33 | } 34 | for k := range mp { 35 | final.Imports = append(final.Imports, k) 36 | } 37 | err := enumTemplate.Execute(&b, final) 38 | if err != nil { 39 | return err 40 | } 41 | bts, err := format.Source(b.Bytes()) 42 | if err != nil { 43 | return err 44 | } 45 | _, err = io.Copy(out, bytes.NewReader(bts)) 46 | return err 47 | } 48 | -------------------------------------------------------------------------------- /internal/genenums/goenums_test.go: -------------------------------------------------------------------------------- 1 | package genenums 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "io/ioutil" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var update = flag.Bool("update", false, "rewrite all golden files") 13 | 14 | func TestGenEnums(t *testing.T) { 15 | d := &Data{ 16 | ImportPath: "pkg.go/enums", 17 | Pkg: "enums", 18 | Name: "one", 19 | GoName: "one", 20 | } 21 | 22 | var b bytes.Buffer 23 | err := Render([]*Data{d}, &b) 24 | require.NoError(t, err) 25 | 26 | if *update { 27 | ioutil.WriteFile("testdata/enums.golden", b.Bytes(), 0660) 28 | return 29 | } 30 | 31 | expected, err := ioutil.ReadFile("testdata/enums.golden") 32 | require.NoError(t, err) 33 | require.Equal(t, string(expected), b.String()) 34 | } 35 | -------------------------------------------------------------------------------- /internal/genenums/template.go: -------------------------------------------------------------------------------- 1 | package genenums 2 | 3 | import "text/template" 4 | 5 | var enumTemplate = template.Must(template.New("").Parse(`// Code generated by marwan.io/protoc-gen-twirpql, DO NOT EDIT. 6 | 7 | package twirpql 8 | 9 | import ( 10 | "context" 11 | "errors" 12 | 13 | {{ range .Imports }} 14 | "{{.}}"{{ end }} 15 | "github.com/99designs/gqlgen/graphql" 16 | "github.com/vektah/gqlparser/v2/ast" 17 | ) 18 | {{ range .Enums }} 19 | func (ec *executionContext) _{{ .Name }}(ctx context.Context, sel ast.SelectionSet, v *{{.Pkg}}.{{.GoName}}) graphql.Marshaler { 20 | return graphql.MarshalString((*v).String()) 21 | } 22 | 23 | func (ec *executionContext) unmarshalInput{{.Name}}(ctx context.Context, v interface{}) ({{.Pkg}}.{{.GoName}}, error) { 24 | switch v := v.(type) { 25 | case string: 26 | intValue, ok := {{.Pkg}}.{{.GoName}}_value[v] 27 | if !ok { 28 | return 0, errors.New("unknown value: " + v) 29 | } 30 | return {{.Pkg}}.{{.GoName}}(intValue), nil 31 | } 32 | return 0, errors.New("wrong type") 33 | } 34 | {{ end }} 35 | `)) 36 | -------------------------------------------------------------------------------- /internal/genenums/testdata/enums.golden: -------------------------------------------------------------------------------- 1 | // Code generated by marwan.io/protoc-gen-twirpql, DO NOT EDIT. 2 | 3 | package twirpql 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | 9 | "github.com/99designs/gqlgen/graphql" 10 | "github.com/vektah/gqlparser/v2/ast" 11 | "pkg.go/enums" 12 | ) 13 | 14 | func (ec *executionContext) _one(ctx context.Context, sel ast.SelectionSet, v *enums.one) graphql.Marshaler { 15 | return graphql.MarshalString((*v).String()) 16 | } 17 | 18 | func (ec *executionContext) unmarshalInputone(ctx context.Context, v interface{}) (enums.one, error) { 19 | switch v := v.(type) { 20 | case string: 21 | intValue, ok := enums.one_value[v] 22 | if !ok { 23 | return 0, errors.New("unknown value: " + v) 24 | } 25 | return enums.one(intValue), nil 26 | } 27 | return 0, errors.New("wrong type") 28 | } 29 | -------------------------------------------------------------------------------- /internal/genresolver/genresolver.go: -------------------------------------------------------------------------------- 1 | package genresolver 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "text/template" 7 | 8 | "github.com/99designs/gqlgen/codegen" 9 | "github.com/99designs/gqlgen/codegen/templates" 10 | "github.com/99designs/gqlgen/plugin" 11 | ) 12 | 13 | func New( 14 | serviceName, 15 | pkgName string, 16 | emptys []string, 17 | scalars map[string]string, 18 | unions map[string]bool, 19 | responseUnions map[string]string, 20 | sdl string, 21 | ) plugin.Plugin { 22 | return &Plugin{ 23 | ServiceName: serviceName, 24 | PackageName: pkgName, 25 | Emptys: emptys, 26 | Scalars: scalars, 27 | Unions: unions, 28 | ResponseUnions: responseUnions, 29 | SDL: sdl, 30 | } 31 | } 32 | 33 | type Plugin struct { 34 | ServiceName string 35 | PackageName string 36 | Emptys []string 37 | Scalars map[string]string 38 | Unions map[string]bool 39 | ResponseUnions map[string]string 40 | SDL string 41 | } 42 | 43 | func (m *Plugin) isEmpty(f *codegen.Field) bool { 44 | name := templates.CurrentImports.LookupType(f.TypeReference.GO) 45 | for _, e := range m.Emptys { 46 | if name == "*"+e { 47 | return true 48 | } 49 | } 50 | return false 51 | } 52 | 53 | var _ plugin.CodeGenerator = &Plugin{} 54 | 55 | func (m *Plugin) Name() string { 56 | return "resovleroverride" 57 | } 58 | 59 | func (m *Plugin) GenerateCode(data *codegen.Data) error { 60 | if !data.Config.Resolver.IsDefined() { 61 | return nil 62 | } 63 | 64 | resolverBuild := &ResolverBuild{ 65 | Data: data, 66 | PackageName: data.Config.Resolver.Package, 67 | ResolverType: data.Config.Resolver.Type, 68 | ServiceName: m.ServiceName, 69 | ServicePackageName: m.PackageName, 70 | SDL: m.SDL, 71 | Federated: m.SDL != "", 72 | } 73 | 74 | return templates.Render(templates.Options{ 75 | GeneratedHeader: true, 76 | Template: tmpl, 77 | PackageName: data.Config.Resolver.Package, 78 | Filename: data.Config.Resolver.Filename, 79 | Data: resolverBuild, 80 | Packages: data.Config.Packages, 81 | Funcs: template.FuncMap{ 82 | "hasPrefix": hasPrefix, 83 | "isEmpty": m.isEmpty, 84 | "getType": func(f *codegen.Field) string { 85 | return strings.Replace(templates.CurrentImports.LookupType(f.TypeReference.GO), "*", "&", 1) 86 | }, 87 | "isScalar": func(s string) bool { 88 | _, ok := m.Scalars[s] 89 | return ok 90 | }, 91 | "isUnion": func(s string) bool { 92 | _, ok := m.Unions[s] 93 | return ok 94 | }, 95 | "isResponseUnion": func(s string) bool { 96 | _, ok := m.ResponseUnions[s] 97 | return ok 98 | }, 99 | "responseUnionName": func(s string) string { 100 | return m.ResponseUnions[s] 101 | }, 102 | "q": func(s string) string { 103 | return fmt.Sprintf("%q", s) 104 | }, 105 | }, 106 | }) 107 | } 108 | 109 | type ResolverBuild struct { 110 | *codegen.Data 111 | 112 | PackageName string 113 | ResolverType string 114 | ServiceName string 115 | ServicePackageName string 116 | SDL string 117 | Federated bool 118 | } 119 | 120 | func hasPrefix(s, prefix string) bool { 121 | return strings.HasPrefix(s, prefix) 122 | } 123 | -------------------------------------------------------------------------------- /internal/genresolver/template.go: -------------------------------------------------------------------------------- 1 | package genresolver 2 | 3 | var tmpl = ` 4 | {{ reserveImport "context" }} 5 | {{ reserveImport "fmt" }} 6 | {{ reserveImport "io" }} 7 | {{ reserveImport "strconv" }} 8 | {{ reserveImport "time" }} 9 | {{ reserveImport "sync" }} 10 | {{ reserveImport "errors" }} 11 | {{ reserveImport "bytes" }} 12 | 13 | {{ reserveImport "github.com/99designs/gqlgen/handler" }} 14 | {{ reserveImport "github.com/vektah/gqlparser/v2" }} 15 | {{ reserveImport "github.com/vektah/gqlparser/v2/ast" }} 16 | {{ reserveImport "github.com/99designs/gqlgen/graphql" }} 17 | {{ reserveImport "github.com/99designs/gqlgen/graphql/introspection" }} 18 | {{ $serviceName := .ServiceName }} 19 | {{ $servicePackageName := .ServicePackageName }} 20 | {{ $federated := .Federated }} 21 | {{ $sdl := .SDL }} 22 | type {{.ResolverType}} struct { 23 | {{$servicePackageName}}.{{$serviceName}} 24 | } 25 | 26 | {{ range $object := .Objects -}} 27 | {{- if $object.HasResolvers -}} 28 | func (r *{{$.ResolverType}}) {{$object.Name}}() {{ $object.ResolverInterface | ref }} { 29 | return &{{lcFirst $object.Name}}Resolver{r} 30 | } 31 | {{ end -}} 32 | {{ end }} 33 | 34 | {{ range $object := .Objects -}} 35 | {{- if $object.HasResolvers -}} 36 | type {{lcFirst $object.Name}}Resolver struct { *Resolver } 37 | 38 | {{ range $field := $object.Fields -}} 39 | {{- if $field.IsResolver -}} 40 | func (r *{{lcFirst $object.Name}}Resolver) {{$field.GoFieldName}}{{ $field.ShortResolverDeclaration }} { 41 | {{- $reqArg := "req" -}} 42 | {{- if (hasPrefix ($field.ShortResolverDeclaration) "(ctx context.Context)") -}} 43 | {{ $reqArg = "nil" }} 44 | {{ end -}} 45 | {{- if (and $federated (eq ($field.GoFieldName) "_service")) -}} 46 | return &_Service{Sdl: {{q $sdl}}}, nil 47 | {{ else if (isEmpty $field) }} 48 | _, err := r.{{$serviceName}}.{{$field.GoFieldName}}(ctx, {{$reqArg}}) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return {{getType $field}}{}, nil 53 | {{ else if (isScalar ($field.GoFieldName)) }} 54 | return obj.Get{{$field.GoFieldName}}(), nil 55 | {{ else if (isUnion ($field.GoFieldName)) }} 56 | return obj.Get{{$field.GoFieldName}}(), nil 57 | {{ else if (isResponseUnion ($field.GoFieldName)) }} 58 | resp, err := r.{{$serviceName}}.{{$field.GoFieldName}}(ctx, {{$reqArg}}) 59 | if err != nil { 60 | {{ $errorTypeName := (responseUnionName ($field.GoFieldName)) }} 61 | if errval, ok := err.(interface { 62 | {{$errorTypeName}}() *{{$servicePackageName}}.{{$errorTypeName}} 63 | }); ok { 64 | newresp := errval.{{$errorTypeName}}() 65 | if newresp != nil { 66 | return newresp, nil 67 | } 68 | } 69 | } 70 | return resp, err 71 | {{- else -}} 72 | return r.{{$serviceName}}.{{$field.GoFieldName}}(ctx, {{$reqArg}}) 73 | {{ end -}} 74 | } 75 | {{ end }} 76 | {{ end -}} 77 | {{ end -}} 78 | {{ end }} 79 | ` 80 | -------------------------------------------------------------------------------- /internal/genscalar/genscalar.go: -------------------------------------------------------------------------------- 1 | package genscalar 2 | 3 | import ( 4 | "bytes" 5 | "go/format" 6 | "io" 7 | "text/template" 8 | ) 9 | 10 | var tmpl = template.Must(template.New("").Parse(templateText)) 11 | 12 | type data struct { 13 | Imports []string 14 | Types map[string]string 15 | } 16 | 17 | // Render renders a scalar 18 | // implementation. 19 | func Render(mp map[string]string, imports map[string]struct{}, out io.Writer) error { 20 | d := &data{Types: mp} 21 | for i := range imports { 22 | d.Imports = append(d.Imports, i) 23 | } 24 | var b bytes.Buffer 25 | err := tmpl.Execute(&b, d) 26 | if err != nil { 27 | return err 28 | } 29 | bts, err := format.Source(b.Bytes()) 30 | if err != nil { 31 | return err 32 | } 33 | _, err = io.Copy(out, bytes.NewReader(bts)) 34 | return err 35 | } 36 | 37 | const templateText = `// Code generated by marwan.io/protoc-gen-twirpql, DO NOT EDIT. 38 | 39 | package twirpql 40 | 41 | import ( 42 | "encoding/json" 43 | "io" 44 | {{ range .Imports }} 45 | "{{ . }}" 46 | {{ end }} 47 | ) 48 | 49 | {{range $key, $val := .Types}} 50 | type {{$key}} {{$val}} 51 | 52 | func (scalar *{{$key}}) UnmarshalGQL(v interface{}) error { 53 | str, ok := v.(string) 54 | if !ok { 55 | return nil 56 | } 57 | return json.Unmarshal([]byte(str), scalar) 58 | } 59 | 60 | func (scalar {{$key}}) MarshalGQL(w io.Writer) { 61 | json.NewEncoder(w).Encode(scalar) 62 | } 63 | {{end}}` 64 | -------------------------------------------------------------------------------- /internal/genscalar/genscalar_test.go: -------------------------------------------------------------------------------- 1 | package genscalar 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "io/ioutil" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var update = flag.Bool("update", false, "rewrite all golden files") 13 | 14 | func TestGenScalar(t *testing.T) { 15 | d := map[string]string{ 16 | "MyMap": "map[string]string", 17 | "MyInts": "map[int64]string", 18 | } 19 | 20 | var b bytes.Buffer 21 | err := Render(d, nil, &b) 22 | require.NoError(t, err) 23 | 24 | if *update { 25 | ioutil.WriteFile("testdata/scalars.golden", b.Bytes(), 0660) 26 | return 27 | } 28 | 29 | expected, err := ioutil.ReadFile("testdata/scalars.golden") 30 | require.NoError(t, err) 31 | require.Equal(t, string(expected), b.String()) 32 | } 33 | -------------------------------------------------------------------------------- /internal/genscalar/testdata/scalars.golden: -------------------------------------------------------------------------------- 1 | // Code generated by marwan.io/protoc-gen-twirpql, DO NOT EDIT. 2 | 3 | package twirpql 4 | 5 | import ( 6 | "encoding/json" 7 | "io" 8 | ) 9 | 10 | type MyInts map[int64]string 11 | 12 | func (scalar *MyInts) UnmarshalGQL(v interface{}) error { 13 | str, ok := v.(string) 14 | if !ok { 15 | return nil 16 | } 17 | return json.Unmarshal([]byte(str), scalar) 18 | } 19 | 20 | func (scalar MyInts) MarshalGQL(w io.Writer) { 21 | json.NewEncoder(w).Encode(scalar) 22 | } 23 | 24 | type MyMap map[string]string 25 | 26 | func (scalar *MyMap) UnmarshalGQL(v interface{}) error { 27 | str, ok := v.(string) 28 | if !ok { 29 | return nil 30 | } 31 | return json.Unmarshal([]byte(str), scalar) 32 | } 33 | 34 | func (scalar MyMap) MarshalGQL(w io.Writer) { 35 | json.NewEncoder(w).Encode(scalar) 36 | } 37 | -------------------------------------------------------------------------------- /internal/genserver/genserver.go: -------------------------------------------------------------------------------- 1 | package genserver 2 | 3 | import ( 4 | "github.com/99designs/gqlgen/codegen" 5 | "github.com/99designs/gqlgen/codegen/templates" 6 | "github.com/99designs/gqlgen/plugin" 7 | ) 8 | 9 | func New(filename, modPath, serviceName string) plugin.Plugin { 10 | return &Plugin{filename, modPath, serviceName} 11 | } 12 | 13 | type Plugin struct { 14 | filename string 15 | modPath string 16 | serviceName string 17 | } 18 | 19 | var _ plugin.CodeGenerator = &Plugin{} 20 | 21 | func (m *Plugin) Name() string { 22 | return "servergen" 23 | } 24 | 25 | func (m *Plugin) GenerateCode(data *codegen.Data) error { 26 | serverBuild := &ServerBuild{ 27 | Data: data, 28 | ExecPackageName: data.Config.Exec.ImportPath(), 29 | ResolverPackageName: data.Config.Resolver.ImportPath(), 30 | ModPath: m.modPath, 31 | ServiceName: m.serviceName, 32 | } 33 | 34 | return templates.Render(templates.Options{ 35 | GeneratedHeader: true, 36 | Template: tmpl, 37 | PackageName: "twirpql", // TODO: dynamic package name 38 | Filename: m.filename, 39 | Data: serverBuild, 40 | Packages: data.Config.Packages, 41 | }) 42 | } 43 | 44 | type ServerBuild struct { 45 | *codegen.Data 46 | 47 | ExecPackageName string 48 | ResolverPackageName string 49 | ModPath string 50 | ServiceName string 51 | } 52 | -------------------------------------------------------------------------------- /internal/genserver/template.go: -------------------------------------------------------------------------------- 1 | package genserver 2 | 3 | var tmpl = `{{ reserveImport "context" }} 4 | {{ reserveImport "net/http" }} 5 | 6 | {{ reserveImport "github.com/99designs/gqlgen/graphql/handler" }} 7 | {{ reserveImport "github.com/twitchtv/twirp" }} 8 | {{ reserveImport "github.com/twitchtv/twirp/ctxsetters" }} 9 | {{ reserveImport "github.com/99designs/gqlgen/graphql" }} 10 | {{ reserveImport "github.com/99designs/gqlgen/graphql/handler/extension" }} 11 | {{ reserveImport "github.com/99designs/gqlgen/graphql/handler/transport" }} 12 | {{ reserveImport "github.com/99designs/gqlgen/graphql/playground" }} 13 | 14 | // Playground is a proxy to github.com/99designs/gqlgen/handler.Playground 15 | // All you need to do is provide a title and the URL Path to the GraphQL handler 16 | func Playground(title, endpoint string) http.Handler { 17 | return playground.Handler(title, endpoint) 18 | } 19 | 20 | // Handler returns a handler to the GraphQL API. 21 | // Server Hooks are optional but if present, they will 22 | // be injected as GraphQL middleware. 23 | func Handler(service {{lookupImport .ModPath}}.{{.ServiceName}}, hooks *twirp.ServerHooks) *handler.Server { 24 | es := NewExecutableSchema(Config{Resolvers: &Resolver{service}}) 25 | srv := handler.New(es) 26 | srv.AddTransport(transport.POST{}) 27 | srv.Use(extension.Introspection{}) 28 | if hooks == nil { 29 | return srv 30 | } 31 | srv.AroundFields(func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) { 32 | f := graphql.GetFieldContext(ctx) 33 | parent := f.Parent.Path().String() 34 | if parent != "" { 35 | return next(ctx) 36 | } 37 | ctx = ctxsetters.WithMethodName(ctx, f.Field.Name) 38 | if hooks.RequestRouted != nil { 39 | ctx, err = hooks.RequestRouted(ctx) 40 | if err != nil { 41 | if terr, ok := err.(twirp.Error); ok && hooks.Error != nil { 42 | ctx = hooks.Error(ctx, terr) 43 | } 44 | return nil, err 45 | } 46 | } 47 | res, err = next(ctx) 48 | if terr, ok := err.(twirp.Error); ok && hooks.Error != nil { 49 | ctx = hooks.Error(ctx, terr) 50 | } 51 | return res, err 52 | }) 53 | return srv 54 | } 55 | ` 56 | -------------------------------------------------------------------------------- /internal/genunions/genunions.go: -------------------------------------------------------------------------------- 1 | package genunions 2 | 3 | import ( 4 | "bytes" 5 | "go/format" 6 | "io" 7 | "text/template" 8 | ) 9 | 10 | var tmpl = template.Must(template.New("genunions").Parse(tmplStr)) 11 | 12 | // Render renders a union 13 | func Render(w io.Writer) error { 14 | var bts bytes.Buffer 15 | err := tmpl.Execute(&bts, nil) 16 | if err != nil { 17 | return err 18 | } 19 | formatted, err := format.Source(bts.Bytes()) 20 | if err != nil { 21 | return err 22 | } 23 | _, err = io.Copy(w, bytes.NewReader(formatted)) 24 | return err 25 | } 26 | 27 | const tmplStr = `// Code generated by marwan.io/protoc-gen-twirpql, DO NOT EDIT. 28 | 29 | package twirpql 30 | 31 | type unionMask interface {} 32 | ` 33 | -------------------------------------------------------------------------------- /internal/gocopy/lazyregexp/lazyre.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package lazyregexp is a thin wrapper over regexp, allowing the use of global 6 | // regexp variables without forcing them to be compiled at init. 7 | package lazyregexp 8 | 9 | import ( 10 | "os" 11 | "regexp" 12 | "strings" 13 | "sync" 14 | ) 15 | 16 | // Regexp is a wrapper around regexp.Regexp, where the underlying regexp will be 17 | // compiled the first time it is needed. 18 | type Regexp struct { 19 | str string 20 | once sync.Once 21 | rx *regexp.Regexp 22 | } 23 | 24 | func (r *Regexp) re() *regexp.Regexp { 25 | r.once.Do(r.build) 26 | return r.rx 27 | } 28 | 29 | func (r *Regexp) build() { 30 | r.rx = regexp.MustCompile(r.str) 31 | r.str = "" 32 | } 33 | 34 | func (r *Regexp) FindSubmatch(s []byte) [][]byte { 35 | return r.re().FindSubmatch(s) 36 | } 37 | 38 | func (r *Regexp) FindStringSubmatch(s string) []string { 39 | return r.re().FindStringSubmatch(s) 40 | } 41 | 42 | func (r *Regexp) FindStringSubmatchIndex(s string) []int { 43 | return r.re().FindStringSubmatchIndex(s) 44 | } 45 | 46 | func (r *Regexp) ReplaceAllString(src, repl string) string { 47 | return r.re().ReplaceAllString(src, repl) 48 | } 49 | 50 | func (r *Regexp) FindString(s string) string { 51 | return r.re().FindString(s) 52 | } 53 | 54 | func (r *Regexp) FindAllString(s string, n int) []string { 55 | return r.re().FindAllString(s, n) 56 | } 57 | 58 | func (r *Regexp) MatchString(s string) bool { 59 | return r.re().MatchString(s) 60 | } 61 | 62 | func (r *Regexp) SubexpNames() []string { 63 | return r.re().SubexpNames() 64 | } 65 | 66 | var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test") 67 | 68 | // New creates a new lazy regexp, delaying the compiling work until it is first 69 | // needed. If the code is being run as part of tests, the regexp compiling will 70 | // happen immediately. 71 | func New(str string) *Regexp { 72 | lr := &Regexp{str: str} 73 | if inTest { 74 | // In tests, always compile the regexps early. 75 | lr.re() 76 | } 77 | return lr 78 | } 79 | -------------------------------------------------------------------------------- /internal/gocopy/modfile/gopkgin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // TODO: Figure out what gopkg.in should do. 6 | 7 | package modfile 8 | 9 | import "strings" 10 | 11 | // ParseGopkgIn splits gopkg.in import paths into their constituent parts 12 | func ParseGopkgIn(path string) (root, repo, major, subdir string, ok bool) { 13 | if !strings.HasPrefix(path, "gopkg.in/") { 14 | return 15 | } 16 | f := strings.Split(path, "/") 17 | if len(f) >= 2 { 18 | if elem, v, ok := dotV(f[1]); ok { 19 | root = strings.Join(f[:2], "/") 20 | repo = "github.com/go-" + elem + "/" + elem 21 | major = v 22 | subdir = strings.Join(f[2:], "/") 23 | return root, repo, major, subdir, true 24 | } 25 | } 26 | if len(f) >= 3 { 27 | if elem, v, ok := dotV(f[2]); ok { 28 | root = strings.Join(f[:3], "/") 29 | repo = "github.com/" + f[1] + "/" + elem 30 | major = v 31 | subdir = strings.Join(f[3:], "/") 32 | return root, repo, major, subdir, true 33 | } 34 | } 35 | return 36 | } 37 | 38 | func dotV(name string) (elem, v string, ok bool) { 39 | i := len(name) - 1 40 | for i >= 0 && '0' <= name[i] && name[i] <= '9' { 41 | i-- 42 | } 43 | if i <= 2 || i+1 >= len(name) || name[i-1] != '.' || name[i] != 'v' || name[i+1] == '0' && len(name) != i+2 { 44 | return "", "", false 45 | } 46 | return name[:i-1], name[i:], true 47 | } 48 | -------------------------------------------------------------------------------- /internal/gocopy/modfile/print.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Module file printer. 6 | 7 | package modfile 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "strings" 13 | ) 14 | 15 | func Format(f *FileSyntax) []byte { 16 | pr := &printer{} 17 | pr.file(f) 18 | return pr.Bytes() 19 | } 20 | 21 | // A printer collects the state during printing of a file or expression. 22 | type printer struct { 23 | bytes.Buffer // output buffer 24 | comment []Comment // pending end-of-line comments 25 | margin int // left margin (indent), a number of tabs 26 | } 27 | 28 | // printf prints to the buffer. 29 | func (p *printer) printf(format string, args ...interface{}) { 30 | fmt.Fprintf(p, format, args...) 31 | } 32 | 33 | // indent returns the position on the current line, in bytes, 0-indexed. 34 | func (p *printer) indent() int { 35 | b := p.Bytes() 36 | n := 0 37 | for n < len(b) && b[len(b)-1-n] != '\n' { 38 | n++ 39 | } 40 | return n 41 | } 42 | 43 | // newline ends the current line, flushing end-of-line comments. 44 | func (p *printer) newline() { 45 | if len(p.comment) > 0 { 46 | p.printf(" ") 47 | for i, com := range p.comment { 48 | if i > 0 { 49 | p.trim() 50 | p.printf("\n") 51 | for i := 0; i < p.margin; i++ { 52 | p.printf("\t") 53 | } 54 | } 55 | p.printf("%s", strings.TrimSpace(com.Token)) 56 | } 57 | p.comment = p.comment[:0] 58 | } 59 | 60 | p.trim() 61 | p.printf("\n") 62 | for i := 0; i < p.margin; i++ { 63 | p.printf("\t") 64 | } 65 | } 66 | 67 | // trim removes trailing spaces and tabs from the current line. 68 | func (p *printer) trim() { 69 | // Remove trailing spaces and tabs from line we're about to end. 70 | b := p.Bytes() 71 | n := len(b) 72 | for n > 0 && (b[n-1] == '\t' || b[n-1] == ' ') { 73 | n-- 74 | } 75 | p.Truncate(n) 76 | } 77 | 78 | // file formats the given file into the print buffer. 79 | func (p *printer) file(f *FileSyntax) { 80 | for _, com := range f.Before { 81 | p.printf("%s", strings.TrimSpace(com.Token)) 82 | p.newline() 83 | } 84 | 85 | for i, stmt := range f.Stmt { 86 | switch x := stmt.(type) { 87 | case *CommentBlock: 88 | // comments already handled 89 | p.expr(x) 90 | 91 | default: 92 | p.expr(x) 93 | p.newline() 94 | } 95 | 96 | for _, com := range stmt.Comment().After { 97 | p.printf("%s", strings.TrimSpace(com.Token)) 98 | p.newline() 99 | } 100 | 101 | if i+1 < len(f.Stmt) { 102 | p.newline() 103 | } 104 | } 105 | } 106 | 107 | func (p *printer) expr(x Expr) { 108 | // Emit line-comments preceding this expression. 109 | if before := x.Comment().Before; len(before) > 0 { 110 | // Want to print a line comment. 111 | // Line comments must be at the current margin. 112 | p.trim() 113 | if p.indent() > 0 { 114 | // There's other text on the line. Start a new line. 115 | p.printf("\n") 116 | } 117 | // Re-indent to margin. 118 | for i := 0; i < p.margin; i++ { 119 | p.printf("\t") 120 | } 121 | for _, com := range before { 122 | p.printf("%s", strings.TrimSpace(com.Token)) 123 | p.newline() 124 | } 125 | } 126 | 127 | switch x := x.(type) { 128 | default: 129 | panic(fmt.Errorf("printer: unexpected type %T", x)) 130 | 131 | case *CommentBlock: 132 | // done 133 | 134 | case *LParen: 135 | p.printf("(") 136 | case *RParen: 137 | p.printf(")") 138 | 139 | case *Line: 140 | sep := "" 141 | for _, tok := range x.Token { 142 | p.printf("%s%s", sep, tok) 143 | sep = " " 144 | } 145 | 146 | case *LineBlock: 147 | for _, tok := range x.Token { 148 | p.printf("%s ", tok) 149 | } 150 | p.expr(&x.LParen) 151 | p.margin++ 152 | for _, l := range x.Line { 153 | p.newline() 154 | p.expr(l) 155 | } 156 | p.margin-- 157 | p.newline() 158 | p.expr(&x.RParen) 159 | } 160 | 161 | // Queue end-of-line comments for printing when we 162 | // reach the end of the line. 163 | p.comment = append(p.comment, x.Comment().Suffix...) 164 | } 165 | -------------------------------------------------------------------------------- /internal/gocopy/modfile/printhtml.go: -------------------------------------------------------------------------------- 1 | package modfile 2 | 3 | // Copyright 2018 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | // Module file HTML printer. 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "strings" 13 | ) 14 | 15 | // FormatHTML is a modified version of Format() 16 | // but emits an HTML representation of a go.mod file. 17 | func FormatHTML(f *FileSyntax, linkMap map[string]string) []byte { 18 | if linkMap == nil { 19 | linkMap = map[string]string{} 20 | } 21 | pr := &htmlprinter{linkMap: linkMap} 22 | pr.file(f) 23 | return pr.Bytes() 24 | } 25 | 26 | // A printer collects the state during printing of a file or expression. 27 | type htmlprinter struct { 28 | bytes.Buffer // output buffer 29 | comment []Comment // pending end-of-line comments 30 | margin int // left margin (indent), a number of tabs 31 | linkMap map[string]string // map of modules to their links 32 | } 33 | 34 | // printf prints to the buffer. 35 | func (p *htmlprinter) printf(format string, args ...interface{}) { 36 | fmt.Fprintf(p, format, args...) 37 | } 38 | 39 | // indent returns the position on the current line, in bytes, 0-indexed. 40 | func (p *htmlprinter) indent() int { 41 | b := p.Bytes() 42 | n := 0 43 | for n < len(b) && b[len(b)-1-n] != '\n' { 44 | n++ 45 | } 46 | return n 47 | } 48 | 49 | // newline ends the current line, flushing end-of-line comments. 50 | func (p *htmlprinter) newline() { 51 | if len(p.comment) > 0 { 52 | p.printf(" ") 53 | for i, com := range p.comment { 54 | if i > 0 { 55 | p.trim() 56 | p.printf("\n") 57 | for i := 0; i < p.margin; i++ { 58 | p.printf("\t") 59 | } 60 | } 61 | p.printf("%s", strings.TrimSpace(com.Token)) 62 | } 63 | p.comment = p.comment[:0] 64 | } 65 | 66 | p.trim() 67 | p.printf("\n") 68 | for i := 0; i < p.margin; i++ { 69 | p.printf("\t") 70 | } 71 | } 72 | 73 | // trim removes trailing spaces and tabs from the current line. 74 | func (p *htmlprinter) trim() { 75 | // Remove trailing spaces and tabs from line we're about to end. 76 | b := p.Bytes() 77 | n := len(b) 78 | for n > 0 && (b[n-1] == '\t' || b[n-1] == ' ') { 79 | n-- 80 | } 81 | p.Truncate(n) 82 | } 83 | 84 | // file formats the given file into the print buffer. 85 | func (p *htmlprinter) file(f *FileSyntax) { 86 | p.printf(`
`)
 87 | 	for _, com := range f.Before {
 88 | 		p.printf("%s", strings.TrimSpace(com.Token))
 89 | 		p.newline()
 90 | 	}
 91 | 
 92 | 	for i, stmt := range f.Stmt {
 93 | 		switch x := stmt.(type) {
 94 | 		case *CommentBlock:
 95 | 			// comments already handled
 96 | 			p.expr(x)
 97 | 
 98 | 		default:
 99 | 			p.expr(x)
100 | 			p.newline()
101 | 		}
102 | 
103 | 		for _, com := range stmt.Comment().After {
104 | 			p.printf("%s", strings.TrimSpace(com.Token))
105 | 			p.newline()
106 | 		}
107 | 
108 | 		if i+1 < len(f.Stmt) {
109 | 			p.newline()
110 | 		}
111 | 	}
112 | 	p.printf("
") 113 | } 114 | 115 | func (p *htmlprinter) expr(x Expr) { 116 | // Emit line-comments preceding this expression. 117 | if before := x.Comment().Before; len(before) > 0 { 118 | // Want to print a line comment. 119 | // Line comments must be at the current margin. 120 | p.trim() 121 | if p.indent() > 0 { 122 | // There's other text on the line. Start a new line. 123 | p.printf("\n") 124 | } 125 | // Re-indent to margin. 126 | for i := 0; i < p.margin; i++ { 127 | p.printf("\t") 128 | } 129 | for _, com := range before { 130 | p.printf("%s", strings.TrimSpace(com.Token)) 131 | p.newline() 132 | } 133 | } 134 | 135 | switch x := x.(type) { 136 | default: 137 | panic(fmt.Errorf("printer: unexpected type %T", x)) 138 | 139 | case *CommentBlock: 140 | // done 141 | 142 | case *LParen: 143 | p.printf("(") 144 | case *RParen: 145 | p.printf(")") 146 | 147 | case *Line: 148 | sep := "" 149 | for idx, tok := range x.Token { 150 | if idx == 0 && oneOf(tok) { 151 | p.printf("%s", tok) 152 | } else { 153 | item := tok 154 | if link, ok := p.linkMap[item]; ok { 155 | item = fmt.Sprintf(`%s`, link, tok) 156 | } 157 | p.printf("%s%s", sep, item) 158 | } 159 | sep = " " 160 | } 161 | 162 | case *LineBlock: 163 | for idx, tok := range x.Token { 164 | if idx == 0 && oneOf(tok) { 165 | p.printf("%s ", tok) 166 | } else { 167 | p.printf("%s ", tok) 168 | } 169 | } 170 | p.expr(&x.LParen) 171 | p.margin++ 172 | for _, l := range x.Line { 173 | p.newline() 174 | p.expr(l) 175 | } 176 | p.margin-- 177 | p.newline() 178 | p.expr(&x.RParen) 179 | } 180 | 181 | // Queue end-of-line comments for printing when we 182 | // reach the end of the line. 183 | p.comment = append(p.comment, x.Comment().Suffix...) 184 | } 185 | 186 | func oneOf(s string) bool { 187 | return map[string]bool{ 188 | "go": true, 189 | "module": true, 190 | "replace": true, 191 | "require": true, 192 | "exclude": true, 193 | }[s] 194 | } 195 | -------------------------------------------------------------------------------- /internal/gocopy/modfile/read.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Module file parser. 6 | // This is a simplified copy of Google's buildifier parser. 7 | 8 | package modfile 9 | 10 | import ( 11 | "bytes" 12 | "fmt" 13 | "os" 14 | "strconv" 15 | "strings" 16 | "unicode" 17 | "unicode/utf8" 18 | ) 19 | 20 | // A Position describes the position between two bytes of input. 21 | type Position struct { 22 | Line int // line in input (starting at 1) 23 | LineRune int // rune in line (starting at 1) 24 | Byte int // byte in input (starting at 0) 25 | } 26 | 27 | // add returns the position at the end of s, assuming it starts at p. 28 | func (p Position) add(s string) Position { 29 | p.Byte += len(s) 30 | if n := strings.Count(s, "\n"); n > 0 { 31 | p.Line += n 32 | s = s[strings.LastIndex(s, "\n")+1:] 33 | p.LineRune = 1 34 | } 35 | p.LineRune += utf8.RuneCountInString(s) 36 | return p 37 | } 38 | 39 | // An Expr represents an input element. 40 | type Expr interface { 41 | // Span returns the start and end position of the expression, 42 | // excluding leading or trailing comments. 43 | Span() (start, end Position) 44 | 45 | // Comment returns the comments attached to the expression. 46 | // This method would normally be named 'Comments' but that 47 | // would interfere with embedding a type of the same name. 48 | Comment() *Comments 49 | } 50 | 51 | // A Comment represents a single // comment. 52 | type Comment struct { 53 | Start Position 54 | Token string // without trailing newline 55 | Suffix bool // an end of line (not whole line) comment 56 | } 57 | 58 | // Comments collects the comments associated with an expression. 59 | type Comments struct { 60 | Before []Comment // whole-line comments before this expression 61 | Suffix []Comment // end-of-line comments after this expression 62 | 63 | // For top-level expressions only, After lists whole-line 64 | // comments following the expression. 65 | After []Comment 66 | } 67 | 68 | // Comment returns the receiver. This isn't useful by itself, but 69 | // a Comments struct is embedded into all the expression 70 | // implementation types, and this gives each of those a Comment 71 | // method to satisfy the Expr interface. 72 | func (c *Comments) Comment() *Comments { 73 | return c 74 | } 75 | 76 | // A FileSyntax represents an entire go.mod file. 77 | type FileSyntax struct { 78 | Name string // file path 79 | Comments 80 | Stmt []Expr 81 | } 82 | 83 | func (x *FileSyntax) Span() (start, end Position) { 84 | if len(x.Stmt) == 0 { 85 | return 86 | } 87 | start, _ = x.Stmt[0].Span() 88 | _, end = x.Stmt[len(x.Stmt)-1].Span() 89 | return start, end 90 | } 91 | 92 | func (x *FileSyntax) addLine(hint Expr, tokens ...string) *Line { 93 | if hint == nil { 94 | // If no hint given, add to the last statement of the given type. 95 | Loop: 96 | for i := len(x.Stmt) - 1; i >= 0; i-- { 97 | stmt := x.Stmt[i] 98 | switch stmt := stmt.(type) { 99 | case *Line: 100 | if stmt.Token != nil && stmt.Token[0] == tokens[0] { 101 | hint = stmt 102 | break Loop 103 | } 104 | case *LineBlock: 105 | if stmt.Token[0] == tokens[0] { 106 | hint = stmt 107 | break Loop 108 | } 109 | } 110 | } 111 | } 112 | 113 | if hint != nil { 114 | for i, stmt := range x.Stmt { 115 | switch stmt := stmt.(type) { 116 | case *Line: 117 | if stmt == hint { 118 | // Convert line to line block. 119 | stmt.InBlock = true 120 | block := &LineBlock{Token: stmt.Token[:1], Line: []*Line{stmt}} 121 | stmt.Token = stmt.Token[1:] 122 | x.Stmt[i] = block 123 | new := &Line{Token: tokens[1:], InBlock: true} 124 | block.Line = append(block.Line, new) 125 | return new 126 | } 127 | case *LineBlock: 128 | if stmt == hint { 129 | new := &Line{Token: tokens[1:], InBlock: true} 130 | stmt.Line = append(stmt.Line, new) 131 | return new 132 | } 133 | for j, line := range stmt.Line { 134 | if line == hint { 135 | // Add new line after hint. 136 | stmt.Line = append(stmt.Line, nil) 137 | copy(stmt.Line[j+2:], stmt.Line[j+1:]) 138 | new := &Line{Token: tokens[1:], InBlock: true} 139 | stmt.Line[j+1] = new 140 | return new 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | new := &Line{Token: tokens} 148 | x.Stmt = append(x.Stmt, new) 149 | return new 150 | } 151 | 152 | func (x *FileSyntax) updateLine(line *Line, tokens ...string) { 153 | if line.InBlock { 154 | tokens = tokens[1:] 155 | } 156 | line.Token = tokens 157 | } 158 | 159 | func (x *FileSyntax) removeLine(line *Line) { 160 | line.Token = nil 161 | } 162 | 163 | // Cleanup cleans up the file syntax x after any edit operations. 164 | // To avoid quadratic behavior, removeLine marks the line as dead 165 | // by setting line.Token = nil but does not remove it from the slice 166 | // in which it appears. After edits have all been indicated, 167 | // calling Cleanup cleans out the dead lines. 168 | func (x *FileSyntax) Cleanup() { 169 | w := 0 170 | for _, stmt := range x.Stmt { 171 | switch stmt := stmt.(type) { 172 | case *Line: 173 | if stmt.Token == nil { 174 | continue 175 | } 176 | case *LineBlock: 177 | ww := 0 178 | for _, line := range stmt.Line { 179 | if line.Token != nil { 180 | stmt.Line[ww] = line 181 | ww++ 182 | } 183 | } 184 | if ww == 0 { 185 | continue 186 | } 187 | if ww == 1 { 188 | // Collapse block into single line. 189 | line := &Line{ 190 | Comments: Comments{ 191 | Before: commentsAdd(stmt.Before, stmt.Line[0].Before), 192 | Suffix: commentsAdd(stmt.Line[0].Suffix, stmt.Suffix), 193 | After: commentsAdd(stmt.Line[0].After, stmt.After), 194 | }, 195 | Token: stringsAdd(stmt.Token, stmt.Line[0].Token), 196 | } 197 | x.Stmt[w] = line 198 | w++ 199 | continue 200 | } 201 | stmt.Line = stmt.Line[:ww] 202 | } 203 | x.Stmt[w] = stmt 204 | w++ 205 | } 206 | x.Stmt = x.Stmt[:w] 207 | } 208 | 209 | func commentsAdd(x, y []Comment) []Comment { 210 | return append(x[:len(x):len(x)], y...) 211 | } 212 | 213 | func stringsAdd(x, y []string) []string { 214 | return append(x[:len(x):len(x)], y...) 215 | } 216 | 217 | // A CommentBlock represents a top-level block of comments separate 218 | // from any rule. 219 | type CommentBlock struct { 220 | Comments 221 | Start Position 222 | } 223 | 224 | func (x *CommentBlock) Span() (start, end Position) { 225 | return x.Start, x.Start 226 | } 227 | 228 | // A Line is a single line of tokens. 229 | type Line struct { 230 | Comments 231 | Start Position 232 | Token []string 233 | InBlock bool 234 | End Position 235 | } 236 | 237 | func (x *Line) Span() (start, end Position) { 238 | return x.Start, x.End 239 | } 240 | 241 | // A LineBlock is a factored block of lines, like 242 | // 243 | // require ( 244 | // "x" 245 | // "y" 246 | // ) 247 | // 248 | type LineBlock struct { 249 | Comments 250 | Start Position 251 | LParen LParen 252 | Token []string 253 | Line []*Line 254 | RParen RParen 255 | } 256 | 257 | func (x *LineBlock) Span() (start, end Position) { 258 | return x.Start, x.RParen.Pos.add(")") 259 | } 260 | 261 | // An LParen represents the beginning of a parenthesized line block. 262 | // It is a place to store suffix comments. 263 | type LParen struct { 264 | Comments 265 | Pos Position 266 | } 267 | 268 | func (x *LParen) Span() (start, end Position) { 269 | return x.Pos, x.Pos.add(")") 270 | } 271 | 272 | // An RParen represents the end of a parenthesized line block. 273 | // It is a place to store whole-line (before) comments. 274 | type RParen struct { 275 | Comments 276 | Pos Position 277 | } 278 | 279 | func (x *RParen) Span() (start, end Position) { 280 | return x.Pos, x.Pos.add(")") 281 | } 282 | 283 | // An input represents a single input file being parsed. 284 | type input struct { 285 | // Lexing state. 286 | filename string // name of input file, for errors 287 | complete []byte // entire input 288 | remaining []byte // remaining input 289 | token []byte // token being scanned 290 | lastToken string // most recently returned token, for error messages 291 | pos Position // current input position 292 | comments []Comment // accumulated comments 293 | endRule int // position of end of current rule 294 | 295 | // Parser state. 296 | file *FileSyntax // returned top-level syntax tree 297 | parseError error // error encountered during parsing 298 | 299 | // Comment assignment state. 300 | pre []Expr // all expressions, in preorder traversal 301 | post []Expr // all expressions, in postorder traversal 302 | } 303 | 304 | func newInput(filename string, data []byte) *input { 305 | return &input{ 306 | filename: filename, 307 | complete: data, 308 | remaining: data, 309 | pos: Position{Line: 1, LineRune: 1, Byte: 0}, 310 | } 311 | } 312 | 313 | // parse parses the input file. 314 | func parse(file string, data []byte) (f *FileSyntax, err error) { 315 | in := newInput(file, data) 316 | // The parser panics for both routine errors like syntax errors 317 | // and for programmer bugs like array index errors. 318 | // Turn both into error returns. Catching bug panics is 319 | // especially important when processing many files. 320 | defer func() { 321 | if e := recover(); e != nil { 322 | if e == in.parseError { 323 | err = in.parseError 324 | } else { 325 | err = fmt.Errorf("%s:%d:%d: internal error: %v", in.filename, in.pos.Line, in.pos.LineRune, e) 326 | } 327 | } 328 | }() 329 | 330 | // Invoke the parser. 331 | in.parseFile() 332 | if in.parseError != nil { 333 | return nil, in.parseError 334 | } 335 | in.file.Name = in.filename 336 | 337 | // Assign comments to nearby syntax. 338 | in.assignComments() 339 | 340 | return in.file, nil 341 | } 342 | 343 | // Error is called to report an error. 344 | // The reason s is often "syntax error". 345 | // Error does not return: it panics. 346 | func (in *input) Error(s string) { 347 | if s == "syntax error" && in.lastToken != "" { 348 | s += " near " + in.lastToken 349 | } 350 | in.parseError = fmt.Errorf("%s:%d:%d: %v", in.filename, in.pos.Line, in.pos.LineRune, s) 351 | panic(in.parseError) 352 | } 353 | 354 | // eof reports whether the input has reached end of file. 355 | func (in *input) eof() bool { 356 | return len(in.remaining) == 0 357 | } 358 | 359 | // peekRune returns the next rune in the input without consuming it. 360 | func (in *input) peekRune() int { 361 | if len(in.remaining) == 0 { 362 | return 0 363 | } 364 | r, _ := utf8.DecodeRune(in.remaining) 365 | return int(r) 366 | } 367 | 368 | // peekPrefix reports whether the remaining input begins with the given prefix. 369 | func (in *input) peekPrefix(prefix string) bool { 370 | // This is like bytes.HasPrefix(in.remaining, []byte(prefix)) 371 | // but without the allocation of the []byte copy of prefix. 372 | for i := 0; i < len(prefix); i++ { 373 | if i >= len(in.remaining) || in.remaining[i] != prefix[i] { 374 | return false 375 | } 376 | } 377 | return true 378 | } 379 | 380 | // readRune consumes and returns the next rune in the input. 381 | func (in *input) readRune() int { 382 | if len(in.remaining) == 0 { 383 | in.Error("internal lexer error: readRune at EOF") 384 | } 385 | r, size := utf8.DecodeRune(in.remaining) 386 | in.remaining = in.remaining[size:] 387 | if r == '\n' { 388 | in.pos.Line++ 389 | in.pos.LineRune = 1 390 | } else { 391 | in.pos.LineRune++ 392 | } 393 | in.pos.Byte += size 394 | return int(r) 395 | } 396 | 397 | type symType struct { 398 | pos Position 399 | endPos Position 400 | text string 401 | } 402 | 403 | // startToken marks the beginning of the next input token. 404 | // It must be followed by a call to endToken, once the token has 405 | // been consumed using readRune. 406 | func (in *input) startToken(sym *symType) { 407 | in.token = in.remaining 408 | sym.text = "" 409 | sym.pos = in.pos 410 | } 411 | 412 | // endToken marks the end of an input token. 413 | // It records the actual token string in sym.text if the caller 414 | // has not done that already. 415 | func (in *input) endToken(sym *symType) { 416 | if sym.text == "" { 417 | tok := string(in.token[:len(in.token)-len(in.remaining)]) 418 | sym.text = tok 419 | in.lastToken = sym.text 420 | } 421 | sym.endPos = in.pos 422 | } 423 | 424 | // lex is called from the parser to obtain the next input token. 425 | // It returns the token value (either a rune like '+' or a symbolic token _FOR) 426 | // and sets val to the data associated with the token. 427 | // For all our input tokens, the associated data is 428 | // val.Pos (the position where the token begins) 429 | // and val.Token (the input string corresponding to the token). 430 | func (in *input) lex(sym *symType) int { 431 | // Skip past spaces, stopping at non-space or EOF. 432 | countNL := 0 // number of newlines we've skipped past 433 | for !in.eof() { 434 | // Skip over spaces. Count newlines so we can give the parser 435 | // information about where top-level blank lines are, 436 | // for top-level comment assignment. 437 | c := in.peekRune() 438 | if c == ' ' || c == '\t' || c == '\r' { 439 | in.readRune() 440 | continue 441 | } 442 | 443 | // Comment runs to end of line. 444 | if in.peekPrefix("//") { 445 | in.startToken(sym) 446 | 447 | // Is this comment the only thing on its line? 448 | // Find the last \n before this // and see if it's all 449 | // spaces from there to here. 450 | i := bytes.LastIndex(in.complete[:in.pos.Byte], []byte("\n")) 451 | suffix := len(bytes.TrimSpace(in.complete[i+1:in.pos.Byte])) > 0 452 | in.readRune() 453 | in.readRune() 454 | 455 | // Consume comment. 456 | for len(in.remaining) > 0 && in.readRune() != '\n' { 457 | } 458 | in.endToken(sym) 459 | 460 | sym.text = strings.TrimRight(sym.text, "\n") 461 | in.lastToken = "comment" 462 | 463 | // If we are at top level (not in a statement), hand the comment to 464 | // the parser as a _COMMENT token. The grammar is written 465 | // to handle top-level comments itself. 466 | if !suffix { 467 | // Not in a statement. Tell parser about top-level comment. 468 | return _COMMENT 469 | } 470 | 471 | // Otherwise, save comment for later attachment to syntax tree. 472 | if countNL > 1 { 473 | in.comments = append(in.comments, Comment{sym.pos, "", false}) 474 | } 475 | in.comments = append(in.comments, Comment{sym.pos, sym.text, suffix}) 476 | countNL = 1 477 | return _EOL 478 | } 479 | 480 | if in.peekPrefix("/*") { 481 | in.Error(fmt.Sprintf("mod files must use // comments (not /* */ comments)")) 482 | } 483 | 484 | // Found non-space non-comment. 485 | break 486 | } 487 | 488 | // Found the beginning of the next token. 489 | in.startToken(sym) 490 | defer in.endToken(sym) 491 | 492 | // End of file. 493 | if in.eof() { 494 | in.lastToken = "EOF" 495 | return _EOF 496 | } 497 | 498 | // Punctuation tokens. 499 | switch c := in.peekRune(); c { 500 | case '\n': 501 | in.readRune() 502 | return c 503 | 504 | case '(': 505 | in.readRune() 506 | return c 507 | 508 | case ')': 509 | in.readRune() 510 | return c 511 | 512 | case '"', '`': // quoted string 513 | quote := c 514 | in.readRune() 515 | for { 516 | if in.eof() { 517 | in.pos = sym.pos 518 | in.Error("unexpected EOF in string") 519 | } 520 | if in.peekRune() == '\n' { 521 | in.Error("unexpected newline in string") 522 | } 523 | c := in.readRune() 524 | if c == quote { 525 | break 526 | } 527 | if c == '\\' && quote != '`' { 528 | if in.eof() { 529 | in.pos = sym.pos 530 | in.Error("unexpected EOF in string") 531 | } 532 | in.readRune() 533 | } 534 | } 535 | in.endToken(sym) 536 | return _STRING 537 | } 538 | 539 | // Checked all punctuation. Must be identifier token. 540 | if c := in.peekRune(); !isIdent(c) { 541 | in.Error(fmt.Sprintf("unexpected input character %#q", c)) 542 | } 543 | 544 | // Scan over identifier. 545 | for isIdent(in.peekRune()) { 546 | if in.peekPrefix("//") { 547 | break 548 | } 549 | if in.peekPrefix("/*") { 550 | in.Error(fmt.Sprintf("mod files must use // comments (not /* */ comments)")) 551 | } 552 | in.readRune() 553 | } 554 | return _IDENT 555 | } 556 | 557 | // isIdent reports whether c is an identifier rune. 558 | // We treat nearly all runes as identifier runes. 559 | func isIdent(c int) bool { 560 | return c != 0 && !unicode.IsSpace(rune(c)) 561 | } 562 | 563 | // Comment assignment. 564 | // We build two lists of all subexpressions, preorder and postorder. 565 | // The preorder list is ordered by start location, with outer expressions first. 566 | // The postorder list is ordered by end location, with outer expressions last. 567 | // We use the preorder list to assign each whole-line comment to the syntax 568 | // immediately following it, and we use the postorder list to assign each 569 | // end-of-line comment to the syntax immediately preceding it. 570 | 571 | // order walks the expression adding it and its subexpressions to the 572 | // preorder and postorder lists. 573 | func (in *input) order(x Expr) { 574 | if x != nil { 575 | in.pre = append(in.pre, x) 576 | } 577 | switch x := x.(type) { 578 | default: 579 | panic(fmt.Errorf("order: unexpected type %T", x)) 580 | case nil: 581 | // nothing 582 | case *LParen, *RParen: 583 | // nothing 584 | case *CommentBlock: 585 | // nothing 586 | case *Line: 587 | // nothing 588 | case *FileSyntax: 589 | for _, stmt := range x.Stmt { 590 | in.order(stmt) 591 | } 592 | case *LineBlock: 593 | in.order(&x.LParen) 594 | for _, l := range x.Line { 595 | in.order(l) 596 | } 597 | in.order(&x.RParen) 598 | } 599 | if x != nil { 600 | in.post = append(in.post, x) 601 | } 602 | } 603 | 604 | // assignComments attaches comments to nearby syntax. 605 | func (in *input) assignComments() { 606 | const debug = false 607 | 608 | // Generate preorder and postorder lists. 609 | in.order(in.file) 610 | 611 | // Split into whole-line comments and suffix comments. 612 | var line, suffix []Comment 613 | for _, com := range in.comments { 614 | if com.Suffix { 615 | suffix = append(suffix, com) 616 | } else { 617 | line = append(line, com) 618 | } 619 | } 620 | 621 | if debug { 622 | for _, c := range line { 623 | fmt.Fprintf(os.Stderr, "LINE %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte) 624 | } 625 | } 626 | 627 | // Assign line comments to syntax immediately following. 628 | for _, x := range in.pre { 629 | start, _ := x.Span() 630 | if debug { 631 | fmt.Printf("pre %T :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte) 632 | } 633 | xcom := x.Comment() 634 | for len(line) > 0 && start.Byte >= line[0].Start.Byte { 635 | if debug { 636 | fmt.Fprintf(os.Stderr, "ASSIGN LINE %q #%d\n", line[0].Token, line[0].Start.Byte) 637 | } 638 | xcom.Before = append(xcom.Before, line[0]) 639 | line = line[1:] 640 | } 641 | } 642 | 643 | // Remaining line comments go at end of file. 644 | in.file.After = append(in.file.After, line...) 645 | 646 | if debug { 647 | for _, c := range suffix { 648 | fmt.Fprintf(os.Stderr, "SUFFIX %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte) 649 | } 650 | } 651 | 652 | // Assign suffix comments to syntax immediately before. 653 | for i := len(in.post) - 1; i >= 0; i-- { 654 | x := in.post[i] 655 | 656 | start, end := x.Span() 657 | if debug { 658 | fmt.Printf("post %T :%d:%d #%d :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte, end.Line, end.LineRune, end.Byte) 659 | } 660 | 661 | // Do not assign suffix comments to end of line block or whole file. 662 | // Instead assign them to the last element inside. 663 | switch x.(type) { 664 | case *FileSyntax: 665 | continue 666 | } 667 | 668 | // Do not assign suffix comments to something that starts 669 | // on an earlier line, so that in 670 | // 671 | // x ( y 672 | // z ) // comment 673 | // 674 | // we assign the comment to z and not to x ( ... ). 675 | if start.Line != end.Line { 676 | continue 677 | } 678 | xcom := x.Comment() 679 | for len(suffix) > 0 && end.Byte <= suffix[len(suffix)-1].Start.Byte { 680 | if debug { 681 | fmt.Fprintf(os.Stderr, "ASSIGN SUFFIX %q #%d\n", suffix[len(suffix)-1].Token, suffix[len(suffix)-1].Start.Byte) 682 | } 683 | xcom.Suffix = append(xcom.Suffix, suffix[len(suffix)-1]) 684 | suffix = suffix[:len(suffix)-1] 685 | } 686 | } 687 | 688 | // We assigned suffix comments in reverse. 689 | // If multiple suffix comments were appended to the same 690 | // expression node, they are now in reverse. Fix that. 691 | for _, x := range in.post { 692 | reverseComments(x.Comment().Suffix) 693 | } 694 | 695 | // Remaining suffix comments go at beginning of file. 696 | in.file.Before = append(in.file.Before, suffix...) 697 | } 698 | 699 | // reverseComments reverses the []Comment list. 700 | func reverseComments(list []Comment) { 701 | for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 { 702 | list[i], list[j] = list[j], list[i] 703 | } 704 | } 705 | 706 | func (in *input) parseFile() { 707 | in.file = new(FileSyntax) 708 | var sym symType 709 | var cb *CommentBlock 710 | for { 711 | tok := in.lex(&sym) 712 | switch tok { 713 | case '\n': 714 | if cb != nil { 715 | in.file.Stmt = append(in.file.Stmt, cb) 716 | cb = nil 717 | } 718 | case _COMMENT: 719 | if cb == nil { 720 | cb = &CommentBlock{Start: sym.pos} 721 | } 722 | com := cb.Comment() 723 | com.Before = append(com.Before, Comment{Start: sym.pos, Token: sym.text}) 724 | case _EOF: 725 | if cb != nil { 726 | in.file.Stmt = append(in.file.Stmt, cb) 727 | } 728 | return 729 | default: 730 | in.parseStmt(&sym) 731 | if cb != nil { 732 | in.file.Stmt[len(in.file.Stmt)-1].Comment().Before = cb.Before 733 | cb = nil 734 | } 735 | } 736 | } 737 | } 738 | 739 | func (in *input) parseStmt(sym *symType) { 740 | start := sym.pos 741 | end := sym.endPos 742 | token := []string{sym.text} 743 | for { 744 | tok := in.lex(sym) 745 | switch tok { 746 | case '\n', _EOF, _EOL: 747 | in.file.Stmt = append(in.file.Stmt, &Line{ 748 | Start: start, 749 | Token: token, 750 | End: end, 751 | }) 752 | return 753 | case '(': 754 | in.file.Stmt = append(in.file.Stmt, in.parseLineBlock(start, token, sym)) 755 | return 756 | default: 757 | token = append(token, sym.text) 758 | end = sym.endPos 759 | } 760 | } 761 | } 762 | 763 | func (in *input) parseLineBlock(start Position, token []string, sym *symType) *LineBlock { 764 | x := &LineBlock{ 765 | Start: start, 766 | Token: token, 767 | LParen: LParen{Pos: sym.pos}, 768 | } 769 | var comments []Comment 770 | for { 771 | tok := in.lex(sym) 772 | switch tok { 773 | case _EOL: 774 | // ignore 775 | case '\n': 776 | if len(comments) == 0 && len(x.Line) > 0 || len(comments) > 0 && comments[len(comments)-1].Token != "" { 777 | comments = append(comments, Comment{}) 778 | } 779 | case _COMMENT: 780 | comments = append(comments, Comment{Start: sym.pos, Token: sym.text}) 781 | case _EOF: 782 | in.Error(fmt.Sprintf("syntax error (unterminated block started at %s:%d:%d)", in.filename, x.Start.Line, x.Start.LineRune)) 783 | case ')': 784 | x.RParen.Before = comments 785 | x.RParen.Pos = sym.pos 786 | tok = in.lex(sym) 787 | if tok != '\n' && tok != _EOF && tok != _EOL { 788 | in.Error("syntax error (expected newline after closing paren)") 789 | } 790 | return x 791 | default: 792 | l := in.parseLine(sym) 793 | x.Line = append(x.Line, l) 794 | l.Comment().Before = comments 795 | comments = nil 796 | } 797 | } 798 | } 799 | 800 | func (in *input) parseLine(sym *symType) *Line { 801 | start := sym.pos 802 | end := sym.endPos 803 | token := []string{sym.text} 804 | for { 805 | tok := in.lex(sym) 806 | switch tok { 807 | case '\n', _EOF, _EOL: 808 | return &Line{ 809 | Start: start, 810 | Token: token, 811 | End: end, 812 | InBlock: true, 813 | } 814 | default: 815 | token = append(token, sym.text) 816 | end = sym.endPos 817 | } 818 | } 819 | } 820 | 821 | const ( 822 | _EOF = -(1 + iota) 823 | _EOL 824 | _IDENT 825 | _STRING 826 | _COMMENT 827 | ) 828 | 829 | var ( 830 | slashSlash = []byte("//") 831 | moduleStr = []byte("module") 832 | ) 833 | 834 | // ModulePath returns the module path from the gomod file text. 835 | // If it cannot find a module path, it returns an empty string. 836 | // It is tolerant of unrelated problems in the go.mod file. 837 | func ModulePath(mod []byte) string { 838 | for len(mod) > 0 { 839 | line := mod 840 | mod = nil 841 | if i := bytes.IndexByte(line, '\n'); i >= 0 { 842 | line, mod = line[:i], line[i+1:] 843 | } 844 | if i := bytes.Index(line, slashSlash); i >= 0 { 845 | line = line[:i] 846 | } 847 | line = bytes.TrimSpace(line) 848 | if !bytes.HasPrefix(line, moduleStr) { 849 | continue 850 | } 851 | line = line[len(moduleStr):] 852 | n := len(line) 853 | line = bytes.TrimSpace(line) 854 | if len(line) == n || len(line) == 0 { 855 | continue 856 | } 857 | 858 | if line[0] == '"' || line[0] == '`' { 859 | p, err := strconv.Unquote(string(line)) 860 | if err != nil { 861 | return "" // malformed quoted string or multiline module path 862 | } 863 | return p 864 | } 865 | 866 | return string(line) 867 | } 868 | return "" // missing module path 869 | } 870 | -------------------------------------------------------------------------------- /internal/gocopy/modfile/rule.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package modfile 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "path/filepath" 12 | "sort" 13 | "strconv" 14 | "strings" 15 | "unicode" 16 | 17 | "marwan.io/protoc-gen-twirpql/internal/gocopy/lazyregexp" 18 | "marwan.io/protoc-gen-twirpql/internal/gocopy/module" 19 | "marwan.io/protoc-gen-twirpql/internal/gocopy/semver" 20 | ) 21 | 22 | // A File is the parsed, interpreted form of a go.mod file. 23 | type File struct { 24 | Module *Module 25 | Go *Go 26 | Require []*Require 27 | Exclude []*Exclude 28 | Replace []*Replace 29 | 30 | Syntax *FileSyntax 31 | } 32 | 33 | // A Module is the module statement. 34 | type Module struct { 35 | Mod module.Version 36 | Syntax *Line 37 | } 38 | 39 | // A Go is the go statement. 40 | type Go struct { 41 | Version string // "1.23" 42 | Syntax *Line 43 | } 44 | 45 | // A Require is a single require statement. 46 | type Require struct { 47 | Mod module.Version 48 | Indirect bool // has "// indirect" comment 49 | Syntax *Line 50 | } 51 | 52 | // An Exclude is a single exclude statement. 53 | type Exclude struct { 54 | Mod module.Version 55 | Syntax *Line 56 | } 57 | 58 | // A Replace is a single replace statement. 59 | type Replace struct { 60 | Old module.Version 61 | New module.Version 62 | Syntax *Line 63 | } 64 | 65 | // AddModuleStmt comment 66 | func (f *File) AddModuleStmt(path string) error { 67 | if f.Syntax == nil { 68 | f.Syntax = new(FileSyntax) 69 | } 70 | if f.Module == nil { 71 | f.Module = &Module{ 72 | Mod: module.Version{Path: path}, 73 | Syntax: f.Syntax.addLine(nil, "module", AutoQuote(path)), 74 | } 75 | } else { 76 | f.Module.Mod.Path = path 77 | f.Syntax.updateLine(f.Module.Syntax, "module", AutoQuote(path)) 78 | } 79 | return nil 80 | } 81 | 82 | // AddComment comment 83 | func (f *File) AddComment(text string) { 84 | if f.Syntax == nil { 85 | f.Syntax = new(FileSyntax) 86 | } 87 | f.Syntax.Stmt = append(f.Syntax.Stmt, &CommentBlock{ 88 | Comments: Comments{ 89 | Before: []Comment{ 90 | { 91 | Token: text, 92 | }, 93 | }, 94 | }, 95 | }) 96 | } 97 | 98 | type VersionFixer func(path, version string) (string, error) 99 | 100 | // Parse parses the data, reported in errors as being from file, 101 | // into a File struct. It applies fix, if non-nil, to canonicalize all module versions found. 102 | func Parse(file string, data []byte, fix VersionFixer) (*File, error) { 103 | return parseToFile(file, data, fix, true) 104 | } 105 | 106 | // ParseLax is like Parse but ignores unknown statements. 107 | // It is used when parsing go.mod files other than the main module, 108 | // under the theory that most statement types we add in the future will 109 | // only apply in the main module, like exclude and replace, 110 | // and so we get better gradual deployments if old go commands 111 | // simply ignore those statements when found in go.mod files 112 | // in dependencies. 113 | func ParseLax(file string, data []byte, fix VersionFixer) (*File, error) { 114 | return parseToFile(file, data, fix, false) 115 | } 116 | 117 | func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (*File, error) { 118 | fs, err := parse(file, data) 119 | if err != nil { 120 | return nil, err 121 | } 122 | f := &File{ 123 | Syntax: fs, 124 | } 125 | 126 | var errs bytes.Buffer 127 | for _, x := range fs.Stmt { 128 | switch x := x.(type) { 129 | case *Line: 130 | f.add(&errs, x, x.Token[0], x.Token[1:], fix, strict) 131 | 132 | case *LineBlock: 133 | if len(x.Token) > 1 { 134 | if strict { 135 | fmt.Fprintf(&errs, "%s:%d: unknown block type: %s\n", file, x.Start.Line, strings.Join(x.Token, " ")) 136 | } 137 | continue 138 | } 139 | switch x.Token[0] { 140 | default: 141 | if strict { 142 | fmt.Fprintf(&errs, "%s:%d: unknown block type: %s\n", file, x.Start.Line, strings.Join(x.Token, " ")) 143 | } 144 | continue 145 | case "module", "require", "exclude", "replace": 146 | for _, l := range x.Line { 147 | f.add(&errs, l, x.Token[0], l.Token, fix, strict) 148 | } 149 | } 150 | } 151 | } 152 | 153 | if errs.Len() > 0 { 154 | return nil, errors.New(strings.TrimRight(errs.String(), "\n")) 155 | } 156 | return f, nil 157 | } 158 | 159 | var GoVersionRE = lazyregexp.New(`([1-9][0-9]*)\.(0|[1-9][0-9]*)`) 160 | 161 | func (f *File) add(errs *bytes.Buffer, line *Line, verb string, args []string, fix VersionFixer, strict bool) { 162 | // If strict is false, this module is a dependency. 163 | // We ignore all unknown directives as well as main-module-only 164 | // directives like replace and exclude. It will work better for 165 | // forward compatibility if we can depend on modules that have unknown 166 | // statements (presumed relevant only when acting as the main module) 167 | // and simply ignore those statements. 168 | if !strict { 169 | switch verb { 170 | case "module", "require", "go": 171 | // want these even for dependency go.mods 172 | default: 173 | return 174 | } 175 | } 176 | 177 | switch verb { 178 | default: 179 | fmt.Fprintf(errs, "%s:%d: unknown directive: %s\n", f.Syntax.Name, line.Start.Line, verb) 180 | 181 | case "go": 182 | if f.Go != nil { 183 | fmt.Fprintf(errs, "%s:%d: repeated go statement\n", f.Syntax.Name, line.Start.Line) 184 | return 185 | } 186 | if len(args) != 1 || !GoVersionRE.MatchString(args[0]) { 187 | fmt.Fprintf(errs, "%s:%d: usage: go 1.23\n", f.Syntax.Name, line.Start.Line) 188 | return 189 | } 190 | f.Go = &Go{Syntax: line} 191 | f.Go.Version = args[0] 192 | case "module": 193 | if f.Module != nil { 194 | fmt.Fprintf(errs, "%s:%d: repeated module statement\n", f.Syntax.Name, line.Start.Line) 195 | return 196 | } 197 | f.Module = &Module{Syntax: line} 198 | if len(args) != 1 { 199 | 200 | fmt.Fprintf(errs, "%s:%d: usage: module module/path [version]\n", f.Syntax.Name, line.Start.Line) 201 | return 202 | } 203 | s, err := parseString(&args[0]) 204 | if err != nil { 205 | fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err) 206 | return 207 | } 208 | f.Module.Mod = module.Version{Path: s} 209 | case "require", "exclude": 210 | if len(args) != 2 { 211 | fmt.Fprintf(errs, "%s:%d: usage: %s module/path v1.2.3\n", f.Syntax.Name, line.Start.Line, verb) 212 | return 213 | } 214 | s, err := parseString(&args[0]) 215 | if err != nil { 216 | fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err) 217 | return 218 | } 219 | old := args[1] 220 | v, err := parseVersion(s, &args[1], fix) 221 | if err != nil { 222 | fmt.Fprintf(errs, "%s:%d: invalid module version %q: %v\n", f.Syntax.Name, line.Start.Line, old, err) 223 | return 224 | } 225 | pathMajor, err := modulePathMajor(s) 226 | if err != nil { 227 | fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err) 228 | return 229 | } 230 | if !module.MatchPathMajor(v, pathMajor) { 231 | if pathMajor == "" { 232 | pathMajor = "v0 or v1" 233 | } 234 | fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, pathMajor, semver.Major(v), v) 235 | return 236 | } 237 | if verb == "require" { 238 | f.Require = append(f.Require, &Require{ 239 | Mod: module.Version{Path: s, Version: v}, 240 | Syntax: line, 241 | Indirect: isIndirect(line), 242 | }) 243 | } else { 244 | f.Exclude = append(f.Exclude, &Exclude{ 245 | Mod: module.Version{Path: s, Version: v}, 246 | Syntax: line, 247 | }) 248 | } 249 | case "replace": 250 | arrow := 2 251 | if len(args) >= 2 && args[1] == "=>" { 252 | arrow = 1 253 | } 254 | if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" { 255 | fmt.Fprintf(errs, "%s:%d: usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory\n", f.Syntax.Name, line.Start.Line, verb, verb) 256 | return 257 | } 258 | s, err := parseString(&args[0]) 259 | if err != nil { 260 | fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err) 261 | return 262 | } 263 | pathMajor, err := modulePathMajor(s) 264 | if err != nil { 265 | fmt.Fprintf(errs, "%s:%d: %v\n", f.Syntax.Name, line.Start.Line, err) 266 | return 267 | } 268 | var v string 269 | if arrow == 2 { 270 | old := args[1] 271 | v, err = parseVersion(s, &args[1], fix) 272 | if err != nil { 273 | fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err) 274 | return 275 | } 276 | if !module.MatchPathMajor(v, pathMajor) { 277 | if pathMajor == "" { 278 | pathMajor = "v0 or v1" 279 | } 280 | fmt.Fprintf(errs, "%s:%d: invalid module: %s should be %s, not %s (%s)\n", f.Syntax.Name, line.Start.Line, s, pathMajor, semver.Major(v), v) 281 | return 282 | } 283 | } 284 | ns, err := parseString(&args[arrow+1]) 285 | if err != nil { 286 | fmt.Fprintf(errs, "%s:%d: invalid quoted string: %v\n", f.Syntax.Name, line.Start.Line, err) 287 | return 288 | } 289 | nv := "" 290 | if len(args) == arrow+2 { 291 | if !IsDirectoryPath(ns) { 292 | fmt.Fprintf(errs, "%s:%d: replacement module without version must be directory path (rooted or starting with ./ or ../)\n", f.Syntax.Name, line.Start.Line) 293 | return 294 | } 295 | if filepath.Separator == '/' && strings.Contains(ns, `\`) { 296 | fmt.Fprintf(errs, "%s:%d: replacement directory appears to be Windows path (on a non-windows system)\n", f.Syntax.Name, line.Start.Line) 297 | return 298 | } 299 | } 300 | if len(args) == arrow+3 { 301 | old := args[arrow+1] 302 | nv, err = parseVersion(ns, &args[arrow+2], fix) 303 | if err != nil { 304 | fmt.Fprintf(errs, "%s:%d: invalid module version %v: %v\n", f.Syntax.Name, line.Start.Line, old, err) 305 | return 306 | } 307 | if IsDirectoryPath(ns) { 308 | fmt.Fprintf(errs, "%s:%d: replacement module directory path %q cannot have version\n", f.Syntax.Name, line.Start.Line, ns) 309 | return 310 | } 311 | } 312 | f.Replace = append(f.Replace, &Replace{ 313 | Old: module.Version{Path: s, Version: v}, 314 | New: module.Version{Path: ns, Version: nv}, 315 | Syntax: line, 316 | }) 317 | } 318 | } 319 | 320 | // isIndirect reports whether line has a "// indirect" comment, 321 | // meaning it is in go.mod only for its effect on indirect dependencies, 322 | // so that it can be dropped entirely once the effective version of the 323 | // indirect dependency reaches the given minimum version. 324 | func isIndirect(line *Line) bool { 325 | if len(line.Suffix) == 0 { 326 | return false 327 | } 328 | f := strings.Fields(line.Suffix[0].Token) 329 | return (len(f) == 2 && f[1] == "indirect" || len(f) > 2 && f[1] == "indirect;") && f[0] == "//" 330 | } 331 | 332 | // setIndirect sets line to have (or not have) a "// indirect" comment. 333 | func setIndirect(line *Line, indirect bool) { 334 | if isIndirect(line) == indirect { 335 | return 336 | } 337 | if indirect { 338 | // Adding comment. 339 | if len(line.Suffix) == 0 { 340 | // New comment. 341 | line.Suffix = []Comment{{Token: "// indirect", Suffix: true}} 342 | return 343 | } 344 | // Insert at beginning of existing comment. 345 | com := &line.Suffix[0] 346 | space := " " 347 | if len(com.Token) > 2 && com.Token[2] == ' ' || com.Token[2] == '\t' { 348 | space = "" 349 | } 350 | com.Token = "// indirect;" + space + com.Token[2:] 351 | return 352 | } 353 | 354 | // Removing comment. 355 | f := strings.Fields(line.Suffix[0].Token) 356 | if len(f) == 2 { 357 | // Remove whole comment. 358 | line.Suffix = nil 359 | return 360 | } 361 | 362 | // Remove comment prefix. 363 | com := &line.Suffix[0] 364 | i := strings.Index(com.Token, "indirect;") 365 | com.Token = "//" + com.Token[i+len("indirect;"):] 366 | } 367 | 368 | // IsDirectoryPath reports whether the given path should be interpreted 369 | // as a directory path. Just like on the go command line, relative paths 370 | // and rooted paths are directory paths; the rest are module paths. 371 | func IsDirectoryPath(ns string) bool { 372 | // Because go.mod files can move from one system to another, 373 | // we check all known path syntaxes, both Unix and Windows. 374 | return strings.HasPrefix(ns, "./") || strings.HasPrefix(ns, "../") || strings.HasPrefix(ns, "/") || 375 | strings.HasPrefix(ns, `.\`) || strings.HasPrefix(ns, `..\`) || strings.HasPrefix(ns, `\`) || 376 | len(ns) >= 2 && ('A' <= ns[0] && ns[0] <= 'Z' || 'a' <= ns[0] && ns[0] <= 'z') && ns[1] == ':' 377 | } 378 | 379 | // MustQuote reports whether s must be quoted in order to appear as 380 | // a single token in a go.mod line. 381 | func MustQuote(s string) bool { 382 | for _, r := range s { 383 | if !unicode.IsPrint(r) || r == ' ' || r == '"' || r == '\'' || r == '`' { 384 | return true 385 | } 386 | } 387 | return s == "" || strings.Contains(s, "//") || strings.Contains(s, "/*") 388 | } 389 | 390 | // AutoQuote returns s or, if quoting is required for s to appear in a go.mod, 391 | // the quotation of s. 392 | func AutoQuote(s string) string { 393 | if MustQuote(s) { 394 | return strconv.Quote(s) 395 | } 396 | return s 397 | } 398 | 399 | func parseString(s *string) (string, error) { 400 | t := *s 401 | if strings.HasPrefix(t, `"`) { 402 | var err error 403 | if t, err = strconv.Unquote(t); err != nil { 404 | return "", err 405 | } 406 | } else if strings.ContainsAny(t, "\"'`") { 407 | // Other quotes are reserved both for possible future expansion 408 | // and to avoid confusion. For example if someone types 'x' 409 | // we want that to be a syntax error and not a literal x in literal quotation marks. 410 | return "", fmt.Errorf("unquoted string cannot contain quote") 411 | } 412 | *s = AutoQuote(t) 413 | return t, nil 414 | } 415 | 416 | func parseVersion(path string, s *string, fix VersionFixer) (string, error) { 417 | t, err := parseString(s) 418 | if err != nil { 419 | return "", err 420 | } 421 | if fix != nil { 422 | var err error 423 | t, err = fix(path, t) 424 | if err != nil { 425 | return "", err 426 | } 427 | } 428 | if v := module.CanonicalVersion(t); v != "" { 429 | *s = v 430 | return *s, nil 431 | } 432 | return "", fmt.Errorf("version must be of the form v1.2.3") 433 | } 434 | 435 | func modulePathMajor(path string) (string, error) { 436 | _, major, ok := module.SplitPathVersion(path) 437 | if !ok { 438 | return "", fmt.Errorf("invalid module path") 439 | } 440 | return major, nil 441 | } 442 | 443 | func (f *File) Format() ([]byte, error) { 444 | return Format(f.Syntax), nil 445 | } 446 | 447 | // Cleanup cleans up the file f after any edit operations. 448 | // To avoid quadratic behavior, modifications like DropRequire 449 | // clear the entry but do not remove it from the slice. 450 | // Cleanup cleans out all the cleared entries. 451 | func (f *File) Cleanup() { 452 | w := 0 453 | for _, r := range f.Require { 454 | if r.Mod.Path != "" { 455 | f.Require[w] = r 456 | w++ 457 | } 458 | } 459 | f.Require = f.Require[:w] 460 | 461 | w = 0 462 | for _, x := range f.Exclude { 463 | if x.Mod.Path != "" { 464 | f.Exclude[w] = x 465 | w++ 466 | } 467 | } 468 | f.Exclude = f.Exclude[:w] 469 | 470 | w = 0 471 | for _, r := range f.Replace { 472 | if r.Old.Path != "" { 473 | f.Replace[w] = r 474 | w++ 475 | } 476 | } 477 | f.Replace = f.Replace[:w] 478 | 479 | f.Syntax.Cleanup() 480 | } 481 | 482 | func (f *File) AddGoStmt(version string) error { 483 | if !GoVersionRE.MatchString(version) { 484 | return fmt.Errorf("invalid language version string %q", version) 485 | } 486 | if f.Go == nil { 487 | f.Go = &Go{ 488 | Version: version, 489 | Syntax: f.Syntax.addLine(nil, "go", version), 490 | } 491 | } else { 492 | f.Go.Version = version 493 | f.Syntax.updateLine(f.Go.Syntax, "go", version) 494 | } 495 | return nil 496 | } 497 | 498 | func (f *File) AddRequire(path, vers string) error { 499 | need := true 500 | for _, r := range f.Require { 501 | if r.Mod.Path == path { 502 | if need { 503 | r.Mod.Version = vers 504 | f.Syntax.updateLine(r.Syntax, "require", AutoQuote(path), vers) 505 | need = false 506 | } else { 507 | f.Syntax.removeLine(r.Syntax) 508 | *r = Require{} 509 | } 510 | } 511 | } 512 | 513 | if need { 514 | f.AddNewRequire(path, vers, false) 515 | } 516 | return nil 517 | } 518 | 519 | func (f *File) AddNewRequire(path, vers string, indirect bool) { 520 | line := f.Syntax.addLine(nil, "require", AutoQuote(path), vers) 521 | setIndirect(line, indirect) 522 | f.Require = append(f.Require, &Require{module.Version{Path: path, Version: vers}, indirect, line}) 523 | } 524 | 525 | func (f *File) SetRequire(req []*Require) { 526 | need := make(map[string]string) 527 | indirect := make(map[string]bool) 528 | for _, r := range req { 529 | need[r.Mod.Path] = r.Mod.Version 530 | indirect[r.Mod.Path] = r.Indirect 531 | } 532 | 533 | for _, r := range f.Require { 534 | if v, ok := need[r.Mod.Path]; ok { 535 | r.Mod.Version = v 536 | r.Indirect = indirect[r.Mod.Path] 537 | } 538 | } 539 | 540 | var newStmts []Expr 541 | for _, stmt := range f.Syntax.Stmt { 542 | switch stmt := stmt.(type) { 543 | case *LineBlock: 544 | if len(stmt.Token) > 0 && stmt.Token[0] == "require" { 545 | var newLines []*Line 546 | for _, line := range stmt.Line { 547 | if p, err := parseString(&line.Token[0]); err == nil && need[p] != "" { 548 | line.Token[1] = need[p] 549 | delete(need, p) 550 | setIndirect(line, indirect[p]) 551 | newLines = append(newLines, line) 552 | } 553 | } 554 | if len(newLines) == 0 { 555 | continue // drop stmt 556 | } 557 | stmt.Line = newLines 558 | } 559 | 560 | case *Line: 561 | if len(stmt.Token) > 0 && stmt.Token[0] == "require" { 562 | if p, err := parseString(&stmt.Token[1]); err == nil && need[p] != "" { 563 | stmt.Token[2] = need[p] 564 | delete(need, p) 565 | setIndirect(stmt, indirect[p]) 566 | } else { 567 | continue // drop stmt 568 | } 569 | } 570 | } 571 | newStmts = append(newStmts, stmt) 572 | } 573 | f.Syntax.Stmt = newStmts 574 | 575 | for path, vers := range need { 576 | f.AddNewRequire(path, vers, indirect[path]) 577 | } 578 | f.SortBlocks() 579 | } 580 | 581 | func (f *File) DropRequire(path string) error { 582 | for _, r := range f.Require { 583 | if r.Mod.Path == path { 584 | f.Syntax.removeLine(r.Syntax) 585 | *r = Require{} 586 | } 587 | } 588 | return nil 589 | } 590 | 591 | func (f *File) AddExclude(path, vers string) error { 592 | var hint *Line 593 | for _, x := range f.Exclude { 594 | if x.Mod.Path == path && x.Mod.Version == vers { 595 | return nil 596 | } 597 | if x.Mod.Path == path { 598 | hint = x.Syntax 599 | } 600 | } 601 | 602 | f.Exclude = append(f.Exclude, &Exclude{Mod: module.Version{Path: path, Version: vers}, Syntax: f.Syntax.addLine(hint, "exclude", AutoQuote(path), vers)}) 603 | return nil 604 | } 605 | 606 | func (f *File) DropExclude(path, vers string) error { 607 | for _, x := range f.Exclude { 608 | if x.Mod.Path == path && x.Mod.Version == vers { 609 | f.Syntax.removeLine(x.Syntax) 610 | *x = Exclude{} 611 | } 612 | } 613 | return nil 614 | } 615 | 616 | func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error { 617 | need := true 618 | old := module.Version{Path: oldPath, Version: oldVers} 619 | new := module.Version{Path: newPath, Version: newVers} 620 | tokens := []string{"replace", AutoQuote(oldPath)} 621 | if oldVers != "" { 622 | tokens = append(tokens, oldVers) 623 | } 624 | tokens = append(tokens, "=>", AutoQuote(newPath)) 625 | if newVers != "" { 626 | tokens = append(tokens, newVers) 627 | } 628 | 629 | var hint *Line 630 | for _, r := range f.Replace { 631 | if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) { 632 | if need { 633 | // Found replacement for old; update to use new. 634 | r.New = new 635 | f.Syntax.updateLine(r.Syntax, tokens...) 636 | need = false 637 | continue 638 | } 639 | // Already added; delete other replacements for same. 640 | f.Syntax.removeLine(r.Syntax) 641 | *r = Replace{} 642 | } 643 | if r.Old.Path == oldPath { 644 | hint = r.Syntax 645 | } 646 | } 647 | if need { 648 | f.Replace = append(f.Replace, &Replace{Old: old, New: new, Syntax: f.Syntax.addLine(hint, tokens...)}) 649 | } 650 | return nil 651 | } 652 | 653 | func (f *File) DropReplace(oldPath, oldVers string) error { 654 | for _, r := range f.Replace { 655 | if r.Old.Path == oldPath && r.Old.Version == oldVers { 656 | f.Syntax.removeLine(r.Syntax) 657 | *r = Replace{} 658 | } 659 | } 660 | return nil 661 | } 662 | 663 | func (f *File) SortBlocks() { 664 | f.removeDups() // otherwise sorting is unsafe 665 | 666 | for _, stmt := range f.Syntax.Stmt { 667 | block, ok := stmt.(*LineBlock) 668 | if !ok { 669 | continue 670 | } 671 | sort.Slice(block.Line, func(i, j int) bool { 672 | li := block.Line[i] 673 | lj := block.Line[j] 674 | for k := 0; k < len(li.Token) && k < len(lj.Token); k++ { 675 | if li.Token[k] != lj.Token[k] { 676 | return li.Token[k] < lj.Token[k] 677 | } 678 | } 679 | return len(li.Token) < len(lj.Token) 680 | }) 681 | } 682 | } 683 | 684 | func (f *File) removeDups() { 685 | have := make(map[module.Version]bool) 686 | kill := make(map[*Line]bool) 687 | for _, x := range f.Exclude { 688 | if have[x.Mod] { 689 | kill[x.Syntax] = true 690 | continue 691 | } 692 | have[x.Mod] = true 693 | } 694 | var excl []*Exclude 695 | for _, x := range f.Exclude { 696 | if !kill[x.Syntax] { 697 | excl = append(excl, x) 698 | } 699 | } 700 | f.Exclude = excl 701 | 702 | have = make(map[module.Version]bool) 703 | // Later replacements take priority over earlier ones. 704 | for i := len(f.Replace) - 1; i >= 0; i-- { 705 | x := f.Replace[i] 706 | if have[x.Old] { 707 | kill[x.Syntax] = true 708 | continue 709 | } 710 | have[x.Old] = true 711 | } 712 | var repl []*Replace 713 | for _, x := range f.Replace { 714 | if !kill[x.Syntax] { 715 | repl = append(repl, x) 716 | } 717 | } 718 | f.Replace = repl 719 | 720 | var stmts []Expr 721 | for _, stmt := range f.Syntax.Stmt { 722 | switch stmt := stmt.(type) { 723 | case *Line: 724 | if kill[stmt] { 725 | continue 726 | } 727 | case *LineBlock: 728 | var lines []*Line 729 | for _, line := range stmt.Line { 730 | if !kill[line] { 731 | lines = append(lines, line) 732 | } 733 | } 734 | stmt.Line = lines 735 | if len(lines) == 0 { 736 | continue 737 | } 738 | } 739 | stmts = append(stmts, stmt) 740 | } 741 | f.Syntax.Stmt = stmts 742 | } 743 | -------------------------------------------------------------------------------- /internal/gocopy/module/module.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package module defines the module.Version type 6 | // along with support code. 7 | package module 8 | 9 | // IMPORTANT NOTE 10 | // 11 | // This file essentially defines the set of valid import paths for the go command. 12 | // There are many subtle considerations, including Unicode ambiguity, 13 | // security, network, and file system representations. 14 | // 15 | // This file also defines the set of valid module path and version combinations, 16 | // another topic with many subtle considerations. 17 | // 18 | // Changes to the semantics in this file require approval from rsc. 19 | 20 | import ( 21 | "fmt" 22 | "sort" 23 | "strings" 24 | "unicode" 25 | "unicode/utf8" 26 | 27 | "marwan.io/protoc-gen-twirpql/internal/gocopy/semver" 28 | ) 29 | 30 | // A Version is defined by a module path and version pair. 31 | type Version struct { 32 | Path string 33 | 34 | // Version is usually a semantic version in canonical form. 35 | // There are two exceptions to this general rule. 36 | // First, the top-level target of a build has no specific version 37 | // and uses Version = "". 38 | // Second, during MVS calculations the version "none" is used 39 | // to represent the decision to take no version of a given module. 40 | Version string `json:",omitempty"` 41 | } 42 | 43 | // Check checks that a given module path, version pair is valid. 44 | // In addition to the path being a valid module path 45 | // and the version being a valid semantic version, 46 | // the two must correspond. 47 | // For example, the path "yaml/v2" only corresponds to 48 | // semantic versions beginning with "v2.". 49 | func Check(path, version string) error { 50 | if err := CheckPath(path); err != nil { 51 | return err 52 | } 53 | if !semver.IsValid(version) { 54 | return fmt.Errorf("malformed semantic version %v", version) 55 | } 56 | _, pathMajor, _ := SplitPathVersion(path) 57 | if !MatchPathMajor(version, pathMajor) { 58 | if pathMajor == "" { 59 | pathMajor = "v0 or v1" 60 | } 61 | if pathMajor[0] == '.' { // .v1 62 | pathMajor = pathMajor[1:] 63 | } 64 | return fmt.Errorf("mismatched module path %v and version %v (want %v)", path, version, pathMajor) 65 | } 66 | return nil 67 | } 68 | 69 | // firstPathOK reports whether r can appear in the first element of a module path. 70 | // The first element of the path must be an LDH domain name, at least for now. 71 | // To avoid case ambiguity, the domain name must be entirely lower case. 72 | func firstPathOK(r rune) bool { 73 | return r == '-' || r == '.' || 74 | '0' <= r && r <= '9' || 75 | 'a' <= r && r <= 'z' 76 | } 77 | 78 | // pathOK reports whether r can appear in an import path element. 79 | // Paths can be ASCII letters, ASCII digits, and limited ASCII punctuation: + - . _ and ~. 80 | // This matches what "go get" has historically recognized in import paths. 81 | // TODO(rsc): We would like to allow Unicode letters, but that requires additional 82 | // care in the safe encoding (see note below). 83 | func pathOK(r rune) bool { 84 | if r < utf8.RuneSelf { 85 | return r == '+' || r == '-' || r == '.' || r == '_' || r == '~' || 86 | '0' <= r && r <= '9' || 87 | 'A' <= r && r <= 'Z' || 88 | 'a' <= r && r <= 'z' 89 | } 90 | return false 91 | } 92 | 93 | // fileNameOK reports whether r can appear in a file name. 94 | // For now we allow all Unicode letters but otherwise limit to pathOK plus a few more punctuation characters. 95 | // If we expand the set of allowed characters here, we have to 96 | // work harder at detecting potential case-folding and normalization collisions. 97 | // See note about "safe encoding" below. 98 | func fileNameOK(r rune) bool { 99 | if r < utf8.RuneSelf { 100 | // Entire set of ASCII punctuation, from which we remove characters: 101 | // ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~ 102 | // We disallow some shell special characters: " ' * < > ? ` | 103 | // (Note that some of those are disallowed by the Windows file system as well.) 104 | // We also disallow path separators / : and \ (fileNameOK is only called on path element characters). 105 | // We allow spaces (U+0020) in file names. 106 | const allowed = "!#$%&()+,-.=@[]^_{}~ " 107 | if '0' <= r && r <= '9' || 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' { 108 | return true 109 | } 110 | for i := 0; i < len(allowed); i++ { 111 | if rune(allowed[i]) == r { 112 | return true 113 | } 114 | } 115 | return false 116 | } 117 | // It may be OK to add more ASCII punctuation here, but only carefully. 118 | // For example Windows disallows < > \, and macOS disallows :, so we must not allow those. 119 | return unicode.IsLetter(r) 120 | } 121 | 122 | // CheckPath checks that a module path is valid. 123 | func CheckPath(path string) error { 124 | if err := checkPath(path, false); err != nil { 125 | return fmt.Errorf("malformed module path %q: %v", path, err) 126 | } 127 | i := strings.Index(path, "/") 128 | if i < 0 { 129 | i = len(path) 130 | } 131 | if i == 0 { 132 | return fmt.Errorf("malformed module path %q: leading slash", path) 133 | } 134 | if !strings.Contains(path[:i], ".") { 135 | return fmt.Errorf("malformed module path %q: missing dot in first path element", path) 136 | } 137 | if path[0] == '-' { 138 | return fmt.Errorf("malformed module path %q: leading dash in first path element", path) 139 | } 140 | for _, r := range path[:i] { 141 | if !firstPathOK(r) { 142 | return fmt.Errorf("malformed module path %q: invalid char %q in first path element", path, r) 143 | } 144 | } 145 | if _, _, ok := SplitPathVersion(path); !ok { 146 | return fmt.Errorf("malformed module path %q: invalid version", path) 147 | } 148 | return nil 149 | } 150 | 151 | // CheckImportPath checks that an import path is valid. 152 | func CheckImportPath(path string) error { 153 | if err := checkPath(path, false); err != nil { 154 | return fmt.Errorf("malformed import path %q: %v", path, err) 155 | } 156 | return nil 157 | } 158 | 159 | // checkPath checks that a general path is valid. 160 | // It returns an error describing why but not mentioning path. 161 | // Because these checks apply to both module paths and import paths, 162 | // the caller is expected to add the "malformed ___ path %q: " prefix. 163 | // fileName indicates whether the final element of the path is a file name 164 | // (as opposed to a directory name). 165 | func checkPath(path string, fileName bool) error { 166 | if !utf8.ValidString(path) { 167 | return fmt.Errorf("invalid UTF-8") 168 | } 169 | if path == "" { 170 | return fmt.Errorf("empty string") 171 | } 172 | if strings.Contains(path, "..") { 173 | return fmt.Errorf("double dot") 174 | } 175 | if strings.Contains(path, "//") { 176 | return fmt.Errorf("double slash") 177 | } 178 | if path[len(path)-1] == '/' { 179 | return fmt.Errorf("trailing slash") 180 | } 181 | elemStart := 0 182 | for i, r := range path { 183 | if r == '/' { 184 | if err := checkElem(path[elemStart:i], fileName); err != nil { 185 | return err 186 | } 187 | elemStart = i + 1 188 | } 189 | } 190 | if err := checkElem(path[elemStart:], fileName); err != nil { 191 | return err 192 | } 193 | return nil 194 | } 195 | 196 | // checkElem checks whether an individual path element is valid. 197 | // fileName indicates whether the element is a file name (not a directory name). 198 | func checkElem(elem string, fileName bool) error { 199 | if elem == "" { 200 | return fmt.Errorf("empty path element") 201 | } 202 | if strings.Count(elem, ".") == len(elem) { 203 | return fmt.Errorf("invalid path element %q", elem) 204 | } 205 | if elem[0] == '.' && !fileName { 206 | return fmt.Errorf("leading dot in path element") 207 | } 208 | if elem[len(elem)-1] == '.' { 209 | return fmt.Errorf("trailing dot in path element") 210 | } 211 | charOK := pathOK 212 | if fileName { 213 | charOK = fileNameOK 214 | } 215 | for _, r := range elem { 216 | if !charOK(r) { 217 | return fmt.Errorf("invalid char %q", r) 218 | } 219 | } 220 | 221 | // Windows disallows a bunch of path elements, sadly. 222 | // See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file 223 | short := elem 224 | if i := strings.Index(short, "."); i >= 0 { 225 | short = short[:i] 226 | } 227 | for _, bad := range badWindowsNames { 228 | if strings.EqualFold(bad, short) { 229 | return fmt.Errorf("%q disallowed as path element component on Windows", short) 230 | } 231 | } 232 | return nil 233 | } 234 | 235 | // CheckFilePath checks whether a slash-separated file path is valid. 236 | func CheckFilePath(path string) error { 237 | if err := checkPath(path, true); err != nil { 238 | return fmt.Errorf("malformed file path %q: %v", path, err) 239 | } 240 | return nil 241 | } 242 | 243 | // badWindowsNames are the reserved file path elements on Windows. 244 | // See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file 245 | var badWindowsNames = []string{ 246 | "CON", 247 | "PRN", 248 | "AUX", 249 | "NUL", 250 | "COM1", 251 | "COM2", 252 | "COM3", 253 | "COM4", 254 | "COM5", 255 | "COM6", 256 | "COM7", 257 | "COM8", 258 | "COM9", 259 | "LPT1", 260 | "LPT2", 261 | "LPT3", 262 | "LPT4", 263 | "LPT5", 264 | "LPT6", 265 | "LPT7", 266 | "LPT8", 267 | "LPT9", 268 | } 269 | 270 | // SplitPathVersion returns prefix and major version such that prefix+pathMajor == path 271 | // and version is either empty or "/vN" for N >= 2. 272 | // As a special case, gopkg.in paths are recognized directly; 273 | // they require ".vN" instead of "/vN", and for all N, not just N >= 2. 274 | func SplitPathVersion(path string) (prefix, pathMajor string, ok bool) { 275 | if strings.HasPrefix(path, "gopkg.in/") { 276 | return splitGopkgIn(path) 277 | } 278 | 279 | i := len(path) 280 | dot := false 281 | for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9' || path[i-1] == '.') { 282 | if path[i-1] == '.' { 283 | dot = true 284 | } 285 | i-- 286 | } 287 | if i <= 1 || i == len(path) || path[i-1] != 'v' || path[i-2] != '/' { 288 | return path, "", true 289 | } 290 | prefix, pathMajor = path[:i-2], path[i-2:] 291 | if dot || len(pathMajor) <= 2 || pathMajor[2] == '0' || pathMajor == "/v1" { 292 | return path, "", false 293 | } 294 | return prefix, pathMajor, true 295 | } 296 | 297 | // splitGopkgIn is like SplitPathVersion but only for gopkg.in paths. 298 | func splitGopkgIn(path string) (prefix, pathMajor string, ok bool) { 299 | if !strings.HasPrefix(path, "gopkg.in/") { 300 | return path, "", false 301 | } 302 | i := len(path) 303 | if strings.HasSuffix(path, "-unstable") { 304 | i -= len("-unstable") 305 | } 306 | for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') { 307 | i-- 308 | } 309 | if i <= 1 || path[i-1] != 'v' || path[i-2] != '.' { 310 | // All gopkg.in paths must end in vN for some N. 311 | return path, "", false 312 | } 313 | prefix, pathMajor = path[:i-2], path[i-2:] 314 | if len(pathMajor) <= 2 || pathMajor[2] == '0' && pathMajor != ".v0" { 315 | return path, "", false 316 | } 317 | return prefix, pathMajor, true 318 | } 319 | 320 | // MatchPathMajor reports whether the semantic version v 321 | // matches the path major version pathMajor. 322 | func MatchPathMajor(v, pathMajor string) bool { 323 | if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") { 324 | pathMajor = strings.TrimSuffix(pathMajor, "-unstable") 325 | } 326 | if strings.HasPrefix(v, "v0.0.0-") && pathMajor == ".v1" { 327 | // Allow old bug in pseudo-versions that generated v0.0.0- pseudoversion for gopkg .v1. 328 | // For example, gopkg.in/yaml.v2@v2.2.1's go.mod requires gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405. 329 | return true 330 | } 331 | m := semver.Major(v) 332 | if pathMajor == "" { 333 | return m == "v0" || m == "v1" || semver.Build(v) == "+incompatible" 334 | } 335 | return (pathMajor[0] == '/' || pathMajor[0] == '.') && m == pathMajor[1:] 336 | } 337 | 338 | // CanonicalVersion returns the canonical form of the version string v. 339 | // It is the same as semver.Canonical(v) except that it preserves the special build suffix "+incompatible". 340 | func CanonicalVersion(v string) string { 341 | cv := semver.Canonical(v) 342 | if semver.Build(v) == "+incompatible" { 343 | cv += "+incompatible" 344 | } 345 | return cv 346 | } 347 | 348 | // Sort sorts the list by Path, breaking ties by comparing Versions. 349 | func Sort(list []Version) { 350 | sort.Slice(list, func(i, j int) bool { 351 | mi := list[i] 352 | mj := list[j] 353 | if mi.Path != mj.Path { 354 | return mi.Path < mj.Path 355 | } 356 | // To help go.sum formatting, allow version/file. 357 | // Compare semver prefix by semver rules, 358 | // file by string order. 359 | vi := mi.Version 360 | vj := mj.Version 361 | var fi, fj string 362 | if k := strings.Index(vi, "/"); k >= 0 { 363 | vi, fi = vi[:k], vi[k:] 364 | } 365 | if k := strings.Index(vj, "/"); k >= 0 { 366 | vj, fj = vj[:k], vj[k:] 367 | } 368 | if vi != vj { 369 | return semver.Compare(vi, vj) < 0 370 | } 371 | return fi < fj 372 | }) 373 | } 374 | 375 | // Safe encodings 376 | // 377 | // Module paths appear as substrings of file system paths 378 | // (in the download cache) and of web server URLs in the proxy protocol. 379 | // In general we cannot rely on file systems to be case-sensitive, 380 | // nor can we rely on web servers, since they read from file systems. 381 | // That is, we cannot rely on the file system to keep rsc.io/QUOTE 382 | // and rsc.io/quote separate. Windows and macOS don't. 383 | // Instead, we must never require two different casings of a file path. 384 | // Because we want the download cache to match the proxy protocol, 385 | // and because we want the proxy protocol to be possible to serve 386 | // from a tree of static files (which might be stored on a case-insensitive 387 | // file system), the proxy protocol must never require two different casings 388 | // of a URL path either. 389 | // 390 | // One possibility would be to make the safe encoding be the lowercase 391 | // hexadecimal encoding of the actual path bytes. This would avoid ever 392 | // needing different casings of a file path, but it would be fairly illegible 393 | // to most programmers when those paths appeared in the file system 394 | // (including in file paths in compiler errors and stack traces) 395 | // in web server logs, and so on. Instead, we want a safe encoding that 396 | // leaves most paths unaltered. 397 | // 398 | // The safe encoding is this: 399 | // replace every uppercase letter with an exclamation mark 400 | // followed by the letter's lowercase equivalent. 401 | // 402 | // For example, 403 | // github.com/Azure/azure-sdk-for-go -> github.com/!azure/azure-sdk-for-go. 404 | // github.com/GoogleCloudPlatform/cloudsql-proxy -> github.com/!google!cloud!platform/cloudsql-proxy 405 | // github.com/Sirupsen/logrus -> github.com/!sirupsen/logrus. 406 | // 407 | // Import paths that avoid upper-case letters are left unchanged. 408 | // Note that because import paths are ASCII-only and avoid various 409 | // problematic punctuation (like : < and >), the safe encoding is also ASCII-only 410 | // and avoids the same problematic punctuation. 411 | // 412 | // Import paths have never allowed exclamation marks, so there is no 413 | // need to define how to encode a literal !. 414 | // 415 | // Although paths are disallowed from using Unicode (see pathOK above), 416 | // the eventual plan is to allow Unicode letters as well, to assume that 417 | // file systems and URLs are Unicode-safe (storing UTF-8), and apply 418 | // the !-for-uppercase convention. Note however that not all runes that 419 | // are different but case-fold equivalent are an upper/lower pair. 420 | // For example, U+004B ('K'), U+006B ('k'), and U+212A ('K' for Kelvin) 421 | // are considered to case-fold to each other. When we do add Unicode 422 | // letters, we must not assume that upper/lower are the only case-equivalent pairs. 423 | // Perhaps the Kelvin symbol would be disallowed entirely, for example. 424 | // Or perhaps it would encode as "!!k", or perhaps as "(212A)". 425 | // 426 | // Also, it would be nice to allow Unicode marks as well as letters, 427 | // but marks include combining marks, and then we must deal not 428 | // only with case folding but also normalization: both U+00E9 ('é') 429 | // and U+0065 U+0301 ('e' followed by combining acute accent) 430 | // look the same on the page and are treated by some file systems 431 | // as the same path. If we do allow Unicode marks in paths, there 432 | // must be some kind of normalization to allow only one canonical 433 | // encoding of any character used in an import path. 434 | 435 | // EncodePath returns the safe encoding of the given module path. 436 | // It fails if the module path is invalid. 437 | func EncodePath(path string) (encoding string, err error) { 438 | if err := CheckPath(path); err != nil { 439 | return "", err 440 | } 441 | 442 | return encodeString(path) 443 | } 444 | 445 | // EncodeVersion returns the safe encoding of the given module version. 446 | // Versions are allowed to be in non-semver form but must be valid file names 447 | // and not contain exclamation marks. 448 | func EncodeVersion(v string) (encoding string, err error) { 449 | if err := checkElem(v, true); err != nil || strings.Contains(v, "!") { 450 | return "", fmt.Errorf("disallowed version string %q", v) 451 | } 452 | return encodeString(v) 453 | } 454 | 455 | func encodeString(s string) (encoding string, err error) { 456 | haveUpper := false 457 | for _, r := range s { 458 | if r == '!' || r >= utf8.RuneSelf { 459 | // This should be disallowed by CheckPath, but diagnose anyway. 460 | // The correctness of the encoding loop below depends on it. 461 | return "", fmt.Errorf("internal error: inconsistency in EncodePath") 462 | } 463 | if 'A' <= r && r <= 'Z' { 464 | haveUpper = true 465 | } 466 | } 467 | 468 | if !haveUpper { 469 | return s, nil 470 | } 471 | 472 | var buf []byte 473 | for _, r := range s { 474 | if 'A' <= r && r <= 'Z' { 475 | buf = append(buf, '!', byte(r+'a'-'A')) 476 | } else { 477 | buf = append(buf, byte(r)) 478 | } 479 | } 480 | return string(buf), nil 481 | } 482 | 483 | // DecodePath returns the module path of the given safe encoding. 484 | // It fails if the encoding is invalid or encodes an invalid path. 485 | func DecodePath(encoding string) (path string, err error) { 486 | path, ok := decodeString(encoding) 487 | if !ok { 488 | return "", fmt.Errorf("invalid module path encoding %q", encoding) 489 | } 490 | if err := CheckPath(path); err != nil { 491 | return "", fmt.Errorf("invalid module path encoding %q: %v", encoding, err) 492 | } 493 | return path, nil 494 | } 495 | 496 | // DecodeVersion returns the version string for the given safe encoding. 497 | // It fails if the encoding is invalid or encodes an invalid version. 498 | // Versions are allowed to be in non-semver form but must be valid file names 499 | // and not contain exclamation marks. 500 | func DecodeVersion(encoding string) (v string, err error) { 501 | v, ok := decodeString(encoding) 502 | if !ok { 503 | return "", fmt.Errorf("invalid version encoding %q", encoding) 504 | } 505 | if err := checkElem(v, true); err != nil { 506 | return "", fmt.Errorf("disallowed version string %q", v) 507 | } 508 | return v, nil 509 | } 510 | 511 | func decodeString(encoding string) (string, bool) { 512 | var buf []byte 513 | 514 | bang := false 515 | for _, r := range encoding { 516 | if r >= utf8.RuneSelf { 517 | return "", false 518 | } 519 | if bang { 520 | bang = false 521 | if r < 'a' || 'z' < r { 522 | return "", false 523 | } 524 | buf = append(buf, byte(r+'A'-'a')) 525 | continue 526 | } 527 | if r == '!' { 528 | bang = true 529 | continue 530 | } 531 | if 'A' <= r && r <= 'Z' { 532 | return "", false 533 | } 534 | buf = append(buf, byte(r)) 535 | } 536 | if bang { 537 | return "", false 538 | } 539 | return string(buf), true 540 | } 541 | -------------------------------------------------------------------------------- /internal/gocopy/semver/semver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package semver implements comparison of semantic version strings. 6 | // In this package, semantic version strings must begin with a leading "v", 7 | // as in "v1.0.0". 8 | // 9 | // The general form of a semantic version string accepted by this package is 10 | // 11 | // vMAJOR[.MINOR[.PATCH[-PRERELEASE][+BUILD]]] 12 | // 13 | // where square brackets indicate optional parts of the syntax; 14 | // MAJOR, MINOR, and PATCH are decimal integers without extra leading zeros; 15 | // PRERELEASE and BUILD are each a series of non-empty dot-separated identifiers 16 | // using only alphanumeric characters and hyphens; and 17 | // all-numeric PRERELEASE identifiers must not have leading zeros. 18 | // 19 | // This package follows Semantic Versioning 2.0.0 (see semver.org) 20 | // with two exceptions. First, it requires the "v" prefix. Second, it recognizes 21 | // vMAJOR and vMAJOR.MINOR (with no prerelease or build suffixes) 22 | // as shorthands for vMAJOR.0.0 and vMAJOR.MINOR.0. 23 | package semver 24 | 25 | // parsed returns the parsed form of a semantic version string. 26 | type parsed struct { 27 | major string 28 | minor string 29 | patch string 30 | short string 31 | prerelease string 32 | build string 33 | err string 34 | } 35 | 36 | // IsValid reports whether v is a valid semantic version string. 37 | func IsValid(v string) bool { 38 | _, ok := parse(v) 39 | return ok 40 | } 41 | 42 | // Canonical returns the canonical formatting of the semantic version v. 43 | // It fills in any missing .MINOR or .PATCH and discards build metadata. 44 | // Two semantic versions compare equal only if their canonical formattings 45 | // are identical strings. 46 | // The canonical invalid semantic version is the empty string. 47 | func Canonical(v string) string { 48 | p, ok := parse(v) 49 | if !ok { 50 | return "" 51 | } 52 | if p.build != "" { 53 | return v[:len(v)-len(p.build)] 54 | } 55 | if p.short != "" { 56 | return v + p.short 57 | } 58 | return v 59 | } 60 | 61 | // Major returns the major version prefix of the semantic version v. 62 | // For example, Major("v2.1.0") == "v2". 63 | // If v is an invalid semantic version string, Major returns the empty string. 64 | func Major(v string) string { 65 | pv, ok := parse(v) 66 | if !ok { 67 | return "" 68 | } 69 | return v[:1+len(pv.major)] 70 | } 71 | 72 | // MajorMinor returns the major.minor version prefix of the semantic version v. 73 | // For example, MajorMinor("v2.1.0") == "v2.1". 74 | // If v is an invalid semantic version string, MajorMinor returns the empty string. 75 | func MajorMinor(v string) string { 76 | pv, ok := parse(v) 77 | if !ok { 78 | return "" 79 | } 80 | i := 1 + len(pv.major) 81 | if j := i + 1 + len(pv.minor); j <= len(v) && v[i] == '.' && v[i+1:j] == pv.minor { 82 | return v[:j] 83 | } 84 | return v[:i] + "." + pv.minor 85 | } 86 | 87 | // Prerelease returns the prerelease suffix of the semantic version v. 88 | // For example, Prerelease("v2.1.0-pre+meta") == "-pre". 89 | // If v is an invalid semantic version string, Prerelease returns the empty string. 90 | func Prerelease(v string) string { 91 | pv, ok := parse(v) 92 | if !ok { 93 | return "" 94 | } 95 | return pv.prerelease 96 | } 97 | 98 | // Build returns the build suffix of the semantic version v. 99 | // For example, Build("v2.1.0+meta") == "+meta". 100 | // If v is an invalid semantic version string, Build returns the empty string. 101 | func Build(v string) string { 102 | pv, ok := parse(v) 103 | if !ok { 104 | return "" 105 | } 106 | return pv.build 107 | } 108 | 109 | // Compare returns an integer comparing two versions according to 110 | // according to semantic version precedence. 111 | // The result will be 0 if v == w, -1 if v < w, or +1 if v > w. 112 | // 113 | // An invalid semantic version string is considered less than a valid one. 114 | // All invalid semantic version strings compare equal to each other. 115 | func Compare(v, w string) int { 116 | pv, ok1 := parse(v) 117 | pw, ok2 := parse(w) 118 | if !ok1 && !ok2 { 119 | return 0 120 | } 121 | if !ok1 { 122 | return -1 123 | } 124 | if !ok2 { 125 | return +1 126 | } 127 | if c := compareInt(pv.major, pw.major); c != 0 { 128 | return c 129 | } 130 | if c := compareInt(pv.minor, pw.minor); c != 0 { 131 | return c 132 | } 133 | if c := compareInt(pv.patch, pw.patch); c != 0 { 134 | return c 135 | } 136 | return comparePrerelease(pv.prerelease, pw.prerelease) 137 | } 138 | 139 | // Max canonicalizes its arguments and then returns the version string 140 | // that compares greater. 141 | func Max(v, w string) string { 142 | v = Canonical(v) 143 | w = Canonical(w) 144 | if Compare(v, w) > 0 { 145 | return v 146 | } 147 | return w 148 | } 149 | 150 | func parse(v string) (p parsed, ok bool) { 151 | if v == "" || v[0] != 'v' { 152 | p.err = "missing v prefix" 153 | return 154 | } 155 | p.major, v, ok = parseInt(v[1:]) 156 | if !ok { 157 | p.err = "bad major version" 158 | return 159 | } 160 | if v == "" { 161 | p.minor = "0" 162 | p.patch = "0" 163 | p.short = ".0.0" 164 | return 165 | } 166 | if v[0] != '.' { 167 | p.err = "bad minor prefix" 168 | ok = false 169 | return 170 | } 171 | p.minor, v, ok = parseInt(v[1:]) 172 | if !ok { 173 | p.err = "bad minor version" 174 | return 175 | } 176 | if v == "" { 177 | p.patch = "0" 178 | p.short = ".0" 179 | return 180 | } 181 | if v[0] != '.' { 182 | p.err = "bad patch prefix" 183 | ok = false 184 | return 185 | } 186 | p.patch, v, ok = parseInt(v[1:]) 187 | if !ok { 188 | p.err = "bad patch version" 189 | return 190 | } 191 | if len(v) > 0 && v[0] == '-' { 192 | p.prerelease, v, ok = parsePrerelease(v) 193 | if !ok { 194 | p.err = "bad prerelease" 195 | return 196 | } 197 | } 198 | if len(v) > 0 && v[0] == '+' { 199 | p.build, v, ok = parseBuild(v) 200 | if !ok { 201 | p.err = "bad build" 202 | return 203 | } 204 | } 205 | if v != "" { 206 | p.err = "junk on end" 207 | ok = false 208 | return 209 | } 210 | ok = true 211 | return 212 | } 213 | 214 | func parseInt(v string) (t, rest string, ok bool) { 215 | if v == "" { 216 | return 217 | } 218 | if v[0] < '0' || '9' < v[0] { 219 | return 220 | } 221 | i := 1 222 | for i < len(v) && '0' <= v[i] && v[i] <= '9' { 223 | i++ 224 | } 225 | if v[0] == '0' && i != 1 { 226 | return 227 | } 228 | return v[:i], v[i:], true 229 | } 230 | 231 | func parsePrerelease(v string) (t, rest string, ok bool) { 232 | // "A pre-release version MAY be denoted by appending a hyphen and 233 | // a series of dot separated identifiers immediately following the patch version. 234 | // Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. 235 | // Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes." 236 | if v == "" || v[0] != '-' { 237 | return 238 | } 239 | i := 1 240 | start := 1 241 | for i < len(v) && v[i] != '+' { 242 | if !isIdentChar(v[i]) && v[i] != '.' { 243 | return 244 | } 245 | if v[i] == '.' { 246 | if start == i || isBadNum(v[start:i]) { 247 | return 248 | } 249 | start = i + 1 250 | } 251 | i++ 252 | } 253 | if start == i || isBadNum(v[start:i]) { 254 | return 255 | } 256 | return v[:i], v[i:], true 257 | } 258 | 259 | func parseBuild(v string) (t, rest string, ok bool) { 260 | if v == "" || v[0] != '+' { 261 | return 262 | } 263 | i := 1 264 | start := 1 265 | for i < len(v) { 266 | if !isIdentChar(v[i]) && v[i] != '.' { 267 | return 268 | } 269 | if v[i] == '.' { 270 | if start == i { 271 | return 272 | } 273 | start = i + 1 274 | } 275 | i++ 276 | } 277 | if start == i { 278 | return 279 | } 280 | return v[:i], v[i:], true 281 | } 282 | 283 | func isIdentChar(c byte) bool { 284 | return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '-' 285 | } 286 | 287 | func isBadNum(v string) bool { 288 | i := 0 289 | for i < len(v) && '0' <= v[i] && v[i] <= '9' { 290 | i++ 291 | } 292 | return i == len(v) && i > 1 && v[0] == '0' 293 | } 294 | 295 | func isNum(v string) bool { 296 | i := 0 297 | for i < len(v) && '0' <= v[i] && v[i] <= '9' { 298 | i++ 299 | } 300 | return i == len(v) 301 | } 302 | 303 | func compareInt(x, y string) int { 304 | if x == y { 305 | return 0 306 | } 307 | if len(x) < len(y) { 308 | return -1 309 | } 310 | if len(x) > len(y) { 311 | return +1 312 | } 313 | if x < y { 314 | return -1 315 | } else { 316 | return +1 317 | } 318 | } 319 | 320 | func comparePrerelease(x, y string) int { 321 | // "When major, minor, and patch are equal, a pre-release version has 322 | // lower precedence than a normal version. 323 | // Example: 1.0.0-alpha < 1.0.0. 324 | // Precedence for two pre-release versions with the same major, minor, 325 | // and patch version MUST be determined by comparing each dot separated 326 | // identifier from left to right until a difference is found as follows: 327 | // identifiers consisting of only digits are compared numerically and 328 | // identifiers with letters or hyphens are compared lexically in ASCII 329 | // sort order. Numeric identifiers always have lower precedence than 330 | // non-numeric identifiers. A larger set of pre-release fields has a 331 | // higher precedence than a smaller set, if all of the preceding 332 | // identifiers are equal. 333 | // Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 334 | // 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0." 335 | if x == y { 336 | return 0 337 | } 338 | if x == "" { 339 | return +1 340 | } 341 | if y == "" { 342 | return -1 343 | } 344 | for x != "" && y != "" { 345 | x = x[1:] // skip - or . 346 | y = y[1:] // skip - or . 347 | var dx, dy string 348 | dx, x = nextIdent(x) 349 | dy, y = nextIdent(y) 350 | if dx != dy { 351 | ix := isNum(dx) 352 | iy := isNum(dy) 353 | if ix != iy { 354 | if ix { 355 | return -1 356 | } else { 357 | return +1 358 | } 359 | } 360 | if ix { 361 | if len(dx) < len(dy) { 362 | return -1 363 | } 364 | if len(dx) > len(dy) { 365 | return +1 366 | } 367 | } 368 | if dx < dy { 369 | return -1 370 | } else { 371 | return +1 372 | } 373 | } 374 | } 375 | if x == "" { 376 | return -1 377 | } else { 378 | return +1 379 | } 380 | } 381 | 382 | func nextIdent(x string) (dx, rest string) { 383 | i := 0 384 | for i < len(x) && x[i] != '.' { 385 | i++ 386 | } 387 | return x[:i], x[i:] 388 | } 389 | -------------------------------------------------------------------------------- /internal/gqlfmt/gqlfmt.go: -------------------------------------------------------------------------------- 1 | package gqlfmt 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/vektah/gqlparser/v2" 10 | "github.com/vektah/gqlparser/v2/ast" 11 | ) 12 | 13 | // Print parses the input as a graphql schema 14 | // and prints to the given io.Writer. 15 | func Print(input string, out io.Writer) error { 16 | schema, err := gqlparser.LoadSchema(&ast.Source{ 17 | Name: "schema.graphql", 18 | Input: input, 19 | }) 20 | if err != nil { 21 | return err 22 | } 23 | f := &formatter{schema: schema, out: out} 24 | f.printSchema() 25 | 26 | return nil 27 | } 28 | 29 | // PrintSchema formats a given schema and returns 30 | // the output as a string 31 | func PrintSchema(s *ast.Schema) (string, error) { 32 | var out strings.Builder 33 | f := &formatter{schema: s, out: &out} 34 | f.printSchema() 35 | return out.String(), nil 36 | } 37 | 38 | type formatter struct { 39 | schema *ast.Schema 40 | out io.Writer 41 | types []string 42 | inputs []string 43 | scalars []string 44 | enums []string 45 | unions []string 46 | directives []string 47 | } 48 | 49 | func (f *formatter) printSchema() { 50 | f.sortDeclarations() 51 | f.printQuery() 52 | f.printMutation() 53 | f.printTypes() 54 | f.printInputs() 55 | f.printEnums() 56 | f.printScalars() 57 | f.printUnions() 58 | f.printDirectiveDefs() 59 | } 60 | 61 | func (f *formatter) sortDeclarations() { 62 | for k, def := range f.schema.Types { 63 | if k == "Query" || k == "Mutation" || def.BuiltIn { 64 | continue 65 | } 66 | switch def.Kind { 67 | case ast.Object: 68 | f.types = append(f.types, k) 69 | case ast.InputObject: 70 | f.inputs = append(f.inputs, k) 71 | case ast.Enum: 72 | f.enums = append(f.enums, k) 73 | case ast.Scalar: 74 | f.scalars = append(f.scalars, k) 75 | case ast.Union: 76 | f.unions = append(f.unions, k) 77 | } 78 | } 79 | sort.Strings(f.types) 80 | sort.Strings(f.inputs) 81 | sort.Strings(f.enums) 82 | sort.Strings(f.scalars) 83 | sort.Strings(f.unions) 84 | for _, d := range f.schema.Directives { 85 | if d.Position.Src.BuiltIn { 86 | continue 87 | } 88 | f.directives = append(f.directives, d.Name) 89 | } 90 | sort.Strings(f.directives) 91 | } 92 | 93 | func (f *formatter) printQuery() { 94 | f.printDoc(f.schema.Query.Description, 0) 95 | f.print("type Query") 96 | f.printDirectives(f.schema.Query.Directives) 97 | f.print(" {\n") 98 | for _, field := range f.schema.Query.Fields { 99 | if strings.HasPrefix(field.Name, "__") { 100 | continue 101 | } 102 | f.printDoc(field.Description, 1) 103 | f.printf("\t%v", field.Name) 104 | f.printArgs(field.Arguments) 105 | f.printf(": %v\n", field.Type.String()) 106 | } 107 | f.print("}\n") 108 | } 109 | 110 | func (f *formatter) printArgs(aa ast.ArgumentDefinitionList) { 111 | if len(aa) == 0 { 112 | return 113 | } 114 | args := []string{} 115 | for _, a := range aa { 116 | arg := a.Name 117 | arg += ": " 118 | arg += a.Type.String() 119 | args = append(args, arg) 120 | } 121 | 122 | f.printf("(%v)", strings.Join(args, ", ")) 123 | } 124 | 125 | func (f *formatter) fmtDirectiveArgument(a *ast.Argument) string { 126 | var b strings.Builder 127 | b.WriteString(a.Name) 128 | b.WriteString(": ") 129 | b.WriteString(a.Value.String()) 130 | 131 | return b.String() 132 | } 133 | 134 | func (f *formatter) printMutation() { 135 | if f.schema.Mutation == nil || len(f.schema.Mutation.Fields) == 0 { 136 | return 137 | } 138 | f.print("\ntype Mutation {\n") 139 | for _, field := range f.schema.Mutation.Fields { 140 | if strings.HasPrefix(field.Name, "__") { 141 | continue 142 | } 143 | doc := strings.TrimSpace(field.Description) 144 | if doc != "" { 145 | f.printDoc(doc, 1) 146 | } 147 | f.printf("\t%v", field.Name) 148 | f.printArgs(field.Arguments) 149 | f.printf(": %v\n", field.Type.String()) 150 | } 151 | f.print("}\n") 152 | } 153 | 154 | func (f *formatter) printTypes() { 155 | for _, t := range f.types { 156 | f.print("\n") 157 | typeDecl := f.schema.Types[t] 158 | f.printDoc(typeDecl.Description, 0) 159 | f.printf("type %v", typeDecl.Name) 160 | f.printDirectives(typeDecl.Directives) 161 | f.print(" {\n") 162 | for _, field := range typeDecl.Fields { 163 | f.printDoc(field.Description, 1) 164 | f.printf("\t%v: %v\n", field.Name, field.Type.String()) 165 | } 166 | f.print("}\n") 167 | } 168 | } 169 | 170 | func (f *formatter) printDirectives(dirs []*ast.Directive) { 171 | if len(dirs) > 0 { 172 | f.print(" ") 173 | } 174 | for _, dir := range dirs { 175 | f.printDirective(dir) 176 | } 177 | } 178 | 179 | func (f *formatter) printDirective(d *ast.Directive) { 180 | f.printf("@%v", d.Name) 181 | if len(d.Arguments) > 0 { 182 | f.print(`(`) 183 | args := []string{} 184 | for _, a := range d.Arguments { 185 | args = append(args, f.fmtDirectiveArgument(a)) 186 | } 187 | f.print(strings.Join(args, ", ")) 188 | f.print(`)`) 189 | } 190 | } 191 | 192 | func (f *formatter) printInputs() { 193 | for _, t := range f.inputs { 194 | f.println() 195 | typeDecl := f.schema.Types[t] 196 | f.printDoc(typeDecl.Description, 0) 197 | f.printf("input %v {\n", typeDecl.Name) 198 | for _, field := range typeDecl.Fields { 199 | f.printDoc(field.Description, 1) 200 | f.printf("\t%v: %v\n", field.Name, field.Type.String()) 201 | } 202 | f.println("}") 203 | } 204 | } 205 | 206 | func (f *formatter) printEnums() { 207 | for _, t := range f.enums { 208 | f.println() 209 | typeDecl := f.schema.Types[t] 210 | f.printDoc(typeDecl.Description, 0) 211 | f.printf("enum %v {\n", typeDecl.Name) 212 | for _, field := range typeDecl.EnumValues { 213 | f.printDoc(field.Description, 1) 214 | f.printf("\t%v\n", field.Name) 215 | } 216 | f.println("}") 217 | } 218 | } 219 | 220 | func (f *formatter) printScalars() { 221 | for _, t := range f.scalars { 222 | f.println() 223 | typeDecl := f.schema.Types[t] 224 | f.printf("scalar %v\n", typeDecl.Name) 225 | } 226 | } 227 | 228 | func (f *formatter) printUnions() { 229 | if len(f.unions) > 0 { 230 | f.println() 231 | } 232 | for _, t := range f.unions { 233 | decl := f.schema.Types[t] 234 | sort.Strings(decl.Types) 235 | f.printf("union %v = %v\n", decl.Name, strings.Join(decl.Types, " | ")) 236 | } 237 | } 238 | 239 | func (f *formatter) printDirectiveDefs() { 240 | if len(f.directives) > 0 { 241 | f.println() 242 | } 243 | for _, t := range f.directives { 244 | decl := f.schema.Directives[t] 245 | locs := []string{} 246 | for _, l := range decl.Locations { 247 | locs = append(locs, string(l)) 248 | } 249 | sort.Strings(locs) 250 | args := "" 251 | if len(decl.Arguments) > 0 { 252 | args += "(" 253 | argList := []string{} 254 | for _, a := range decl.Arguments { 255 | arg := a.Name 256 | arg += ": " 257 | arg += a.Type.String() 258 | if a.DefaultValue != nil { 259 | arg += " = " 260 | arg += a.DefaultValue.String() 261 | } 262 | argList = append(argList, arg) 263 | } 264 | args += strings.Join(argList, ", ") 265 | args += ")" 266 | } 267 | f.printf("directive @%v%v on %v\n", decl.Name, args, strings.Join(locs, " | ")) 268 | } 269 | } 270 | 271 | func (f *formatter) printDoc(doc string, indent int) { 272 | doc = strings.TrimSpace(doc) 273 | if doc == "" { 274 | return 275 | } 276 | tab := strings.Repeat("\t", indent) 277 | f.print(tab) 278 | f.print(`"""`) 279 | f.println() 280 | for _, line := range strings.Split(doc, "\n") { 281 | line = strings.TrimSpace(line) 282 | f.print(tab) 283 | f.print(line) 284 | f.println() 285 | } 286 | f.print(tab) 287 | f.print(`"""`) 288 | f.println() 289 | } 290 | 291 | func (f *formatter) print(a ...interface{}) { 292 | fmt.Fprint(f.out, a...) 293 | } 294 | 295 | func (f *formatter) println(a ...interface{}) { 296 | fmt.Fprintln(f.out, a...) 297 | } 298 | 299 | func (f *formatter) printf(s string, a ...interface{}) { 300 | fmt.Fprintf(f.out, s, a...) 301 | } 302 | -------------------------------------------------------------------------------- /internal/gqlfmt/gqlfmt_test.go: -------------------------------------------------------------------------------- 1 | package gqlfmt 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "io/ioutil" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var update = flag.Bool("update", false, "update expected file to the given results") 13 | 14 | func TestPrint(t *testing.T) { 15 | input, err := ioutil.ReadFile("testdata/given.graphql") 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | var b bytes.Buffer 21 | err = Print(string(input), &b) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if *update { 27 | ioutil.WriteFile("testdata/expected.graphql", b.Bytes(), 0660) 28 | return 29 | } 30 | 31 | expected, err := ioutil.ReadFile("testdata/expected.graphql") 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | require.Equal(t, string(expected), b.String()) 37 | } 38 | -------------------------------------------------------------------------------- /internal/gqlfmt/testdata/expected.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | Hello(req: HelloReq): HelloResp! 3 | TrafficJam(req: TrafficJamReq): TrafficJamResp! 4 | """ 5 | Good bye says good bye 6 | """ 7 | GoodBye: GoodByeResp! 8 | } 9 | 10 | type GoodByeResp { 11 | text: String! 12 | anInt: Int! 13 | aBool: Boolean! 14 | } 15 | 16 | type HelloResp { 17 | text: String! 18 | } 19 | 20 | """ 21 | TrafficJamResp is the response to a traffic jam 22 | """ 23 | type TrafficJamResp { 24 | next: TrafficLight! 25 | height: Float! 26 | } 27 | 28 | """ 29 | HelloReq is some input 30 | """ 31 | input HelloReq { 32 | name: String! 33 | } 34 | 35 | input TrafficJamReq { 36 | color: TrafficLight! 37 | } 38 | 39 | """ 40 | TrafficLight has 3 lights 41 | """ 42 | enum TrafficLight { 43 | RED 44 | YELLOW 45 | GREEN 46 | } 47 | -------------------------------------------------------------------------------- /internal/gqlfmt/testdata/given.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | } 4 | 5 | type Query { 6 | Hello(req: HelloReq): HelloResp! 7 | 8 | TrafficJam(req: TrafficJamReq): TrafficJamResp! 9 | 10 | """ 11 | Good bye says good bye 12 | """ 13 | GoodBye: GoodByeResp! 14 | } 15 | 16 | 17 | """ 18 | TrafficJamResp is the response to a traffic jam 19 | """ 20 | type TrafficJamResp { 21 | next: TrafficLight! 22 | height: Float! 23 | } 24 | 25 | type HelloResp { 26 | text: String! 27 | } 28 | 29 | 30 | """ 31 | HelloReq is some input 32 | """ 33 | input HelloReq { 34 | name: String! 35 | } 36 | 37 | input TrafficJamReq { 38 | color: TrafficLight! 39 | } 40 | """ 41 | TrafficLight has 3 lights 42 | """ 43 | enum TrafficLight { 44 | RED 45 | YELLOW 46 | GREEN 47 | } 48 | type GoodByeResp { 49 | text: String! 50 | anInt: Int! 51 | aBool: Boolean! 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | 8 | pgs "github.com/lyft/protoc-gen-star" 9 | "marwan.io/protoc-gen-twirpql/internal/gocopy/modfile" 10 | "marwan.io/protoc-gen-twirpql/twirpql" 11 | ) 12 | 13 | func main() { 14 | modname := getImportPath() 15 | log.SetOutput(ioutil.Discard) 16 | pgs.Init(pgs.DebugEnv("DEBUG")). 17 | RegisterModule(twirpql.New(modname)). 18 | Render() 19 | } 20 | 21 | func getImportPath() string { 22 | bts, err := ioutil.ReadFile("go.mod") 23 | if os.IsNotExist(err) { 24 | return "" 25 | } else { 26 | must(err) 27 | } 28 | modf, err := modfile.Parse("go.mod", bts, nil) 29 | must(err) 30 | return modf.Module.Mod.Path 31 | } 32 | 33 | func must(err error) { 34 | if err != nil { 35 | panic(err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /options/gen.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | //go:generate protoc -I . -I /usr/local/include --go_out=paths=source_relative:. options.proto 4 | -------------------------------------------------------------------------------- /options/options.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: options.proto 3 | 4 | package options 5 | 6 | import ( 7 | fmt "fmt" 8 | proto "github.com/golang/protobuf/proto" 9 | descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" 10 | math "math" 11 | ) 12 | 13 | // Reference imports to suppress errors if they are not otherwise used. 14 | var _ = proto.Marshal 15 | var _ = fmt.Errorf 16 | var _ = math.Inf 17 | 18 | // This is a compile-time assertion to ensure that this generated file 19 | // is compatible with the proto package it is being compiled against. 20 | // A compilation error at this line likely means your copy of the 21 | // proto package needs to be updated. 22 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 23 | 24 | type Schema struct { 25 | Federated bool `protobuf:"varint,1,opt,name=federated,proto3" json:"federated,omitempty"` 26 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 27 | XXX_unrecognized []byte `json:"-"` 28 | XXX_sizecache int32 `json:"-"` 29 | } 30 | 31 | func (m *Schema) Reset() { *m = Schema{} } 32 | func (m *Schema) String() string { return proto.CompactTextString(m) } 33 | func (*Schema) ProtoMessage() {} 34 | func (*Schema) Descriptor() ([]byte, []int) { 35 | return fileDescriptor_110d40819f1994f9, []int{0} 36 | } 37 | 38 | func (m *Schema) XXX_Unmarshal(b []byte) error { 39 | return xxx_messageInfo_Schema.Unmarshal(m, b) 40 | } 41 | func (m *Schema) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 42 | return xxx_messageInfo_Schema.Marshal(b, m, deterministic) 43 | } 44 | func (m *Schema) XXX_Merge(src proto.Message) { 45 | xxx_messageInfo_Schema.Merge(m, src) 46 | } 47 | func (m *Schema) XXX_Size() int { 48 | return xxx_messageInfo_Schema.Size(m) 49 | } 50 | func (m *Schema) XXX_DiscardUnknown() { 51 | xxx_messageInfo_Schema.DiscardUnknown(m) 52 | } 53 | 54 | var xxx_messageInfo_Schema proto.InternalMessageInfo 55 | 56 | func (m *Schema) GetFederated() bool { 57 | if m != nil { 58 | return m.Federated 59 | } 60 | return false 61 | } 62 | 63 | type RPC struct { 64 | Mutation bool `protobuf:"varint,1,opt,name=mutation,proto3" json:"mutation,omitempty"` 65 | Skip bool `protobuf:"varint,2,opt,name=skip,proto3" json:"skip,omitempty"` 66 | RespondsWith []string `protobuf:"bytes,3,rep,name=respondsWith,proto3" json:"respondsWith,omitempty"` 67 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 68 | XXX_unrecognized []byte `json:"-"` 69 | XXX_sizecache int32 `json:"-"` 70 | } 71 | 72 | func (m *RPC) Reset() { *m = RPC{} } 73 | func (m *RPC) String() string { return proto.CompactTextString(m) } 74 | func (*RPC) ProtoMessage() {} 75 | func (*RPC) Descriptor() ([]byte, []int) { 76 | return fileDescriptor_110d40819f1994f9, []int{1} 77 | } 78 | 79 | func (m *RPC) XXX_Unmarshal(b []byte) error { 80 | return xxx_messageInfo_RPC.Unmarshal(m, b) 81 | } 82 | func (m *RPC) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 83 | return xxx_messageInfo_RPC.Marshal(b, m, deterministic) 84 | } 85 | func (m *RPC) XXX_Merge(src proto.Message) { 86 | xxx_messageInfo_RPC.Merge(m, src) 87 | } 88 | func (m *RPC) XXX_Size() int { 89 | return xxx_messageInfo_RPC.Size(m) 90 | } 91 | func (m *RPC) XXX_DiscardUnknown() { 92 | xxx_messageInfo_RPC.DiscardUnknown(m) 93 | } 94 | 95 | var xxx_messageInfo_RPC proto.InternalMessageInfo 96 | 97 | func (m *RPC) GetMutation() bool { 98 | if m != nil { 99 | return m.Mutation 100 | } 101 | return false 102 | } 103 | 104 | func (m *RPC) GetSkip() bool { 105 | if m != nil { 106 | return m.Skip 107 | } 108 | return false 109 | } 110 | 111 | func (m *RPC) GetRespondsWith() []string { 112 | if m != nil { 113 | return m.RespondsWith 114 | } 115 | return nil 116 | } 117 | 118 | var E_Rpc = &proto.ExtensionDesc{ 119 | ExtendedType: (*descriptor.MethodOptions)(nil), 120 | ExtensionType: (*RPC)(nil), 121 | Field: 1070, 122 | Name: "twirpql.options.rpc", 123 | Tag: "bytes,1070,opt,name=rpc", 124 | Filename: "options.proto", 125 | } 126 | 127 | var E_Schema = &proto.ExtensionDesc{ 128 | ExtendedType: (*descriptor.FileOptions)(nil), 129 | ExtensionType: (*Schema)(nil), 130 | Field: 1070, 131 | Name: "twirpql.options.schema", 132 | Tag: "bytes,1070,opt,name=schema", 133 | Filename: "options.proto", 134 | } 135 | 136 | func init() { 137 | proto.RegisterType((*Schema)(nil), "twirpql.options.Schema") 138 | proto.RegisterType((*RPC)(nil), "twirpql.options.RPC") 139 | proto.RegisterExtension(E_Rpc) 140 | proto.RegisterExtension(E_Schema) 141 | } 142 | 143 | func init() { proto.RegisterFile("options.proto", fileDescriptor_110d40819f1994f9) } 144 | 145 | var fileDescriptor_110d40819f1994f9 = []byte{ 146 | // 268 bytes of a gzipped FileDescriptorProto 147 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x90, 0xc1, 0x4b, 0xc3, 0x30, 148 | 0x14, 0xc6, 0xa9, 0x95, 0xd2, 0x45, 0x45, 0x08, 0x82, 0x65, 0x88, 0x94, 0x1e, 0xa4, 0x07, 0x97, 149 | 0x82, 0xde, 0xe6, 0xcd, 0x81, 0x9e, 0xd4, 0x11, 0x0f, 0xa2, 0xb7, 0xae, 0x79, 0x6b, 0x83, 0x6d, 150 | 0x5f, 0x4c, 0x32, 0xf6, 0x5f, 0xf9, 0x37, 0xca, 0x9a, 0xa8, 0x6c, 0x3b, 0x25, 0xf9, 0xf2, 0xbe, 151 | 0x1f, 0xef, 0xfb, 0xc8, 0x09, 0x2a, 0x2b, 0xb1, 0x37, 0x4c, 0x69, 0xb4, 0x48, 0x4f, 0xed, 0x5a, 152 | 0x6a, 0xf5, 0xd5, 0x32, 0x2f, 0x8f, 0xd3, 0x1a, 0xb1, 0x6e, 0xa1, 0x18, 0xbe, 0x17, 0xab, 0x65, 153 | 0x21, 0xc0, 0x54, 0x5a, 0x2a, 0x8b, 0xda, 0x59, 0xb2, 0x2b, 0x12, 0xbd, 0x56, 0x0d, 0x74, 0x25, 154 | 0xbd, 0x20, 0xa3, 0x25, 0x08, 0xd0, 0xa5, 0x05, 0x91, 0x04, 0x69, 0x90, 0xc7, 0xfc, 0x5f, 0xc8, 155 | 0xde, 0x49, 0xc8, 0xe7, 0x33, 0x3a, 0x26, 0x71, 0xb7, 0xb2, 0xe5, 0x86, 0xee, 0x67, 0xfe, 0xde, 156 | 0x94, 0x92, 0x43, 0xf3, 0x29, 0x55, 0x72, 0x30, 0xe8, 0xc3, 0x9d, 0x66, 0xe4, 0x58, 0x83, 0x51, 157 | 0xd8, 0x0b, 0xf3, 0x26, 0x6d, 0x93, 0x84, 0x69, 0x98, 0x8f, 0xf8, 0x96, 0x36, 0x7d, 0x24, 0xa1, 158 | 0x56, 0x15, 0xbd, 0x64, 0x6e, 0x59, 0xf6, 0xbb, 0x2c, 0x7b, 0x02, 0xdb, 0xa0, 0x78, 0x71, 0x59, 159 | 0x92, 0xef, 0x38, 0x0d, 0xf2, 0xa3, 0x9b, 0x33, 0xb6, 0x13, 0x92, 0xf1, 0xf9, 0x8c, 0x6f, 0x08, 160 | 0xd3, 0x67, 0x12, 0x19, 0x9f, 0x65, 0x8f, 0xf5, 0x20, 0x5b, 0xd8, 0x21, 0x9d, 0xef, 0x91, 0x5c, 161 | 0x13, 0xdc, 0x53, 0xee, 0xd9, 0xc7, 0x75, 0x57, 0xea, 0x75, 0xd9, 0x33, 0x89, 0xae, 0xc2, 0x6a, 162 | 0x52, 0x43, 0x3f, 0xf1, 0xb6, 0xc2, 0xdb, 0xee, 0xfc, 0xb9, 0x88, 0x86, 0x99, 0xdb, 0x9f, 0x00, 163 | 0x00, 0x00, 0xff, 0xff, 0x16, 0x1b, 0x30, 0xbb, 0x96, 0x01, 0x00, 0x00, 164 | } 165 | -------------------------------------------------------------------------------- /options/options.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package twirpql.options; 3 | option go_package = "marwan.io/protoc-gen-twirpql/options;options"; 4 | 5 | import "google/protobuf/descriptor.proto"; 6 | 7 | extend google.protobuf.MethodOptions { 8 | // ID assigned by protobuf-global-extension-registry@google.com for twirpql project. 9 | RPC rpc = 1070; 10 | } 11 | 12 | extend google.protobuf.FileOptions { 13 | // ID assigned by protobuf-global-extension-registry@google.com for twirpql. 14 | Schema schema = 1070; 15 | } 16 | 17 | message Schema { 18 | bool federated = 1; 19 | } 20 | 21 | message RPC { 22 | bool mutation = 1; 23 | bool skip = 2; 24 | repeated string respondsWith = 3; 25 | } 26 | -------------------------------------------------------------------------------- /twirpql/testdata/mixed/code_generator_request.pb.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marwan-at-work/protoc-gen-twirpql/3729a2838854db8f1d764ff0e75e1388690d4fcf/twirpql/testdata/mixed/code_generator_request.pb.bin -------------------------------------------------------------------------------- /twirpql/testdata/mixed/gen.go: -------------------------------------------------------------------------------- 1 | package mixed 2 | 3 | //go:generate protoc --debug_out=.:. mixed.proto 4 | -------------------------------------------------------------------------------- /twirpql/testdata/mixed/gqlgen.yml.golden: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | schema: 4 | - twirpql/schema.graphql 5 | exec: 6 | filename: twirpql/generated.go 7 | model: 8 | filename: twirpql/models_gen.go 9 | resolver: 10 | filename: twirpql/resolver.go 11 | type: Resolver 12 | dir: "" 13 | autobind: [] 14 | models: 15 | HelloMsg: 16 | model: 17 | - mixed.HelloMsg 18 | HelloMsgInput: 19 | model: 20 | - mixed.HelloMsg 21 | -------------------------------------------------------------------------------- /twirpql/testdata/mixed/mixed.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mixed; 3 | option go_package = "mixed"; 4 | 5 | service Service { 6 | rpc Hello(HelloMsg) returns (HelloMsg); 7 | } 8 | 9 | message HelloMsg { 10 | string text = 1; 11 | bool OK = 2; 12 | } 13 | -------------------------------------------------------------------------------- /twirpql/testdata/mixed/schema.graphql.golden: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | type Query { 4 | hello(req: HelloMsgInput): HelloMsg! 5 | } 6 | 7 | type HelloMsg { 8 | text: String! 9 | OK: Boolean! 10 | } 11 | 12 | input HelloMsgInput { 13 | text: String! 14 | OK: Boolean! 15 | } 16 | -------------------------------------------------------------------------------- /twirpql/testdata/multifile/code_generator_request.pb.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marwan-at-work/protoc-gen-twirpql/3729a2838854db8f1d764ff0e75e1388690d4fcf/twirpql/testdata/multifile/code_generator_request.pb.bin -------------------------------------------------------------------------------- /twirpql/testdata/multifile/gen.go: -------------------------------------------------------------------------------- 1 | package multifile 2 | 3 | //go:generate protoc --debug_out=.:. multifile.proto 4 | -------------------------------------------------------------------------------- /twirpql/testdata/multifile/gqlgen.yml.golden: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | schema: 4 | - twirpql/schema.graphql 5 | exec: 6 | filename: twirpql/generated.go 7 | model: 8 | filename: twirpql/models_gen.go 9 | resolver: 10 | filename: twirpql/resolver.go 11 | type: Resolver 12 | dir: "" 13 | autobind: [] 14 | models: 15 | HelloResp: 16 | model: 17 | - multifile.HelloResp 18 | Second_SecondReq: 19 | model: 20 | - multifile.SecondReq 21 | Second_SecondResp: 22 | model: 23 | - multifile.SecondResp 24 | -------------------------------------------------------------------------------- /twirpql/testdata/multifile/multifile.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package multifile; 3 | option go_package = "multifile"; 4 | import "second.proto"; 5 | 6 | service Service { 7 | rpc Hello(HelloReq) returns (HelloResp); 8 | rpc Second(second.SecondReq) returns (second.SecondResp); 9 | } 10 | 11 | message HelloReq { 12 | 13 | } 14 | 15 | message HelloResp { 16 | string text = 1; 17 | } 18 | -------------------------------------------------------------------------------- /twirpql/testdata/multifile/schema.graphql.golden: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | type Query { 4 | hello: HelloResp! 5 | second(req: Second_SecondReq): Second_SecondResp! 6 | } 7 | 8 | type HelloResp { 9 | text: String! 10 | } 11 | 12 | type Second_SecondResp { 13 | second: String! 14 | } 15 | 16 | input Second_SecondReq { 17 | second: String! 18 | } 19 | -------------------------------------------------------------------------------- /twirpql/testdata/multifile/second.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package second; 3 | option go_package = "multifile"; 4 | 5 | message SecondReq { 6 | string second = 1; 7 | } 8 | 9 | message SecondResp { 10 | string second = 1; 11 | } 12 | -------------------------------------------------------------------------------- /twirpql/testdata/multitypes/code_generator_request.pb.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marwan-at-work/protoc-gen-twirpql/3729a2838854db8f1d764ff0e75e1388690d4fcf/twirpql/testdata/multitypes/code_generator_request.pb.bin -------------------------------------------------------------------------------- /twirpql/testdata/multitypes/gen.go: -------------------------------------------------------------------------------- 1 | package multitypes 2 | 3 | //go:generate protoc --debug_out=.:. multitypes.proto 4 | -------------------------------------------------------------------------------- /twirpql/testdata/multitypes/gqlgen.yml.golden: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | schema: 4 | - twirpql/schema.graphql 5 | exec: 6 | filename: twirpql/generated.go 7 | model: 8 | filename: twirpql/models_gen.go 9 | resolver: 10 | filename: twirpql/resolver.go 11 | type: Resolver 12 | dir: "" 13 | autobind: [] 14 | models: 15 | ByeReq: 16 | model: 17 | - multitypes.ByeReq 18 | ByeResp: 19 | model: 20 | - multitypes.ByeResp 21 | HelloReq: 22 | model: 23 | - multitypes.HelloReq 24 | HelloResp: 25 | model: 26 | - multitypes.HelloResp 27 | Traffic: 28 | model: 29 | - multitypes.Traffic 30 | -------------------------------------------------------------------------------- /twirpql/testdata/multitypes/multitypes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package multitypes; 3 | option go_package = "multitipes"; 4 | 5 | service Service { 6 | rpc Hello(HelloReq) returns (HelloResp); 7 | rpc Bye(ByeReq) returns (ByeResp); 8 | } 9 | 10 | message HelloReq { 11 | string one = 1; 12 | bool two = 2; 13 | repeated string three = 3; 14 | repeated bool four = 4; 15 | } 16 | 17 | message HelloResp { 18 | string one = 1; 19 | bool two = 2; 20 | repeated string three = 3; 21 | repeated bool four = 4; 22 | } 23 | 24 | message ByeReq { 25 | int64 one = 1; 26 | int32 two = 2; 27 | repeated int64 three = 3; 28 | repeated int64 four = 4; 29 | } 30 | 31 | message ByeResp { 32 | int64 one = 1; 33 | int32 two = 2; 34 | repeated int64 three = 3; 35 | repeated int64 four = 4; 36 | Traffic traffic = 5; 37 | } 38 | 39 | enum Traffic { 40 | GREEN = 0; 41 | YELLOW = 1; 42 | RED = 2; 43 | } -------------------------------------------------------------------------------- /twirpql/testdata/multitypes/schema.graphql.golden: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | type Query { 4 | hello(req: HelloReq): HelloResp! 5 | bye(req: ByeReq): ByeResp! 6 | } 7 | 8 | type ByeResp { 9 | one: Int! 10 | two: Int! 11 | three: [Int]! 12 | four: [Int]! 13 | traffic: Traffic! 14 | } 15 | 16 | type HelloResp { 17 | one: String! 18 | two: Boolean! 19 | three: [String]! 20 | four: [Boolean]! 21 | } 22 | 23 | input ByeReq { 24 | one: Int! 25 | two: Int! 26 | three: [Int]! 27 | four: [Int]! 28 | } 29 | 30 | input HelloReq { 31 | one: String! 32 | two: Boolean! 33 | three: [String]! 34 | four: [Boolean]! 35 | } 36 | 37 | enum Traffic { 38 | GREEN 39 | YELLOW 40 | RED 41 | } 42 | -------------------------------------------------------------------------------- /twirpql/testdata/mutations/code_generator_request.pb.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marwan-at-work/protoc-gen-twirpql/3729a2838854db8f1d764ff0e75e1388690d4fcf/twirpql/testdata/mutations/code_generator_request.pb.bin -------------------------------------------------------------------------------- /twirpql/testdata/mutations/gen.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | //go:generate protoc -I . -I ../../../options -I /usr/local/include --debug_out=.:. mutations.proto 4 | -------------------------------------------------------------------------------- /twirpql/testdata/mutations/gqlgen.yml.golden: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | schema: 4 | - twirpql/schema.graphql 5 | exec: 6 | filename: twirpql/generated.go 7 | model: 8 | filename: twirpql/models_gen.go 9 | resolver: 10 | filename: twirpql/resolver.go 11 | type: Resolver 12 | dir: "" 13 | autobind: [] 14 | models: 15 | HelloMsg: 16 | model: 17 | - mutations.HelloMsg 18 | HelloMsgInput: 19 | model: 20 | - mutations.HelloMsg 21 | -------------------------------------------------------------------------------- /twirpql/testdata/mutations/mutations.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mixed; 3 | option go_package = "mixed"; 4 | 5 | import "options.proto"; 6 | 7 | service Service { 8 | rpc Hello(HelloMsg) returns (HelloMsg); 9 | rpc Goodbye(HelloMsg) returns (HelloMsg) { 10 | option (twirpql.options.rpc) = { 11 | mutation: true; 12 | }; 13 | }; 14 | } 15 | 16 | message HelloMsg { 17 | string text = 1; 18 | bool OK = 2; 19 | } 20 | -------------------------------------------------------------------------------- /twirpql/testdata/mutations/schema.graphql.golden: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | type Query { 4 | hello(req: HelloMsgInput): HelloMsg! 5 | } 6 | 7 | type Mutation { 8 | goodbye(req: HelloMsgInput): HelloMsg! 9 | } 10 | 11 | type HelloMsg { 12 | text: String! 13 | OK: Boolean! 14 | } 15 | 16 | input HelloMsgInput { 17 | text: String! 18 | OK: Boolean! 19 | } 20 | -------------------------------------------------------------------------------- /twirpql/testdata/noinput/code_generator_request.pb.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marwan-at-work/protoc-gen-twirpql/3729a2838854db8f1d764ff0e75e1388690d4fcf/twirpql/testdata/noinput/code_generator_request.pb.bin -------------------------------------------------------------------------------- /twirpql/testdata/noinput/gen.go: -------------------------------------------------------------------------------- 1 | package noinput 2 | 3 | //go:generate protoc --debug_out=.:. noinput.proto 4 | -------------------------------------------------------------------------------- /twirpql/testdata/noinput/gqlgen.yml.golden: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | schema: 4 | - twirpql/schema.graphql 5 | exec: 6 | filename: twirpql/generated.go 7 | model: 8 | filename: twirpql/models_gen.go 9 | resolver: 10 | filename: twirpql/resolver.go 11 | type: Resolver 12 | dir: "" 13 | autobind: [] 14 | models: 15 | HelloResp: 16 | model: 17 | - noinput.HelloResp 18 | -------------------------------------------------------------------------------- /twirpql/testdata/noinput/noinput.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package noinput; 3 | option go_package = "noinput"; 4 | 5 | service Service { 6 | rpc Hello(HelloReq) returns (HelloResp); 7 | } 8 | 9 | message HelloReq { 10 | 11 | } 12 | 13 | message HelloResp { 14 | string text = 1; 15 | } 16 | -------------------------------------------------------------------------------- /twirpql/testdata/noinput/schema.graphql.golden: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | type Query { 4 | hello: HelloResp! 5 | } 6 | 7 | type HelloResp { 8 | text: String! 9 | } 10 | -------------------------------------------------------------------------------- /twirpql/testdata/simple/code_generator_request.pb.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marwan-at-work/protoc-gen-twirpql/3729a2838854db8f1d764ff0e75e1388690d4fcf/twirpql/testdata/simple/code_generator_request.pb.bin -------------------------------------------------------------------------------- /twirpql/testdata/simple/gen.go: -------------------------------------------------------------------------------- 1 | package simple 2 | 3 | //go:generate protoc --debug_out=.:. simple.proto 4 | -------------------------------------------------------------------------------- /twirpql/testdata/simple/gqlgen.yml.golden: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | schema: 4 | - twirpql/schema.graphql 5 | exec: 6 | filename: twirpql/generated.go 7 | model: 8 | filename: twirpql/models_gen.go 9 | resolver: 10 | filename: twirpql/resolver.go 11 | type: Resolver 12 | dir: "" 13 | autobind: [] 14 | models: 15 | HelloReq: 16 | model: 17 | - simple.HelloReq 18 | HelloResp: 19 | model: 20 | - simple.HelloResp 21 | -------------------------------------------------------------------------------- /twirpql/testdata/simple/schema.graphql.golden: -------------------------------------------------------------------------------- 1 | # Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT. 2 | 3 | type Query { 4 | hello(req: HelloReq): HelloResp! 5 | } 6 | 7 | type HelloResp { 8 | text: String! 9 | } 10 | 11 | input HelloReq { 12 | name: String! 13 | } 14 | -------------------------------------------------------------------------------- /twirpql/testdata/simple/simple.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package simple; 3 | option go_package = "simple"; 4 | 5 | service Service { 6 | rpc Hello(HelloReq) returns (HelloResp); 7 | } 8 | 9 | message HelloReq { 10 | string name = 1; 11 | } 12 | 13 | message HelloResp { 14 | string text = 1; 15 | } 16 | -------------------------------------------------------------------------------- /twirpql/tmpl.go: -------------------------------------------------------------------------------- 1 | package twirpql 2 | 3 | import ( 4 | "strings" 5 | "text/template" 6 | ) 7 | 8 | var schemaFuncs = template.FuncMap{ 9 | "fmtUnions": func(types []string) string { 10 | return strings.Join(types, " | ") 11 | }, 12 | } 13 | 14 | const schemaTemplate = ` 15 | {{ if (gt (len .Service.Methods) 0) }} 16 | 17 | type Query { {{range .Service.Methods}} 18 | """ 19 | {{.Doc}} 20 | """ 21 | {{.Name}}{{.Request}}: {{.Response}}!{{end}} 22 | } 23 | 24 | {{ end }} 25 | 26 | {{ if (gt (len .Service.Mutations) 0) }} 27 | 28 | type Mutation { {{range .Service.Mutations}} 29 | {{.Name}}{{.Request}}: {{.Response}}!{{end}} 30 | } 31 | 32 | {{ end }} 33 | 34 | {{range .Types}} 35 | """ 36 | {{ .Doc }} 37 | """ 38 | type {{.Name}} { {{- range .Fields}} 39 | """ 40 | {{ .Doc }} 41 | """ 42 | {{.Name}}: {{.Type}}!{{end}} 43 | {{- if (eq (len .Fields) 0) }} 44 | responseMessage: String! 45 | {{- end }} 46 | } 47 | {{end}} 48 | {{range .Inputs}} 49 | """ 50 | {{ .Doc }} 51 | """ 52 | input {{.Name}} { {{range .Fields}} 53 | """ 54 | {{ .Doc }} 55 | """ 56 | {{.Name}}: {{.Type}}!{{end}} 57 | } 58 | {{end}} 59 | {{range .Enums}} 60 | """ 61 | {{ .Doc }} 62 | """ 63 | enum {{.Name}} { {{range .Fields}} 64 | """ 65 | {{ .Doc }} 66 | """ 67 | {{.Name}}{{end}} 68 | }{{end}} 69 | {{range .Scalars}} 70 | scalar {{.}} 71 | {{end}} 72 | {{range .Unions}} 73 | union {{.Name}} = {{fmtUnions .Types }} 74 | {{ end }} 75 | ` 76 | -------------------------------------------------------------------------------- /twirpql/twirpql.go: -------------------------------------------------------------------------------- 1 | package twirpql 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | "text/template" 12 | 13 | "github.com/99designs/gqlgen/api" 14 | gqlconfig "github.com/99designs/gqlgen/codegen/config" 15 | "github.com/99designs/gqlgen/plugin/modelgen" 16 | "github.com/golang/protobuf/proto" 17 | pgs "github.com/lyft/protoc-gen-star" 18 | pgsgo "github.com/lyft/protoc-gen-star/lang/go" 19 | "gopkg.in/yaml.v2" 20 | "marwan.io/protoc-gen-twirpql/internal/genenums" 21 | "marwan.io/protoc-gen-twirpql/internal/genresolver" 22 | "marwan.io/protoc-gen-twirpql/internal/genscalar" 23 | "marwan.io/protoc-gen-twirpql/internal/genserver" 24 | "marwan.io/protoc-gen-twirpql/internal/genunions" 25 | "marwan.io/protoc-gen-twirpql/internal/gqlfmt" 26 | "marwan.io/protoc-gen-twirpql/options" 27 | ) 28 | 29 | // twirpql creates a report of all the target messages generated by the 30 | // protoc run, writing the file into the /tmp directory. 31 | type twirpql struct { 32 | *pgs.ModuleBase 33 | 34 | // an input is a protobuf "message" that is 35 | // found inside an RPC's Request so that GraphQL 36 | // interprets it as an Input declaration. 37 | // Note that if the same input is also found 38 | // in an rpc's "returns" value, then the name 39 | // will be suffixed with the word "Input" 40 | // because GraphQL does not allow types and 41 | // inputs with matching names. 42 | inputs map[string]*serviceType 43 | 44 | // a "type" is a protobuf "message" that is 45 | // found inside an RPC's Return so that GraphQL 46 | // interprets it as a "Type" declaration. 47 | types map[string]*serviceType 48 | 49 | // a "union" represents a schema.graphql 50 | // Union definition which originates from 51 | // a protobuf `oneof` declaration inside 52 | // a message. 53 | unions map[string]*union 54 | unionNames map[string]bool 55 | 56 | // responseUnions represent the name 57 | // of all the RPCs that want their 58 | // responses combined with an error type 59 | // This way, the resolver can replace 60 | // a response with the error type 61 | // instead of actually returning the error. 62 | responseUnions map[string]string 63 | 64 | // an empty type keeps track of empty returns 65 | // because GraphQL Types can't be empty 66 | // and therefore we need to inject a dummy 67 | // field. 68 | emptys map[string]bool 69 | 70 | // enums are integers in protobuf but strings 71 | // in GraphQL. Therefore, we need to keep track 72 | // of declared enums in the proto file so that 73 | // we create proper conversion for the GraphQL queries. 74 | enums map[string]*enumData 75 | 76 | // maps are all map declarations 77 | // in a protobuf file. Those get turned into 78 | // scalar values in GraphQL. The go type 79 | // here is a map of the scalar name (the field name) 80 | // to the full Go type representation. 81 | // For example if we have a protobuf that looks like 82 | // map myMap = 1; 83 | // Then this map would look like {"MyMap": "map[string]int64"} 84 | maps map[string]string 85 | 86 | // mapImports correspond to any import paths 87 | // the above maps field requires, such as 88 | // when the map ends up being something 89 | // like map[string]*ptypes.Timestamp 90 | mapImports map[string]struct{} 91 | 92 | sdl string 93 | 94 | // gqlTypes are specific for the gqlgen config file 95 | // so that we make all the input/output GraphQL 96 | // types point to the generated .pb.go types. 97 | gqlTypes gqlconfig.TypeMap 98 | 99 | // this is the graphql schema template 100 | tmpl *template.Template 101 | 102 | // this context holds Go related information about 103 | // a protobuf file. 104 | ctx pgsgo.Context 105 | 106 | // modname is the import path that "go list" 107 | // returns from inside the target .proto file 108 | modname string 109 | 110 | // gopkgname is the `option go_package` value 111 | gopkgname string 112 | 113 | // svcname is the name of the "service" 114 | // declaration in a protofile. 115 | svcname string 116 | 117 | // destpkgname is the directory path 118 | // where the GraphQL generated code will 119 | // live. It defaults to a "twirpql". 120 | destpkgname string 121 | 122 | // is the import path that will import 123 | // the twirpql sub-package 124 | destimportpath string 125 | 126 | svc pgs.Service 127 | protopkg pgs.Package 128 | } 129 | 130 | type enumData struct { 131 | Name string 132 | ImportPath string 133 | PackageName string 134 | Values []*serviceField 135 | Doc string 136 | } 137 | 138 | // New configures the module with an instance of ModuleBase 139 | func New(importPath string) pgs.Module { 140 | return &twirpql{ 141 | ModuleBase: &pgs.ModuleBase{}, 142 | inputs: map[string]*serviceType{}, 143 | types: map[string]*serviceType{}, 144 | emptys: map[string]bool{}, 145 | enums: map[string]*enumData{}, 146 | maps: map[string]string{}, 147 | mapImports: map[string]struct{}{}, 148 | unions: map[string]*union{}, 149 | unionNames: map[string]bool{}, 150 | responseUnions: map[string]string{}, 151 | gqlTypes: gqlconfig.TypeMap{}, 152 | tmpl: template.Must(template.New("").Funcs(schemaFuncs).Parse(schemaTemplate)), 153 | modname: importPath, 154 | ctx: pgsgo.InitContext(pgs.ParseParameters("")), 155 | destpkgname: "./twirpql", 156 | destimportpath: "", 157 | } 158 | } 159 | 160 | // Name is the identifier used to identify the module. This value is 161 | // automatically attached to the BuildContext associated with the ModuleBase. 162 | func (tql *twirpql) Name() string { return "twirpql" } 163 | 164 | func (tql *twirpql) InitContext(c pgs.BuildContext) { 165 | tql.ModuleBase.InitContext(c) 166 | tql.ctx = pgsgo.InitContext(c.Parameters()) 167 | } 168 | 169 | // Execute is passed the target files as well as its dependencies in the pkgs 170 | // map. The implementation should return a slice of Artifacts that represent 171 | // the files to be generated. In this case, "/tmp/report.txt" will be created 172 | // outside of the normal protoc flow. 173 | func (tql *twirpql) Execute(targets map[string]pgs.File, pkgs map[string]pgs.Package) []pgs.Artifact { 174 | tql.destpkgname = tql.Parameters().StrDefault("dest", tql.destpkgname) 175 | os.MkdirAll(tql.destpkgname, 0777) 176 | 177 | if len(targets) != 1 { 178 | panic("only one proto file is supported at this moment; see https://twirpql.dev/docs/multiple-services") 179 | } 180 | 181 | for fileName, targetFile := range targets { 182 | if targetFile.Syntax() != pgs.Proto3 { 183 | panic("only proto3 is supported") 184 | } 185 | tql.svc = tql.pickServiceFromFile(tql.Parameters().Str("service"), targetFile) 186 | if len(tql.svc.Methods()) == 0 { 187 | panic("service must have at least on rpc") 188 | } 189 | tql.protopkg = targetFile.Package() 190 | serviceDir := filepath.Dir(fileName) 191 | tql.setImportPath(serviceDir) 192 | if serviceDir == "." { 193 | tql.destimportpath = tql.modname 194 | } else { 195 | tql.destimportpath = tql.goList(".") 196 | } 197 | var schemaBuffer bytes.Buffer 198 | f, err := os.Create(tql.path("schema.graphql")) 199 | must(err) 200 | defer f.Close() 201 | tql.generateSchema(targetFile, io.MultiWriter(&schemaBuffer, f)) 202 | if tql.isFederated(targetFile) { 203 | tql.sdl = strings.Replace(schemaBuffer.String(), "type Query", "extend type Query", 1) 204 | } 205 | } 206 | 207 | if len(tql.maps) > 0 { 208 | f, err := os.Create(tql.path("scalars.go")) 209 | must(err) 210 | defer f.Close() 211 | must(genscalar.Render(tql.maps, tql.mapImports, f)) 212 | } 213 | 214 | f, err := os.Create(tql.path("gqlgen.yml")) 215 | must(err) 216 | defer f.Close() 217 | tql.touchConfig(f) 218 | if len(tql.enums) > 0 { 219 | tql.bridgeEnums() 220 | } 221 | if len(tql.unions) > 0 { 222 | tql.writeUnionMask() 223 | } 224 | tql.initGql(tql.svcname) 225 | 226 | return tql.Artifacts() 227 | } 228 | 229 | func (tql *twirpql) pickServiceFromFile(svc string, f pgs.File) pgs.Service { 230 | switch len(f.Services()) { 231 | case 0: 232 | panic("proto file must have at least one service") 233 | case 1: 234 | return f.Services()[0] 235 | } 236 | if svc == "" { 237 | panic("service name must be provided if proto file has multiple services; see https://twirpql.dev/docs/multiple-services") 238 | } 239 | for _, service := range f.Services() { 240 | if svc == service.Name().String() { 241 | return service 242 | } 243 | } 244 | panic("protofile does not have the given service: " + svc) 245 | } 246 | 247 | func (tql *twirpql) goList(dir string) string { 248 | cmd := exec.Command("go", "list") 249 | cmd.Dir = dir 250 | cmd.Env = os.Environ() 251 | var stderr bytes.Buffer 252 | cmd.Stderr = &stderr 253 | pkgpath, err := cmd.Output() 254 | if err != nil { 255 | msg := fmt.Sprintf("go list failed: %v - stdout: %v - stderr: %v", err, string(pkgpath), stderr.String()) 256 | if strings.Contains(stderr.String(), "cannot find module providing package") { 257 | msg = "go list failed. Make sure you have .go files where your .proto file is." + 258 | "Also make sure to run the --go_out=. --twirp_out=. plugins on a separate command before you run --twirpql_out" 259 | } 260 | panic(msg) 261 | } 262 | return strings.TrimSpace(string(pkgpath)) 263 | } 264 | 265 | func (tql *twirpql) setImportPath(serviceDir string) { 266 | modname := tql.goList(serviceDir) 267 | tql.modname = tql.Parameters().StrDefault("importpath", modname) 268 | if tql.modname == "" { 269 | panic("import path must be provided by `go list` in the .proto directory or through the importpath plugin parameter") 270 | } 271 | } 272 | 273 | func (tql *twirpql) generateSchema(f pgs.File, out io.Writer) { 274 | out.Write([]byte("# Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT.\n\n")) 275 | tql.svcname = tql.svc.Name().String() 276 | tql.gopkgname = tql.ctx.PackageName(f).String() 277 | gqlFile := &file{} 278 | gqlFile.Service = tql.getService(tql.svc) 279 | for _, v := range tql.inputs { 280 | gqlFile.Inputs = append(gqlFile.Inputs, v) 281 | } 282 | for _, v := range tql.types { 283 | gqlFile.Types = append(gqlFile.Types, v) 284 | } 285 | for k, v := range tql.enums { 286 | gqlFile.Enums = append(gqlFile.Enums, &enums{ 287 | Name: k, 288 | Fields: v.Values, 289 | Doc: v.Doc, 290 | }) 291 | } 292 | for k := range tql.maps { 293 | gqlFile.Scalars = append(gqlFile.Scalars, k) 294 | } 295 | for _, v := range tql.unions { 296 | gqlFile.Unions = append(gqlFile.Unions, v) 297 | } 298 | if tql.isFederated(f) { 299 | gqlFile.Service.Methods = append(gqlFile.Service.Methods, &method{ 300 | Name: "_service", 301 | Request: "", 302 | Response: "_Service", 303 | }) 304 | gqlFile.Types = append(gqlFile.Types, &serviceType{ 305 | Name: "_Service", 306 | Fields: []*serviceField{&serviceField{ 307 | Name: "sdl", 308 | Type: "String", 309 | }}, 310 | }) 311 | } 312 | 313 | var buf bytes.Buffer 314 | 315 | err := tql.tmpl.Execute(&buf, gqlFile) 316 | must(err) 317 | err = gqlfmt.Print(buf.String(), out) 318 | must(err) 319 | } 320 | 321 | // bridgeEnums creates a type conversion between 322 | // protobuf's enums (int32) and GraphQL's enums (string). 323 | func (tql *twirpql) bridgeEnums() { 324 | f, err := os.Create(tql.path("enums.gen.go")) 325 | must(err) 326 | defer f.Close() 327 | all := []*genenums.Data{} 328 | for k, v := range tql.enums { 329 | all = append(all, &genenums.Data{ 330 | ImportPath: v.ImportPath, 331 | Pkg: v.PackageName, 332 | Name: k, 333 | GoName: v.Name, 334 | }) 335 | } 336 | must(genenums.Render(all, f)) 337 | } 338 | 339 | func (tql *twirpql) touchConfig(out io.Writer) { 340 | out.Write([]byte("# Code was generated by marwan.io/protoc-gen-twirpql. DO NOT EDIT.\n\n")) 341 | var cfg gqlconfig.Config 342 | cfg.SchemaFilename = gqlconfig.StringList{tql.path("schema.graphql")} 343 | cfg.Exec = gqlconfig.PackageConfig{Filename: tql.path("generated.go")} 344 | cfg.Resolver = gqlconfig.ResolverConfig{Filename: tql.path("resolver.go"), Type: "Resolver"} 345 | cfg.Models = tql.gqlTypes 346 | cfg.Model = gqlconfig.PackageConfig{Filename: tql.path("models_gen.go")} 347 | must(yaml.NewEncoder(out).Encode(&cfg)) 348 | } 349 | 350 | func (tql *twirpql) initGql(svcName string) { 351 | cfg, err := gqlconfig.LoadConfig(tql.path("gqlgen.yml")) 352 | must(err) 353 | emptys := []string{} 354 | for k := range tql.emptys { 355 | emptys = append(emptys, k) 356 | } 357 | 358 | err = api.Generate( 359 | cfg, 360 | api.NoPlugins(), 361 | api.AddPlugin(modelgen.New()), 362 | api.AddPlugin(genresolver.New( 363 | svcName, 364 | tql.gopkgname, 365 | emptys, 366 | tql.maps, 367 | tql.unionNames, 368 | tql.responseUnions, 369 | tql.sdl, 370 | )), 371 | api.AddPlugin(genserver.New(tql.path("server.go"), tql.modname, svcName)), 372 | ) 373 | must(err) 374 | } 375 | 376 | func (tql *twirpql) getService(svc pgs.Service) *service { 377 | var s service 378 | s.Methods, s.Mutations = tql.getMethods(svc.Methods()) 379 | return &s 380 | } 381 | 382 | func (tql *twirpql) getMethods(protoMethods []pgs.Method) ([]*method, []*method) { 383 | methods := []*method{} 384 | mutations := []*method{} 385 | 386 | // collect all types first, so that we de-dupe mixed 387 | // inputs && types 388 | for _, pm := range protoMethods { 389 | tql.setType(pm.Output()) 390 | } 391 | 392 | for _, pm := range protoMethods { 393 | if tql.isSkipped(pm) { 394 | continue 395 | } 396 | var m method 397 | m.Name = pm.Name().LowerCamelCase().String() 398 | m.Doc = pm.SourceCodeInfo().LeadingComments() 399 | // TODO: make oneOf fields a scalar in inputs 400 | emptyInput := len(pm.Input().NonOneOfFields()) == 0 401 | if !emptyInput { 402 | tql.setInput(pm.Input()) 403 | m.Request = tql.formatQueryInput(pm.Input()) 404 | } 405 | if tql.hasResponseCombination(pm) { 406 | m.Response = tql.setResponseCombination(pm) 407 | } else { 408 | m.Response = tql.getQualifiedName(pm.Output()) 409 | } 410 | if tql.isMutation(pm) { 411 | mutations = append(mutations, &m) 412 | } else { 413 | methods = append(methods, &m) 414 | } 415 | } 416 | return methods, mutations 417 | } 418 | 419 | func (tql *twirpql) setResponseCombination(m pgs.Method) string { 420 | rpc := getModifiers(m) 421 | typeName := rpc.GetRespondsWith()[0] 422 | f := m.File() 423 | var msg pgs.Message 424 | for _, m := range f.Messages() { 425 | if typeName == m.Name().String() { 426 | msg = m 427 | } 428 | } 429 | if msg == nil { 430 | panic(typeName + " is not defined in proto file") 431 | } 432 | tql.setType(msg) 433 | responseName := tql.getQualifiedName(m.Output()) 434 | unionName := responseName + "Set" 435 | tql.unions[unionName] = &union{ 436 | Name: unionName, 437 | Types: []string{responseName, typeName}, 438 | } 439 | tql.responseUnions[m.Name().UpperCamelCase().String()] = typeName 440 | importpath := tql.destimportpath + "/twirpql" 441 | tql.gqlTypes[unionName] = gqlconfig.TypeMapEntry{ 442 | Model: gqlconfig.StringList{importpath + "." + "unionMask"}, 443 | } 444 | return unionName 445 | } 446 | 447 | func (tql *twirpql) hasResponseCombination(m pgs.Method) bool { 448 | rpc := getModifiers(m) 449 | return len(rpc.GetRespondsWith()) > 0 450 | } 451 | 452 | func (tql *twirpql) isMutation(pm pgs.Method) bool { 453 | val := getModifiers(pm) 454 | return val.GetMutation() 455 | } 456 | 457 | func (tql *twirpql) isFederated(f pgs.File) bool { 458 | opts := f.Descriptor().GetOptions() 459 | if proto.HasExtension(opts, options.E_Schema) { 460 | mut, err := proto.GetExtension(opts, options.E_Schema) 461 | must(err) 462 | val, ok := mut.(*options.Schema) 463 | if !ok { 464 | panic(fmt.Sprintf("invalid mutation type: %T\n", mut)) 465 | } 466 | return val.GetFederated() 467 | } 468 | return false 469 | } 470 | 471 | func (tql *twirpql) isSkipped(pm pgs.Method) bool { 472 | val := getModifiers(pm) 473 | return val.GetSkip() 474 | } 475 | 476 | func getModifiers(pm pgs.Method) *options.RPC { 477 | opts := pm.Descriptor().GetOptions() 478 | if proto.HasExtension(opts, options.E_Rpc) { 479 | rpc, err := proto.GetExtension(opts, options.E_Rpc) 480 | must(err) 481 | val, ok := rpc.(*options.RPC) 482 | if !ok { 483 | panic(fmt.Sprintf("invalid rpc type: %T\n", rpc)) 484 | } 485 | return val 486 | } 487 | return nil 488 | } 489 | 490 | func (tql *twirpql) setType(msg pgs.Message) { 491 | typeName := tql.getQualifiedName(msg) 492 | if _, ok := tql.types[typeName]; ok { 493 | return 494 | } 495 | var i serviceType 496 | i.Name = typeName 497 | i.Doc = msg.SourceCodeInfo().LeadingComments() 498 | tql.types[i.Name] = &i 499 | tql.setGraphQLType(i.Name, msg) 500 | i.Fields = tql.getFields(msg.NonOneOfFields(), true) 501 | i.Fields = append(i.Fields, tql.getUnionFields(msg)...) 502 | } 503 | 504 | func (tql *twirpql) getUnionFields(msg pgs.Message) []*serviceField { 505 | sff := []*serviceField{} 506 | for _, oo := range msg.OneOfs() { 507 | unionTypes := []string{} 508 | unionName := tql.getUnionName(oo) 509 | for _, f := range oo.Fields() { 510 | tql.setUnionType(f) // side effect 511 | unionTypes = append(unionTypes, tql.getUnionFieldWrapperName(f)) 512 | } 513 | // side effect 514 | tql.unionNames[oo.Name().UpperCamelCase().String()] = true 515 | tql.unions[unionName] = &union{ 516 | Name: unionName, 517 | Types: unionTypes, 518 | } 519 | importpath := tql.destimportpath + "/twirpql" 520 | tql.gqlTypes[unionName] = gqlconfig.TypeMapEntry{ 521 | Model: gqlconfig.StringList{importpath + "." + "unionMask"}, 522 | } 523 | var sf serviceField 524 | sf.Name = oo.Name().String() 525 | sf.Type = unionName 526 | sff = append(sff, &sf) 527 | } 528 | return sff 529 | } 530 | 531 | func (tql *twirpql) setUnionType(f pgs.Field) { 532 | typeName := tql.getUnionFieldWrapperName(f) 533 | if _, ok := tql.types[typeName]; ok { 534 | return 535 | } 536 | var i serviceType 537 | i.Name = typeName 538 | i.Fields = []*serviceField{tql.getField(f, true)} 539 | tql.types[i.Name] = &i 540 | // protoName might have unlimited trailing "_"s. 541 | // See: https://github.com/golang/protobuf/blob/master/protoc-gen-go/generator/generator.go#L2334 542 | protoName := f.Message().Name().String() + "_" + strings.Title(f.Name().String()) 543 | tql.gqlTypes[i.Name] = gqlconfig.TypeMapEntry{ 544 | Model: gqlconfig.StringList{tql.deduceImportPath(f) + "." + protoName}, 545 | } 546 | } 547 | 548 | func (tql *twirpql) getUnionFieldWrapperName(f pgs.Field) string { 549 | return tql.getUnionName(f.OneOf()) + f.Name().UpperCamelCase().String() 550 | } 551 | 552 | func (tql *twirpql) getUnionName(field pgs.OneOf) string { 553 | return tql.getQualifiedName(field.Message()) + field.Name().UpperCamelCase().String() 554 | } 555 | 556 | // getQualifiedName returns the name that will be defined inside the GraphQL Schema File. 557 | // For messgae declarations that are part of the target .proto file, they will stay the same 558 | // but if it's part of an import like "google.protobuf.Timestamp" then we combine the package name 559 | // with the Message namd to ensure we have no clashes so it becomes: "google_protobuf_Timestamp" 560 | func (tql *twirpql) getQualifiedName(msg pgs.Entity) string { 561 | msgGoTypeName := tql.ctx.Name(msg).String() 562 | if msg.Package() == tql.protopkg { 563 | return msgGoTypeName 564 | } 565 | pkgName := strings.ReplaceAll(msg.Package().ProtoName().String(), ".", "_") 566 | return strings.Title(pkgName + "_" + msgGoTypeName) 567 | } 568 | 569 | func (tql *twirpql) setInput(msg pgs.Message) { 570 | if _, ok := tql.inputs[tql.getInputName(msg)]; ok { 571 | return 572 | } 573 | var i serviceType 574 | i.Name = tql.getInputName(msg) 575 | i.Doc = msg.SourceCodeInfo().LeadingComments() 576 | tql.inputs[i.Name] = &i 577 | tql.setGraphQLType(i.Name, msg) 578 | // TODO: make oneOf fields scalars. 579 | i.Fields = tql.getFields(msg.NonOneOfFields(), false) 580 | } 581 | 582 | // getInputName returns exactly the name of the message declaration: 583 | // message SomeMessage { 584 | // ... fields 585 | // } 586 | // would return SomeMessage. However, if SomeMessage was also 587 | // used as an Output and not just Input, then GraphQL will 588 | // not allow an Input and a Type to be the same name, therefore 589 | // we will append an "Input" so that it becomes SomeMessageInput. 590 | func (tql *twirpql) getInputName(msg pgs.Message) string { 591 | msgName := tql.getQualifiedName(msg) 592 | if _, ok := tql.types[msgName]; ok { 593 | return msgName + "Input" 594 | } 595 | return msgName 596 | } 597 | 598 | func (tql *twirpql) setGraphQLType(name string, msg pgs.Message) { 599 | if len(msg.Fields()) == 0 { 600 | tql.emptys[name] = true 601 | return 602 | } 603 | msgName := tql.ctx.Name(msg).String() 604 | importpath := tql.deduceImportPath(msg) 605 | tql.gqlTypes[name] = gqlconfig.TypeMapEntry{ 606 | Model: gqlconfig.StringList{importpath + "." + msgName}, 607 | } 608 | } 609 | 610 | // deduceImportPath takes a protobuf message and does its best 611 | // to tell you what the Go import path is for that message. 612 | // At first, it checks if the go_package option is the same 613 | // as the current working directory, if that's the case 614 | // we already called "go list" and we just return tql.modname. 615 | // Second, if the import path contains one or more "/" chars, 616 | // then we return exactly the go_package option because this 617 | // could mean the import path is somewhere outside of the .proto 618 | // file such as "google.protobuf.Timestamp" pointing to 619 | // "github.com/golang/protobuf/ptypes/timestamp". 620 | // Last, assume the location of the .proto file is in a 621 | // subdirectory from within the project, so we just call 622 | // "go list" from within that subdirectory. 623 | func (tql *twirpql) deduceImportPath(msg pgs.Entity) string { 624 | gopkg := tql.ctx.ImportPath(msg.File()).String() 625 | if gopkg == "." { 626 | return tql.modname 627 | } 628 | if strings.Contains(gopkg, "/") { 629 | return gopkg 630 | } 631 | 632 | return tql.goList(msg.File().InputPath().Dir().String()) 633 | } 634 | 635 | func (tql *twirpql) setEnum(protoEnum pgs.Enum) { 636 | name := tql.getQualifiedName(protoEnum) 637 | if _, ok := tql.enums[name]; ok { 638 | return 639 | } 640 | vals := []*serviceField{} 641 | for _, v := range protoEnum.Values() { 642 | vals = append(vals, &serviceField{ 643 | Name: v.Name().String(), 644 | Doc: v.SourceCodeInfo().LeadingComments(), 645 | }) 646 | } 647 | tql.enums[name] = &enumData{ 648 | Name: tql.ctx.Name(protoEnum).String(), 649 | Doc: protoEnum.SourceCodeInfo().LeadingComments(), 650 | ImportPath: tql.deduceImportPath(protoEnum), 651 | PackageName: tql.ctx.PackageName(protoEnum.File()).String(), 652 | Values: vals, 653 | } 654 | tql.setGraphQLEnum(name, protoEnum) 655 | } 656 | 657 | func (tql *twirpql) setGraphQLEnum(name string, enum pgs.Enum) { 658 | importpath := tql.deduceImportPath(enum) 659 | enumGoTypeName := tql.ctx.Name(enum).String() 660 | tql.gqlTypes[name] = gqlconfig.TypeMapEntry{ 661 | Model: gqlconfig.StringList{importpath + "." + enumGoTypeName}, 662 | } 663 | } 664 | 665 | func (tql *twirpql) setBytes(fieldName string, f pgs.Field) { 666 | tql.maps["ProtoBytes"] = tql.ctx.Type(f).Value().String() 667 | tql.gqlTypes["ProtoBytes"] = gqlconfig.TypeMapEntry{ 668 | Model: gqlconfig.StringList{tql.destimportpath + "/twirpql." + "ProtoBytes"}, 669 | } 670 | } 671 | 672 | func (tql *twirpql) setMap(fieldName string, f pgs.Field) { 673 | upField := strings.Title(fieldName) 674 | switch f.Type().Element().ProtoType().Proto() { 675 | case 11: 676 | mapValue := f.Type().Element().Embed() 677 | tql.mapImports[tql.deduceImportPath(mapValue)] = struct{}{} 678 | goTypeDeclaration := strings.ReplaceAll( 679 | tql.ctx.Type(f).Value().String(), 680 | mapValue.Name().String(), 681 | tql.ctx.PackageName(mapValue).String()+"."+ 682 | mapValue.Name().String(), 683 | ) 684 | tql.maps[upField] = goTypeDeclaration 685 | case 14: 686 | mapValue := f.Type().Element().Enum() 687 | tql.mapImports[tql.deduceImportPath(mapValue)] = struct{}{} 688 | goTypeDeclaration := strings.ReplaceAll( 689 | tql.ctx.Type(f).Value().String(), 690 | mapValue.Name().String(), 691 | tql.ctx.PackageName(mapValue).String()+"."+ 692 | mapValue.Name().String(), 693 | ) 694 | tql.maps[upField] = goTypeDeclaration 695 | default: 696 | tql.maps[upField] = tql.ctx.Type(f).Value().String() 697 | } 698 | tql.gqlTypes[upField] = gqlconfig.TypeMapEntry{ 699 | Model: gqlconfig.StringList{tql.destimportpath + "/twirpql." + upField}, 700 | } 701 | } 702 | 703 | func (tql *twirpql) getFields(protoFields []pgs.Field, isType bool) []*serviceField { 704 | fields := []*serviceField{} 705 | for _, pf := range protoFields { 706 | fields = append(fields, tql.getField(pf, isType)) 707 | } 708 | return fields 709 | } 710 | 711 | func (tql *twirpql) writeUnionMask() { 712 | f, err := os.Create(filepath.Join(tql.destpkgname, "unions.gen.go")) 713 | must(err) 714 | defer f.Close() 715 | err = genunions.Render(f) 716 | must(err) 717 | } 718 | 719 | func (tql *twirpql) getField(pf pgs.Field, isType bool) *serviceField { 720 | var f serviceField 721 | f.Name = pf.Name().String() 722 | f.Doc = pf.SourceCodeInfo().LeadingComments() 723 | pt := pf.Type().ProtoType().Proto() 724 | var tmp string 725 | switch pt { 726 | case 11: 727 | if pf.Type().IsMap() { 728 | tql.setMap(f.Name, pf) 729 | tmp = strings.Title(f.Name) 730 | } else { 731 | var msg pgs.Message 732 | if pf.Type().IsRepeated() { 733 | msg = pf.Type().Element().Embed() 734 | } else { 735 | msg = pf.Type().Embed() 736 | } 737 | if isType { 738 | tmp = tql.getQualifiedName(msg) 739 | tql.setType(msg) 740 | } else { 741 | tmp = tql.getInputName(msg) 742 | tql.setInput(msg) 743 | } 744 | } 745 | case 14: 746 | e := pf.Type().Enum() 747 | if pf.Type().IsRepeated() { 748 | e = pf.Type().Element().Enum() 749 | } 750 | tql.setEnum(e) 751 | tmp = tql.getQualifiedName(e) 752 | case 12: 753 | tmp = "ProtoBytes" 754 | tql.setBytes(tmp, pf) 755 | default: 756 | tmp = protoTypesToGqlTypes[pt.String()] 757 | if tmp == "" { 758 | panic("unsupported type: " + pt.String()) 759 | } 760 | } 761 | if pf.Type().IsRepeated() { 762 | tmp = fmt.Sprintf("[%v]", tmp) 763 | } 764 | f.Type = tmp 765 | return &f 766 | } 767 | 768 | // formatQueryInput returns a template-formatted representation 769 | // of a query input. In GraphQL a query looks like this: 770 | // `someQuery(req: Request): Response` 771 | // However, if we don't want to have an input at all in a query, 772 | // the query will now have to look like this: 773 | // `someQuery: Response` 774 | func (tql *twirpql) formatQueryInput(msg pgs.Message) string { 775 | return fmt.Sprintf("(req: %v)", tql.getInputName(msg)) 776 | } 777 | 778 | func (tql *twirpql) path(s string) string { 779 | return filepath.Join(tql.destpkgname, s) 780 | } 781 | 782 | var protoTypesToGqlTypes = map[string]string{ 783 | "TYPE_DOUBLE": "Float", 784 | "TYPE_FLOAT": "Float", 785 | "TYPE_INT64": "Int", 786 | "TYPE_UINT64": "Int", 787 | "TYPE_INT32": "Int", 788 | "TYPE_FIXED64": "Float", 789 | "TYPE_FIXED32": "Float", 790 | "TYPE_BOOL": "Boolean", 791 | "TYPE_STRING": "String", 792 | // "TYPE_GROUP": "", 793 | // "TYPE_MESSAGE": "", // must be mapped to its sibling type 794 | // "TYPE_BYTES": "", 795 | "TYPE_UINT32": "Int", 796 | // "TYPE_ENUM": "", // mapped to its sibling type 797 | // "TYPE_SFIXED32": "", 798 | // "TYPE_SFIXED64": "", 799 | // "TYPE_SINT32": "", 800 | // "TYPE_SINT64": "", 801 | } 802 | 803 | func must(err error) { 804 | if err != nil { 805 | panic(err) 806 | } 807 | } 808 | -------------------------------------------------------------------------------- /twirpql/twirpql_test.go: -------------------------------------------------------------------------------- 1 | package twirpql 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "io/ioutil" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/golang/protobuf/proto" 11 | plugin_go "github.com/golang/protobuf/protoc-gen-go/plugin" 12 | pgs "github.com/lyft/protoc-gen-star" 13 | pgsgo "github.com/lyft/protoc-gen-star/lang/go" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | var update = flag.Bool("update", false, "rewrite all golden files") 18 | 19 | func TestSchema(t *testing.T) { 20 | dirs, err := ioutil.ReadDir("testdata") 21 | require.NoError(t, err) 22 | for _, dir := range dirs { 23 | t.Run(dir.Name(), func(t *testing.T) { 24 | m, f := getModule(t, dir.Name()) 25 | var bts bytes.Buffer 26 | m.generateSchema(f, &bts) 27 | if *update { 28 | writeGoldenSchema(t, bts.Bytes(), dir.Name()) 29 | return 30 | } 31 | given := readGoldenSchema(t, dir.Name()) 32 | require.Equal(t, given, bts.String()) 33 | }) 34 | } 35 | } 36 | 37 | func TestGQLConfig(t *testing.T) { 38 | dirs, err := ioutil.ReadDir("testdata") 39 | require.NoError(t, err) 40 | for _, dir := range dirs { 41 | t.Run(dir.Name(), func(t *testing.T) { 42 | m, f := getModule(t, dir.Name()) 43 | m.generateSchema(f, ioutil.Discard) 44 | var bts bytes.Buffer 45 | m.touchConfig(&bts) 46 | if *update { 47 | writeGoldenConfig(t, bts.Bytes(), dir.Name()) 48 | return 49 | } 50 | given := readGoldenConfig(t, dir.Name()) 51 | require.Equal(t, given, bts.String()) 52 | }) 53 | } 54 | } 55 | 56 | func getModule(t *testing.T, dirName string) (*twirpql, pgs.File) { 57 | t.Helper() 58 | ast := buildGraph(t, dirName) 59 | f := ast.Targets()[dirName+".proto"] 60 | ctx := pgsgo.InitContext(pgs.ParseParameters("")) 61 | m := New(dirName).(*twirpql) 62 | m.ctx = ctx 63 | m.svc = f.Services()[0] 64 | m.protopkg = f.Package() 65 | return m, f 66 | } 67 | 68 | func writeGoldenConfig(t *testing.T, bts []byte, dir ...string) { 69 | t.Helper() 70 | dirs := append(append([]string{"testdata"}, dir...), "gqlgen.yml.golden") 71 | filename := filepath.Join(dirs...) 72 | err := ioutil.WriteFile(filename, bts, 0660) 73 | require.NoError(t, err) 74 | } 75 | 76 | func readGoldenConfig(t *testing.T, dir ...string) string { 77 | t.Helper() 78 | dirs := append(append([]string{"testdata"}, dir...), "gqlgen.yml.golden") 79 | filename := filepath.Join(dirs...) 80 | 81 | data, err := ioutil.ReadFile(filename) 82 | require.NoError(t, err, "unable to read CDR at %q", filename) 83 | 84 | return string(data) 85 | } 86 | 87 | func writeGoldenSchema(t *testing.T, bts []byte, dir ...string) { 88 | t.Helper() 89 | dirs := append(append([]string{"testdata"}, dir...), "schema.graphql.golden") 90 | filename := filepath.Join(dirs...) 91 | err := ioutil.WriteFile(filename, bts, 0660) 92 | require.NoError(t, err) 93 | } 94 | 95 | func readGoldenSchema(t *testing.T, dir ...string) string { 96 | t.Helper() 97 | dirs := append(append([]string{"testdata"}, dir...), "schema.graphql.golden") 98 | filename := filepath.Join(dirs...) 99 | 100 | data, err := ioutil.ReadFile(filename) 101 | require.NoError(t, err, "unable to read CDR at %q", filename) 102 | 103 | return string(data) 104 | } 105 | 106 | func readCodeGenReq(t *testing.T, dir ...string) *plugin_go.CodeGeneratorRequest { 107 | t.Helper() 108 | dirs := append(append([]string{"testdata"}, dir...), "code_generator_request.pb.bin") 109 | filename := filepath.Join(dirs...) 110 | 111 | data, err := ioutil.ReadFile(filename) 112 | require.NoError(t, err, "unable to read CDR at %q", filename) 113 | 114 | req := &plugin_go.CodeGeneratorRequest{} 115 | err = proto.Unmarshal(data, req) 116 | require.NoError(t, err, "unable to unmarshal CDR data at %q", filename) 117 | 118 | return req 119 | } 120 | 121 | func buildGraph(t *testing.T, dir ...string) pgs.AST { 122 | t.Helper() 123 | d := pgs.InitMockDebugger() 124 | ast := pgs.ProcessCodeGeneratorRequest(d, readCodeGenReq(t, dir...)) 125 | require.False(t, d.Failed(), "failed to build graph (see previous log statements)") 126 | return ast 127 | } 128 | -------------------------------------------------------------------------------- /twirpql/types.go: -------------------------------------------------------------------------------- 1 | package twirpql 2 | 3 | type file struct { 4 | Service *service // TODO: multiple services 5 | Types []*serviceType 6 | Inputs []*serviceType 7 | Enums []*enums 8 | Scalars []string 9 | Unions []*union // TODO: 10 | } 11 | 12 | type service struct { 13 | Methods []*method 14 | Mutations []*method 15 | } 16 | 17 | type enums struct { 18 | Name string 19 | Fields []*serviceField 20 | Doc string 21 | } 22 | 23 | type serviceType struct { 24 | Name string 25 | Fields []*serviceField 26 | Doc string 27 | } 28 | 29 | type serviceField struct { 30 | Name string 31 | Type string 32 | Doc string 33 | } 34 | 35 | type method struct { 36 | Name, Request, Response string 37 | Doc string 38 | } 39 | 40 | type union struct { 41 | Name string 42 | Types []string 43 | } 44 | 45 | type oneOf struct { 46 | Name string 47 | Type string 48 | Fields []*serviceField 49 | } 50 | --------------------------------------------------------------------------------