├── .github └── workflows │ ├── full_codegen.yml │ └── test.yml ├── .gitignore ├── .idea ├── .gitignore ├── go-gql-client.iml ├── modules.xml └── vcs.xml ├── LICENSE ├── Makefile ├── OPS.md ├── README.md ├── client ├── client.go ├── extensions │ └── apq.go ├── interface.go ├── transport │ ├── response.go │ ├── response_chan.go │ ├── response_proxy.go │ ├── response_single.go │ ├── transport.go │ ├── transport_http.go │ ├── transport_mock.go │ ├── transport_split.go │ ├── transport_ws.go │ ├── upload.go │ └── wshandler.go └── transport_mock_test.go ├── clientgen ├── LICENSE ├── client.go ├── modelgen.go ├── query.go ├── query_source.go ├── source.go ├── source_generator.go ├── template.go └── template.gotpl ├── config └── config.go ├── example ├── .gqlgen.yml ├── .gqlgenc.yml ├── client │ ├── gen_client.go │ └── scalars.go ├── client_apq_test.go ├── client_http_test.go ├── client_split_test.go ├── client_unstable_ws_test.go ├── client_ws_test.go ├── cmd │ └── memleak.go ├── codegen_test.go ├── common_test.go ├── go.mod ├── go.sum ├── query.go ├── query.graphql ├── schema.graphql ├── server │ ├── generated │ │ └── generated.go │ ├── model │ │ └── models_gen.go │ ├── resolver.go │ ├── schema.resolvers.go │ └── tools.go └── somelib │ └── lib.go ├── generator └── generator.go ├── go.mod ├── go.sum ├── introspection ├── LICENSE ├── parse.go ├── parser_test.go ├── query.go ├── testdata │ └── introspection_result_no_mutation.json └── type.go └── main.go /.github/workflows/full_codegen.yml: -------------------------------------------------------------------------------- 1 | name: Full Codegen 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | name: Build & Test 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.16 20 | 21 | - name: Full Codegen 22 | run: make example-fullgen 23 | 24 | - name: Test Example 25 | run: make example-test 26 | 27 | - name: Codegen 28 | run: make example-genall 29 | 30 | - name: Test Example 31 | run: make example-test 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | go: [ '1.15', '1.16', '1.17', '1.18' ] 16 | name: Build & Test Go ${{ matrix.go }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: ${{ matrix.go }} 24 | 25 | - name: Build 26 | run: go build -v ./... 27 | 28 | - name: Test 29 | run: make test 30 | 31 | - name: Test Example 32 | run: make example-test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 4 | 5 | # User-specific stuff 6 | .idea/**/workspace.xml 7 | .idea/**/tasks.xml 8 | .idea/**/usage.statistics.xml 9 | .idea/**/dictionaries 10 | .idea/**/shelf 11 | 12 | # Generated files 13 | .idea/**/contentModel.xml 14 | 15 | # Sensitive or high-churn files 16 | .idea/**/dataSources/ 17 | .idea/**/dataSources.ids 18 | .idea/**/dataSources.local.xml 19 | .idea/**/sqlDataSources.xml 20 | .idea/**/dynamic.xml 21 | .idea/**/uiDesigner.xml 22 | .idea/**/dbnavigator.xml 23 | 24 | # Gradle 25 | .idea/**/gradle.xml 26 | .idea/**/libraries 27 | 28 | # Gradle and Maven with auto-import 29 | # When using Gradle or Maven with auto-import, you should exclude module files, 30 | # since they will be recreated, and may cause churn. Uncomment if using 31 | # auto-import. 32 | # .idea/artifacts 33 | # .idea/compiler.xml 34 | # .idea/jarRepositories.xml 35 | # .idea/modules.xml 36 | # .idea/*.iml 37 | # .idea/modules 38 | # *.iml 39 | # *.ipr 40 | 41 | # CMake 42 | cmake-build-*/ 43 | 44 | # Mongo Explorer plugin 45 | .idea/**/mongoSettings.xml 46 | 47 | # File-based project format 48 | *.iws 49 | 50 | # IntelliJ 51 | out/ 52 | 53 | # mpeltonen/sbt-idea plugin 54 | .idea_modules/ 55 | 56 | # JIRA plugin 57 | atlassian-ide-plugin.xml 58 | 59 | # Cursive Clojure plugin 60 | .idea/replstate.xml 61 | 62 | # Crashlytics plugin (for Android Studio and IntelliJ) 63 | com_crashlytics_export_strings.xml 64 | crashlytics.properties 65 | crashlytics-build.properties 66 | fabric.properties 67 | 68 | # Editor-based Rest Client 69 | .idea/httpRequests 70 | 71 | # Android studio 3.1+ serialized cache file 72 | .idea/caches/build_file_checksums.ser 73 | 74 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/go-gql-client.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Infiot Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test -v -count=1 ./... 3 | 4 | example-fullgen: 5 | rm example/client/gen_client.go || true 6 | rm example/server/generated/generated.go || true 7 | rm example/server/model/models_gen.go || true 8 | make example-genall 9 | 10 | example-genall: 11 | make example-gqlgen 12 | make example-gqlgenc 13 | 14 | example-gqlgen: 15 | cd example && go run github.com/99designs/gqlgen 16 | 17 | example-gqlgenc: 18 | cd example && go run github.com/infiotinc/gqlgenc 19 | 20 | example-test: 21 | cd example && go test -v -count=1 ./... 22 | 23 | example-run-memleak: 24 | cd example && go run ./cmd/memleak.go 25 | 26 | tag: 27 | git tag -a ${TAG} -m ${TAG} 28 | git push origin ${TAG} 29 | -------------------------------------------------------------------------------- /OPS.md: -------------------------------------------------------------------------------- 1 | # OPS 2 | 3 | > For maintenance purposes 4 | 5 | ## Release 6 | 7 | TAG=v0.0.x make tag 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gqlgenc 2 | 3 | > gqlgenc is a fully featured go gql client, powered by codegen 4 | 5 | ## Why yet another Go GQL client ? 6 | 7 | | Package | Codegen | Subscription | Extensions | 8 | |---------------------------------------------|---------|--------------|------------| 9 | | https://github.com/shurcooL/graphql | ❌ | ❌ |❌ | 10 | | https://github.com/Yamashou/gqlgenc | ✅ | ❌ |❌ | 11 | | https://github.com/hasura/go-graphql-client | ❌ | ✅ |❌ | 12 | | ✨[https://github.com/infiotinc/gqlgenc](https://github.com/infiotinc/gqlgenc)✨| ✅ | ✅ | ✅ | 13 | 14 | ## GQL Client 15 | 16 | ### Transports 17 | 18 | gqlgenc is transport agnostic, and ships with 3 transport implementations: 19 | 20 | - http: Transports GQL queries over http 21 | - ws: Transports GQL queries over websocket 22 | - split: Can be used to have a single client use multiple transports depending on the type of query (`query`, `mutation` over http and `subscription` over ws) 23 | 24 | ### Quickstart 25 | 26 | Quickstart with a client with http & ws transports: 27 | 28 | ```go 29 | package main 30 | 31 | import ( 32 | "context" 33 | "github.com/infiotinc/gqlgenc/client" 34 | "github.com/infiotinc/gqlgenc/client/transport" 35 | ) 36 | 37 | func main() { 38 | wstr := &transport.Ws{ 39 | URL: "wss://example.org/graphql", 40 | } 41 | wstr.Start(context.Background()) 42 | defer wstr.Close() 43 | 44 | httptr := &transport.Http{ 45 | URL: "https://example.org/graphql", 46 | } 47 | 48 | tr := transport.SplitSubscription(wstr, httptr) 49 | 50 | cli := &client.Client { 51 | Transport: tr, 52 | } 53 | } 54 | ``` 55 | 56 | ### Query/Mutation 57 | 58 | ```go 59 | var res struct { 60 | Room string `json:"room"` 61 | } 62 | _, err := cli.Query(ctx, "", "query { room }", nil, &res) // or Mutation 63 | if err != nil { 64 | panic(err) 65 | } 66 | ``` 67 | 68 | ### Subscription 69 | 70 | ```go 71 | sub, stop := cli.Subscription(ctx, "", "subscription { newRoom }", nil) 72 | defer stop() 73 | 74 | for sub.Next() { 75 | msg := sub.Get() 76 | 77 | if len(msg.Errors) > 0 { 78 | // Do something with them 79 | } 80 | 81 | var res struct { 82 | Room string `json:"newRoom"` 83 | } 84 | err := msg.UnmarshalData(&res) 85 | if err != nil { 86 | // Do something with that 87 | } 88 | } 89 | 90 | if err := sub.Err(); err != nil { 91 | panic(err) 92 | } 93 | ``` 94 | 95 | ## GQL Client Codegen 96 | 97 | Create a `.gqlgenc.yml` at the root of your module: 98 | 99 | ```yaml 100 | client: 101 | package: graph 102 | filename: ./graph/gen_client.go 103 | models: 104 | Int: 105 | model: github.com/99designs/gqlgen/graphql.Int64 106 | DateTime: 107 | model: github.com/99designs/gqlgen/graphql.Time 108 | # The schema can be fetched from files or through introspection 109 | schema: 110 | - schema.graphqls 111 | endpoint: 112 | url: https://api.annict.com/graphql # Where do you want to send your request? 113 | headers: # If you need header for getting introspection query, set it 114 | Authorization: "Bearer ${ANNICT_KEY}" # support environment variables 115 | query: 116 | - query.graphql 117 | 118 | ``` 119 | 120 | Fill your `query.graphql` with queries: 121 | ```graphql 122 | query GetRoom { 123 | room(name: "secret room") { 124 | name 125 | } 126 | } 127 | ``` 128 | 129 | Run `go run github.com/infiotinc/gqlgenc` 130 | 131 | Enjoy: 132 | ```go 133 | // Create codegen client 134 | gql := &graph.Client{ 135 | Client: cli, 136 | } 137 | 138 | gql.GetRoom(...) 139 | ``` 140 | 141 | ## Input as `map` 142 | 143 | In Go, working with JSON and nullity can be tricky. The recommended way to deal with such case is through maps. You can ask gqlgenc to generate such maps with helpers through config: 144 | 145 | Globally: 146 | ```yaml 147 | client: 148 | input_as_map: true 149 | ``` 150 | 151 | Per model: 152 | ```yaml 153 | models: 154 | SomeInput: 155 | as_map: true 156 | ``` 157 | 158 | ## Extensions 159 | 160 | ### APQ 161 | 162 | [Automatic Persisted Queries](https://www.apollographql.com/docs/apollo-server/performance/apq/) can be enabled by adding: 163 | 164 | ```go 165 | cli.Use(&extensions.APQ{}) 166 | ``` 167 | 168 | ## File Upload 169 | 170 | - In the `Http` transport, set `UseFormMultipart` to `true` 171 | 172 | - In `.gqlgenc.yaml`: 173 | 174 | ```yaml 175 | models: 176 | Upload: 177 | model: github.com/infiotinc/gqlgenc/client/transport.Upload 178 | ``` 179 | 180 | - Profit 181 | 182 | ```go 183 | up := transport.NewUpload(someFile) 184 | 185 | _, _, err := gql.MyUploadFile(ctx, up) 186 | ``` 187 | 188 | ## Acknowledgements 189 | 190 | This repo is based on the great work of [Yamashou/gqlgenc](https://github.com/Yamashou/gqlgenc) and [hasura/go-graphql-client](https://github.com/hasura/go-graphql-client) 191 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/infiotinc/gqlgenc/client/transport" 7 | ) 8 | 9 | type extensions struct { 10 | aroundRequest []AroundRequest 11 | } 12 | 13 | func (es *extensions) Use(e Extension) { 14 | if a, ok := e.(AroundRequest); ok { 15 | es.aroundRequest = append(es.aroundRequest, a) 16 | } 17 | } 18 | 19 | func (es *extensions) RunAroundRequest(req transport.Request, h RequestHandler) transport.Response { 20 | run := h 21 | 22 | for _, _e := range es.aroundRequest { 23 | e := _e // Local ref 24 | next := run // Local ref 25 | run = func(req transport.Request) transport.Response { 26 | return e.AroundRequest(req, next) 27 | } 28 | } 29 | 30 | return run(req) 31 | } 32 | 33 | type Client struct { 34 | Transport transport.Transport 35 | 36 | extensions 37 | } 38 | 39 | func (c *Client) doSingle( 40 | ctx context.Context, 41 | operation transport.Operation, 42 | operationName string, 43 | query string, 44 | variables map[string]interface{}, 45 | t interface{}, 46 | ) (transport.OperationResponse, error) { 47 | res := c.do(transport.Request{ 48 | Context: ctx, 49 | Operation: operation, 50 | Query: query, 51 | OperationName: operationName, 52 | Variables: variables, 53 | }) 54 | defer res.Close() 55 | 56 | if !res.Next() { 57 | if err := res.Err(); err != nil { 58 | return transport.OperationResponse{}, err 59 | } 60 | 61 | return transport.OperationResponse{}, fmt.Errorf("no response") 62 | } 63 | 64 | opres := res.Get() 65 | 66 | err := opres.UnmarshalData(t) 67 | 68 | if len(opres.Errors) > 0 { 69 | return opres, opres.Errors 70 | } 71 | 72 | return opres, err 73 | } 74 | 75 | func (c *Client) do(req transport.Request) transport.Response { 76 | if req.Extensions == nil { 77 | req.Extensions = map[string]interface{}{} 78 | } 79 | 80 | res := c.RunAroundRequest(req, c.Transport.Request) 81 | 82 | go func() { 83 | select { 84 | case <-req.Context.Done(): 85 | res.Close() 86 | case <-res.Done(): 87 | } 88 | }() 89 | 90 | return res 91 | } 92 | 93 | // Query runs a query 94 | // operationName is optional 95 | func (c *Client) Query(ctx context.Context, operationName string, query string, variables map[string]interface{}, t interface{}) (transport.OperationResponse, error) { 96 | return c.doSingle(ctx, transport.Query, operationName, query, variables, t) 97 | } 98 | 99 | // Mutation runs a mutation 100 | // operationName is optional 101 | func (c *Client) Mutation(ctx context.Context, operationName string, query string, variables map[string]interface{}, t interface{}) (transport.OperationResponse, error) { 102 | return c.doSingle(ctx, transport.Mutation, operationName, query, variables, t) 103 | } 104 | 105 | // Subscription starts a GQL subscription 106 | // operationName is optional 107 | func (c *Client) Subscription(ctx context.Context, operationName string, query string, variables map[string]interface{}) transport.Response { 108 | return c.do(transport.Request{ 109 | Context: ctx, 110 | Operation: transport.Subscription, 111 | Query: query, 112 | OperationName: operationName, 113 | Variables: variables, 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /client/extensions/apq.go: -------------------------------------------------------------------------------- 1 | package extensions 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "github.com/infiotinc/gqlgenc/client" 7 | "github.com/infiotinc/gqlgenc/client/transport" 8 | ) 9 | 10 | const APQKey = "persistedQuery" 11 | 12 | type APQExtension struct { 13 | Version int64 `json:"version"` 14 | Sha256Hash string `json:"sha256Hash"` 15 | } 16 | 17 | type APQ struct{} 18 | 19 | var _ client.AroundRequest = (*APQ)(nil) 20 | 21 | func (a *APQ) ExtensionName() string { 22 | return "apq" 23 | } 24 | 25 | func (a *APQ) AroundRequest(req transport.Request, next client.RequestHandler) transport.Response { 26 | if _, ok := req.Extensions[APQKey]; !ok { 27 | sum := sha256.Sum256([]byte(req.Query)) 28 | req.Extensions[APQKey] = APQExtension{ 29 | Version: 1, 30 | Sha256Hash: fmt.Sprintf("%x", sum), 31 | } 32 | } 33 | 34 | res := next(transport.Request{ 35 | Context: req.Context, 36 | Operation: req.Operation, 37 | OperationName: req.OperationName, 38 | Variables: req.Variables, 39 | Extensions: req.Extensions, 40 | }) 41 | 42 | nres := transport.NewProxyResponse() 43 | 44 | nres.Bind(res, func(opres transport.OperationResponse, send func()) { 45 | for _, err := range opres.Errors { 46 | if code, ok := err.Extensions["code"]; ok { 47 | if code == "PERSISTED_QUERY_NOT_FOUND" { 48 | nres.Unbind(res) 49 | go res.Close() 50 | 51 | nres.Bind(next(req), nil) 52 | return 53 | } 54 | } 55 | } 56 | 57 | send() 58 | }) 59 | 60 | return nres 61 | } 62 | -------------------------------------------------------------------------------- /client/interface.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/infiotinc/gqlgenc/client/transport" 5 | ) 6 | 7 | type ( 8 | Extension interface{ 9 | ExtensionName() string 10 | } 11 | 12 | RequestHandler func(req transport.Request) transport.Response 13 | 14 | AroundRequest interface { 15 | AroundRequest(req transport.Request, next RequestHandler) transport.Response 16 | } 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /client/transport/response.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import "sync" 4 | 5 | type Response interface { 6 | Next() bool 7 | Get() OperationResponse 8 | Close() 9 | CloseWithError(err error) 10 | Err() error 11 | Done() <-chan struct{} 12 | } 13 | 14 | type SendResponse interface { 15 | Response 16 | Send(OperationResponse) 17 | } 18 | 19 | type responseError struct { 20 | err error 21 | m sync.Mutex 22 | } 23 | 24 | func (r *responseError) CloseWithError(err error) { 25 | if err == nil { 26 | panic("CloseWithError: err must be non-nil") 27 | } 28 | 29 | r.m.Lock() 30 | defer r.m.Unlock() 31 | 32 | if r.err != nil { 33 | return 34 | } 35 | 36 | r.err = err 37 | } 38 | 39 | func (r *responseError) Err() error { 40 | r.m.Lock() 41 | defer r.m.Unlock() 42 | 43 | return r.err 44 | } 45 | -------------------------------------------------------------------------------- /client/transport/response_chan.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import "sync" 4 | 5 | type ChanResponse struct { 6 | responseError 7 | 8 | ch chan OperationResponse 9 | close func() error 10 | closed bool 11 | 12 | cor OperationResponse 13 | m sync.Mutex 14 | dc chan struct{} 15 | } 16 | 17 | func NewChanResponse(onClose func() error) *ChanResponse { 18 | return &ChanResponse{ 19 | ch: make(chan OperationResponse), 20 | dc: make(chan struct{}), 21 | close: onClose, 22 | } 23 | } 24 | 25 | func (r *ChanResponse) Next() bool { 26 | if r.err != nil { 27 | return false 28 | } 29 | 30 | or, ok := <-r.ch 31 | r.cor = or 32 | return ok 33 | } 34 | 35 | func (r *ChanResponse) Get() OperationResponse { 36 | return r.cor 37 | } 38 | 39 | func (r *ChanResponse) Close() { 40 | if r.close != nil { 41 | r.err = r.close() 42 | } 43 | r.CloseCh() 44 | } 45 | 46 | func (r *ChanResponse) CloseWithError(err error) { 47 | r.responseError.CloseWithError(err) 48 | r.CloseCh() 49 | 50 | if r.close != nil { 51 | _ = r.close() 52 | } 53 | } 54 | 55 | func (r *ChanResponse) CloseCh() { 56 | r.m.Lock() 57 | defer r.m.Unlock() 58 | 59 | if r.closed { 60 | return 61 | } 62 | 63 | close(r.ch) 64 | close(r.dc) 65 | r.closed = true 66 | } 67 | 68 | func (r *ChanResponse) Done() <-chan struct{} { 69 | return r.dc 70 | } 71 | 72 | func (r *ChanResponse) Send(op OperationResponse) { 73 | r.m.Lock() 74 | defer r.m.Unlock() 75 | 76 | select { 77 | case r.ch <- op: 78 | case <-r.Done(): 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/transport/response_proxy.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | type proxyTarget SendResponse 9 | 10 | type ProxyResponse struct { 11 | proxyTarget 12 | binds []Response 13 | m sync.RWMutex 14 | inFlight int32 15 | } 16 | 17 | func (p *ProxyResponse) Bound(res Response) bool { 18 | p.m.RLock() 19 | defer p.m.RUnlock() 20 | 21 | for _, b := range p.binds { 22 | if b == res { 23 | return true 24 | } 25 | } 26 | 27 | return false 28 | } 29 | 30 | func (p *ProxyResponse) Bind(res Response, onOpres func(response OperationResponse, send func())) { 31 | atomic.AddInt32(&p.inFlight, 1) 32 | 33 | if onOpres == nil { 34 | onOpres = func(_ OperationResponse, send func()) { 35 | send() 36 | } 37 | } 38 | 39 | p.m.Lock() 40 | p.binds = append(p.binds, res) 41 | p.m.Unlock() 42 | 43 | go func() { 44 | go func() { 45 | select { 46 | case <-res.Done(): 47 | case <-p.Done(): 48 | res.Close() 49 | } 50 | }() 51 | 52 | for res.Next() { 53 | opres := res.Get() 54 | 55 | onOpres(opres, func() { 56 | if p.Bound(res) { 57 | p.Send(opres) 58 | } 59 | }) 60 | 61 | if !p.Bound(res) { 62 | break 63 | } 64 | } 65 | 66 | if p.Bound(res) { 67 | if err := res.Err(); err != nil { 68 | p.CloseWithError(err) 69 | } 70 | } 71 | 72 | atomic.AddInt32(&p.inFlight, -1) 73 | p.Unbind(res) 74 | }() 75 | } 76 | 77 | func (p *ProxyResponse) Unbind(res Response) { 78 | p.m.Lock() 79 | binds := make([]Response, 0) 80 | for _, b := range p.binds { 81 | if b == res { 82 | continue 83 | } 84 | 85 | binds = append(binds, b) 86 | } 87 | p.binds = binds 88 | p.m.Unlock() 89 | 90 | if atomic.LoadInt32(&p.inFlight) == 0 { 91 | p.Close() 92 | } 93 | } 94 | 95 | func NewProxyResponse() *ProxyResponse { 96 | return &ProxyResponse{ 97 | proxyTarget: NewChanResponse(nil), 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /client/transport/response_single.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import "sync" 4 | 5 | type SingleResponse struct { 6 | or OperationResponse 7 | 8 | calledNext bool 9 | dm sync.Mutex 10 | dc chan struct{} 11 | 12 | responseError 13 | } 14 | 15 | func NewSingleResponse(or OperationResponse) *SingleResponse { 16 | return &SingleResponse{or: or} 17 | } 18 | 19 | func NewErrorResponse(err error) Response { 20 | res := NewSingleResponse(OperationResponse{}) 21 | res.CloseWithError(err) 22 | 23 | return res 24 | } 25 | 26 | func (r *SingleResponse) Next() bool { 27 | if r.calledNext || r.err != nil { 28 | return false 29 | } 30 | 31 | r.calledNext = true 32 | 33 | return true 34 | } 35 | 36 | func (r *SingleResponse) Get() OperationResponse { 37 | return r.or 38 | } 39 | 40 | func (r *SingleResponse) Close() {} 41 | 42 | func (r *SingleResponse) Done() <-chan struct{} { 43 | r.dm.Lock() 44 | if r.dc == nil { 45 | r.dc = make(chan struct{}) 46 | close(r.dc) 47 | } 48 | r.dm.Unlock() 49 | 50 | return r.dc 51 | } 52 | -------------------------------------------------------------------------------- /client/transport/transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/vektah/gqlparser/v2/gqlerror" 7 | ) 8 | 9 | type Operation string 10 | 11 | const ( 12 | Query Operation = "query" 13 | Mutation Operation = "mutation" 14 | Subscription Operation = "subscription" 15 | ) 16 | 17 | type OperationRequest struct { 18 | Query string `json:"query,omitempty"` 19 | OperationName string `json:"operationName,omitempty"` 20 | Variables map[string]interface{} `json:"variables,omitempty"` 21 | Extensions map[string]interface{} `json:"extensions,omitempty"` 22 | } 23 | 24 | func NewOperationRequestFromRequest(req Request) OperationRequest { 25 | return OperationRequest{ 26 | Query: req.Query, 27 | OperationName: req.OperationName, 28 | Variables: req.Variables, 29 | Extensions: req.Extensions, 30 | } 31 | } 32 | 33 | type OperationResponse struct { 34 | Data json.RawMessage `json:"data,omitempty"` 35 | Errors gqlerror.List `json:"errors,omitempty"` 36 | Extensions RawExtensions `json:"extensions,omitempty"` 37 | } 38 | 39 | func (r OperationResponse) UnmarshalData(t interface{}) error { 40 | if r.Data == nil { 41 | return nil 42 | } 43 | 44 | return json.Unmarshal(r.Data, t) 45 | } 46 | 47 | type RawExtensions map[string]json.RawMessage 48 | 49 | func (es RawExtensions) Unmarshal(name string, t interface{}) error { 50 | if es == nil { 51 | return nil 52 | } 53 | 54 | ex, ok := es[name] 55 | if !ok { 56 | return nil 57 | } 58 | 59 | return json.Unmarshal(ex, t) 60 | } 61 | 62 | type Request struct { 63 | Context context.Context 64 | Operation Operation 65 | 66 | OperationName string 67 | Query string 68 | Variables map[string]interface{} 69 | Extensions map[string]interface{} 70 | } 71 | 72 | type Transport interface { 73 | Request(req Request) Response 74 | } 75 | -------------------------------------------------------------------------------- /client/transport/transport_http.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "mime/multipart" 10 | "net/http" 11 | "reflect" 12 | "strings" 13 | ) 14 | 15 | type HttpRequestOption func(req *http.Request) 16 | 17 | type Http struct { 18 | URL string 19 | // Client defaults to http.DefaultClient 20 | Client *http.Client 21 | RequestOptions []HttpRequestOption 22 | UseFormMultipart bool 23 | } 24 | 25 | func (h *Http) Request(req Request) Response { 26 | opres, err := h.request(req) 27 | if err != nil { 28 | return NewErrorResponse(err) 29 | } 30 | 31 | return NewSingleResponse(*opres) 32 | } 33 | 34 | func (h *Http) request(gqlreq Request) (*OperationResponse, error) { 35 | if h.Client == nil { 36 | h.Client = http.DefaultClient 37 | } 38 | 39 | bodyb, err := json.Marshal(NewOperationRequestFromRequest(gqlreq)) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | var req *http.Request 45 | if h.UseFormMultipart { 46 | req, err = h.formReq(gqlreq, bodyb) 47 | if err != nil { 48 | return nil, err 49 | } 50 | } else { 51 | req, err = http.NewRequestWithContext(gqlreq.Context, "POST", h.URL, bytes.NewReader(bodyb)) 52 | if err != nil { 53 | return nil, err 54 | } 55 | req.Header.Set("Content-Type", "application/json") 56 | } 57 | 58 | for _, ro := range h.RequestOptions { 59 | ro(req) 60 | } 61 | 62 | res, err := h.Client.Do(req) 63 | if err != nil { 64 | return nil, err 65 | } 66 | defer res.Body.Close() 67 | 68 | data, err := ioutil.ReadAll(res.Body) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | var opres OperationResponse 74 | err = json.Unmarshal(data, &opres) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | if len(opres.Data) == 0 && len(opres.Errors) == 0 { 80 | return nil, fmt.Errorf("no data nor errors, got %v: %.1000s", res.StatusCode, data) 81 | } 82 | 83 | return &opres, nil 84 | } 85 | 86 | func (h *Http) jsonFormField(w *multipart.Writer, name string, v interface{}) error { 87 | fw, err := w.CreateFormField(name) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return json.NewEncoder(fw).Encode(v) 93 | } 94 | 95 | func (h *Http) formReq(gqlreq Request, bodyb []byte) (*http.Request, error) { 96 | var b bytes.Buffer 97 | w := multipart.NewWriter(&b) 98 | 99 | filesMap := make(map[string][]string) 100 | 101 | i := 0 102 | for p, f := range h.collectUploads("variables", gqlreq.Variables) { 103 | k := fmt.Sprintf("%v", i) 104 | fw, err := w.CreateFormFile(k, f.Name) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | // Write file to field 110 | if _, err := io.Copy(fw, f.File); err != nil { 111 | return nil, err 112 | } 113 | 114 | filesMap[k] = []string{p} 115 | i++ 116 | } 117 | 118 | err := w.WriteField("operations", string(bodyb)) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | err = h.jsonFormField(w, "map", filesMap) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | err = w.Close() 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | req, err := http.NewRequest("POST", h.URL, &b) 134 | if err != nil { 135 | return nil, err 136 | } 137 | req.Header.Set("Content-Type", w.FormDataContentType()) 138 | 139 | return req, nil 140 | } 141 | 142 | func (h *Http) collectUploads(path string, in interface{}) map[string]Upload { 143 | if up, ok := in.(Upload); ok { 144 | return map[string]Upload{ 145 | path: up, 146 | } 147 | } 148 | 149 | v := reflect.ValueOf(in) 150 | switch v.Kind() { 151 | case reflect.Slice, reflect.Array: 152 | rs := make(map[string]Upload) 153 | for i := 0; i < v.Len(); i++ { 154 | p := fmt.Sprintf("%v.%v", path, i) 155 | for fk, f := range h.collectUploads(p, v.Index(i).Interface()) { 156 | rs[fk] = f 157 | } 158 | } 159 | return rs 160 | case reflect.Struct: 161 | rs := make(map[string]Upload) 162 | for i := 0; i < v.NumField(); i++ { 163 | f := v.Field(i) 164 | 165 | if !f.CanInterface() { 166 | continue // private field 167 | } 168 | 169 | ft := v.Type() 170 | k := ft.Field(i).Tag.Get("json") 171 | 172 | if strings.Contains(k, ",") { 173 | i := strings.Index(k, ",") 174 | k = k[:i] 175 | } 176 | 177 | if k == "-" { 178 | continue 179 | } 180 | 181 | p := fmt.Sprintf("%v.%v", path, k) 182 | for fk, f := range h.collectUploads(p, f.Interface()) { 183 | rs[fk] = f 184 | } 185 | } 186 | return rs 187 | case reflect.Map: 188 | rs := make(map[string]Upload) 189 | iter := v.MapRange() 190 | for iter.Next() { 191 | p := fmt.Sprintf("%v.%v", path, iter.Key().Interface()) 192 | for fk, f := range h.collectUploads(p, iter.Value().Interface()) { 193 | rs[fk] = f 194 | } 195 | } 196 | return rs 197 | 198 | case reflect.Ptr: 199 | if v.IsNil() { 200 | return nil 201 | } 202 | 203 | return h.collectUploads(path, v.Elem().Interface()) 204 | } 205 | 206 | return nil 207 | } 208 | -------------------------------------------------------------------------------- /client/transport/transport_mock.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/vektah/gqlparser/v2/gqlerror" 7 | ) 8 | 9 | type Mock map[string]Func 10 | 11 | func (m Mock) Request(req Request) Response { 12 | h, ok := m[req.Query] 13 | if !ok { 14 | panic(fmt.Sprintf("query not handled: \n%v", req.Query)) 15 | } 16 | 17 | return h(req) 18 | } 19 | 20 | func NewMockOperationResponse(v interface{}, errs gqlerror.List) OperationResponse { 21 | data, _ := json.Marshal(v) 22 | 23 | return OperationResponse{ 24 | Data: data, 25 | Errors: errs, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/transport/transport_split.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | type Func func(Request) Response 4 | 5 | func (f Func) Request(req Request) Response { 6 | return f(req) 7 | } 8 | 9 | func Split(f func(Request) (Transport, error)) Transport { 10 | return Func(func(req Request) Response { 11 | tr, err := f(req) 12 | if err != nil { 13 | return NewErrorResponse(err) 14 | } 15 | 16 | return tr.Request(req) 17 | }) 18 | } 19 | 20 | // SplitSubscription routes subscription to subtr, and other type of queries to othertr 21 | func SplitSubscription(subtr, othertr Transport) Transport { 22 | return Split(func(req Request) (Transport, error) { 23 | if req.Operation == Subscription { 24 | return subtr, nil 25 | } 26 | 27 | return othertr, nil 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /client/transport/transport_ws.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | // Original work from https://github.com/hasura/go-graphql-client/blob/0806e5ec7/subscription.go 4 | import ( 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "github.com/vektah/gqlparser/v2/gqlerror" 10 | "io" 11 | "nhooyr.io/websocket" 12 | "os" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | "sync/atomic" 17 | "time" 18 | ) 19 | 20 | type OperationMessageType string 21 | 22 | const ( 23 | // GQL_CONNECTION_INIT the Client sends this message after plain websocket connection to start the communication with the server 24 | GQL_CONNECTION_INIT OperationMessageType = "connection_init" 25 | // GQL_CONNECTION_ERROR The server may responses with this message to the GQL_CONNECTION_INIT from client, indicates the server rejected the connection. 26 | GQL_CONNECTION_ERROR OperationMessageType = "conn_err" 27 | // GQL_START Client sends this message to execute GraphQL operation 28 | GQL_START OperationMessageType = "start" 29 | // GQL_STOP Client sends this message in order to stop a running GraphQL operation execution (for example: unsubscribe) 30 | GQL_STOP OperationMessageType = "stop" 31 | // GQL_ERROR Server sends this message upon a failing operation, before the GraphQL execution, usually due to GraphQL validation errors (resolver errors are part of GQL_DATA message, and will be added as errors array) 32 | GQL_ERROR OperationMessageType = "error" 33 | // GQL_DATA The server sends this message to transfter the GraphQL execution result from the server to the client, this message is a response for GQL_START message. 34 | GQL_DATA OperationMessageType = "data" 35 | // GQL_COMPLETE Server sends this message to indicate that a GraphQL operation is done, and no more data will arrive for the specific operation. 36 | GQL_COMPLETE OperationMessageType = "complete" 37 | // GQL_CONNECTION_KEEP_ALIVE Server message that should be sent right after each GQL_CONNECTION_ACK processed and then periodically to keep the client connection alive. 38 | // The client starts to consider the keep alive message only upon the first received keep alive message from the server. 39 | GQL_CONNECTION_KEEP_ALIVE OperationMessageType = "ka" 40 | // GQL_CONNECTION_ACK The server may responses with this message to the GQL_CONNECTION_INIT from client, indicates the server accepted the connection. May optionally include a payload. 41 | GQL_CONNECTION_ACK OperationMessageType = "connection_ack" 42 | // GQL_CONNECTION_TERMINATE the Client sends this message to terminate the connection. 43 | GQL_CONNECTION_TERMINATE OperationMessageType = "connection_terminate" 44 | 45 | // GQL_UNKNOWN is an Unknown operation type, for logging only 46 | GQL_UNKNOWN OperationMessageType = "unknown" 47 | // GQL_INTERNAL is the Internal status, for logging only 48 | GQL_INTERNAL OperationMessageType = "internal" 49 | ) 50 | 51 | type WebsocketConn interface { 52 | ReadJSON(v interface{}) error 53 | WriteJSON(v interface{}) error 54 | Close() error 55 | // SetReadLimit sets the maximum size in bytes for a message read from the peer. If a 56 | // message exceeds the limit, the connection sends a close message to the peer 57 | // and returns ErrReadLimit to the application. 58 | SetReadLimit(limit int64) 59 | } 60 | 61 | type OperationMessage struct { 62 | ID string `json:"id,omitempty"` 63 | Type OperationMessageType `json:"type"` 64 | Payload json.RawMessage `json:"payload,omitempty"` 65 | } 66 | 67 | func (msg OperationMessage) String() string { 68 | return fmt.Sprintf("%v %v %s", msg.ID, msg.Type, msg.Payload) 69 | } 70 | 71 | type ConnOptions struct { 72 | Context context.Context 73 | URL string 74 | Timeout time.Duration 75 | } 76 | 77 | type wsResponse struct { 78 | *ChanResponse 79 | Context context.Context 80 | OperationRequest OperationRequest 81 | started bool 82 | } 83 | 84 | type WebsocketConnProvider func(ctx context.Context, URL string) (WebsocketConn, error) 85 | 86 | type Status int 87 | 88 | const ( 89 | StatusDisconnected Status = iota 90 | StatusConnected 91 | StatusReady 92 | ) 93 | 94 | func (s Status) String() string { 95 | switch s { 96 | case StatusDisconnected: 97 | return "disconnected" 98 | case StatusConnected: 99 | return "connected" 100 | case StatusReady: 101 | return "ready" 102 | } 103 | 104 | panic("unknown status") 105 | } 106 | 107 | // Ws transports GQL queries over websocket 108 | // Start() must be called to initiate the websocket connection 109 | // Close() must be called to dispose of the transport 110 | type Ws struct { 111 | URL string 112 | // WebsocketConnProvider defaults to DefaultWebsocketConnProvider(30 * time.Second) 113 | WebsocketConnProvider WebsocketConnProvider 114 | // ConnectionParams will be sent during the connection init 115 | ConnectionParams interface{} 116 | 117 | cancel context.CancelFunc 118 | conn WebsocketConn 119 | running bool 120 | status Status 121 | sc *sync.Cond 122 | 123 | ops map[string]*wsResponse 124 | opsm sync.Mutex 125 | 126 | o sync.Once 127 | errCh chan error 128 | i uint64 129 | rm sync.Mutex 130 | log bool 131 | } 132 | 133 | func (t *Ws) sendErr(err error) { 134 | select { 135 | case t.errCh <- err: // Attempt to write err 136 | default: 137 | } 138 | } 139 | 140 | func (t *Ws) init() { 141 | t.o.Do(func() { 142 | t.ops = make(map[string]*wsResponse) 143 | 144 | t.sc = sync.NewCond(&sync.Mutex{}) 145 | 146 | t.conn = &closedWs{} 147 | 148 | if t.WebsocketConnProvider == nil { 149 | t.WebsocketConnProvider = DefaultWebsocketConnProvider(30 * time.Second) 150 | } 151 | 152 | t.log, _ = strconv.ParseBool(os.Getenv("GQLGENC_WS_LOG")) 153 | }) 154 | } 155 | 156 | func (t *Ws) WaitFor(st Status, s time.Duration) { 157 | t.init() 158 | 159 | t.printLog(GQL_INTERNAL, "WAIT FOR", st) 160 | 161 | for { 162 | t.waitFor(st) 163 | 164 | time.Sleep(s) 165 | 166 | // After timeout, is it still in the expected status ? 167 | if t.status == st { 168 | break 169 | } 170 | } 171 | 172 | t.printLog(GQL_INTERNAL, "DONE WAIT FOR", st) 173 | } 174 | 175 | func (t *Ws) waitFor(s Status) { 176 | t.init() 177 | 178 | t.sc.L.Lock() 179 | defer t.sc.L.Unlock() 180 | for t.status != s { 181 | t.sc.Wait() 182 | } 183 | } 184 | 185 | func (t *Ws) setRunning(v bool) { 186 | t.printLog(GQL_INTERNAL, "SET ISRUNNING", v) 187 | t.running = v 188 | if v == false { 189 | t.setStatus(StatusDisconnected) 190 | } else { 191 | t.sc.Broadcast() 192 | } 193 | } 194 | 195 | func (t *Ws) setStatus(s Status) { 196 | if t.status == s { 197 | return 198 | } 199 | 200 | t.printLog(GQL_INTERNAL, "SET STATUS", s) 201 | t.status = s 202 | t.sc.Broadcast() 203 | } 204 | 205 | func (t *Ws) Start(ctx context.Context) <-chan error { 206 | t.init() 207 | 208 | if t.running { 209 | panic("transport is already running") 210 | } 211 | 212 | t.errCh = make(chan error) 213 | 214 | go t.run(ctx) 215 | 216 | return t.errCh 217 | } 218 | 219 | func (t *Ws) readJson(v interface{}) error { 220 | return t.conn.ReadJSON(v) 221 | } 222 | 223 | func (t *Ws) writeJson(v interface{}) error { 224 | return t.conn.WriteJSON(v) 225 | } 226 | 227 | func (t *Ws) run(inctx context.Context) { 228 | defer func() { 229 | t.setRunning(false) 230 | close(t.errCh) 231 | }() 232 | 233 | t.setRunning(true) 234 | 235 | ctx := inctx 236 | for { 237 | //t.printLog(GQL_INTERNAL, "STATUS", t.status) 238 | 239 | select { 240 | case <-inctx.Done(): 241 | err := inctx.Err() 242 | t.printLog(GQL_INTERNAL, "CTX DONE", err) 243 | t.sendErr(err) 244 | return 245 | default: 246 | // continue... 247 | } 248 | 249 | if t.status == StatusDisconnected { 250 | t.printLog(GQL_INTERNAL, "CANCEL PREV CTX") 251 | 252 | if t.cancel != nil { 253 | t.cancel() 254 | } 255 | ctx, t.cancel = context.WithCancel(inctx) 256 | 257 | t.printLog(GQL_INTERNAL, "CONNECTING") 258 | conn, err := t.WebsocketConnProvider(ctx, t.URL) 259 | if err != nil { 260 | t.printLog(GQL_INTERNAL, "WebsocketConnProvider ERR", err) 261 | t.ResetWithErr(err) 262 | time.Sleep(time.Second) 263 | continue 264 | } 265 | t.printLog(GQL_INTERNAL, "HAS CONN") 266 | t.conn = conn 267 | t.setStatus(StatusConnected) 268 | 269 | err = t.sendConnectionInit() 270 | if err != nil { 271 | t.printLog(GQL_INTERNAL, "sendConnectionInit ERR", err) 272 | t.ResetWithErr(err) 273 | time.Sleep(time.Second) 274 | continue 275 | } 276 | 277 | t.printLog(GQL_INTERNAL, "CONNECTED") 278 | } 279 | 280 | var message OperationMessage 281 | if err := t.readJson(&message); err != nil { 282 | // Is expected as part of conn.ReadJSON timeout, we have not received a message or 283 | // a KA, the connection is probably dead... RIP 284 | if errors.Is(err, context.DeadlineExceeded) { 285 | t.printLog(GQL_INTERNAL, "READ DEADLINE EXCEEDED") 286 | t.ResetWithErr(err) 287 | continue 288 | } 289 | 290 | if errors.Is(err, io.EOF) || strings.Contains(err.Error(), "EOF") { 291 | t.printLog(GQL_INTERNAL, "EOF") 292 | t.ResetWithErr(err) 293 | continue 294 | } 295 | 296 | closeStatus := websocket.CloseStatus(err) 297 | if closeStatus == websocket.StatusNormalClosure { 298 | t.printLog(GQL_INTERNAL, "NORMAL CLOSURE") 299 | // close event from websocket client, exiting... 300 | return 301 | } 302 | 303 | t.printLog(GQL_INTERNAL, "READ JSON ERR", err) 304 | t.ResetWithErr(err) 305 | continue 306 | } 307 | 308 | switch message.Type { 309 | case GQL_CONNECTION_ACK: 310 | t.printLog(GQL_CONNECTION_ACK, message) 311 | t.setStatus(StatusReady) 312 | 313 | t.opsm.Lock() 314 | for id, op := range t.ops { 315 | if err := t.startOp(id, op); err != nil { 316 | t.printLog(GQL_INTERNAL, "ACK: START OP FAILED") 317 | _ = t.cancelOp(id) 318 | t.sendErr(err) 319 | } 320 | } 321 | t.opsm.Unlock() 322 | case GQL_CONNECTION_KEEP_ALIVE: 323 | t.printLog(GQL_CONNECTION_KEEP_ALIVE, message) 324 | case GQL_CONNECTION_ERROR: 325 | t.printLog(GQL_CONNECTION_ERROR, message) 326 | t.setStatus(StatusDisconnected) 327 | t.ResetWithErr(fmt.Errorf("gql conn error: %v", message)) 328 | case GQL_COMPLETE: 329 | t.printLog(GQL_COMPLETE, message) 330 | _ = t.cancelOp(message.ID) 331 | case GQL_ERROR: 332 | t.printLog(GQL_ERROR, message) 333 | fallthrough 334 | case GQL_DATA: 335 | t.printLog(GQL_DATA, message) 336 | 337 | id := message.ID 338 | t.opsm.Lock() 339 | op, ok := t.ops[id] 340 | if !ok { 341 | continue 342 | } 343 | t.opsm.Unlock() 344 | 345 | var out OperationResponse 346 | err := json.Unmarshal(message.Payload, &out) 347 | if err != nil { 348 | out.Errors = append(out.Errors, gqlerror.WrapPath(nil, err)) 349 | } 350 | op.Send(out) 351 | default: 352 | t.printLog(GQL_UNKNOWN, message) 353 | } 354 | } 355 | } 356 | 357 | func (t *Ws) sendConnectionInit() error { 358 | var bParams []byte = nil 359 | if t.ConnectionParams != nil { 360 | var err error 361 | bParams, err = json.Marshal(t.ConnectionParams) 362 | if err != nil { 363 | return err 364 | } 365 | } 366 | 367 | msg := OperationMessage{ 368 | Type: GQL_CONNECTION_INIT, 369 | Payload: bParams, 370 | } 371 | 372 | t.printLog(GQL_CONNECTION_INIT, msg) 373 | return t.writeJson(msg) 374 | } 375 | 376 | func (t *Ws) Reset() { 377 | t.init() 378 | 379 | t.ResetWithErr(nil) 380 | } 381 | 382 | func (t *Ws) ResetWithErr(err error) { 383 | t.init() 384 | 385 | t.rm.Lock() 386 | defer t.rm.Unlock() 387 | 388 | if t.status == StatusDisconnected { 389 | return 390 | } 391 | 392 | t.setStatus(StatusDisconnected) 393 | 394 | t.printLog(GQL_INTERNAL, "RESET", err) 395 | 396 | if err != nil { 397 | t.sendErr(err) 398 | } 399 | 400 | for id, op := range t.ops { 401 | if op.started { 402 | _ = t.stopOp(id) 403 | op.started = false 404 | } 405 | } 406 | 407 | _ = t.closeConn() 408 | 409 | atomic.StoreUint64(&t.i, 0) 410 | } 411 | 412 | func (t *Ws) terminate() error { 413 | msg := OperationMessage{ 414 | Type: GQL_CONNECTION_TERMINATE, 415 | } 416 | 417 | t.printLog(GQL_CONNECTION_TERMINATE, msg) 418 | return t.writeJson(msg) 419 | } 420 | 421 | func (t *Ws) closeConn() error { 422 | _ = t.terminate() 423 | err := t.conn.Close() 424 | t.conn = &closedWs{} 425 | t.cancel() 426 | 427 | t.printLog(GQL_INTERNAL, "DONE CLOSE CONN", err) 428 | 429 | return err 430 | } 431 | 432 | func (t *Ws) Close() error { 433 | t.init() 434 | 435 | t.printLog(GQL_INTERNAL, "CLOSE") 436 | 437 | for id := range t.ops { 438 | _ = t.cancelOp(id) 439 | } 440 | 441 | return t.closeConn() 442 | } 443 | 444 | func (t *Ws) Request(req Request) Response { 445 | t.init() 446 | 447 | t.printLog(GQL_INTERNAL, "REQ") 448 | 449 | id := fmt.Sprintf("%v", atomic.AddUint64(&t.i, 1)) 450 | 451 | res := &wsResponse{ 452 | Context: req.Context, 453 | OperationRequest: NewOperationRequestFromRequest(req), 454 | ChanResponse: NewChanResponse( 455 | func() error { 456 | t.printLog(GQL_INTERNAL, "CLOSE RES") 457 | return t.cancelOp(id) 458 | }, 459 | ), 460 | } 461 | 462 | t.printLog(GQL_INTERNAL, "ADD TO OPS") 463 | t.opsm.Lock() 464 | t.ops[id] = res 465 | t.opsm.Unlock() 466 | 467 | if t.status == StatusReady { 468 | err := t.startOp(id, res) 469 | if err != nil { 470 | return NewErrorResponse(err) 471 | } 472 | } 473 | 474 | return res 475 | } 476 | 477 | func (t *Ws) printLog(typ OperationMessageType, rest ...interface{}) { 478 | if t.log { 479 | fmt.Printf("# %-20v: ", typ) 480 | fmt.Println(rest...) 481 | } 482 | } 483 | 484 | func (t *Ws) registerOp(id string, op *wsResponse) { 485 | t.opsm.Lock() 486 | defer t.opsm.Unlock() 487 | 488 | t.ops[id] = op 489 | } 490 | 491 | func (t *Ws) startOp(id string, op *wsResponse) error { 492 | if op.started { 493 | return nil 494 | } 495 | 496 | t.printLog(GQL_INTERNAL, "START OP") 497 | 498 | payload, err := json.Marshal(op.OperationRequest) 499 | if err != nil { 500 | return err 501 | } 502 | 503 | msg := OperationMessage{ 504 | ID: id, 505 | Type: GQL_START, 506 | Payload: payload, 507 | } 508 | 509 | t.printLog(GQL_START, msg) 510 | if err := t.writeJson(msg); err != nil { 511 | t.printLog(GQL_INTERNAL, "GQL_START ERR", err) 512 | return err 513 | } 514 | 515 | op.started = true 516 | 517 | return nil 518 | } 519 | 520 | func (t *Ws) stopOp(id string) error { 521 | t.printLog(GQL_INTERNAL, "STOP OP", id) 522 | 523 | msg := OperationMessage{ 524 | ID: id, 525 | Type: GQL_STOP, 526 | } 527 | 528 | t.printLog(GQL_STOP, msg) 529 | return t.writeJson(msg) 530 | } 531 | 532 | func (t *Ws) cancelOp(id string) error { 533 | t.printLog(GQL_INTERNAL, "CANCEL OP", id) 534 | 535 | t.opsm.Lock() 536 | op, ok := t.ops[id] 537 | if !ok { 538 | t.opsm.Unlock() 539 | return nil 540 | } 541 | delete(t.ops, id) 542 | t.opsm.Unlock() 543 | 544 | op.CloseCh() 545 | 546 | return t.stopOp(id) 547 | } 548 | 549 | func (t *Ws) GetConn() WebsocketConn { 550 | return t.conn 551 | } 552 | -------------------------------------------------------------------------------- /client/transport/upload.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | func NewUpload(f *os.File) Upload { 10 | return Upload{ 11 | File: f, 12 | Name: f.Name(), 13 | } 14 | } 15 | 16 | type Upload struct { 17 | Name string 18 | File io.Reader 19 | } 20 | 21 | func (u Upload) MarshalJSON() ([]byte, error) { 22 | return []byte(`null`), nil // Should be marshaled to null, will be taken care of in multipart form 23 | } 24 | 25 | func (u *Upload) UnmarshalJSON(data []byte) error { 26 | return fmt.Errorf("type Upload should not be unmarshaled") 27 | } 28 | -------------------------------------------------------------------------------- /client/transport/wshandler.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | // Original work from https://github.com/hasura/go-graphql-client/blob/0806e5ec7/subscription.go 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "nhooyr.io/websocket" 9 | "nhooyr.io/websocket/wsjson" 10 | "time" 11 | ) 12 | 13 | type closedWs struct{} 14 | 15 | var ErrClosedConnection = fmt.Errorf("closed connection") 16 | 17 | func (c closedWs) ReadJSON(v interface{}) error { 18 | return ErrClosedConnection 19 | } 20 | 21 | func (c closedWs) WriteJSON(v interface{}) error { 22 | return ErrClosedConnection 23 | } 24 | 25 | func (c closedWs) Close() error { 26 | return nil 27 | } 28 | 29 | func (c closedWs) SetReadLimit(limit int64) { 30 | 31 | } 32 | 33 | var _ WebsocketConn = (*closedWs)(nil) 34 | 35 | // WebsocketHandler is default websocket handler implementation using https://github.com/nhooyr/websocket 36 | type WebsocketHandler struct { 37 | ctx context.Context 38 | timeout time.Duration 39 | *websocket.Conn 40 | } 41 | 42 | func (wh *WebsocketHandler) WriteJSON(v interface{}) error { 43 | ctx, cancel := context.WithTimeout(wh.ctx, wh.timeout) 44 | defer cancel() 45 | 46 | return wsjson.Write(ctx, wh.Conn, v) 47 | } 48 | 49 | func (wh *WebsocketHandler) ReadJSON(v interface{}) error { 50 | if wh.timeout > 0 { 51 | ctx, cancel := context.WithTimeout(wh.ctx, wh.timeout) 52 | defer cancel() 53 | 54 | return wsjson.Read(ctx, wh.Conn, v) 55 | } 56 | 57 | return wsjson.Read(wh.ctx, wh.Conn, v) 58 | } 59 | 60 | func (wh *WebsocketHandler) Close() error { 61 | return wh.Conn.Close(websocket.StatusNormalClosure, "close websocket") 62 | } 63 | 64 | type WsDialOption func(o *websocket.DialOptions) 65 | 66 | // DefaultWebsocketConnProvider is the connections factory 67 | // A timeout of 0 means no timeout, reading will block until a message is received or the context is canceled 68 | // If your server supports the keepalive, set the timeout to something greater than the server keepalive 69 | // (for example 15s for a 10s keepalive) 70 | func DefaultWebsocketConnProvider(timeout time.Duration, optionfs ...WsDialOption) WebsocketConnProvider { 71 | return func(ctx context.Context, URL string) (WebsocketConn, error) { 72 | options := &websocket.DialOptions{ 73 | Subprotocols: []string{"graphql-ws"}, 74 | } 75 | for _, f := range optionfs { 76 | f(options) 77 | } 78 | 79 | c, _, err := websocket.Dial(ctx, URL, options) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | return &WebsocketHandler{ 85 | ctx: ctx, 86 | Conn: c, 87 | timeout: timeout, 88 | }, nil 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /client/transport_mock_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/infiotinc/gqlgenc/client/transport" 7 | "github.com/stretchr/testify/assert" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | func TestSingleResponse(t *testing.T) { 13 | _ = os.Setenv("GQLGENC_WS_LOG", "1") 14 | 15 | cli := &Client{ 16 | Transport: transport.Mock{ 17 | "query": func(req transport.Request) transport.Response { 18 | return transport.NewSingleResponse(transport.NewMockOperationResponse("hey", nil)) 19 | }, 20 | }, 21 | } 22 | 23 | var res string 24 | _, err := cli.Query(context.Background(), "", "query", nil, &res) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | assert.Equal(t, "hey", res) 30 | } 31 | 32 | func TestChanResponse(t *testing.T) { 33 | _ = os.Setenv("GQLGENC_WS_LOG", "1") 34 | 35 | cli := &Client{ 36 | Transport: transport.Mock{ 37 | "query": func(req transport.Request) transport.Response { 38 | res := transport.NewChanResponse(nil) 39 | 40 | go func() { 41 | for i := 0; i < 3; i++ { 42 | res.Send(transport.NewMockOperationResponse(fmt.Sprintf("hey%v", i), nil)) 43 | } 44 | res.CloseCh() 45 | }() 46 | 47 | return res 48 | }, 49 | }, 50 | } 51 | 52 | sub := cli.Subscription(context.Background(), "", "query", nil) 53 | defer sub.Close() 54 | 55 | msgs := make([]string, 0) 56 | 57 | for sub.Next() { 58 | res := sub.Get() 59 | 60 | var data string 61 | err := res.UnmarshalData(&data) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | msgs = append(msgs, data) 67 | } 68 | 69 | if err := sub.Err(); err != nil { 70 | t.Fatal(err) 71 | } 72 | 73 | assert.Len(t, msgs, 3) 74 | } 75 | -------------------------------------------------------------------------------- /clientgen/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yamashou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /clientgen/client.go: -------------------------------------------------------------------------------- 1 | package clientgen 2 | 3 | import ( 4 | "fmt" 5 | gqlgenCfg "github.com/99designs/gqlgen/codegen/config" 6 | "github.com/99designs/gqlgen/plugin" 7 | "github.com/infiotinc/gqlgenc/config" 8 | ) 9 | 10 | var _ plugin.ConfigMutator = &Plugin{} 11 | 12 | type Plugin struct { 13 | queryFilePaths []string 14 | Client gqlgenCfg.PackageConfig 15 | GenerateConfig *config.GenerateConfig 16 | Cfg *config.Config 17 | } 18 | 19 | func New(cfg *config.Config) *Plugin { 20 | return &Plugin{ 21 | queryFilePaths: cfg.Query, 22 | Client: cfg.Client.PackageConfig, 23 | GenerateConfig: cfg.Generate, 24 | Cfg: cfg, 25 | } 26 | } 27 | 28 | func (p *Plugin) Name() string { 29 | return "clientgen" 30 | } 31 | 32 | func (p *Plugin) MutateConfig(cfg *gqlgenCfg.Config) error { 33 | querySources, err := LoadQuerySources(p.queryFilePaths) 34 | if err != nil { 35 | return fmt.Errorf("load query sources failed: %w", err) 36 | } 37 | 38 | // 1. 全体のqueryDocumentを1度にparse 39 | // 1. Parse document from source of query 40 | queryDocument, err := ParseQueryDocuments(cfg.Schema, querySources) 41 | if err != nil { 42 | return fmt.Errorf(": %w", err) 43 | } 44 | 45 | // 2. OperationごとのqueryDocumentを作成 46 | // 2. Separate documents for each operation 47 | queryDocuments, err := QueryDocumentsByOperations(cfg.Schema, queryDocument.Operations) 48 | if err != nil { 49 | return fmt.Errorf("parse query document failed: %w", err) 50 | } 51 | 52 | // 3. テンプレートと情報ソースを元にコード生成 53 | // 3. Generate code from template and document source 54 | sourceGenerator := NewSourceGenerator(cfg, p.Cfg, p.Client) 55 | source := NewSource(cfg.Schema, queryDocument, sourceGenerator, p.GenerateConfig) 56 | 57 | err = source.ExtraTypes() 58 | if err != nil { 59 | return fmt.Errorf("generating extra types failed: %w", err) 60 | } 61 | 62 | err = source.Fragments() 63 | if err != nil { 64 | return fmt.Errorf("generating fragment failed: %w", err) 65 | } 66 | 67 | operationResponses, err := source.OperationResponses() 68 | if err != nil { 69 | return fmt.Errorf("generating operation response failed: %w", err) 70 | } 71 | 72 | operations := source.Operations(queryDocuments, operationResponses) 73 | 74 | genTypes := sourceGenerator.GenTypes() 75 | ptrTypes := sourceGenerator.PtrTypes() 76 | 77 | generateClient := p.GenerateConfig.ShouldGenerateClient() 78 | if err := RenderTemplate(cfg, genTypes, ptrTypes, operations, generateClient, p.Client); err != nil { 79 | return fmt.Errorf("template failed: %w", err) 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /clientgen/modelgen.go: -------------------------------------------------------------------------------- 1 | package clientgen 2 | 3 | import ( 4 | "fmt" 5 | "github.com/99designs/gqlgen/codegen/templates" 6 | "github.com/vektah/gqlparser/v2/ast" 7 | "go/constant" 8 | "go/types" 9 | ) 10 | 11 | func (r *SourceGenerator) genFromDefinition(def *ast.Definition) types.Type { 12 | switch def.Kind { 13 | case ast.InputObject: 14 | if r.ccfg.Client.InputAsMap || r.ccfg.Models[def.Name].AsMap { 15 | genType := r.GetGenType(NewFieldPath(def.Kind, def.Name).Name()) 16 | 17 | for _, field := range def.Fields { 18 | fieldDef := r.cfg.Schema.Types[field.Type.Name()] 19 | 20 | typ := r.namedType(NewFieldPath(fieldDef.Kind, fieldDef.Name), func() types.Type { 21 | return r.genFromDefinition(fieldDef) 22 | }) 23 | 24 | typ = r.binder.CopyModifiersFromAst(field.Type, typ) 25 | 26 | f := MapField{ 27 | Name: field.Name, 28 | Type: typ, 29 | } 30 | 31 | if field.Type.NonNull { 32 | genType.MapReq = append(genType.MapReq, f) 33 | } else { 34 | r.collectPtrTypes(typ, false) 35 | 36 | genType.MapOpt = append(genType.MapOpt, f) 37 | } 38 | } 39 | 40 | return types.NewMap( 41 | types.Typ[types.String], 42 | types.NewInterfaceType(nil, nil), 43 | ) 44 | } 45 | 46 | fallthrough // Not input as map, treat as object 47 | case ast.Object: 48 | vars := make([]*types.Var, 0, len(def.Fields)) 49 | tags := make([]string, 0, len(def.Fields)) 50 | 51 | for _, field := range def.Fields { 52 | fieldDef := r.cfg.Schema.Types[field.Type.Name()] 53 | 54 | typ := r.namedType(NewFieldPath(fieldDef.Kind, fieldDef.Name), func() types.Type { 55 | return r.genFromDefinition(fieldDef) 56 | }) 57 | 58 | typ = r.binder.CopyModifiersFromAst(field.Type, typ) 59 | 60 | if isStruct(typ) && (fieldDef.Kind == ast.Object || fieldDef.Kind == ast.InputObject) { 61 | typ = types.NewPointer(typ) 62 | } 63 | 64 | name := field.Name 65 | if nameOveride := r.cfg.Models[def.Name].Fields[field.Name].FieldName; nameOveride != "" { 66 | name = nameOveride 67 | } 68 | 69 | vars = append(vars, types.NewVar(0, nil, templates.ToGo(name), typ)) 70 | tags = append(tags, `json:"`+name+`"`) 71 | } 72 | 73 | return types.NewStruct(vars, tags) 74 | 75 | case ast.Enum: 76 | genType := r.GetGenType(NewFieldPath(def.Kind, def.Name).Name()) 77 | 78 | consts := make([]*types.Const, 0) 79 | for _, v := range def.EnumValues { 80 | consts = append(consts, types.NewConst( 81 | 0, 82 | r.client.Pkg(), 83 | fmt.Sprintf("%v%v", templates.ToGo(def.Name), templates.ToGo(v.Name)), 84 | genType.RefType, 85 | constant.MakeString(v.Name), 86 | )) 87 | } 88 | 89 | genType.Consts = consts 90 | 91 | return types.Typ[types.String] 92 | case ast.Scalar: 93 | panic("scalars must be predeclared: " + def.Name) 94 | } 95 | 96 | panic("cannot generate type for def: " + def.Name) 97 | } 98 | 99 | func isStruct(t types.Type) bool { 100 | _, is := t.Underlying().(*types.Struct) 101 | return is 102 | } 103 | -------------------------------------------------------------------------------- /clientgen/query.go: -------------------------------------------------------------------------------- 1 | package clientgen 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/vektah/gqlparser/v2/ast" 7 | "github.com/vektah/gqlparser/v2/parser" 8 | "github.com/vektah/gqlparser/v2/validator" 9 | ) 10 | 11 | func ParseQueryDocuments(schema *ast.Schema, querySources []*ast.Source) (*ast.QueryDocument, error) { 12 | var queryDocument ast.QueryDocument 13 | for _, querySource := range querySources { 14 | query, gqlerr := parser.ParseQuery(querySource) 15 | if gqlerr != nil { 16 | return nil, fmt.Errorf(": %w", gqlerr) 17 | } 18 | 19 | mergeQueryDocument(&queryDocument, query) 20 | } 21 | 22 | if errs := validator.Validate(schema, &queryDocument); errs != nil { 23 | return nil, fmt.Errorf(": %w", errs) 24 | } 25 | 26 | return &queryDocument, nil 27 | } 28 | 29 | func mergeQueryDocument(q, other *ast.QueryDocument) { 30 | q.Operations = append(q.Operations, other.Operations...) 31 | q.Fragments = append(q.Fragments, other.Fragments...) 32 | } 33 | 34 | func QueryDocumentsByOperations(schema *ast.Schema, operations ast.OperationList) ([]*ast.QueryDocument, error) { 35 | queryDocuments := make([]*ast.QueryDocument, 0, len(operations)) 36 | for _, operation := range operations { 37 | fragments := fragmentsInOperationDefinition(operation) 38 | 39 | queryDocument := &ast.QueryDocument{ 40 | Operations: ast.OperationList{operation}, 41 | Fragments: fragments, 42 | Position: nil, 43 | } 44 | 45 | if errs := validator.Validate(schema, queryDocument); errs != nil { 46 | return nil, fmt.Errorf(": %w", errs) 47 | } 48 | 49 | queryDocuments = append(queryDocuments, queryDocument) 50 | } 51 | 52 | return queryDocuments, nil 53 | } 54 | 55 | func fragmentsInOperationDefinition(operation *ast.OperationDefinition) ast.FragmentDefinitionList { 56 | fragments := fragmentsInOperationWalker(operation.SelectionSet) 57 | uniqueFragments := fragmentsUnique(fragments) 58 | 59 | return uniqueFragments 60 | } 61 | 62 | func fragmentsUnique(fragments ast.FragmentDefinitionList) ast.FragmentDefinitionList { 63 | uniqueMap := make(map[string]*ast.FragmentDefinition) 64 | for _, fragment := range fragments { 65 | uniqueMap[fragment.Name] = fragment 66 | } 67 | 68 | uniqueFragments := make(ast.FragmentDefinitionList, 0, len(uniqueMap)) 69 | for _, fragment := range uniqueMap { 70 | uniqueFragments = append(uniqueFragments, fragment) 71 | } 72 | 73 | return uniqueFragments 74 | } 75 | 76 | func fragmentsInOperationWalker(selectionSet ast.SelectionSet) ast.FragmentDefinitionList { 77 | var fragments ast.FragmentDefinitionList 78 | for _, selection := range selectionSet { 79 | var selectionSet ast.SelectionSet 80 | switch selection := selection.(type) { 81 | case *ast.Field: 82 | selectionSet = selection.SelectionSet 83 | case *ast.InlineFragment: 84 | selectionSet = selection.SelectionSet 85 | case *ast.FragmentSpread: 86 | fragments = append(fragments, selection.Definition) 87 | selectionSet = selection.Definition.SelectionSet 88 | } 89 | 90 | fragments = append(fragments, fragmentsInOperationWalker(selectionSet)...) 91 | } 92 | 93 | return fragments 94 | } 95 | -------------------------------------------------------------------------------- /clientgen/query_source.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020 gqlgen authors 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | */ 22 | 23 | package clientgen 24 | 25 | import ( 26 | "fmt" 27 | "io/ioutil" 28 | "os" 29 | "path/filepath" 30 | "regexp" 31 | "strings" 32 | 33 | "github.com/99designs/gqlgen/codegen/config" 34 | "github.com/vektah/gqlparser/v2/ast" 35 | ) 36 | 37 | var path2regex = strings.NewReplacer( 38 | `.`, `\.`, 39 | `*`, `.+`, 40 | `\`, `[\\/]`, 41 | `/`, `[\\/]`, 42 | ) 43 | 44 | // LoadQuerySourceなどは、gqlgenがLoadConfigでSchemaを読み込む時の実装をコピーして一部修正している 45 | // **/test/*.graphqlなどに対応している 46 | func LoadQuerySources(queryFileNames []string) ([]*ast.Source, error) { 47 | var noGlobQueryFileNames config.StringList 48 | 49 | var err error 50 | preGlobbing := queryFileNames 51 | for _, f := range preGlobbing { 52 | var matches []string 53 | 54 | // for ** we want to override default globbing patterns and walk all 55 | // subdirectories to match schema files. 56 | if strings.Contains(f, "**") { 57 | pathParts := strings.SplitN(f, "**", 2) 58 | rest := strings.TrimPrefix(strings.TrimPrefix(pathParts[1], `\`), `/`) 59 | // turn the rest of the glob into a regex, anchored only at the end because ** allows 60 | // for any number of dirs in between and walk will let us match against the full path name 61 | globRe := regexp.MustCompile(path2regex.Replace(rest) + `$`) 62 | 63 | if err := filepath.Walk(pathParts[0], func(path string, info os.FileInfo, err error) error { 64 | if err != nil { 65 | return err 66 | } 67 | 68 | if globRe.MatchString(strings.TrimPrefix(path, pathParts[0])) { 69 | matches = append(matches, path) 70 | } 71 | 72 | return nil 73 | }); err != nil { 74 | return nil, fmt.Errorf("failed to walk schema at root %s: %w", pathParts[0], err) 75 | } 76 | } else { 77 | matches, err = filepath.Glob(f) 78 | if err != nil { 79 | return nil, fmt.Errorf("failed to glob schema filename %v: %w", f, err) 80 | } 81 | } 82 | 83 | for _, m := range matches { 84 | if noGlobQueryFileNames.Has(m) { 85 | continue 86 | } 87 | 88 | noGlobQueryFileNames = append(noGlobQueryFileNames, m) 89 | } 90 | } 91 | 92 | querySources := make([]*ast.Source, 0, len(noGlobQueryFileNames)) 93 | for _, filename := range noGlobQueryFileNames { 94 | filename = filepath.ToSlash(filename) 95 | var err error 96 | var schemaRaw []byte 97 | schemaRaw, err = ioutil.ReadFile(filename) 98 | if err != nil { 99 | return nil, fmt.Errorf("unable to open schema: %w", err) 100 | } 101 | 102 | querySources = append(querySources, &ast.Source{Name: filename, Input: string(schemaRaw)}) 103 | } 104 | 105 | return querySources, nil 106 | } 107 | -------------------------------------------------------------------------------- /clientgen/source.go: -------------------------------------------------------------------------------- 1 | package clientgen 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/infiotinc/gqlgenc/config" 7 | "github.com/vektah/gqlparser/v2/ast" 8 | "github.com/vektah/gqlparser/v2/formatter" 9 | "go/types" 10 | ) 11 | 12 | type Source struct { 13 | schema *ast.Schema 14 | queryDocument *ast.QueryDocument 15 | sourceGenerator *SourceGenerator 16 | generateConfig *config.GenerateConfig 17 | } 18 | 19 | func NewSource(schema *ast.Schema, queryDocument *ast.QueryDocument, sourceGenerator *SourceGenerator, generateConfig *config.GenerateConfig) *Source { 20 | return &Source{ 21 | schema: schema, 22 | queryDocument: queryDocument, 23 | sourceGenerator: sourceGenerator, 24 | generateConfig: generateConfig, 25 | } 26 | } 27 | 28 | type TypeTarget struct { 29 | Type types.Type 30 | Name string 31 | } 32 | 33 | type MapField struct { 34 | Name string 35 | Type types.Type 36 | } 37 | 38 | type Type struct { 39 | Name string 40 | Path FieldPath 41 | Type types.Type 42 | UnmarshalTypes map[string]TypeTarget 43 | RefType *types.Named 44 | Consts []*types.Const 45 | 46 | MapReq []MapField 47 | MapOpt []MapField 48 | } 49 | 50 | func (t Type) IsInputMap() bool { 51 | return len(t.MapReq) > 0 || len(t.MapOpt) > 0 52 | } 53 | 54 | func (s *Source) Fragments() error { 55 | for _, fragment := range s.queryDocument.Fragments { 56 | path := NewFieldPath(fragment.Definition.Kind, fragment.Name) 57 | _ = s.sourceGenerator.namedType(path, func() types.Type { 58 | responseFields := s.sourceGenerator.NewResponseFields(path, &fragment.SelectionSet) 59 | 60 | typ := s.sourceGenerator.genFromResponseFields(path, responseFields) 61 | return typ 62 | }) 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (s *Source) ExtraTypes() error { 69 | for _, t := range s.sourceGenerator.ccfg.Client.ExtraTypes { 70 | def := s.sourceGenerator.cfg.Schema.Types[t] 71 | 72 | if def == nil { 73 | panic("type " + t + " does not exist in schema") 74 | } 75 | 76 | _ = s.sourceGenerator.namedType(NewFieldPath(def.Kind, def.Name), func() types.Type { 77 | return s.sourceGenerator.genFromDefinition(def) 78 | }) 79 | } 80 | 81 | return nil 82 | } 83 | 84 | type Operation struct { 85 | Name string 86 | ResponseType types.Type 87 | Operation string 88 | OperationType string 89 | Args []*Argument 90 | VariableDefinitions ast.VariableDefinitionList 91 | } 92 | 93 | func NewOperation(operation *OperationResponse, queryDocument *ast.QueryDocument, args []*Argument) *Operation { 94 | return &Operation{ 95 | Name: operation.Name, 96 | OperationType: string(operation.Operation.Operation), 97 | ResponseType: operation.Type, 98 | Operation: queryString(queryDocument), 99 | Args: args, 100 | VariableDefinitions: operation.Operation.VariableDefinitions, 101 | } 102 | } 103 | 104 | func (s *Source) Operations(queryDocuments []*ast.QueryDocument, operationResponses []*OperationResponse) []*Operation { 105 | operations := make([]*Operation, 0, len(s.queryDocument.Operations)) 106 | 107 | queryDocumentsMap := queryDocumentMapByOperationName(queryDocuments) 108 | operationArgsMap := s.operationArgsMapByOperationName() 109 | for _, operation := range operationResponses { 110 | queryDocument := queryDocumentsMap[operation.Name] 111 | args := operationArgsMap[operation.Name] 112 | operations = append(operations, NewOperation(operation, queryDocument, args)) 113 | } 114 | 115 | return operations 116 | } 117 | 118 | func (s *Source) operationArgsMapByOperationName() map[string][]*Argument { 119 | operationArgsMap := make(map[string][]*Argument) 120 | for _, operation := range s.queryDocument.Operations { 121 | operationArgsMap[operation.Name] = s.sourceGenerator.OperationArguments(operation.VariableDefinitions) 122 | } 123 | 124 | return operationArgsMap 125 | } 126 | 127 | func queryDocumentMapByOperationName(queryDocuments []*ast.QueryDocument) map[string]*ast.QueryDocument { 128 | queryDocumentMap := make(map[string]*ast.QueryDocument) 129 | for _, queryDocument := range queryDocuments { 130 | operation := queryDocument.Operations[0] 131 | queryDocumentMap[operation.Name] = queryDocument 132 | } 133 | 134 | return queryDocumentMap 135 | } 136 | 137 | func queryString(queryDocument *ast.QueryDocument) string { 138 | var buf bytes.Buffer 139 | astFormatter := formatter.NewFormatter(&buf) 140 | astFormatter.FormatQueryDocument(queryDocument) 141 | 142 | return buf.String() 143 | } 144 | 145 | type OperationResponse struct { 146 | Operation *ast.OperationDefinition 147 | Name string 148 | Type types.Type 149 | } 150 | 151 | const OperationKind ast.DefinitionKind = "OPERATION" 152 | 153 | func (s *Source) OperationResponses() ([]*OperationResponse, error) { 154 | operationResponses := make([]*OperationResponse, 0, len(s.queryDocument.Operations)) 155 | for _, operationResponse := range s.queryDocument.Operations { 156 | name := getResponseStructName(operationResponse, s.generateConfig) 157 | 158 | path := NewFieldPath(OperationKind, name) 159 | namedType := s.sourceGenerator.namedType(path, func() types.Type { 160 | responseFields := s.sourceGenerator.NewResponseFields(path, &operationResponse.SelectionSet) 161 | 162 | typ := s.sourceGenerator.genFromResponseFields(path, responseFields) 163 | return typ 164 | }) 165 | 166 | operationResponses = append(operationResponses, &OperationResponse{ 167 | Operation: operationResponse, 168 | Name: name, 169 | Type: namedType, 170 | }) 171 | } 172 | 173 | return operationResponses, nil 174 | } 175 | 176 | func getResponseStructName(operation *ast.OperationDefinition, generateConfig *config.GenerateConfig) string { 177 | name := operation.Name 178 | if generateConfig != nil { 179 | if generateConfig.Prefix != nil { 180 | if operation.Operation == ast.Subscription { 181 | name = fmt.Sprintf("%s%s", generateConfig.Prefix.Subscription, name) 182 | } 183 | 184 | if operation.Operation == ast.Mutation { 185 | name = fmt.Sprintf("%s%s", generateConfig.Prefix.Mutation, name) 186 | } 187 | 188 | if operation.Operation == ast.Query { 189 | name = fmt.Sprintf("%s%s", generateConfig.Prefix.Query, name) 190 | } 191 | } 192 | 193 | if generateConfig.Suffix != nil { 194 | if operation.Operation == ast.Subscription { 195 | name = fmt.Sprintf("%s%s", name, generateConfig.Suffix.Subscription) 196 | } 197 | 198 | if operation.Operation == ast.Mutation { 199 | name = fmt.Sprintf("%s%s", name, generateConfig.Suffix.Mutation) 200 | } 201 | 202 | if operation.Operation == ast.Query { 203 | name = fmt.Sprintf("%s%s", name, generateConfig.Suffix.Query) 204 | } 205 | } 206 | } 207 | 208 | return name 209 | } 210 | -------------------------------------------------------------------------------- /clientgen/source_generator.go: -------------------------------------------------------------------------------- 1 | package clientgen 2 | 3 | import ( 4 | "fmt" 5 | "github.com/99designs/gqlgen/codegen/config" 6 | "github.com/99designs/gqlgen/codegen/templates" 7 | config2 "github.com/infiotinc/gqlgenc/config" 8 | "github.com/vektah/gqlparser/v2/ast" 9 | "go/types" 10 | "math" 11 | "sort" 12 | "strings" 13 | ) 14 | 15 | type FieldPath struct { 16 | Kind ast.DefinitionKind 17 | path []string 18 | } 19 | 20 | func NewFieldPath(kind ast.DefinitionKind, name string) FieldPath { 21 | return FieldPath{ 22 | Kind: kind, 23 | path: []string{name}, 24 | } 25 | } 26 | 27 | func (p FieldPath) With(n string) FieldPath { 28 | p.path = append(p.path, n) 29 | return p 30 | } 31 | 32 | func (p FieldPath) Name() string { 33 | pn := make([]string, 0, len(p.path)) 34 | for _, n := range p.path { 35 | pn = append(pn, templates.ToGo(n)) 36 | } 37 | 38 | return strings.Join(pn, "_") 39 | } 40 | 41 | func (p FieldPath) String() string { 42 | return strings.Join(p.path, ".") 43 | } 44 | 45 | type Argument struct { 46 | Variable string 47 | Type types.Type 48 | } 49 | 50 | type ResponseField struct { 51 | Name string 52 | IsFragmentSpread bool 53 | IsInlineFragment bool 54 | Type types.Type 55 | Tags []string 56 | ResponseFields ResponseFieldList 57 | } 58 | 59 | type ResponseFieldList []*ResponseField 60 | 61 | func (rs ResponseFieldList) IsFragment() bool { 62 | if len(rs) != 1 { 63 | return false 64 | } 65 | 66 | return rs[0].IsInlineFragment || rs[0].IsFragmentSpread 67 | } 68 | 69 | func (rs ResponseFieldList) IsBasicType() bool { 70 | return len(rs) == 0 71 | } 72 | 73 | func (rs ResponseFieldList) IsStructType() bool { 74 | return len(rs) > 0 && !rs.IsFragment() 75 | } 76 | 77 | type genType struct { 78 | name string 79 | typ *Type 80 | } 81 | 82 | type PtrType struct { 83 | Name string 84 | Type types.Type 85 | } 86 | 87 | type SourceGenerator struct { 88 | cfg *config.Config 89 | ccfg *config2.Config 90 | binder *config.Binder 91 | client config.PackageConfig 92 | 93 | genTypes []genType 94 | ptrTypes map[types.Type]PtrType 95 | } 96 | 97 | func (r *SourceGenerator) RegisterGenType(name string, typ *Type) { 98 | if gt := r.GetGenType(name); gt != nil { 99 | panic(name + ": gen type already defined") 100 | } 101 | 102 | if typ.RefType == nil { 103 | typ.RefType = types.NewNamed( 104 | types.NewTypeName(0, r.client.Pkg(), name, nil), 105 | nil, 106 | nil, 107 | ) 108 | } 109 | 110 | r.genTypes = append(r.genTypes, genType{ 111 | name: name, 112 | typ: typ, 113 | }) 114 | } 115 | 116 | func (r *SourceGenerator) GetGenType(name string) *Type { 117 | for _, gt := range r.genTypes { 118 | if gt.name == name { 119 | return gt.typ 120 | } 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func (r *SourceGenerator) GenTypes() []*Type { 127 | typs := make([]*Type, 0) 128 | for _, gt := range r.genTypes { 129 | typs = append(typs, gt.typ) 130 | } 131 | 132 | sort.SliceStable(typs, func(i, j int) bool { 133 | pi := typs[i].Path.path 134 | pj := typs[j].Path.path 135 | 136 | for n := 0; n < int(math.Min(float64(len(pi)), float64(len(pj)))); n++ { 137 | if pi[n] != pj[n] { 138 | return pi[n] < pj[n] 139 | } 140 | } 141 | 142 | return len(pi) < len(pj) 143 | }) 144 | 145 | return typs 146 | } 147 | 148 | func (r *SourceGenerator) PtrTypes() []PtrType { 149 | typs := make([]PtrType, 0) 150 | for _, t := range r.ptrTypes { 151 | typs = append(typs, t) 152 | } 153 | 154 | sort.SliceStable(typs, func(i, j int) bool { 155 | pi := typs[i].Name 156 | pj := typs[j].Name 157 | 158 | return pi < pj 159 | }) 160 | 161 | return typs 162 | } 163 | 164 | func NewSourceGenerator(cfg *config.Config, ccfg *config2.Config, client config.PackageConfig) *SourceGenerator { 165 | return &SourceGenerator{ 166 | cfg: cfg, 167 | ccfg: ccfg, 168 | binder: cfg.NewBinder(), 169 | client: client, 170 | } 171 | } 172 | 173 | func (r *SourceGenerator) addTypenameIfInlineFragment(selectionSet *ast.SelectionSet) { 174 | for _, s := range *selectionSet { 175 | switch s.(type) { 176 | case *ast.InlineFragment: 177 | for _, s := range *selectionSet { 178 | if field, ok := s.(*ast.Field); ok { 179 | if field.Alias == "__typename" { 180 | return // Already has it 181 | } 182 | } 183 | } 184 | 185 | *selectionSet = append(ast.SelectionSet{&ast.Field{ 186 | Name: "__typename", 187 | Alias: "__typename", 188 | Definition: &ast.FieldDefinition{ 189 | Name: "Typename", 190 | Type: ast.NonNullNamedType("String", nil), 191 | Arguments: ast.ArgumentDefinitionList{ 192 | {Name: "name", Type: ast.NonNullNamedType("String", nil)}, 193 | }, 194 | }, 195 | }}, *selectionSet...) 196 | return 197 | } 198 | } 199 | } 200 | 201 | func (r *SourceGenerator) NewResponseFields(path FieldPath, selectionSet *ast.SelectionSet) ResponseFieldList { 202 | r.addTypenameIfInlineFragment(selectionSet) 203 | 204 | responseFields := make(ResponseFieldList, 0, len(*selectionSet)) 205 | for _, selection := range *selectionSet { 206 | rf := r.NewResponseField(path, selection) 207 | responseFields = append(responseFields, rf) 208 | } 209 | 210 | return responseFields 211 | } 212 | 213 | func (r *SourceGenerator) GetNamedType(fullname string) types.Type { 214 | if gt := r.GetGenType(fullname); gt != nil { 215 | return gt.Type 216 | } 217 | 218 | if r.cfg.Models.Exists(fullname) && len(r.cfg.Models[fullname].Model) > 0 { 219 | model := r.cfg.Models[fullname].Model[0] 220 | 221 | typ, err := r.binder.FindTypeFromName(model) 222 | if err != nil { 223 | panic(fmt.Errorf("cannot get type for %v (%v): %w", fullname, model, err)) 224 | } 225 | 226 | if n, is := typ.(*types.Named); is { 227 | return n.Underlying() 228 | 229 | } 230 | 231 | return typ 232 | } 233 | 234 | return nil 235 | } 236 | 237 | func (r *SourceGenerator) namedType(path FieldPath, gen func() types.Type) types.Type { 238 | fullname := path.Name() 239 | 240 | if gt := r.GetGenType(fullname); gt != nil { 241 | return gt.RefType 242 | } 243 | 244 | if r.cfg.Models.Exists(fullname) && len(r.cfg.Models[fullname].Model) > 0 { 245 | model := r.cfg.Models[fullname].Model[0] 246 | fmt.Printf("%s is already declared: %v\n", fullname, model) 247 | 248 | typ, err := r.binder.FindTypeFromName(model) 249 | if err != nil { 250 | panic(fmt.Errorf("cannot get type for %v (%v): %w", fullname, model, err)) 251 | } 252 | 253 | return typ 254 | } else { 255 | genTyp := &Type{ 256 | Name: fullname, 257 | Path: path, 258 | } 259 | 260 | r.RegisterGenType(fullname, genTyp) 261 | 262 | genTyp.Type = gen() 263 | 264 | return genTyp.RefType 265 | } 266 | } 267 | 268 | func (r *SourceGenerator) genFromResponseFields(path FieldPath, fieldsResponseFields ResponseFieldList) types.Type { 269 | fullname := path.Name() 270 | 271 | vars := make([]*types.Var, 0, len(fieldsResponseFields)) 272 | tags := make([]string, 0, len(fieldsResponseFields)) 273 | unmarshalTypes := map[string]TypeTarget{} 274 | for _, field := range fieldsResponseFields { 275 | typ := field.Type 276 | fieldName := templates.ToGo(field.Name) 277 | if field.IsInlineFragment { 278 | unmarshalTypes[field.Name] = TypeTarget{ 279 | Type: typ, 280 | Name: fieldName, 281 | } 282 | typ = types.NewPointer(typ) 283 | } 284 | 285 | vars = append(vars, types.NewVar(0, nil, fieldName, typ)) 286 | tags = append(tags, strings.Join(field.Tags, " ")) 287 | } 288 | 289 | genType := r.GetGenType(fullname) 290 | genType.UnmarshalTypes = unmarshalTypes 291 | 292 | return types.NewStruct(vars, tags) 293 | } 294 | 295 | func (r *SourceGenerator) AstTypeToType(path FieldPath, fields ResponseFieldList, typ *ast.Type) types.Type { 296 | switch { 297 | case fields.IsBasicType(): 298 | def := r.cfg.Schema.Types[typ.Name()] 299 | 300 | return r.namedType(NewFieldPath(def.Kind, def.Name), func() types.Type { 301 | return r.genFromDefinition(def) 302 | }) 303 | case fields.IsFragment(): 304 | // if a child field is fragment, this field type became fragment. 305 | return fields[0].Type 306 | case fields.IsStructType(): 307 | return r.namedType(path, func() types.Type { 308 | return r.genFromResponseFields(path, fields) 309 | }) 310 | default: 311 | // ここにきたらバグ 312 | // here is bug 313 | panic("not match type") 314 | } 315 | } 316 | 317 | func (r *SourceGenerator) NewResponseField(path FieldPath, selection ast.Selection) *ResponseField { 318 | switch selection := selection.(type) { 319 | case *ast.Field: 320 | fieldPath := path.With(selection.Name) 321 | fieldsResponseFields := r.NewResponseFields(fieldPath, &selection.SelectionSet) 322 | baseType := r.AstTypeToType(fieldPath, fieldsResponseFields, selection.Definition.Type) 323 | 324 | // GraphQLの定義がオプショナルのはtypeのポインタ型が返り、配列の定義場合はポインタのスライスの型になって返ってきます 325 | // return pointer type then optional type or slice pointer then slice type of definition in GraphQL. 326 | typ := r.binder.CopyModifiersFromAst(selection.Definition.Type, baseType) 327 | 328 | return &ResponseField{ 329 | Name: selection.Alias, 330 | Type: typ, 331 | Tags: []string{ 332 | fmt.Sprintf(`json:"%s"`, selection.Alias), 333 | }, 334 | ResponseFields: fieldsResponseFields, 335 | } 336 | 337 | case *ast.FragmentSpread: 338 | fieldsResponseFields := r.NewResponseFields(path, &selection.Definition.SelectionSet) 339 | 340 | name := selection.Definition.Name 341 | typ := r.namedType(NewFieldPath(selection.ObjectDefinition.Kind, name), func() types.Type { 342 | panic(fmt.Sprintf("fragment %v must already be generated", name)) 343 | }) 344 | 345 | return &ResponseField{ 346 | Name: selection.Name, 347 | Type: typ, 348 | IsFragmentSpread: true, 349 | ResponseFields: fieldsResponseFields, 350 | } 351 | 352 | case *ast.InlineFragment: 353 | // InlineFragmentは子要素をそのままstructとしてもつので、ここで、構造体の型を作成します 354 | path := path.With(selection.TypeCondition) 355 | fieldsResponseFields := r.NewResponseFields(path, &selection.SelectionSet) 356 | typ := r.namedType(path, func() types.Type { 357 | return r.genFromResponseFields(path, fieldsResponseFields) 358 | }) 359 | return &ResponseField{ 360 | Name: selection.TypeCondition, 361 | Type: typ, 362 | IsInlineFragment: true, 363 | ResponseFields: fieldsResponseFields, 364 | Tags: []string{`json:"-"`}, 365 | } 366 | } 367 | 368 | panic("unexpected selection type") 369 | } 370 | 371 | func (r *SourceGenerator) collectPtrTypes(t types.Type, ptr bool) { 372 | if r.ptrTypes == nil { 373 | r.ptrTypes = map[types.Type]PtrType{} 374 | } 375 | 376 | if _, ok := r.ptrTypes[t]; ok { 377 | return 378 | } 379 | 380 | if p, is := t.(*types.Pointer); is { 381 | r.collectPtrTypes(p.Elem(), true) 382 | } else if n, is := t.(*types.Named); is { 383 | if ptr { 384 | r.ptrTypes[n] = PtrType{ 385 | Name: n.Obj().Name(), 386 | Type: n, 387 | } 388 | } 389 | 390 | t := r.GetNamedType(n.Obj().Name()) 391 | if t == nil { 392 | return 393 | } 394 | 395 | if s, is := t.(*types.Struct); is { 396 | for i := 0; i < s.NumFields(); i++ { 397 | r.collectPtrTypes(s.Field(i).Type(), false) 398 | } 399 | } 400 | } else if b, is := t.(*types.Basic); is { 401 | if ptr { 402 | r.ptrTypes[b] = PtrType{ 403 | Name: b.Name(), 404 | Type: b, 405 | } 406 | } 407 | } 408 | } 409 | 410 | func (r *SourceGenerator) OperationArguments(variableDefinitions ast.VariableDefinitionList) []*Argument { 411 | argumentTypes := make([]*Argument, 0, len(variableDefinitions)) 412 | for _, v := range variableDefinitions { 413 | baseType := r.namedType(NewFieldPath(v.Definition.Kind, v.Definition.Name), func() types.Type { 414 | return r.genFromDefinition(v.Definition) 415 | }) 416 | 417 | typ := r.binder.CopyModifiersFromAst(v.Type, baseType) 418 | 419 | r.collectPtrTypes(typ, false) 420 | 421 | argumentTypes = append(argumentTypes, &Argument{ 422 | Variable: v.Variable, 423 | Type: typ, 424 | }) 425 | } 426 | 427 | return argumentTypes 428 | } 429 | -------------------------------------------------------------------------------- /clientgen/template.go: -------------------------------------------------------------------------------- 1 | package clientgen 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/99designs/gqlgen/codegen/config" 7 | "github.com/99designs/gqlgen/codegen/templates" 8 | ) 9 | 10 | func RenderTemplate(cfg *config.Config, types []*Type, ptrTypes []PtrType, operations []*Operation, generateClient bool, client config.PackageConfig) error { 11 | if err := templates.Render(templates.Options{ 12 | PackageName: client.Package, 13 | Filename: client.Filename, 14 | Data: map[string]interface{}{ 15 | "Types": types, 16 | "PtrTypes": ptrTypes, 17 | "Operations": operations, 18 | "GenerateClient": generateClient, 19 | }, 20 | Packages: cfg.Packages, 21 | PackageDoc: "// Code generated by github.com/infiotinc/gqlgenc, DO NOT EDIT.\n", 22 | }); err != nil { 23 | return fmt.Errorf("%s generating failed: %w", client.Filename, err) 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /clientgen/template.gotpl: -------------------------------------------------------------------------------- 1 | {{- if .GenerateClient }} 2 | {{ reserveImport "bytes" }} 3 | {{ reserveImport "context" }} 4 | {{ reserveImport "encoding/json" }} 5 | {{ reserveImport "fmt" }} 6 | {{ reserveImport "io" }} 7 | {{ reserveImport "io/ioutil" }} 8 | {{ reserveImport "net/http" }} 9 | {{ reserveImport "net/url" }} 10 | {{ reserveImport "path" }} 11 | {{ reserveImport "time" }} 12 | 13 | {{ reserveImport "github.com/infiotinc/gqlgenc/client" }} 14 | {{ reserveImport "github.com/infiotinc/gqlgenc/client/transport" }} 15 | 16 | type Client struct { 17 | Client *client.Client 18 | } 19 | {{- end }} 20 | 21 | {{/* Greek character used to prevent name conflicts: */}} 22 | {{/* > prefix with Ξ */}} 23 | {{/* > ctх (х in cyrillic alphabet) because it is user facing */}} 24 | 25 | {{- range $_, $element := .Types }} 26 | // {{ .Path.Kind }}: {{ .Path.String }} 27 | type {{ .Name }} {{ .Type | ref }} 28 | 29 | {{- if .IsInputMap }} 30 | func New{{ $element.Name }}({{- range $f := .MapReq }}{{$f.Name}} {{$f.Type|ref}},{{- end }}) {{ $element.Name }} { 31 | return map[string]interface{}{ 32 | {{- range $f := .MapReq }} 33 | "{{$f.Name}}": {{$f.Name}}, 34 | {{- end }} 35 | } 36 | } 37 | 38 | {{- range $f := .MapOpt }} 39 | func (t {{ $element.Name }}) With{{$f.Name|go}}(v {{$f.Type|ref}}) {{ $element.Name }} { 40 | t["{{$f.Name}}"] = v 41 | return t 42 | } 43 | {{- end }} 44 | {{- end }} 45 | 46 | {{- if .UnmarshalTypes }} 47 | func (t *{{ .Name }}) UnmarshalJSON(data []byte) error { 48 | type ΞAlias {{ .Name }} 49 | var r ΞAlias 50 | 51 | err := json.Unmarshal(data, &r) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | *t = {{ .Name }}(r) 57 | 58 | switch r.Typename { 59 | {{- range $typename, $target := .UnmarshalTypes }} 60 | case "{{ $typename }}": 61 | var a {{ $target.Type | ref }} 62 | err = json.Unmarshal(data, &a) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | t.{{ $target.Name }} = &a 68 | {{- end }} 69 | } 70 | 71 | return nil 72 | } 73 | {{- end }} 74 | 75 | {{- if .Consts }} 76 | const ( 77 | {{- range $const := .Consts }} 78 | {{$const.Name}} {{$const.Type|ref}} = {{$const.Val.ExactString}} 79 | {{- end }} 80 | ) 81 | {{- end }} 82 | {{- end }} 83 | 84 | // Pointer helpers 85 | {{- range $_, $element := .PtrTypes }} 86 | func {{ $element.Name|go }}Ptr(v {{ $element.Type|ref }}) *{{ $element.Type|ref }} { 87 | return &v 88 | } 89 | {{- end }} 90 | 91 | {{- range $op := .Operations }} 92 | const {{ $op.Name|go }}Document = `{{ $op.Operation }}` 93 | 94 | {{- if $.GenerateClient }} 95 | {{- if eq $op.OperationType "subscription" }} 96 | type Message{{ $op.Name|go }} struct { 97 | Data *{{ $op.ResponseType | ref }} 98 | Error error 99 | Extensions transport.RawExtensions 100 | } 101 | 102 | func (Ξc *Client) {{ $op.Name|go }} (ctх context.Context{{- range $arg := .Args }}, {{ $arg.Variable | goPrivate }} {{ $arg.Type | ref }} {{- end }}) (<-chan Message{{ $op.Name|go }}, func()) { 103 | Ξvars := map[string]interface{}{ 104 | {{- range $args := .VariableDefinitions}} 105 | "{{ $args.Variable }}": {{ $args.Variable | goPrivate }}, 106 | {{- end }} 107 | } 108 | 109 | { {{/* New block to prevent var names conflicts */}} 110 | res := Ξc.Client.Subscription(ctх, "{{ $op.Name }}", {{ $op.Name|go }}Document, Ξvars) 111 | 112 | ch := make(chan Message{{ $op.Name|go }}) 113 | 114 | go func() { 115 | for res.Next() { 116 | opres := res.Get() 117 | 118 | var msg Message{{ $op.Name|go }} 119 | if len(opres.Errors) > 0 { 120 | msg.Error = opres.Errors 121 | } 122 | 123 | err := opres.UnmarshalData(&msg.Data) 124 | if err != nil && msg.Error == nil { 125 | msg.Error = err 126 | } 127 | 128 | msg.Extensions = opres.Extensions 129 | 130 | ch <- msg 131 | } 132 | 133 | if err := res.Err(); err != nil { 134 | ch <- Message{{ $op.Name|go }} { 135 | Error: err, 136 | } 137 | } 138 | close(ch) 139 | }() 140 | 141 | return ch, res.Close 142 | } 143 | } 144 | {{- else}} 145 | func (Ξc *Client) {{ $op.Name|go }} (ctх context.Context{{- range $arg := .Args }}, {{ $arg.Variable | goPrivate }} {{ $arg.Type | ref }} {{- end }}) (*{{ $op.ResponseType | ref }}, transport.OperationResponse, error) { 146 | Ξvars := map[string]interface{}{ 147 | {{- range $args := .VariableDefinitions}} 148 | "{{ $args.Variable }}": {{ $args.Variable | goPrivate }}, 149 | {{- end }} 150 | } 151 | 152 | { {{/* New block to prevent var names conflicts */}} 153 | var data {{ $op.ResponseType | ref }} 154 | res, err := Ξc.Client.{{ $op.OperationType|ucFirst }}(ctх, "{{ $op.Name }}", {{ $op.Name|go }}Document, Ξvars, &data) 155 | if err != nil { 156 | return nil, transport.OperationResponse{}, err 157 | } 158 | 159 | return &data, res, nil 160 | } 161 | } 162 | {{- end}} 163 | {{- end}} 164 | {{- end}} 165 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Original work from https://github.com/Yamashou/gqlgenc/blob/1ef8055d/config/config.go 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "regexp" 13 | "strings" 14 | 15 | "github.com/infiotinc/gqlgenc/client" 16 | "github.com/infiotinc/gqlgenc/client/transport" 17 | "github.com/infiotinc/gqlgenc/introspection" 18 | 19 | "github.com/99designs/gqlgen/codegen/config" 20 | "github.com/vektah/gqlparser/v2" 21 | "github.com/vektah/gqlparser/v2/ast" 22 | "github.com/vektah/gqlparser/v2/validator" 23 | "gopkg.in/yaml.v2" 24 | ) 25 | 26 | type Client struct { 27 | config.PackageConfig `yaml:",inline"` 28 | 29 | ExtraTypes []string `yaml:"extra_types,omitempty"` 30 | InputAsMap bool `yaml:"input_as_map"` 31 | } 32 | 33 | type TypeMapEntry struct { 34 | config.TypeMapEntry `yaml:",inline"` 35 | 36 | AsMap bool `yaml:"as_map,omitempty"` 37 | } 38 | 39 | type TypeMap map[string]TypeMapEntry 40 | 41 | // Config extends the gqlgen basic config 42 | // and represents the config file 43 | type Config struct { 44 | SchemaFilename StringList `yaml:"schema,omitempty"` 45 | Client Client `yaml:"client,omitempty"` 46 | Models TypeMap `yaml:"models,omitempty"` 47 | Endpoint *EndPointConfig `yaml:"endpoint,omitempty"` 48 | Generate *GenerateConfig `yaml:"generate,omitempty"` 49 | 50 | Query []string `yaml:"query"` 51 | 52 | // gqlgen config struct 53 | GQLConfig *config.Config `yaml:"-"` 54 | } 55 | 56 | var cfgFilenames = []string{".gqlgenc.yml", "gqlgenc.yml", "gqlgenc.yaml"} 57 | 58 | // StringList is a simple array of strings 59 | type StringList []string 60 | 61 | // Has checks if the strings array has a give value 62 | func (a StringList) Has(file string) bool { 63 | for _, existing := range a { 64 | if existing == file { 65 | return true 66 | } 67 | } 68 | 69 | return false 70 | } 71 | 72 | // LoadConfigFromDefaultLocations looks for a config file in the current directory, and all parent directories 73 | // walking up the tree. The closest config file will be returned. 74 | func LoadConfigFromDefaultLocations() (*Config, error) { 75 | cfgFile, err := findCfg() 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | err = os.Chdir(filepath.Dir(cfgFile)) 81 | if err != nil { 82 | return nil, fmt.Errorf("unable to enter config dir: %w", err) 83 | } 84 | 85 | return LoadConfig(cfgFile) 86 | } 87 | 88 | // EndPointConfig are the allowed options for the 'endpoint' config 89 | type EndPointConfig struct { 90 | URL string `yaml:"url"` 91 | Headers map[string]string `yaml:"headers,omitempty"` 92 | } 93 | 94 | // findCfg searches for the config file in this directory and all parents up the tree 95 | // looking for the closest match 96 | func findCfg() (string, error) { 97 | dir, err := os.Getwd() 98 | if err != nil { 99 | return "", fmt.Errorf("unable to get working dir to findCfg: %w", err) 100 | } 101 | 102 | cfg := findCfgInDir(dir) 103 | 104 | for cfg == "" && dir != filepath.Dir(dir) { 105 | dir = filepath.Dir(dir) 106 | cfg = findCfgInDir(dir) 107 | } 108 | 109 | if cfg == "" { 110 | return "", os.ErrNotExist 111 | } 112 | 113 | return cfg, nil 114 | } 115 | 116 | func findCfgInDir(dir string) string { 117 | for _, cfgName := range cfgFilenames { 118 | path := filepath.Join(dir, cfgName) 119 | if _, err := os.Stat(path); err == nil { 120 | return path 121 | } 122 | } 123 | 124 | return "" 125 | } 126 | 127 | var path2regex = strings.NewReplacer( 128 | `.`, `\.`, 129 | `*`, `.+`, 130 | `\`, `[\\/]`, 131 | `/`, `[\\/]`, 132 | ) 133 | 134 | // LoadConfig loads and parses the config gqlgenc config 135 | func LoadConfig(filename string) (*Config, error) { 136 | var cfg Config 137 | b, err := ioutil.ReadFile(filename) 138 | if err != nil { 139 | return nil, fmt.Errorf("unable to read config: %w", err) 140 | } 141 | 142 | confContent := []byte(os.ExpandEnv(string(b))) 143 | if err := yaml.UnmarshalStrict(confContent, &cfg); err != nil { 144 | return nil, fmt.Errorf("unable to parse config: %w", err) 145 | } 146 | 147 | if cfg.SchemaFilename != nil && cfg.Endpoint != nil { 148 | return nil, fmt.Errorf("'schema' and 'endpoint' both specified. Use schema to load from a local file, use endpoint to load from a remote server (using introspection)") 149 | } 150 | 151 | if cfg.SchemaFilename == nil && cfg.Endpoint == nil { 152 | return nil, fmt.Errorf("neither 'schema' nor 'endpoint' specified. Use schema to load from a local file, use endpoint to load from a remote server (using introspection)") 153 | } 154 | 155 | // https://github.com/99designs/gqlgen/blob/3a31a752df764738b1f6e99408df3b169d514784/codegen/config/config.go#L120 156 | for _, f := range cfg.SchemaFilename { 157 | var matches []string 158 | 159 | // for ** we want to override default globbing patterns and walk all 160 | // subdirectories to match schema files. 161 | if strings.Contains(f, "**") { 162 | pathParts := strings.SplitN(f, "**", 2) 163 | rest := strings.TrimPrefix(strings.TrimPrefix(pathParts[1], `\`), `/`) 164 | // turn the rest of the glob into a regex, anchored only at the end because ** allows 165 | // for any number of dirs in between and walk will let us match against the full path name 166 | globRe := regexp.MustCompile(path2regex.Replace(rest) + `$`) 167 | 168 | if err := filepath.Walk(pathParts[0], func(path string, info os.FileInfo, err error) error { 169 | if err != nil { 170 | return err 171 | } 172 | 173 | if globRe.MatchString(strings.TrimPrefix(path, pathParts[0])) { 174 | matches = append(matches, path) 175 | } 176 | 177 | return nil 178 | }); err != nil { 179 | return nil, fmt.Errorf("failed to walk schema at root %s: %w", pathParts[0], err) 180 | } 181 | } else { 182 | matches, err = filepath.Glob(f) 183 | if err != nil { 184 | return nil, fmt.Errorf("failed to glob schema filename %s: %w", f, err) 185 | } 186 | } 187 | 188 | files := StringList{} 189 | for _, m := range matches { 190 | if !files.Has(m) { 191 | files = append(files, m) 192 | } 193 | } 194 | 195 | cfg.SchemaFilename = files 196 | } 197 | 198 | models := make(config.TypeMap) 199 | if cfg.Models != nil { 200 | for k, v := range cfg.Models { 201 | models[k] = v.TypeMapEntry 202 | } 203 | } 204 | 205 | sources := make([]*ast.Source, 0) 206 | 207 | for _, filename := range cfg.SchemaFilename { 208 | filename = filepath.ToSlash(filename) 209 | var err error 210 | var schemaRaw []byte 211 | schemaRaw, err = ioutil.ReadFile(filename) 212 | if err != nil { 213 | return nil, fmt.Errorf("unable to open schema: %w", err) 214 | } 215 | 216 | sources = append(sources, &ast.Source{Name: filename, Input: string(schemaRaw)}) 217 | } 218 | 219 | cfg.GQLConfig = &config.Config{ 220 | Models: models, 221 | // TODO: gqlgen must be set exec but client not used 222 | Exec: config.ExecConfig{Filename: "generated.go"}, 223 | Directives: map[string]config.DirectiveConfig{}, 224 | Sources: sources, 225 | } 226 | 227 | if err := cfg.Client.Check(); err != nil { 228 | return nil, fmt.Errorf("config.exec: %w", err) 229 | } 230 | 231 | return &cfg, nil 232 | } 233 | 234 | // LoadSchema load and parses the schema from a local file or a remote server 235 | func (c *Config) LoadSchema(ctx context.Context) error { 236 | var schema *ast.Schema 237 | 238 | if c.SchemaFilename != nil { 239 | s, err := c.loadLocalSchema() 240 | if err != nil { 241 | return fmt.Errorf("load local schema failed: %w", err) 242 | } 243 | 244 | schema = s 245 | } else { 246 | s, err := c.loadRemoteSchema(ctx) 247 | if err != nil { 248 | return fmt.Errorf("load remote schema failed: %w", err) 249 | } 250 | 251 | schema = s 252 | } 253 | 254 | if schema.Query == nil { 255 | schema.Query = &ast.Definition{ 256 | Kind: ast.Object, 257 | Name: "Query", 258 | } 259 | schema.Types["Query"] = schema.Query 260 | } 261 | 262 | c.GQLConfig.Schema = schema 263 | 264 | return nil 265 | } 266 | 267 | func (c *Config) loadRemoteSchema(ctx context.Context) (*ast.Schema, error) { 268 | addHeaders := func(req *http.Request) { 269 | for key, value := range c.Endpoint.Headers { 270 | req.Header.Set(key, value) 271 | } 272 | } 273 | 274 | gqlClient := client.Client{ 275 | Transport: &transport.Http{ 276 | URL: c.Endpoint.URL, 277 | RequestOptions: []transport.HttpRequestOption{addHeaders}, 278 | }, 279 | } 280 | 281 | var res introspection.Query 282 | if _, err := gqlClient.Query(ctx, "", introspection.Introspection, nil, &res); err != nil { 283 | return nil, fmt.Errorf("introspection query failed: %w", err) 284 | } 285 | 286 | schema, err := validator.ValidateSchemaDocument(introspection.ParseIntrospectionQuery(c.Endpoint.URL, res)) 287 | if err != nil { 288 | return nil, fmt.Errorf("validation error: %w", err) 289 | } 290 | 291 | return schema, nil 292 | } 293 | 294 | func (c *Config) loadLocalSchema() (*ast.Schema, error) { 295 | schema, err := gqlparser.LoadSchema(c.GQLConfig.Sources...) 296 | if err != nil { 297 | return nil, err 298 | } 299 | 300 | return schema, nil 301 | } 302 | 303 | type GenerateConfig struct { 304 | Prefix *NamingConfig `yaml:"prefix,omitempty"` 305 | Suffix *NamingConfig `yaml:"suffix,omitempty"` 306 | } 307 | 308 | func (c *GenerateConfig) ShouldGenerateClient() bool { 309 | return true 310 | } 311 | 312 | type NamingConfig struct { 313 | Query string `yaml:"query,omitempty"` 314 | Mutation string `yaml:"mutation,omitempty"` 315 | Subscription string `yaml:"subscription,omitempty"` 316 | } 317 | -------------------------------------------------------------------------------- /example/.gqlgen.yml: -------------------------------------------------------------------------------- 1 | # Where are all the schema files located? globs are supported eg src/**/*.graphqls 2 | schema: 3 | - "schema.graphql" 4 | 5 | # Where should the generated server code go? 6 | exec: 7 | filename: server/generated/generated.go 8 | package: generated 9 | 10 | # Where should any generated models go? 11 | model: 12 | filename: server/model/models_gen.go 13 | package: model 14 | 15 | # Where should the resolver implementations go? 16 | resolver: 17 | layout: follow-schema 18 | dir: server 19 | package: server 20 | filename_template: "{name}.resolvers.go" 21 | 22 | # Optional: turn on use ` + "`" + `gqlgen:"fieldName"` + "`" + ` tags in your models 23 | # struct_tag: json 24 | 25 | # Optional: turn on to use []Thing instead of []*Thing 26 | # omit_slice_element_pointers: false 27 | 28 | # Optional: set to speed up generation time by not performing a final validation pass. 29 | # skip_validation: true 30 | 31 | # gqlgen will search for any type names in the schema in these go packages 32 | # if they match it will use them, otherwise it will generate them. 33 | #autobind: 34 | # - "github.com/your/app/graph/model" 35 | 36 | # This section declares type mapping between the GraphQL and go type systems 37 | # 38 | # The first line in each type will be used as defaults for resolver arguments and 39 | # modelgen, the others will be allowed when binding to fields. Configure them to 40 | # your liking 41 | models: 42 | ID: 43 | model: 44 | - github.com/99designs/gqlgen/graphql.ID 45 | - github.com/99designs/gqlgen/graphql.Int 46 | - github.com/99designs/gqlgen/graphql.Int64 47 | - github.com/99designs/gqlgen/graphql.Int32 48 | Int: 49 | model: 50 | - github.com/99designs/gqlgen/graphql.Int 51 | - github.com/99designs/gqlgen/graphql.Int64 52 | - github.com/99designs/gqlgen/graphql.Int32 53 | AsMapInput: 54 | model: map[string]interface{} 55 | -------------------------------------------------------------------------------- /example/.gqlgenc.yml: -------------------------------------------------------------------------------- 1 | client: 2 | filename: ./client/gen_client.go 3 | package: client 4 | extra_types: 5 | - SomeExtraType 6 | - Cyclic2_1 7 | models: 8 | Int: 9 | model: github.com/99designs/gqlgen/graphql.Int64 10 | DateTime: 11 | model: github.com/99designs/gqlgen/graphql.Time 12 | GetRoomCustom: 13 | model: example/somelib.CustomRoom 14 | Upload: 15 | model: github.com/infiotinc/gqlgenc/client/transport.Upload 16 | AsMapInput: 17 | as_map: true 18 | Value1: 19 | model: example/client.Value1 20 | Value2: 21 | model: example/client.Value2 22 | schema: 23 | - schema.graphql 24 | query: 25 | - query.graphql 26 | -------------------------------------------------------------------------------- /example/client/gen_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/infiotinc/gqlgenc, DO NOT EDIT. 2 | 3 | package client 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "example/somelib" 9 | 10 | "github.com/infiotinc/gqlgenc/client" 11 | "github.com/infiotinc/gqlgenc/client/transport" 12 | ) 13 | 14 | type Client struct { 15 | Client *client.Client 16 | } 17 | 18 | // OPERATION: AsMap 19 | type AsMap struct { 20 | AsMap string "json:\"asMap\"" 21 | } 22 | 23 | // INPUT_OBJECT: AsMapInput 24 | type AsMapInput map[string]interface{} 25 | 26 | func NewAsMapInput(reqStr string, reqEp Episode) AsMapInput { 27 | return map[string]interface{}{ 28 | "reqStr": reqStr, 29 | "reqEp": reqEp, 30 | } 31 | } 32 | func (t AsMapInput) WithOptStr(v *string) AsMapInput { 33 | t["optStr"] = v 34 | return t 35 | } 36 | func (t AsMapInput) WithOptEp(v *Episode) AsMapInput { 37 | t["optEp"] = v 38 | return t 39 | } 40 | 41 | // OPERATION: CreatePost 42 | type CreatePost struct { 43 | Post CreatePost_Post "json:\"post\"" 44 | } 45 | 46 | // OPERATION: CreatePost.post 47 | type CreatePost_Post struct { 48 | ID string "json:\"id\"" 49 | Text string "json:\"text\"" 50 | } 51 | 52 | // OPERATION: Cyclic1 53 | type Cyclic1 struct { 54 | Cyclic *Cyclic1_Cyclic "json:\"cyclic\"" 55 | } 56 | 57 | // OPERATION: Cyclic1.cyclic 58 | type Cyclic1_Cyclic struct { 59 | Child *Cyclic1_Cyclic_Child "json:\"child\"" 60 | } 61 | 62 | // OPERATION: Cyclic1.cyclic.child 63 | type Cyclic1_Cyclic_Child struct { 64 | Child *Cyclic1_Cyclic_Child_Child "json:\"child\"" 65 | } 66 | 67 | // OPERATION: Cyclic1.cyclic.child.child 68 | type Cyclic1_Cyclic_Child_Child struct { 69 | Child *Cyclic1_Cyclic_Child_Child_Child "json:\"child\"" 70 | } 71 | 72 | // OPERATION: Cyclic1.cyclic.child.child.child 73 | type Cyclic1_Cyclic_Child_Child_Child struct { 74 | ID string "json:\"id\"" 75 | } 76 | 77 | // OBJECT: Cyclic2_1 78 | type Cyclic2_1 struct { 79 | ID string "json:\"id\"" 80 | Child *Cyclic2_2 "json:\"child\"" 81 | } 82 | 83 | // OBJECT: Cyclic2_2 84 | type Cyclic2_2 struct { 85 | ID string "json:\"id\"" 86 | Child *Cyclic2_1 "json:\"child\"" 87 | } 88 | 89 | // ENUM: Episode 90 | type Episode string 91 | 92 | const ( 93 | EpisodeNewhope Episode = "NEWHOPE" 94 | EpisodeEmpire Episode = "EMPIRE" 95 | EpisodeJedi Episode = "JEDI" 96 | ) 97 | 98 | // ENUM: FooType_hash1 99 | type FooTypeHash1 string 100 | 101 | const ( 102 | FooTypeHash1Hash1 FooTypeHash1 = "hash_1" 103 | FooTypeHash1Hash2 FooTypeHash1 = "hash_2" 104 | ) 105 | 106 | // OPERATION: GetBooks 107 | type GetBooks struct { 108 | Books []GetBooks_Books "json:\"books\"" 109 | } 110 | 111 | // OPERATION: GetBooks.books 112 | type GetBooks_Books struct { 113 | Typename string "json:\"__typename\"" 114 | Title string "json:\"title\"" 115 | Textbook *GetBooks_Books_Textbook "json:\"-\"" 116 | ColoringBook *GetBooks_Books_ColoringBook "json:\"-\"" 117 | } 118 | 119 | func (t *GetBooks_Books) UnmarshalJSON(data []byte) error { 120 | type ΞAlias GetBooks_Books 121 | var r ΞAlias 122 | 123 | err := json.Unmarshal(data, &r) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | *t = GetBooks_Books(r) 129 | 130 | switch r.Typename { 131 | case "ColoringBook": 132 | var a GetBooks_Books_ColoringBook 133 | err = json.Unmarshal(data, &a) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | t.ColoringBook = &a 139 | case "Textbook": 140 | var a GetBooks_Books_Textbook 141 | err = json.Unmarshal(data, &a) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | t.Textbook = &a 147 | } 148 | 149 | return nil 150 | } 151 | 152 | // OPERATION: GetBooks.books.ColoringBook 153 | type GetBooks_Books_ColoringBook struct { 154 | Colors []string "json:\"colors\"" 155 | } 156 | 157 | // OPERATION: GetBooks.books.Textbook 158 | type GetBooks_Books_Textbook struct { 159 | Courses []string "json:\"courses\"" 160 | } 161 | 162 | // OPERATION: GetEpisodes 163 | type GetEpisodes struct { 164 | Episodes []Episode "json:\"episodes\"" 165 | } 166 | 167 | // OPERATION: GetMedias 168 | type GetMedias struct { 169 | Medias []GetMedias_Medias "json:\"medias\"" 170 | } 171 | 172 | // OPERATION: GetMedias.medias 173 | type GetMedias_Medias struct { 174 | Typename string "json:\"__typename\"" 175 | Image *GetMedias_Medias_Image "json:\"-\"" 176 | Video *GetMedias_Medias_Video "json:\"-\"" 177 | } 178 | 179 | func (t *GetMedias_Medias) UnmarshalJSON(data []byte) error { 180 | type ΞAlias GetMedias_Medias 181 | var r ΞAlias 182 | 183 | err := json.Unmarshal(data, &r) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | *t = GetMedias_Medias(r) 189 | 190 | switch r.Typename { 191 | case "Image": 192 | var a GetMedias_Medias_Image 193 | err = json.Unmarshal(data, &a) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | t.Image = &a 199 | case "Video": 200 | var a GetMedias_Medias_Video 201 | err = json.Unmarshal(data, &a) 202 | if err != nil { 203 | return err 204 | } 205 | 206 | t.Video = &a 207 | } 208 | 209 | return nil 210 | } 211 | 212 | // OPERATION: GetMedias.medias.Image 213 | type GetMedias_Medias_Image struct { 214 | Size int64 "json:\"size\"" 215 | } 216 | 217 | // OPERATION: GetMedias.medias.Video 218 | type GetMedias_Medias_Video struct { 219 | Duration int64 "json:\"duration\"" 220 | } 221 | 222 | // OPERATION: GetRoom 223 | type GetRoom struct { 224 | Room *GetRoom_Room "json:\"room\"" 225 | } 226 | 227 | // OPERATION: GetRoom.room 228 | type GetRoom_Room struct { 229 | Name string "json:\"name\"" 230 | Hash *FooTypeHash1 "json:\"hash\"" 231 | } 232 | 233 | // OPERATION: GetRoomFragment 234 | type GetRoomFragment struct { 235 | Room *RoomFragment "json:\"room\"" 236 | } 237 | 238 | // OPERATION: GetRoomNonNull 239 | type GetRoomNonNull struct { 240 | RoomNonNull GetRoomNonNull_RoomNonNull "json:\"roomNonNull\"" 241 | } 242 | 243 | // OPERATION: GetRoomNonNull.roomNonNull 244 | type GetRoomNonNull_RoomNonNull struct { 245 | Name string "json:\"name\"" 246 | } 247 | 248 | // OPERATION: Issue8 249 | type Issue8 struct { 250 | Issue8 *Issue8_Issue8 "json:\"issue8\"" 251 | } 252 | 253 | // OPERATION: Issue8.issue8 254 | type Issue8_Issue8 struct { 255 | Foo1 Issue8_Issue8_Foo1 "json:\"foo1\"" 256 | Foo2 *Issue8_Issue8_Foo2 "json:\"foo2\"" 257 | } 258 | 259 | // OPERATION: Issue8.issue8.foo1 260 | type Issue8_Issue8_Foo1 struct { 261 | A Issue8_Issue8_Foo1_A "json:\"a\"" 262 | } 263 | 264 | // OPERATION: Issue8.issue8.foo1.a 265 | type Issue8_Issue8_Foo1_A struct { 266 | Aa string "json:\"Aa\"" 267 | } 268 | 269 | // OPERATION: Issue8.issue8.foo2 270 | type Issue8_Issue8_Foo2 struct { 271 | A Issue8_Issue8_Foo2_A "json:\"a\"" 272 | } 273 | 274 | // OPERATION: Issue8.issue8.foo2.a 275 | type Issue8_Issue8_Foo2_A struct { 276 | Aa string "json:\"Aa\"" 277 | } 278 | 279 | // OPERATION: OptValue1 280 | type OptValue1 struct { 281 | OptValue1 *bool "json:\"optValue1\"" 282 | } 283 | 284 | // OPERATION: OptValue2 285 | type OptValue2 struct { 286 | OptValue2 *bool "json:\"optValue2\"" 287 | } 288 | 289 | // INPUT_OBJECT: OptionalValue1 290 | type OptionalValue1 struct { 291 | Value *Value1 "json:\"value\"" 292 | } 293 | 294 | // INPUT_OBJECT: OptionalValue2 295 | type OptionalValue2 struct { 296 | Value *Value2 "json:\"value\"" 297 | } 298 | 299 | // INPUT_OBJECT: PostCreateInput 300 | type PostCreateInput struct { 301 | Text string "json:\"text\"" 302 | } 303 | 304 | // OBJECT: RoomFragment 305 | type RoomFragment struct { 306 | Name string "json:\"name\"" 307 | } 308 | 309 | // OBJECT: SomeExtraType 310 | type SomeExtraType struct { 311 | Child SomeExtraTypeChild "json:\"child\"" 312 | } 313 | 314 | // OBJECT: SomeExtraTypeChild 315 | type SomeExtraTypeChild struct { 316 | Child SomeExtraTypeChildChild "json:\"child\"" 317 | } 318 | 319 | // OBJECT: SomeExtraTypeChildChild 320 | type SomeExtraTypeChildChild struct { 321 | ID string "json:\"id\"" 322 | } 323 | 324 | // OPERATION: SubscribeMessageAdded 325 | type SubscribeMessageAdded struct { 326 | MessageAdded SubscribeMessageAdded_MessageAdded "json:\"messageAdded\"" 327 | } 328 | 329 | // OPERATION: SubscribeMessageAdded.messageAdded 330 | type SubscribeMessageAdded_MessageAdded struct { 331 | ID string "json:\"id\"" 332 | } 333 | 334 | // OPERATION: UploadFile 335 | type UploadFile struct { 336 | UploadFile UploadFile_UploadFile "json:\"uploadFile\"" 337 | } 338 | 339 | // OPERATION: UploadFile.uploadFile 340 | type UploadFile_UploadFile struct { 341 | Size int64 "json:\"size\"" 342 | } 343 | 344 | // OPERATION: UploadFiles 345 | type UploadFiles struct { 346 | UploadFiles []UploadFiles_UploadFiles "json:\"uploadFiles\"" 347 | } 348 | 349 | // OPERATION: UploadFiles.uploadFiles 350 | type UploadFiles_UploadFiles struct { 351 | Size int64 "json:\"size\"" 352 | } 353 | 354 | // OPERATION: UploadFilesMap 355 | type UploadFilesMap struct { 356 | UploadFilesMap UploadFilesMap_UploadFilesMap "json:\"uploadFilesMap\"" 357 | } 358 | 359 | // OPERATION: UploadFilesMap.uploadFilesMap 360 | type UploadFilesMap_UploadFilesMap struct { 361 | Somefile UploadFilesMap_UploadFilesMap_Somefile "json:\"somefile\"" 362 | } 363 | 364 | // OPERATION: UploadFilesMap.uploadFilesMap.somefile 365 | type UploadFilesMap_UploadFilesMap_Somefile struct { 366 | Size int64 "json:\"size\"" 367 | } 368 | 369 | // INPUT_OBJECT: UploadFilesMapInput 370 | type UploadFilesMapInput struct { 371 | Somefile transport.Upload "json:\"somefile\"" 372 | } 373 | 374 | // Pointer helpers 375 | func AsMapInputPtr(v AsMapInput) *AsMapInput { 376 | return &v 377 | } 378 | func EpisodePtr(v Episode) *Episode { 379 | return &v 380 | } 381 | func OptionalValue2Ptr(v OptionalValue2) *OptionalValue2 { 382 | return &v 383 | } 384 | func Value1Ptr(v Value1) *Value1 { 385 | return &v 386 | } 387 | func Value2Ptr(v Value2) *Value2 { 388 | return &v 389 | } 390 | func StringPtr(v string) *string { 391 | return &v 392 | } 393 | 394 | const GetRoomDocument = `query GetRoom ($name: String!) { 395 | room(name: $name) { 396 | name 397 | hash 398 | } 399 | } 400 | ` 401 | 402 | func (Ξc *Client) GetRoom(ctх context.Context, name string) (*GetRoom, transport.OperationResponse, error) { 403 | Ξvars := map[string]interface{}{ 404 | "name": name, 405 | } 406 | 407 | { 408 | var data GetRoom 409 | res, err := Ξc.Client.Query(ctх, "GetRoom", GetRoomDocument, Ξvars, &data) 410 | if err != nil { 411 | return nil, transport.OperationResponse{}, err 412 | } 413 | 414 | return &data, res, nil 415 | } 416 | } 417 | 418 | const GetRoomNonNullDocument = `query GetRoomNonNull ($name: String!) { 419 | roomNonNull(name: $name) { 420 | name 421 | } 422 | } 423 | ` 424 | 425 | func (Ξc *Client) GetRoomNonNull(ctх context.Context, name string) (*GetRoomNonNull, transport.OperationResponse, error) { 426 | Ξvars := map[string]interface{}{ 427 | "name": name, 428 | } 429 | 430 | { 431 | var data GetRoomNonNull 432 | res, err := Ξc.Client.Query(ctх, "GetRoomNonNull", GetRoomNonNullDocument, Ξvars, &data) 433 | if err != nil { 434 | return nil, transport.OperationResponse{}, err 435 | } 436 | 437 | return &data, res, nil 438 | } 439 | } 440 | 441 | const GetRoomFragmentDocument = `query GetRoomFragment ($name: String!) { 442 | room(name: $name) { 443 | ... RoomFragment 444 | } 445 | } 446 | fragment RoomFragment on Chatroom { 447 | name 448 | } 449 | ` 450 | 451 | func (Ξc *Client) GetRoomFragment(ctх context.Context, name string) (*GetRoomFragment, transport.OperationResponse, error) { 452 | Ξvars := map[string]interface{}{ 453 | "name": name, 454 | } 455 | 456 | { 457 | var data GetRoomFragment 458 | res, err := Ξc.Client.Query(ctх, "GetRoomFragment", GetRoomFragmentDocument, Ξvars, &data) 459 | if err != nil { 460 | return nil, transport.OperationResponse{}, err 461 | } 462 | 463 | return &data, res, nil 464 | } 465 | } 466 | 467 | const GetRoomCustomDocument = `query GetRoomCustom ($name: String!) { 468 | room(name: $name) { 469 | name 470 | } 471 | } 472 | ` 473 | 474 | func (Ξc *Client) GetRoomCustom(ctх context.Context, name string) (*somelib.CustomRoom, transport.OperationResponse, error) { 475 | Ξvars := map[string]interface{}{ 476 | "name": name, 477 | } 478 | 479 | { 480 | var data somelib.CustomRoom 481 | res, err := Ξc.Client.Query(ctх, "GetRoomCustom", GetRoomCustomDocument, Ξvars, &data) 482 | if err != nil { 483 | return nil, transport.OperationResponse{}, err 484 | } 485 | 486 | return &data, res, nil 487 | } 488 | } 489 | 490 | const GetMediasDocument = `query GetMedias { 491 | medias { 492 | __typename 493 | ... on Image { 494 | size 495 | } 496 | ... on Video { 497 | duration 498 | } 499 | } 500 | } 501 | ` 502 | 503 | func (Ξc *Client) GetMedias(ctх context.Context) (*GetMedias, transport.OperationResponse, error) { 504 | Ξvars := map[string]interface{}{} 505 | 506 | { 507 | var data GetMedias 508 | res, err := Ξc.Client.Query(ctх, "GetMedias", GetMediasDocument, Ξvars, &data) 509 | if err != nil { 510 | return nil, transport.OperationResponse{}, err 511 | } 512 | 513 | return &data, res, nil 514 | } 515 | } 516 | 517 | const GetBooksDocument = `query GetBooks { 518 | books { 519 | __typename 520 | title 521 | ... on Textbook { 522 | courses 523 | } 524 | ... on ColoringBook { 525 | colors 526 | } 527 | } 528 | } 529 | ` 530 | 531 | func (Ξc *Client) GetBooks(ctх context.Context) (*GetBooks, transport.OperationResponse, error) { 532 | Ξvars := map[string]interface{}{} 533 | 534 | { 535 | var data GetBooks 536 | res, err := Ξc.Client.Query(ctх, "GetBooks", GetBooksDocument, Ξvars, &data) 537 | if err != nil { 538 | return nil, transport.OperationResponse{}, err 539 | } 540 | 541 | return &data, res, nil 542 | } 543 | } 544 | 545 | const SubscribeMessageAddedDocument = `subscription SubscribeMessageAdded { 546 | messageAdded(roomName: "test") { 547 | id 548 | } 549 | } 550 | ` 551 | 552 | type MessageSubscribeMessageAdded struct { 553 | Data *SubscribeMessageAdded 554 | Error error 555 | Extensions transport.RawExtensions 556 | } 557 | 558 | func (Ξc *Client) SubscribeMessageAdded(ctх context.Context) (<-chan MessageSubscribeMessageAdded, func()) { 559 | Ξvars := map[string]interface{}{} 560 | 561 | { 562 | res := Ξc.Client.Subscription(ctх, "SubscribeMessageAdded", SubscribeMessageAddedDocument, Ξvars) 563 | 564 | ch := make(chan MessageSubscribeMessageAdded) 565 | 566 | go func() { 567 | for res.Next() { 568 | opres := res.Get() 569 | 570 | var msg MessageSubscribeMessageAdded 571 | if len(opres.Errors) > 0 { 572 | msg.Error = opres.Errors 573 | } 574 | 575 | err := opres.UnmarshalData(&msg.Data) 576 | if err != nil && msg.Error == nil { 577 | msg.Error = err 578 | } 579 | 580 | msg.Extensions = opres.Extensions 581 | 582 | ch <- msg 583 | } 584 | 585 | if err := res.Err(); err != nil { 586 | ch <- MessageSubscribeMessageAdded{ 587 | Error: err, 588 | } 589 | } 590 | close(ch) 591 | }() 592 | 593 | return ch, res.Close 594 | } 595 | } 596 | 597 | const CreatePostDocument = `mutation CreatePost ($input: PostCreateInput!) { 598 | post(input: $input) { 599 | id 600 | text 601 | } 602 | } 603 | ` 604 | 605 | func (Ξc *Client) CreatePost(ctх context.Context, input PostCreateInput) (*CreatePost, transport.OperationResponse, error) { 606 | Ξvars := map[string]interface{}{ 607 | "input": input, 608 | } 609 | 610 | { 611 | var data CreatePost 612 | res, err := Ξc.Client.Mutation(ctх, "CreatePost", CreatePostDocument, Ξvars, &data) 613 | if err != nil { 614 | return nil, transport.OperationResponse{}, err 615 | } 616 | 617 | return &data, res, nil 618 | } 619 | } 620 | 621 | const UploadFileDocument = `mutation UploadFile ($file: Upload!) { 622 | uploadFile(file: $file) { 623 | size 624 | } 625 | } 626 | ` 627 | 628 | func (Ξc *Client) UploadFile(ctх context.Context, file transport.Upload) (*UploadFile, transport.OperationResponse, error) { 629 | Ξvars := map[string]interface{}{ 630 | "file": file, 631 | } 632 | 633 | { 634 | var data UploadFile 635 | res, err := Ξc.Client.Mutation(ctх, "UploadFile", UploadFileDocument, Ξvars, &data) 636 | if err != nil { 637 | return nil, transport.OperationResponse{}, err 638 | } 639 | 640 | return &data, res, nil 641 | } 642 | } 643 | 644 | const UploadFilesDocument = `mutation UploadFiles ($files: [Upload!]!) { 645 | uploadFiles(files: $files) { 646 | size 647 | } 648 | } 649 | ` 650 | 651 | func (Ξc *Client) UploadFiles(ctх context.Context, files []*transport.Upload) (*UploadFiles, transport.OperationResponse, error) { 652 | Ξvars := map[string]interface{}{ 653 | "files": files, 654 | } 655 | 656 | { 657 | var data UploadFiles 658 | res, err := Ξc.Client.Mutation(ctх, "UploadFiles", UploadFilesDocument, Ξvars, &data) 659 | if err != nil { 660 | return nil, transport.OperationResponse{}, err 661 | } 662 | 663 | return &data, res, nil 664 | } 665 | } 666 | 667 | const UploadFilesMapDocument = `mutation UploadFilesMap ($files: UploadFilesMapInput!) { 668 | uploadFilesMap(files: $files) { 669 | somefile { 670 | size 671 | } 672 | } 673 | } 674 | ` 675 | 676 | func (Ξc *Client) UploadFilesMap(ctх context.Context, files UploadFilesMapInput) (*UploadFilesMap, transport.OperationResponse, error) { 677 | Ξvars := map[string]interface{}{ 678 | "files": files, 679 | } 680 | 681 | { 682 | var data UploadFilesMap 683 | res, err := Ξc.Client.Mutation(ctх, "UploadFilesMap", UploadFilesMapDocument, Ξvars, &data) 684 | if err != nil { 685 | return nil, transport.OperationResponse{}, err 686 | } 687 | 688 | return &data, res, nil 689 | } 690 | } 691 | 692 | const Issue8Document = `query Issue8 { 693 | issue8 { 694 | foo1 { 695 | a { 696 | Aa 697 | } 698 | } 699 | foo2 { 700 | a { 701 | Aa 702 | } 703 | } 704 | } 705 | } 706 | ` 707 | 708 | func (Ξc *Client) Issue8(ctх context.Context) (*Issue8, transport.OperationResponse, error) { 709 | Ξvars := map[string]interface{}{} 710 | 711 | { 712 | var data Issue8 713 | res, err := Ξc.Client.Query(ctх, "Issue8", Issue8Document, Ξvars, &data) 714 | if err != nil { 715 | return nil, transport.OperationResponse{}, err 716 | } 717 | 718 | return &data, res, nil 719 | } 720 | } 721 | 722 | const GetEpisodesDocument = `query GetEpisodes { 723 | episodes 724 | } 725 | ` 726 | 727 | func (Ξc *Client) GetEpisodes(ctх context.Context) (*GetEpisodes, transport.OperationResponse, error) { 728 | Ξvars := map[string]interface{}{} 729 | 730 | { 731 | var data GetEpisodes 732 | res, err := Ξc.Client.Query(ctх, "GetEpisodes", GetEpisodesDocument, Ξvars, &data) 733 | if err != nil { 734 | return nil, transport.OperationResponse{}, err 735 | } 736 | 737 | return &data, res, nil 738 | } 739 | } 740 | 741 | const Cyclic1Document = `query Cyclic1 { 742 | cyclic { 743 | child { 744 | child { 745 | child { 746 | id 747 | } 748 | } 749 | } 750 | } 751 | } 752 | ` 753 | 754 | func (Ξc *Client) Cyclic1(ctх context.Context) (*Cyclic1, transport.OperationResponse, error) { 755 | Ξvars := map[string]interface{}{} 756 | 757 | { 758 | var data Cyclic1 759 | res, err := Ξc.Client.Query(ctх, "Cyclic1", Cyclic1Document, Ξvars, &data) 760 | if err != nil { 761 | return nil, transport.OperationResponse{}, err 762 | } 763 | 764 | return &data, res, nil 765 | } 766 | } 767 | 768 | const AsMapDocument = `query AsMap ($req: AsMapInput!, $opt: AsMapInput) { 769 | asMap(req: $req, opt: $opt) 770 | } 771 | ` 772 | 773 | func (Ξc *Client) AsMap(ctх context.Context, req AsMapInput, opt *AsMapInput) (*AsMap, transport.OperationResponse, error) { 774 | Ξvars := map[string]interface{}{ 775 | "req": req, 776 | "opt": opt, 777 | } 778 | 779 | { 780 | var data AsMap 781 | res, err := Ξc.Client.Query(ctх, "AsMap", AsMapDocument, Ξvars, &data) 782 | if err != nil { 783 | return nil, transport.OperationResponse{}, err 784 | } 785 | 786 | return &data, res, nil 787 | } 788 | } 789 | 790 | const OptValue1Document = `query OptValue1 ($v: OptionalValue1!) { 791 | optValue1(req: $v) 792 | } 793 | ` 794 | 795 | func (Ξc *Client) OptValue1(ctх context.Context, v OptionalValue1) (*OptValue1, transport.OperationResponse, error) { 796 | Ξvars := map[string]interface{}{ 797 | "v": v, 798 | } 799 | 800 | { 801 | var data OptValue1 802 | res, err := Ξc.Client.Query(ctх, "OptValue1", OptValue1Document, Ξvars, &data) 803 | if err != nil { 804 | return nil, transport.OperationResponse{}, err 805 | } 806 | 807 | return &data, res, nil 808 | } 809 | } 810 | 811 | const OptValue2Document = `query OptValue2 ($v: OptionalValue2) { 812 | optValue2(opt: $v) 813 | } 814 | ` 815 | 816 | func (Ξc *Client) OptValue2(ctх context.Context, v *OptionalValue2) (*OptValue2, transport.OperationResponse, error) { 817 | Ξvars := map[string]interface{}{ 818 | "v": v, 819 | } 820 | 821 | { 822 | var data OptValue2 823 | res, err := Ξc.Client.Query(ctх, "OptValue2", OptValue2Document, Ξvars, &data) 824 | if err != nil { 825 | return nil, transport.OperationResponse{}, err 826 | } 827 | 828 | return &data, res, nil 829 | } 830 | } 831 | -------------------------------------------------------------------------------- /example/client/scalars.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | type Value1 bool 4 | type Value2 bool 5 | -------------------------------------------------------------------------------- /example/client_apq_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "context" 5 | "github.com/99designs/gqlgen/graphql/handler/extension" 6 | "github.com/infiotinc/gqlgenc/client/extensions" 7 | "github.com/infiotinc/gqlgenc/client/transport" 8 | "github.com/stretchr/testify/assert" 9 | "testing" 10 | ) 11 | 12 | func TestHttpAPQQuery(t *testing.T) { 13 | ctx := context.Background() 14 | 15 | cli, teardown := httpcli(ctx) 16 | defer teardown() 17 | 18 | cli.Use(&extensions.APQ{}) 19 | 20 | runAssertQuery(t, ctx, cli, func(opres transport.OperationResponse, data RoomQueryResponse) { 21 | var stats extension.ApqStats 22 | err := opres.Extensions.Unmarshal("apqStats", &stats) 23 | if err != nil { 24 | assert.Fail(t, err.Error()) 25 | } 26 | assert.NotNil(t, stats) 27 | assert.True(t, stats.SentQuery) 28 | }) 29 | 30 | runAssertQuery(t, ctx, cli, func(opres transport.OperationResponse, data RoomQueryResponse) { 31 | var stats extension.ApqStats 32 | err := opres.Extensions.Unmarshal("apqStats", &stats) 33 | if err != nil { 34 | assert.Fail(t, err.Error()) 35 | } 36 | assert.NotNil(t, stats) 37 | assert.False(t, stats.SentQuery) 38 | }) 39 | } 40 | 41 | func TestSplitAPQQuery(t *testing.T) { 42 | ctx := context.Background() 43 | 44 | cli, teardown, _ := splitcli(ctx) 45 | defer teardown() 46 | 47 | cli.Use(&extensions.APQ{}) 48 | } 49 | 50 | func TestSplitAPQSubscription(t *testing.T) { 51 | ctx := context.Background() 52 | 53 | cli, teardown, _ := splitcli(ctx) 54 | defer teardown() 55 | 56 | cli.Use(&extensions.APQ{}) 57 | 58 | runAssertSub(t, ctx, cli, func(opres transport.OperationResponse, data MessagesSubResponse) { 59 | var stats extension.ApqStats 60 | err := opres.Extensions.Unmarshal("apqStats", &stats) 61 | if err != nil { 62 | assert.Fail(t, err.Error()) 63 | } 64 | assert.NotNil(t, stats) 65 | assert.True(t, stats.SentQuery) 66 | }) 67 | 68 | runAssertSub(t, ctx, cli, func(opres transport.OperationResponse, data MessagesSubResponse) { 69 | var stats extension.ApqStats 70 | err := opres.Extensions.Unmarshal("apqStats", &stats) 71 | if err != nil { 72 | assert.Fail(t, err.Error()) 73 | } 74 | assert.NotNil(t, stats) 75 | assert.False(t, stats.SentQuery) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /example/client_http_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/infiotinc/gqlgenc/client" 7 | "github.com/infiotinc/gqlgenc/client/transport" 8 | "github.com/stretchr/testify/assert" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | ) 13 | 14 | func httpcli(ctx context.Context) (*client.Client, func()) { 15 | return clifactory(ctx, func(ts *httptest.Server) (transport.Transport, func()) { 16 | return httptr(ctx, ts.URL), nil 17 | }) 18 | } 19 | 20 | func TestRawHttpQuery(t *testing.T) { 21 | ctx := context.Background() 22 | 23 | cli, teardown := httpcli(ctx) 24 | defer teardown() 25 | 26 | runAssertQuery(t, ctx, cli) 27 | } 28 | 29 | func TestRawHttpQueryError(t *testing.T) { 30 | ctx := context.Background() 31 | 32 | cli, teardown := httpcli(ctx) 33 | defer teardown() 34 | 35 | var opres RoomQueryResponse 36 | _, err := cli.Query(ctx, "", RoomQuery, map[string]interface{}{"name": "error"}, &opres) 37 | assert.EqualError(t, err, "input: room that's an invalid room\n") 38 | } 39 | 40 | func TestRawHttpError(t *testing.T) { 41 | ctx := context.Background() 42 | 43 | cli, teardown := clifactorywith(ctx, func(ts *httptest.Server) (transport.Transport, func()) { 44 | return httptr(ctx, ts.URL), nil 45 | }, func(h http.Handler) http.Handler { 46 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 47 | b, _ := json.Marshal(map[string]string{ 48 | "error": "Yeah that went wrong", 49 | }) 50 | 51 | w.WriteHeader(http.StatusForbidden) 52 | w.Write(b) 53 | }) 54 | }) 55 | defer teardown() 56 | 57 | var opres RoomQueryResponse 58 | _, err := cli.Query(ctx, "", RoomQuery, map[string]interface{}{"name": "name"}, &opres) 59 | assert.EqualError(t, err, `no data nor errors, got 403: {"error":"Yeah that went wrong"}`) 60 | } 61 | -------------------------------------------------------------------------------- /example/client_split_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "context" 5 | "github.com/infiotinc/gqlgenc/client" 6 | "github.com/infiotinc/gqlgenc/client/transport" 7 | "github.com/stretchr/testify/assert" 8 | "net/http/httptest" 9 | "testing" 10 | ) 11 | 12 | type splitStats map[string]int 13 | 14 | func (ss splitStats) wrapTr(name string, tr transport.Transport) transport.Transport { 15 | return transport.Func(func(req transport.Request) transport.Response { 16 | if _, ok := ss[name]; !ok { 17 | ss[name] = 0 18 | } 19 | 20 | ss[name]++ 21 | 22 | return tr.Request(req) 23 | }) 24 | } 25 | 26 | func (ss splitStats) get(name string) int { 27 | if v, ok := ss[name]; ok { 28 | return v 29 | } 30 | 31 | return 0 32 | } 33 | 34 | func splitcli(ctx context.Context) (*client.Client, func(), *splitStats) { 35 | ss := &splitStats{} 36 | 37 | c, td := clifactory(ctx, func(ts *httptest.Server) (transport.Transport, func()) { 38 | wstr := wstr(ctx, ts.URL) 39 | httptr := httptr(ctx, ts.URL) 40 | 41 | s_wstr := ss.wrapTr("ws", wstr) 42 | s_httptr := ss.wrapTr("http", httptr) 43 | 44 | tr := transport.SplitSubscription(s_wstr, s_httptr) 45 | 46 | return tr, func() { 47 | wstr.Close() 48 | } 49 | }) 50 | 51 | return c, td, ss 52 | } 53 | 54 | func TestRawSplitQuery(t *testing.T) { 55 | ctx := context.Background() 56 | 57 | cli, teardown, ss := splitcli(ctx) 58 | defer teardown() 59 | 60 | runAssertQuery(t, ctx, cli) 61 | 62 | t.Log(ss) 63 | 64 | assert.Equal(t, 1, ss.get("http")) 65 | assert.Equal(t, 0, ss.get("ws")) 66 | } 67 | 68 | func TestRawSplitSubscription(t *testing.T) { 69 | ctx := context.Background() 70 | 71 | cli, teardown, ss := splitcli(ctx) 72 | defer teardown() 73 | 74 | runAssertSub(t, ctx, cli) 75 | 76 | t.Log(ss) 77 | 78 | assert.Equal(t, 0, ss.get("http")) 79 | assert.Equal(t, 1, ss.get("ws")) 80 | } 81 | -------------------------------------------------------------------------------- /example/client_unstable_ws_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/infiotinc/gqlgenc/client" 7 | "github.com/infiotinc/gqlgenc/client/transport" 8 | "net/http/httptest" 9 | "nhooyr.io/websocket" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | type unstableWebsocketConn struct { 15 | ctx context.Context 16 | wsconn *transport.WebsocketHandler 17 | } 18 | 19 | func (u *unstableWebsocketConn) dropConn() { 20 | fmt.Println("## DROP CONN") 21 | _ = u.wsconn.Conn.Close(websocket.StatusProtocolError, "conn drop") 22 | } 23 | 24 | func (u *unstableWebsocketConn) ReadJSON(v interface{}) error { 25 | return u.wsconn.ReadJSON(v) 26 | } 27 | 28 | func (u *unstableWebsocketConn) WriteJSON(v interface{}) error { 29 | return u.wsconn.WriteJSON(v) 30 | } 31 | 32 | func (u *unstableWebsocketConn) Close() error { 33 | return u.wsconn.Close() 34 | } 35 | 36 | func (u *unstableWebsocketConn) SetReadLimit(limit int64) { 37 | u.wsconn.SetReadLimit(limit) 38 | } 39 | 40 | func newUnstableConn(ctx context.Context, URL string) (transport.WebsocketConn, error) { 41 | wsconn, err := transport.DefaultWebsocketConnProvider(time.Second)(ctx, URL) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return &unstableWebsocketConn{ 47 | ctx: ctx, 48 | wsconn: wsconn.(*transport.WebsocketHandler), 49 | }, nil 50 | } 51 | 52 | func unstablewscli(ctx context.Context, newWebsocketConn transport.WebsocketConnProvider) (*client.Client, func()) { 53 | return clifactory(ctx, func(ts *httptest.Server) (transport.Transport, func()) { 54 | tr := cwstr(ctx, ts.URL, newWebsocketConn) 55 | 56 | return tr, func() { 57 | tr.Close() 58 | } 59 | }) 60 | } 61 | 62 | func TestRawWSUnstableQuery(t *testing.T) { 63 | ctx := context.Background() 64 | 65 | cli, teardown := unstablewscli(ctx, newUnstableConn) 66 | defer teardown() 67 | tr := cli.Transport.(*transport.Ws) 68 | 69 | for i := 0; i < 5; i++ { 70 | fmt.Println("> Attempt", i) 71 | tr.GetConn().(*unstableWebsocketConn).dropConn() 72 | 73 | tr.WaitFor(transport.StatusReady, time.Second) 74 | 75 | runAssertQuery(t, ctx, cli) 76 | } 77 | } 78 | 79 | func TestRawWSUnstableSubscription(t *testing.T) { 80 | ctx := context.Background() 81 | 82 | cli, teardown := unstablewscli(ctx, newUnstableConn) 83 | defer teardown() 84 | tr := cli.Transport.(*transport.Ws) 85 | 86 | for i := 0; i < 5; i++ { 87 | fmt.Println("> Attempt", i) 88 | tr.GetConn().(*unstableWebsocketConn).dropConn() 89 | 90 | tr.WaitFor(transport.StatusReady, time.Second) 91 | 92 | runAssertSub(t, ctx, cli) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /example/client_ws_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "context" 5 | "github.com/infiotinc/gqlgenc/client" 6 | "github.com/infiotinc/gqlgenc/client/transport" 7 | "net/http/httptest" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func wscli(ctx context.Context) (*client.Client, func()) { 13 | return clifactory(ctx, func(ts *httptest.Server) (transport.Transport, func()) { 14 | tr := wstr(ctx, ts.URL) 15 | 16 | return tr, func() { 17 | tr.Close() 18 | } 19 | }) 20 | } 21 | 22 | func TestRawWSQuery(t *testing.T) { 23 | ctx := context.Background() 24 | 25 | cli, teardown := wscli(ctx) 26 | defer teardown() 27 | 28 | runAssertQuery(t, ctx, cli) 29 | } 30 | 31 | func TestRawWSSubscription(t *testing.T) { 32 | ctx := context.Background() 33 | 34 | cli, teardown := wscli(ctx) 35 | defer teardown() 36 | 37 | runAssertSub(t, ctx, cli) 38 | } 39 | 40 | func TestWSCtxCancel1(t *testing.T) { 41 | ctx, cancel := context.WithCancel(context.Background()) 42 | 43 | cli, teardown := wscli(ctx) 44 | 45 | runAssertQuery(t, ctx, cli) 46 | 47 | teardown() 48 | time.Sleep(time.Second) 49 | cancel() 50 | } 51 | 52 | func TestWSCtxCancel2(t *testing.T) { 53 | ctx, cancel := context.WithCancel(context.Background()) 54 | 55 | cli, teardown := wscli(ctx) 56 | 57 | runAssertQuery(t, ctx, cli) 58 | 59 | cancel() 60 | time.Sleep(time.Second) 61 | teardown() 62 | } 63 | -------------------------------------------------------------------------------- /example/cmd/memleak.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "example" 6 | "example/server" 7 | "example/server/generated" 8 | _ "expvar" // Register the expvar handlers 9 | "fmt" 10 | "github.com/99designs/gqlgen/graphql" 11 | "github.com/99designs/gqlgen/graphql/handler" 12 | "github.com/99designs/gqlgen/graphql/handler/extension" 13 | htransport "github.com/99designs/gqlgen/graphql/handler/transport" 14 | "github.com/99designs/gqlgen/graphql/playground" 15 | "github.com/gorilla/websocket" 16 | "github.com/infiotinc/gqlgenc/client" 17 | "github.com/infiotinc/gqlgenc/client/extensions" 18 | "github.com/infiotinc/gqlgenc/client/transport" 19 | "net/http" 20 | _ "net/http/pprof" // Register the pprof handlers 21 | "runtime" 22 | "sync" 23 | "sync/atomic" 24 | "time" 25 | ) 26 | 27 | func round(cli *client.Client) { 28 | var v interface{} 29 | _, err := cli.Query(context.Background(), "", example.RoomQuery, map[string]interface{}{"name": "test"}, &v) 30 | if err != nil { 31 | fmt.Println("ERROR QUERY: ", err) 32 | } 33 | 34 | res := cli.Subscription(context.Background(), "", example.MessagesSub, nil) 35 | if err != nil { 36 | fmt.Println("ERROR SUB: ", err) 37 | } 38 | defer res.Close() 39 | 40 | for res.Next() { 41 | // Consume res 42 | } 43 | 44 | if res.Err() != nil { 45 | fmt.Println("ERROR SUB RES: ", res.Err()) 46 | } 47 | } 48 | 49 | func main() { 50 | srv := handler.New(generated.NewExecutableSchema(generated.Config{ 51 | Resolvers: &server.Resolver{}, 52 | })) 53 | 54 | srv.AddTransport(htransport.POST{}) 55 | srv.AddTransport(htransport.Websocket{ 56 | KeepAlivePingInterval: 500 * time.Millisecond, 57 | Upgrader: websocket.Upgrader{ 58 | CheckOrigin: func(r *http.Request) bool { 59 | return true 60 | }, 61 | }, 62 | }) 63 | srv.Use(extension.AutomaticPersistedQuery{Cache: graphql.MapCache{}}) 64 | srv.Use(extension.Introspection{}) 65 | 66 | http.Handle("/playground", playground.Handler("Playground", "/")) 67 | http.Handle("/", srv) 68 | 69 | go func() { 70 | err := http.ListenAndServe(":8080", nil) 71 | if err != nil { 72 | panic(err) 73 | } 74 | }() 75 | 76 | httptr := &transport.Http{ 77 | URL: "http://localhost:8080", 78 | } 79 | wstr := &transport.Ws{ 80 | URL: "ws://localhost:8080", 81 | WebsocketConnProvider: transport.DefaultWebsocketConnProvider(time.Second), 82 | } 83 | wstr.Start(context.Background()) 84 | defer wstr.Close() 85 | 86 | tr := transport.SplitSubscription(wstr, httptr) 87 | 88 | cli := &client.Client{ 89 | Transport: tr, 90 | } 91 | cli.Use(&extensions.APQ{}) 92 | 93 | pm := collectMemStats() 94 | 95 | fmt.Println("Starting queries") 96 | 97 | var wg sync.WaitGroup 98 | ch := make(chan struct{}, 5) // Concurrency 99 | var di int64 100 | for i := 0; i < 100_000; i++ { 101 | wg.Add(1) 102 | 103 | ch <- struct{}{} 104 | go func() { 105 | round(cli) 106 | di := atomic.AddInt64(&di, 1) 107 | if di%1000 == 0 { 108 | fmt.Println(di) 109 | } 110 | <-ch 111 | wg.Done() 112 | }() 113 | } 114 | 115 | wg.Wait() 116 | 117 | time.Sleep(2 * time.Second) 118 | 119 | m := collectMemStats() 120 | 121 | printMemStats("Before", pm) 122 | printMemStats("After ", m) 123 | } 124 | 125 | func collectMemStats() runtime.MemStats { 126 | fmt.Println("Running GC") 127 | runtime.GC() 128 | 129 | fmt.Println("Collecting MemStats") 130 | var m runtime.MemStats 131 | runtime.ReadMemStats(&m) 132 | return m 133 | } 134 | 135 | func printMemStats(s string, m runtime.MemStats) { 136 | fmt.Printf("%v: ", s) 137 | fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc)) 138 | fmt.Printf("\tTotalAlloc = %v MiB", bToMb(m.TotalAlloc)) 139 | fmt.Printf("\tSys = %v MiB", bToMb(m.Sys)) 140 | fmt.Printf("\tNumGC = %v\n", m.NumGC) 141 | } 142 | 143 | func bToMb(b uint64) uint64 { 144 | return b / 1024 / 1024 145 | } 146 | -------------------------------------------------------------------------------- /example/codegen_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "context" 5 | "example/client" 6 | client2 "github.com/infiotinc/gqlgenc/client" 7 | "github.com/infiotinc/gqlgenc/client/transport" 8 | "github.com/stretchr/testify/assert" 9 | "io/ioutil" 10 | "net/http/httptest" 11 | "os" 12 | "reflect" 13 | "testing" 14 | ) 15 | 16 | func TestSubscription(t *testing.T) { 17 | t.Parallel() 18 | 19 | ctx := context.Background() 20 | 21 | cli, td, _ := splitcli(ctx) 22 | defer td() 23 | 24 | gql := &client.Client{ 25 | Client: cli, 26 | } 27 | 28 | ch, stop := gql.SubscribeMessageAdded(ctx) 29 | defer stop() 30 | 31 | ids := make([]string, 0) 32 | 33 | for msg := range ch { 34 | if msg.Error != nil { 35 | t.Fatal(msg.Error) 36 | } 37 | 38 | ids = append(ids, msg.Data.MessageAdded.ID) 39 | } 40 | 41 | assert.Len(t, ids, 3) 42 | } 43 | 44 | func isPointer(v interface{}) bool { 45 | return reflect.ValueOf(v).Kind() == reflect.Ptr 46 | } 47 | 48 | func TestQuery(t *testing.T) { 49 | t.Parallel() 50 | 51 | ctx := context.Background() 52 | 53 | cli, td, _ := splitcli(ctx) 54 | defer td() 55 | 56 | gql := &client.Client{ 57 | Client: cli, 58 | } 59 | 60 | room, _, err := gql.GetRoom(ctx, "test") 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | assert.Equal(t, "test", room.Room.Name) 66 | assert.True(t, isPointer(room.Room), "room must be a pointer") 67 | } 68 | 69 | func TestQueryNonNull(t *testing.T) { 70 | t.Parallel() 71 | 72 | ctx := context.Background() 73 | 74 | cli, td, _ := splitcli(ctx) 75 | defer td() 76 | 77 | gql := &client.Client{ 78 | Client: cli, 79 | } 80 | 81 | room, _, err := gql.GetRoomNonNull(ctx, "test") 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | assert.Equal(t, "test", room.RoomNonNull.Name) 87 | assert.False(t, isPointer(room.RoomNonNull), "room must not be a pointer") 88 | } 89 | 90 | func TestQueryCustomType(t *testing.T) { 91 | t.Parallel() 92 | 93 | ctx := context.Background() 94 | 95 | cli, td, _ := splitcli(ctx) 96 | defer td() 97 | 98 | gql := &client.Client{ 99 | Client: cli, 100 | } 101 | 102 | room, _, err := gql.GetRoomCustom(ctx, "test") 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | 107 | assert.Equal(t, "Room: test", room.String()) 108 | } 109 | 110 | func TestQueryFragment(t *testing.T) { 111 | t.Parallel() 112 | 113 | ctx := context.Background() 114 | 115 | cli, td, _ := splitcli(ctx) 116 | defer td() 117 | 118 | gql := &client.Client{ 119 | Client: cli, 120 | } 121 | 122 | res, _, err := gql.GetRoomFragment(ctx, "test") 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | 127 | assert.Equal(t, "test", res.Room.Name) 128 | } 129 | 130 | func TestQueryUnion(t *testing.T) { 131 | t.Parallel() 132 | 133 | ctx := context.Background() 134 | 135 | cli, td, _ := splitcli(ctx) 136 | defer td() 137 | 138 | gql := &client.Client{ 139 | Client: cli, 140 | } 141 | 142 | res, _, err := gql.GetMedias(ctx) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | 147 | assert.Len(t, res.Medias, 2) 148 | 149 | assert.Equal(t, int64(100), res.Medias[0].Image.Size) 150 | assert.Equal(t, int64(200), res.Medias[1].Video.Duration) 151 | } 152 | 153 | func TestQueryInterface(t *testing.T) { 154 | t.Parallel() 155 | 156 | ctx := context.Background() 157 | 158 | cli, td, _ := splitcli(ctx) 159 | defer td() 160 | 161 | gql := &client.Client{ 162 | Client: cli, 163 | } 164 | 165 | res, _, err := gql.GetBooks(ctx) 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | 170 | assert.Len(t, res.Books, 2) 171 | 172 | assert.Equal(t, "Some textbook", res.Books[0].Title) 173 | assert.Equal(t, []string{"course 1", "course 2"}, res.Books[0].Textbook.Courses) 174 | 175 | assert.Equal(t, "Some Coloring Book", res.Books[1].Title) 176 | assert.Equal(t, []string{"red", "blue"}, res.Books[1].ColoringBook.Colors) 177 | } 178 | 179 | func TestMutationInput(t *testing.T) { 180 | t.Parallel() 181 | 182 | ctx := context.Background() 183 | 184 | cli, td, _ := splitcli(ctx) 185 | defer td() 186 | 187 | gql := &client.Client{ 188 | Client: cli, 189 | } 190 | 191 | res, _, err := gql.CreatePost(ctx, client.PostCreateInput{ 192 | Text: "some text", 193 | }) 194 | if err != nil { 195 | t.Fatal(err) 196 | } 197 | 198 | assert.Equal(t, "some text", res.Post.Text) 199 | } 200 | 201 | func uploadcli(ctx context.Context) (*client2.Client, func()) { 202 | return clifactory(ctx, func(ts *httptest.Server) (transport.Transport, func()) { 203 | tr := httptr(ctx, ts.URL) 204 | tr.UseFormMultipart = true 205 | 206 | return tr, nil 207 | }) 208 | } 209 | 210 | func createUploadFile(t *testing.T) (transport.Upload, int64, func()) { 211 | f, err := ioutil.TempFile("", "") 212 | if err != nil { 213 | t.Fatal(err) 214 | } 215 | 216 | _, err = f.WriteString("some content") 217 | if err != nil { 218 | t.Fatal(err) 219 | } 220 | 221 | err = f.Sync() 222 | if err != nil { 223 | t.Fatal(err) 224 | } 225 | 226 | _, err = f.Seek(0, 0) 227 | if err != nil { 228 | t.Fatal(err) 229 | } 230 | 231 | up := transport.NewUpload(f) 232 | 233 | return up, 12, func() { 234 | os.Remove(f.Name()) 235 | } 236 | } 237 | 238 | func TestMutationUploadFile(t *testing.T) { 239 | t.Parallel() 240 | 241 | ctx := context.Background() 242 | 243 | cli, td := uploadcli(ctx) 244 | defer td() 245 | 246 | gql := &client.Client{ 247 | Client: cli, 248 | } 249 | 250 | up, l, rm := createUploadFile(t) 251 | defer rm() 252 | 253 | res, _, err := gql.UploadFile(ctx, up) 254 | if err != nil { 255 | t.Fatal(err) 256 | } 257 | 258 | assert.Equal(t, l, res.UploadFile.Size) 259 | } 260 | 261 | func TestMutationUploadFiles(t *testing.T) { 262 | t.Parallel() 263 | 264 | ctx := context.Background() 265 | 266 | cli, td := uploadcli(ctx) 267 | defer td() 268 | 269 | gql := &client.Client{ 270 | Client: cli, 271 | } 272 | 273 | up, l, rm := createUploadFile(t) 274 | defer rm() 275 | 276 | res, _, err := gql.UploadFiles(ctx, []*transport.Upload{&up}) 277 | if err != nil { 278 | t.Fatal(err) 279 | } 280 | 281 | assert.Equal(t, l, res.UploadFiles[0].Size) 282 | } 283 | 284 | func TestMutationUploadFilesMap(t *testing.T) { 285 | t.Parallel() 286 | 287 | ctx := context.Background() 288 | 289 | cli, td := uploadcli(ctx) 290 | defer td() 291 | 292 | gql := &client.Client{ 293 | Client: cli, 294 | } 295 | 296 | up, l, rm := createUploadFile(t) 297 | defer rm() 298 | 299 | res, _, err := gql.UploadFilesMap(ctx, client.UploadFilesMapInput{ 300 | Somefile: up, 301 | }) 302 | if err != nil { 303 | t.Fatal(err) 304 | } 305 | 306 | assert.Equal(t, l, res.UploadFilesMap.Somefile.Size) 307 | } 308 | 309 | func TestIssue8(t *testing.T) { 310 | t.Parallel() 311 | 312 | ctx := context.Background() 313 | 314 | cli, td := uploadcli(ctx) 315 | defer td() 316 | 317 | gql := &client.Client{ 318 | Client: cli, 319 | } 320 | 321 | res, _, err := gql.Issue8(ctx) 322 | if err != nil { 323 | t.Fatal(err) 324 | } 325 | 326 | assert.False(t, isPointer(res.Issue8.Foo1)) 327 | assert.True(t, isPointer(res.Issue8.Foo2)) 328 | 329 | assert.Equal(t, "foo1", res.Issue8.Foo1.A.Aa) 330 | assert.Equal(t, "foo2", res.Issue8.Foo2.A.Aa) 331 | } 332 | 333 | func TestEnum(t *testing.T) { 334 | t.Parallel() 335 | 336 | ctx := context.Background() 337 | 338 | cli, td := uploadcli(ctx) 339 | defer td() 340 | 341 | gql := &client.Client{ 342 | Client: cli, 343 | } 344 | 345 | res, _, err := gql.GetEpisodes(ctx) 346 | if err != nil { 347 | t.Fatal(err) 348 | } 349 | 350 | assert.Equal(t, client.EpisodeJedi, res.Episodes[0]) 351 | assert.Equal(t, client.EpisodeNewhope, res.Episodes[1]) 352 | assert.Equal(t, client.EpisodeEmpire, res.Episodes[2]) 353 | } 354 | 355 | func TestInputAsMapReq(t *testing.T) { 356 | t.Parallel() 357 | 358 | ctx := context.Background() 359 | 360 | cli, td := uploadcli(ctx) 361 | defer td() 362 | 363 | gql := &client.Client{ 364 | Client: cli, 365 | } 366 | 367 | res, _, err := gql.AsMap( 368 | ctx, 369 | client.NewAsMapInput("str1", client.EpisodeJedi).WithOptEp(nil), 370 | nil, 371 | ) 372 | if err != nil { 373 | t.Fatal(err) 374 | } 375 | 376 | assert.Equal(t, "req: map[optEp: reqEp:JEDI reqStr:str1] opt: map[]", res.AsMap) 377 | } 378 | 379 | func TestInputAsMapOpt(t *testing.T) { 380 | t.Parallel() 381 | 382 | ctx := context.Background() 383 | 384 | cli, td := uploadcli(ctx) 385 | defer td() 386 | 387 | gql := &client.Client{ 388 | Client: cli, 389 | } 390 | 391 | res, _, err := gql.AsMap( 392 | ctx, 393 | client.NewAsMapInput("str1", client.EpisodeJedi), 394 | client.AsMapInputPtr( 395 | client.NewAsMapInput("str2", client.EpisodeEmpire). 396 | WithOptStr(client.StringPtr("str3")). 397 | WithOptEp(client.EpisodePtr(client.EpisodeNewhope)), 398 | ), 399 | ) 400 | if err != nil { 401 | t.Fatal(err) 402 | } 403 | 404 | assert.Equal(t, "req: map[reqEp:JEDI reqStr:str1] opt: map[optEp:NEWHOPE optStr:str3 reqEp:EMPIRE reqStr:str2]", res.AsMap) 405 | } 406 | 407 | func TestGenExtraType(t *testing.T) { 408 | t.Parallel() 409 | 410 | // This should fail compiling if the types are missing 411 | _ = client.SomeExtraType{} 412 | _ = client.SomeExtraTypeChild{} 413 | _ = client.SomeExtraTypeChildChild{} 414 | } 415 | 416 | func TestGenHelpers(t *testing.T) { 417 | t.Parallel() 418 | 419 | // This should fail compiling if the helpers are missing 420 | _ = client.StringPtr 421 | _ = client.EpisodePtr 422 | _ = client.AsMapInputPtr 423 | 424 | _ = client.OptionalValue2Ptr 425 | _ = client.Value1Ptr 426 | _ = client.Value2Ptr 427 | } 428 | -------------------------------------------------------------------------------- /example/common_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "context" 5 | "example/server" 6 | "example/server/generated" 7 | "fmt" 8 | "github.com/99designs/gqlgen/graphql" 9 | "github.com/99designs/gqlgen/graphql/handler" 10 | "github.com/99designs/gqlgen/graphql/handler/extension" 11 | htransport "github.com/99designs/gqlgen/graphql/handler/transport" 12 | "github.com/99designs/gqlgen/graphql/playground" 13 | "github.com/gorilla/websocket" 14 | "github.com/infiotinc/gqlgenc/client" 15 | "github.com/infiotinc/gqlgenc/client/transport" 16 | "github.com/stretchr/testify/assert" 17 | "log" 18 | "net/http" 19 | "net/http/httptest" 20 | "os" 21 | "strings" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | func wstr(ctx context.Context, u string) *transport.Ws { 27 | return cwstr( 28 | ctx, 29 | u, 30 | nil, 31 | ) 32 | } 33 | 34 | func cwstr(ctx context.Context, u string, newWebsocketConn transport.WebsocketConnProvider) *transport.Ws { 35 | _ = os.Setenv("GQLGENC_WS_LOG", "1") 36 | 37 | if strings.HasPrefix(u, "http") { 38 | u = "ws" + strings.TrimPrefix(u, "http") 39 | } 40 | 41 | tr := &transport.Ws{ 42 | URL: u, 43 | WebsocketConnProvider: newWebsocketConn, 44 | } 45 | errCh := tr.Start(ctx) 46 | go func() { 47 | for err := range errCh { 48 | log.Println("Ws Transport error: ", err) 49 | } 50 | }() 51 | 52 | tr.WaitFor(transport.StatusReady, time.Second) 53 | 54 | return tr 55 | } 56 | 57 | func httptr(ctx context.Context, u string) *transport.Http { 58 | tr := &transport.Http{ 59 | URL: u, 60 | } 61 | 62 | return tr 63 | } 64 | 65 | func clifactory(ctx context.Context, trf func(server *httptest.Server) (transport.Transport, func())) (*client.Client, func()) { 66 | return clifactorywith(ctx, trf, func(h http.Handler) http.Handler { 67 | return h 68 | }) 69 | } 70 | 71 | func clifactorywith(ctx context.Context, trf func(server *httptest.Server) (transport.Transport, func()), hw func(http.Handler) http.Handler) (*client.Client, func()) { 72 | h := handler.New(generated.NewExecutableSchema(generated.Config{ 73 | Resolvers: &server.Resolver{}, 74 | })) 75 | 76 | h.AddTransport(htransport.POST{}) 77 | h.AddTransport(htransport.MultipartForm{}) 78 | h.AddTransport(htransport.Websocket{ 79 | KeepAlivePingInterval: 500 * time.Millisecond, 80 | Upgrader: websocket.Upgrader{ 81 | CheckOrigin: func(r *http.Request) bool { 82 | return true 83 | }, 84 | }, 85 | InitFunc: func(ctx context.Context, initPayload htransport.InitPayload) (context.Context, error) { 86 | fmt.Println("WS Server init received") 87 | 88 | return ctx, nil 89 | }, 90 | }) 91 | h.Use(extension.AutomaticPersistedQuery{Cache: graphql.MapCache{}}) 92 | h.Use(extension.Introspection{}) 93 | 94 | h.AroundResponses(func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response { 95 | stats := extension.GetApqStats(ctx) 96 | 97 | if stats != nil { 98 | graphql.RegisterExtension(ctx, "apqStats", stats) 99 | } 100 | 101 | return next(ctx) 102 | }) 103 | 104 | srv := http.NewServeMux() 105 | srv.Handle("/playground", playground.Handler("Playground", "/")) 106 | srv.Handle("/", hw(h)) 107 | 108 | ts := httptest.NewServer(srv) 109 | 110 | fmt.Println("TS URL: ", ts.URL) 111 | 112 | tr, trteardown := trf(ts) 113 | 114 | return &client.Client{ 115 | Transport: tr, 116 | }, func() { 117 | if trteardown != nil { 118 | fmt.Println("CLOSE TR") 119 | trteardown() 120 | } 121 | 122 | if ts != nil { 123 | fmt.Println("CLOSE HTTPTEST") 124 | ts.Close() 125 | } 126 | } 127 | } 128 | 129 | type QueryAsserter func(transport.OperationResponse, RoomQueryResponse) 130 | 131 | func runAssertQuery(t *testing.T, ctx context.Context, cli *client.Client, asserters ...QueryAsserter) { 132 | fmt.Println("ASSERT QUERY") 133 | var data RoomQueryResponse 134 | opres, err := cli.Query(ctx, "", RoomQuery, map[string]interface{}{"name": "test"}, &data) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | 139 | assert.Equal(t, "test", data.Room.Name) 140 | 141 | for _, a := range asserters { 142 | a(opres, data) 143 | } 144 | } 145 | 146 | type SubAsserter func(transport.OperationResponse, MessagesSubResponse) 147 | 148 | func runAssertSub(t *testing.T, ctx context.Context, cli *client.Client, asserters ...SubAsserter) { 149 | fmt.Println("ASSERT SUB") 150 | res := cli.Subscription(ctx, "", MessagesSub, nil) 151 | 152 | ids := make([]string, 0) 153 | 154 | for res.Next() { 155 | opres := res.Get() 156 | 157 | var data MessagesSubResponse 158 | err := opres.UnmarshalData(&data) 159 | if err != nil { 160 | t.Fatal(err) 161 | } 162 | ids = append(ids, data.MessageAdded.ID) 163 | 164 | for _, a := range asserters { 165 | a(opres, data) 166 | } 167 | } 168 | 169 | if err := res.Err(); err != nil { 170 | t.Fatal(err) 171 | } 172 | 173 | assert.Len(t, ids, 3) 174 | } 175 | -------------------------------------------------------------------------------- /example/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | replace github.com/infiotinc/gqlgenc => ../ 4 | 5 | go 1.15 6 | 7 | require ( 8 | github.com/99designs/gqlgen v0.16.0 9 | github.com/gorilla/websocket v1.4.2 10 | github.com/infiotinc/gqlgenc v0.0.0-00010101000000-000000000000 11 | github.com/pkg/errors v0.9.1 12 | github.com/stretchr/testify v1.4.0 13 | github.com/urfave/cli/v2 v2.3.0 14 | github.com/vektah/gqlparser/v2 v2.2.0 15 | golang.org/x/tools v0.1.5 16 | nhooyr.io/websocket v1.8.7 17 | ) 18 | -------------------------------------------------------------------------------- /example/go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/gqlgen v0.16.0 h1:7Qc4Ll3mfN3doAyUWOgtGLcBGu+KDgK48HdkBGLZVFs= 2 | github.com/99designs/gqlgen v0.16.0/go.mod h1:nbeSjFkqphIqpZsYe1ULVz0yfH8hjpJdJIQoX/e0G2I= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= 5 | github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM= 6 | github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= 7 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= 8 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 9 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= 10 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= 11 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= 18 | github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= 19 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 20 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 21 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 22 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 23 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 24 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 25 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 26 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 27 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 28 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 29 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 30 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 31 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= 32 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 33 | github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= 34 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 35 | github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= 36 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 37 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 38 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= 39 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 40 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 42 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 43 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 44 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 45 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 46 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 47 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 48 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 49 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 50 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 51 | github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM= 52 | github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= 53 | github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 54 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 55 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 56 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 57 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 58 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 59 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 60 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 61 | github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= 62 | github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE= 63 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 64 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 65 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 66 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 67 | github.com/mitchellh/mapstructure v1.2.3 h1:f/MjBEBDLttYCGfRaKBbKSRVF5aV2O6fnBpzknuE3jU= 68 | github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 69 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 70 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 71 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 72 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 73 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 74 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 75 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 76 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 77 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 78 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 79 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 80 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 81 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 82 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 83 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 84 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 85 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 86 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 87 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 88 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 89 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 90 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 91 | github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= 92 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 93 | github.com/vektah/gqlparser/v2 v2.2.0 h1:bAc3slekAAJW6sZTi07aGq0OrfaCjj4jxARAaC7g2EM= 94 | github.com/vektah/gqlparser/v2 v2.2.0/go.mod h1:i3mQIGIrbK2PD1RrCeMTlVbkF2FJ6WkU1KJlJlC+3F4= 95 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 96 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 97 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 98 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 99 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 100 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 101 | golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= 102 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 103 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 104 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 105 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 106 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 107 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 109 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 110 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 111 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 112 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= 118 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 119 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 120 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 121 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 122 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 123 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 124 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 125 | golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 126 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 127 | golang.org/x/tools v0.0.0-20200815165600-90abf76919f3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 128 | golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= 129 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 130 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 131 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 132 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 133 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 134 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 136 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 137 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 138 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 139 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 140 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 141 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 142 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 143 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 144 | nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= 145 | nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= 146 | -------------------------------------------------------------------------------- /example/query.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | const RoomQuery = ` 4 | query query($name: String!) { 5 | room(name: $name) { 6 | name 7 | } 8 | } 9 | ` 10 | 11 | type RoomQueryResponse struct { 12 | Room struct { 13 | Name string `json:"name"` 14 | } `json:"room"` 15 | } 16 | 17 | const MessagesSub = ` 18 | subscription query{ 19 | messageAdded(roomName: "test") { 20 | id 21 | } 22 | } 23 | ` 24 | 25 | type MessagesSubResponse struct { 26 | MessageAdded struct { 27 | ID string `json:"id"` 28 | } `json:"messageAdded"` 29 | } 30 | -------------------------------------------------------------------------------- /example/query.graphql: -------------------------------------------------------------------------------- 1 | query GetRoom($name: String!) { 2 | room(name: $name) { 3 | name 4 | hash 5 | } 6 | } 7 | 8 | query GetRoomNonNull($name: String!) { 9 | roomNonNull(name: $name) { 10 | name 11 | } 12 | } 13 | 14 | fragment RoomFragment on Chatroom { 15 | name 16 | } 17 | 18 | query GetRoomFragment($name: String!) { 19 | room(name: $name) { 20 | ... RoomFragment 21 | } 22 | } 23 | 24 | query GetRoomCustom($name: String!) { 25 | room(name: $name) { 26 | name 27 | } 28 | } 29 | 30 | query GetMedias { 31 | medias { 32 | ... on Image { 33 | size 34 | } 35 | ... on Video { 36 | duration 37 | } 38 | } 39 | } 40 | 41 | query GetBooks { 42 | books { 43 | title 44 | ... on Textbook { 45 | courses 46 | } 47 | ... on ColoringBook { 48 | colors 49 | } 50 | } 51 | } 52 | 53 | subscription SubscribeMessageAdded { 54 | messageAdded(roomName: "test") { 55 | id 56 | } 57 | } 58 | 59 | mutation CreatePost($input: PostCreateInput!) { 60 | post(input: $input) { 61 | id 62 | text 63 | } 64 | } 65 | 66 | mutation UploadFile($file: Upload!) { 67 | uploadFile(file: $file) { 68 | size 69 | } 70 | } 71 | 72 | mutation UploadFiles($files: [Upload!]!) { 73 | uploadFiles(files: $files) { 74 | size 75 | } 76 | } 77 | 78 | mutation UploadFilesMap($files: UploadFilesMapInput!) { 79 | uploadFilesMap(files: $files) { 80 | somefile { 81 | size 82 | } 83 | } 84 | } 85 | 86 | query Issue8 { 87 | issue8 { 88 | foo1 { 89 | a { 90 | Aa 91 | } 92 | } 93 | foo2 { 94 | a { 95 | Aa 96 | } 97 | } 98 | } 99 | } 100 | 101 | query GetEpisodes { 102 | episodes 103 | } 104 | 105 | query Cyclic1 { 106 | cyclic { 107 | child { 108 | child { 109 | child { 110 | id 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | query AsMap($req: AsMapInput!, $opt: AsMapInput) { 118 | asMap(req: $req, opt: $opt) 119 | } 120 | 121 | query OptValue1($v: OptionalValue1!) { 122 | optValue1(req: $v) 123 | } 124 | 125 | query OptValue2($v: OptionalValue2) { 126 | optValue2(opt: $v) 127 | } 128 | -------------------------------------------------------------------------------- /example/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar Time 2 | scalar Upload 3 | 4 | union Media = Image | Video 5 | 6 | type Image { 7 | size: Int! 8 | } 9 | 10 | type Video { 11 | duration: Int! 12 | } 13 | 14 | interface Book { 15 | title: String! 16 | } 17 | 18 | type Textbook implements Book { 19 | title: String! 20 | courses: [String!]! 21 | } 22 | 23 | type ColoringBook implements Book { 24 | title: String! 25 | colors: [String!]! 26 | } 27 | 28 | type Chatroom { 29 | name: String! 30 | messages: [Message!]! 31 | hash: FooType_hash1 32 | } 33 | 34 | type Message { 35 | id: ID! 36 | text: String! 37 | createdBy: String! 38 | createdAt: Time! 39 | } 40 | 41 | scalar Value1 42 | input OptionalValue1 { 43 | value: Value1 44 | } 45 | 46 | scalar Value2 47 | input OptionalValue2 { 48 | value: Value2 49 | } 50 | 51 | type Query { 52 | room(name:String!): Chatroom 53 | roomNonNull(name:String!): Chatroom! 54 | medias: [Media!]! 55 | books: [Book!]! 56 | issue8: Issue8Payload 57 | cyclic: Cyclic1_1 58 | episodes: [Episode!]! 59 | asMap(req: AsMapInput!, opt: AsMapInput): String! 60 | optValue1(req: OptionalValue1!): Boolean 61 | optValue2(opt: OptionalValue2): Boolean 62 | } 63 | 64 | type Issue8Payload { 65 | foo1: Issue8PayloadFoo! 66 | foo2: Issue8PayloadFoo 67 | } 68 | 69 | type Issue8PayloadFoo { 70 | a: Issue8PayloadFooA! 71 | } 72 | 73 | type Issue8PayloadFooA { 74 | Aa: String! 75 | } 76 | 77 | input PostCreateInput { 78 | text: String! 79 | } 80 | 81 | type UploadData { 82 | size: Int! 83 | } 84 | 85 | 86 | input UploadFilesMapInput { 87 | somefile: Upload! 88 | } 89 | 90 | type UploadFilesMap { 91 | somefile: UploadData! 92 | } 93 | 94 | type Mutation { 95 | post(input: PostCreateInput!): Message! 96 | uploadFile(file: Upload!): UploadData! 97 | uploadFiles(files: [Upload!]!): [UploadData!] 98 | uploadFilesMap(files: UploadFilesMapInput!): UploadFilesMap! 99 | } 100 | 101 | type Subscription { 102 | messageAdded(roomName: String!): Message! 103 | } 104 | 105 | # Should be generated by the config 106 | type SomeExtraType { 107 | child: SomeExtraTypeChild! 108 | } 109 | 110 | type SomeExtraTypeChild { 111 | child: SomeExtraTypeChildChild! 112 | } 113 | 114 | type SomeExtraTypeChildChild { 115 | id: String! 116 | } 117 | 118 | enum Episode { 119 | NEWHOPE 120 | EMPIRE 121 | JEDI 122 | } 123 | 124 | type Cyclic1_1 { 125 | id: String! 126 | child: Cyclic1_2 127 | } 128 | 129 | type Cyclic1_2 { 130 | id: String! 131 | child: Cyclic1_1 132 | } 133 | 134 | type Cyclic2_1 { 135 | id: String! 136 | child: Cyclic2_2 137 | } 138 | 139 | type Cyclic2_2 { 140 | id: String! 141 | child: Cyclic2_1 142 | } 143 | 144 | input AsMapInput { 145 | reqStr: String! 146 | optStr: String 147 | 148 | reqEp: Episode! 149 | optEp: Episode 150 | } 151 | 152 | input InputIssue14 { 153 | ids: [String!] 154 | } 155 | 156 | enum FooType_hash1 { 157 | hash_1 158 | hash_2 159 | } 160 | -------------------------------------------------------------------------------- /example/server/model/models_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package model 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/99designs/gqlgen/graphql" 12 | ) 13 | 14 | type Book interface { 15 | IsBook() 16 | } 17 | 18 | type Media interface { 19 | IsMedia() 20 | } 21 | 22 | type Chatroom struct { 23 | Name string `json:"name"` 24 | Messages []*Message `json:"messages"` 25 | Hash *FooTypeHash1 `json:"hash"` 26 | } 27 | 28 | type ColoringBook struct { 29 | Title string `json:"title"` 30 | Colors []string `json:"colors"` 31 | } 32 | 33 | func (ColoringBook) IsBook() {} 34 | 35 | type Cyclic1_1 struct { 36 | ID string `json:"id"` 37 | Child *Cyclic1_2 `json:"child"` 38 | } 39 | 40 | type Cyclic1_2 struct { 41 | ID string `json:"id"` 42 | Child *Cyclic1_1 `json:"child"` 43 | } 44 | 45 | type Cyclic2_1 struct { 46 | ID string `json:"id"` 47 | Child *Cyclic2_2 `json:"child"` 48 | } 49 | 50 | type Cyclic2_2 struct { 51 | ID string `json:"id"` 52 | Child *Cyclic2_1 `json:"child"` 53 | } 54 | 55 | type Image struct { 56 | Size int `json:"size"` 57 | } 58 | 59 | func (Image) IsMedia() {} 60 | 61 | type InputIssue14 struct { 62 | Ids []string `json:"ids"` 63 | } 64 | 65 | type Issue8Payload struct { 66 | Foo1 *Issue8PayloadFoo `json:"foo1"` 67 | Foo2 *Issue8PayloadFoo `json:"foo2"` 68 | } 69 | 70 | type Issue8PayloadFoo struct { 71 | A *Issue8PayloadFooA `json:"a"` 72 | } 73 | 74 | type Issue8PayloadFooA struct { 75 | Aa string `json:"Aa"` 76 | } 77 | 78 | type Message struct { 79 | ID string `json:"id"` 80 | Text string `json:"text"` 81 | CreatedBy string `json:"createdBy"` 82 | CreatedAt time.Time `json:"createdAt"` 83 | } 84 | 85 | type OptionalValue1 struct { 86 | Value *string `json:"value"` 87 | } 88 | 89 | type OptionalValue2 struct { 90 | Value *string `json:"value"` 91 | } 92 | 93 | type PostCreateInput struct { 94 | Text string `json:"text"` 95 | } 96 | 97 | type SomeExtraType struct { 98 | Child *SomeExtraTypeChild `json:"child"` 99 | } 100 | 101 | type SomeExtraTypeChild struct { 102 | Child *SomeExtraTypeChildChild `json:"child"` 103 | } 104 | 105 | type SomeExtraTypeChildChild struct { 106 | ID string `json:"id"` 107 | } 108 | 109 | type Textbook struct { 110 | Title string `json:"title"` 111 | Courses []string `json:"courses"` 112 | } 113 | 114 | func (Textbook) IsBook() {} 115 | 116 | type UploadData struct { 117 | Size int `json:"size"` 118 | } 119 | 120 | type UploadFilesMap struct { 121 | Somefile *UploadData `json:"somefile"` 122 | } 123 | 124 | type UploadFilesMapInput struct { 125 | Somefile graphql.Upload `json:"somefile"` 126 | } 127 | 128 | type Video struct { 129 | Duration int `json:"duration"` 130 | } 131 | 132 | func (Video) IsMedia() {} 133 | 134 | type Episode string 135 | 136 | const ( 137 | EpisodeNewhope Episode = "NEWHOPE" 138 | EpisodeEmpire Episode = "EMPIRE" 139 | EpisodeJedi Episode = "JEDI" 140 | ) 141 | 142 | var AllEpisode = []Episode{ 143 | EpisodeNewhope, 144 | EpisodeEmpire, 145 | EpisodeJedi, 146 | } 147 | 148 | func (e Episode) IsValid() bool { 149 | switch e { 150 | case EpisodeNewhope, EpisodeEmpire, EpisodeJedi: 151 | return true 152 | } 153 | return false 154 | } 155 | 156 | func (e Episode) String() string { 157 | return string(e) 158 | } 159 | 160 | func (e *Episode) UnmarshalGQL(v interface{}) error { 161 | str, ok := v.(string) 162 | if !ok { 163 | return fmt.Errorf("enums must be strings") 164 | } 165 | 166 | *e = Episode(str) 167 | if !e.IsValid() { 168 | return fmt.Errorf("%s is not a valid Episode", str) 169 | } 170 | return nil 171 | } 172 | 173 | func (e Episode) MarshalGQL(w io.Writer) { 174 | fmt.Fprint(w, strconv.Quote(e.String())) 175 | } 176 | 177 | type FooTypeHash1 string 178 | 179 | const ( 180 | FooTypeHash1Hash1 FooTypeHash1 = "hash_1" 181 | FooTypeHash1Hash2 FooTypeHash1 = "hash_2" 182 | ) 183 | 184 | var AllFooTypeHash1 = []FooTypeHash1{ 185 | FooTypeHash1Hash1, 186 | FooTypeHash1Hash2, 187 | } 188 | 189 | func (e FooTypeHash1) IsValid() bool { 190 | switch e { 191 | case FooTypeHash1Hash1, FooTypeHash1Hash2: 192 | return true 193 | } 194 | return false 195 | } 196 | 197 | func (e FooTypeHash1) String() string { 198 | return string(e) 199 | } 200 | 201 | func (e *FooTypeHash1) UnmarshalGQL(v interface{}) error { 202 | str, ok := v.(string) 203 | if !ok { 204 | return fmt.Errorf("enums must be strings") 205 | } 206 | 207 | *e = FooTypeHash1(str) 208 | if !e.IsValid() { 209 | return fmt.Errorf("%s is not a valid FooType_hash1", str) 210 | } 211 | return nil 212 | } 213 | 214 | func (e FooTypeHash1) MarshalGQL(w io.Writer) { 215 | fmt.Fprint(w, strconv.Quote(e.String())) 216 | } 217 | -------------------------------------------------------------------------------- /example/server/resolver.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | // This file will not be regenerated automatically. 4 | // 5 | // It serves as dependency injection for your app, add any dependencies you require here. 6 | 7 | type Resolver struct{} 8 | -------------------------------------------------------------------------------- /example/server/schema.resolvers.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "example/server/generated" 10 | "example/server/model" 11 | "fmt" 12 | "os" 13 | "strconv" 14 | 15 | "github.com/99designs/gqlgen/graphql" 16 | ) 17 | 18 | func (r *mutationResolver) Post(ctx context.Context, input model.PostCreateInput) (*model.Message, error) { 19 | return &model.Message{ 20 | Text: input.Text, 21 | }, nil 22 | } 23 | 24 | func (r *mutationResolver) UploadFile(ctx context.Context, file graphql.Upload) (*model.UploadData, error) { 25 | return &model.UploadData{ 26 | Size: int(file.Size), 27 | }, nil 28 | } 29 | 30 | func (r *mutationResolver) UploadFiles(ctx context.Context, files []*graphql.Upload) ([]*model.UploadData, error) { 31 | ds := make([]*model.UploadData, 0) 32 | for _, f := range files { 33 | ds = append(ds, &model.UploadData{ 34 | Size: int(f.Size), 35 | }) 36 | } 37 | return ds, nil 38 | } 39 | 40 | func (r *mutationResolver) UploadFilesMap(ctx context.Context, files model.UploadFilesMapInput) (*model.UploadFilesMap, error) { 41 | return &model.UploadFilesMap{ 42 | Somefile: &model.UploadData{ 43 | Size: int(files.Somefile.Size), 44 | }, 45 | }, nil 46 | } 47 | 48 | func (r *queryResolver) Room(ctx context.Context, name string) (*model.Chatroom, error) { 49 | if name == "error" { 50 | return nil, errors.New("that's an invalid room") 51 | } 52 | 53 | return &model.Chatroom{ 54 | Name: name, 55 | Messages: nil, 56 | }, nil 57 | } 58 | 59 | func (r *queryResolver) RoomNonNull(ctx context.Context, name string) (*model.Chatroom, error) { 60 | return r.Room(ctx, name) 61 | } 62 | 63 | func (r *queryResolver) Medias(ctx context.Context) ([]model.Media, error) { 64 | return []model.Media{ 65 | &model.Image{ 66 | Size: 100, 67 | }, 68 | &model.Video{ 69 | Duration: 200, 70 | }, 71 | }, nil 72 | } 73 | 74 | func (r *queryResolver) Books(ctx context.Context) ([]model.Book, error) { 75 | return []model.Book{ 76 | &model.Textbook{ 77 | Title: "Some textbook", 78 | Courses: []string{"course 1", "course 2"}, 79 | }, 80 | &model.ColoringBook{ 81 | Title: "Some Coloring Book", 82 | Colors: []string{"red", "blue"}, 83 | }, 84 | }, nil 85 | } 86 | 87 | func (r *queryResolver) Issue8(ctx context.Context) (*model.Issue8Payload, error) { 88 | return &model.Issue8Payload{ 89 | Foo1: &model.Issue8PayloadFoo{A: &model.Issue8PayloadFooA{Aa: "foo1"}}, 90 | Foo2: &model.Issue8PayloadFoo{A: &model.Issue8PayloadFooA{Aa: "foo2"}}, 91 | }, nil 92 | } 93 | 94 | func (r *queryResolver) Cyclic(ctx context.Context) (*model.Cyclic1_1, error) { 95 | panic(fmt.Errorf("not implemented")) 96 | } 97 | 98 | func (r *queryResolver) Episodes(ctx context.Context) ([]model.Episode, error) { 99 | return []model.Episode{ 100 | model.EpisodeJedi, 101 | model.EpisodeNewhope, 102 | model.EpisodeEmpire, 103 | }, nil 104 | } 105 | 106 | func (r *queryResolver) AsMap(ctx context.Context, req map[string]interface{}, opt map[string]interface{}) (string, error) { 107 | return fmt.Sprintf("req: %+v opt: %+v", req, opt), nil 108 | } 109 | 110 | func (r *queryResolver) OptValue1(ctx context.Context, req model.OptionalValue1) (*bool, error) { 111 | panic(fmt.Errorf("not implemented")) 112 | } 113 | 114 | func (r *queryResolver) OptValue2(ctx context.Context, opt *model.OptionalValue2) (*bool, error) { 115 | panic(fmt.Errorf("not implemented")) 116 | } 117 | 118 | func (r *subscriptionResolver) MessageAdded(ctx context.Context, roomName string) (<-chan *model.Message, error) { 119 | ch := make(chan *model.Message) 120 | debug, _ := strconv.ParseBool(os.Getenv("GQLGENC_WS_LOG")) 121 | 122 | debugPrint := func(a ...interface{}) { 123 | if debug { 124 | fmt.Println(a...) 125 | } 126 | } 127 | 128 | debugPrint("MESSAGE ADDED") 129 | 130 | go func() { 131 | i := 0 132 | for { 133 | if i == 3 { 134 | close(ch) 135 | debugPrint("DONE MESSAGE ADDED") 136 | return 137 | } 138 | 139 | msg := &model.Message{ 140 | ID: fmt.Sprintf("msg%v", i), 141 | } 142 | 143 | select { 144 | case <-ctx.Done(): 145 | close(ch) 146 | debugPrint("DONE ctx") 147 | return 148 | case ch <- msg: 149 | debugPrint("SEND") 150 | i++ 151 | } 152 | } 153 | }() 154 | 155 | return ch, nil 156 | } 157 | 158 | // Mutation returns generated.MutationResolver implementation. 159 | func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } 160 | 161 | // Query returns generated.QueryResolver implementation. 162 | func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } 163 | 164 | // Subscription returns generated.SubscriptionResolver implementation. 165 | func (r *Resolver) Subscription() generated.SubscriptionResolver { return &subscriptionResolver{r} } 166 | 167 | type mutationResolver struct{ *Resolver } 168 | type queryResolver struct{ *Resolver } 169 | type subscriptionResolver struct{ *Resolver } 170 | -------------------------------------------------------------------------------- /example/server/tools.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | // See https://github.com/golang/go/issues/44129#issuecomment-815008489 4 | import ( 5 | // GQLGen generate command dependencies. 6 | _ "github.com/pkg/errors" 7 | _ "github.com/urfave/cli/v2" 8 | _ "golang.org/x/tools/go/ast/astutil" 9 | _ "golang.org/x/tools/go/packages" 10 | _ "golang.org/x/tools/imports" 11 | ) 12 | -------------------------------------------------------------------------------- /example/somelib/lib.go: -------------------------------------------------------------------------------- 1 | package somelib 2 | 3 | import "encoding/json" 4 | 5 | type CustomRoom string 6 | 7 | func (b *CustomRoom) UnmarshalJSON(data []byte) error { 8 | var v struct { 9 | Room struct { 10 | Name string `json:"name"` 11 | } `json:"room"` 12 | } 13 | if err := json.Unmarshal(data, &v); err != nil { 14 | return err 15 | } 16 | 17 | *b = CustomRoom("Room: " + v.Room.Name) 18 | 19 | return nil 20 | } 21 | 22 | func (b CustomRoom) String() string { 23 | return string(b) 24 | } 25 | -------------------------------------------------------------------------------- /generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | // Original work from https://github.com/Yamashou/gqlgenc/blob/1ef8055d/generator/generater.go 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "github.com/infiotinc/gqlgenc/config" 9 | 10 | "github.com/99designs/gqlgen/api" 11 | "github.com/99designs/gqlgen/plugin" 12 | ) 13 | 14 | func Generate(ctx context.Context, cfg *config.Config, option ...api.Option) error { 15 | var plugins []plugin.Plugin 16 | for _, o := range option { 17 | o(cfg.GQLConfig, &plugins) 18 | } 19 | 20 | if err := cfg.LoadSchema(ctx); err != nil { 21 | return fmt.Errorf("failed to load schema: %w", err) 22 | } 23 | 24 | if err := cfg.GQLConfig.Init(); err != nil { 25 | return fmt.Errorf("generating core failed: %w", err) 26 | } 27 | 28 | for _, p := range plugins { 29 | if mut, ok := p.(plugin.ConfigMutator); ok { 30 | err := mut.MutateConfig(cfg.GQLConfig) 31 | if err != nil { 32 | return fmt.Errorf("%s failed: %w", p.Name(), err) 33 | } 34 | } 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/infiotinc/gqlgenc 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.16.0 7 | github.com/google/go-cmp v0.5.4 // indirect 8 | github.com/stretchr/testify v1.4.0 9 | github.com/vektah/gqlparser/v2 v2.2.0 10 | gopkg.in/yaml.v2 v2.3.0 11 | nhooyr.io/websocket v1.8.7 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/gqlgen v0.16.0 h1:7Qc4Ll3mfN3doAyUWOgtGLcBGu+KDgK48HdkBGLZVFs= 2 | github.com/99designs/gqlgen v0.16.0/go.mod h1:nbeSjFkqphIqpZsYe1ULVz0yfH8hjpJdJIQoX/e0G2I= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= 5 | github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM= 6 | github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= 7 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= 8 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 9 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= 10 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= 11 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= 17 | github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= 18 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 19 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 20 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 21 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 22 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 23 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 24 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 25 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 26 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 27 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 28 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 29 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 30 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= 31 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 32 | github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= 33 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 34 | github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= 35 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 36 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 37 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= 38 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 39 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 40 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 41 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 42 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 43 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 44 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 45 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 46 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 47 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 48 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 49 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 50 | github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM= 51 | github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= 52 | github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 53 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 54 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 55 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 56 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 57 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 58 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 59 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 60 | github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= 61 | github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE= 62 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 63 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 64 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 65 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 66 | github.com/mitchellh/mapstructure v1.2.3 h1:f/MjBEBDLttYCGfRaKBbKSRVF5aV2O6fnBpzknuE3jU= 67 | github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 68 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 69 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 70 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 71 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 72 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 73 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 74 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 75 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 76 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 77 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 78 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 79 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 80 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 81 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 82 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 83 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 84 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 85 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 86 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 87 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 88 | github.com/vektah/gqlparser/v2 v2.2.0 h1:bAc3slekAAJW6sZTi07aGq0OrfaCjj4jxARAaC7g2EM= 89 | github.com/vektah/gqlparser/v2 v2.2.0/go.mod h1:i3mQIGIrbK2PD1RrCeMTlVbkF2FJ6WkU1KJlJlC+3F4= 90 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 91 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 92 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 93 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 94 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 95 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 96 | golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= 97 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 98 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 99 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 100 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 101 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 102 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 103 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 105 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 106 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 107 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 110 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= 113 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 114 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 115 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 116 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 117 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 118 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 119 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 120 | golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 121 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 122 | golang.org/x/tools v0.0.0-20200815165600-90abf76919f3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 123 | golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= 124 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 125 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 126 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 127 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 128 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 129 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 130 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 131 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 132 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 133 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 134 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 135 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 136 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 137 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 138 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 139 | nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= 140 | nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= 141 | -------------------------------------------------------------------------------- /introspection/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yamashou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /introspection/parse.go: -------------------------------------------------------------------------------- 1 | package introspection 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/vektah/gqlparser/v2/ast" 7 | ) 8 | 9 | func ParseIntrospectionQuery(url string, query Query) *ast.SchemaDocument { 10 | parser := parser{ 11 | sharedPosition: &ast.Position{Src: &ast.Source{ 12 | Name: "remote", 13 | BuiltIn: false, 14 | }}, 15 | } 16 | 17 | if url != "" { 18 | parser.sharedPosition.Src.Name = url 19 | } 20 | 21 | return parser.parseIntrospectionQuery(query) 22 | } 23 | 24 | type parser struct { 25 | sharedPosition *ast.Position 26 | } 27 | 28 | func (p parser) parseIntrospectionQuery(query Query) *ast.SchemaDocument { 29 | var doc ast.SchemaDocument 30 | typeMap := query.Schema.Types.NameMap() 31 | 32 | doc.Schema = append(doc.Schema, p.parseSchemaDefinition(query, typeMap)) 33 | doc.Position = p.sharedPosition 34 | 35 | for _, typeVale := range typeMap { 36 | doc.Definitions = append(doc.Definitions, p.parseTypeSystemDefinition(typeVale)) 37 | } 38 | 39 | for _, directiveValue := range query.Schema.Directives { 40 | doc.Directives = append(doc.Directives, p.parseDirectiveDefinition(directiveValue)) 41 | } 42 | 43 | return &doc 44 | } 45 | 46 | func (p parser) parseSchemaDefinition(query Query, typeMap map[string]*FullType) *ast.SchemaDefinition { 47 | def := ast.SchemaDefinition{} 48 | def.Position = p.sharedPosition 49 | 50 | if query.Schema.QueryType.Name != nil { 51 | def.OperationTypes = append(def.OperationTypes, 52 | p.parseOperationTypeDefinitionForQuery(typeMap[*query.Schema.QueryType.Name]), 53 | ) 54 | } 55 | 56 | if query.Schema.MutationType != nil { 57 | def.OperationTypes = append(def.OperationTypes, 58 | p.parseOperationTypeDefinitionForMutation(typeMap[*query.Schema.MutationType.Name]), 59 | ) 60 | } 61 | 62 | if query.Schema.SubscriptionType != nil { 63 | def.OperationTypes = append(def.OperationTypes, 64 | p.parseOperationTypeDefinitionForSubscription(typeMap[*query.Schema.SubscriptionType.Name]), 65 | ) 66 | } 67 | 68 | return &def 69 | } 70 | 71 | func (p parser) parseOperationTypeDefinitionForQuery(fullType *FullType) *ast.OperationTypeDefinition { 72 | var op ast.OperationTypeDefinition 73 | op.Operation = ast.Query 74 | op.Type = *fullType.Name 75 | op.Position = p.sharedPosition 76 | 77 | return &op 78 | } 79 | 80 | func (p parser) parseOperationTypeDefinitionForMutation(fullType *FullType) *ast.OperationTypeDefinition { 81 | var op ast.OperationTypeDefinition 82 | op.Operation = ast.Mutation 83 | op.Type = *fullType.Name 84 | op.Position = p.sharedPosition 85 | 86 | return &op 87 | } 88 | 89 | func (p parser) parseOperationTypeDefinitionForSubscription(fullType *FullType) *ast.OperationTypeDefinition { 90 | var op ast.OperationTypeDefinition 91 | op.Operation = ast.Subscription 92 | op.Type = *fullType.Name 93 | op.Position = p.sharedPosition 94 | 95 | return &op 96 | } 97 | 98 | func (p parser) parseDirectiveDefinition(directiveValue *DirectiveType) *ast.DirectiveDefinition { 99 | args := make(ast.ArgumentDefinitionList, 0, len(directiveValue.Args)) 100 | for _, arg := range directiveValue.Args { 101 | argumentDefinition := p.buildInputValue(arg) 102 | args = append(args, argumentDefinition) 103 | } 104 | locations := make([]ast.DirectiveLocation, 0, len(directiveValue.Locations)) 105 | for _, locationValue := range directiveValue.Locations { 106 | locations = append(locations, ast.DirectiveLocation(locationValue)) 107 | } 108 | 109 | return &ast.DirectiveDefinition{ 110 | Description: pointerString(directiveValue.Description), 111 | Name: directiveValue.Name, 112 | Arguments: args, 113 | Locations: locations, 114 | Position: p.sharedPosition, 115 | } 116 | } 117 | 118 | func (p parser) parseObjectFields(typeVale *FullType) ast.FieldList { 119 | fieldList := make(ast.FieldList, 0, len(typeVale.Fields)) 120 | for _, field := range typeVale.Fields { 121 | typ := p.getType(&field.Type) 122 | args := make(ast.ArgumentDefinitionList, 0, len(field.Args)) 123 | for _, arg := range field.Args { 124 | argumentDefinition := p.buildInputValue(arg) 125 | args = append(args, argumentDefinition) 126 | } 127 | 128 | fieldDefinition := &ast.FieldDefinition{ 129 | Description: pointerString(field.Description), 130 | Name: field.Name, 131 | Arguments: args, 132 | Type: typ, 133 | Position: p.sharedPosition, 134 | } 135 | fieldList = append(fieldList, fieldDefinition) 136 | } 137 | 138 | return fieldList 139 | } 140 | 141 | func (p parser) parseInputObjectFields(typeVale *FullType) ast.FieldList { 142 | fieldList := make(ast.FieldList, 0, len(typeVale.InputFields)) 143 | for _, field := range typeVale.InputFields { 144 | typ := p.getType(&field.Type) 145 | fieldDefinition := &ast.FieldDefinition{ 146 | Description: pointerString(field.Description), 147 | Name: field.Name, 148 | Type: typ, 149 | Position: p.sharedPosition, 150 | } 151 | fieldList = append(fieldList, fieldDefinition) 152 | } 153 | 154 | return fieldList 155 | } 156 | 157 | func (p parser) parseObjectTypeDefinition(typeVale *FullType) *ast.Definition { 158 | fieldList := p.parseObjectFields(typeVale) 159 | interfaces := make([]string, 0, len(typeVale.Interfaces)) 160 | for _, intf := range typeVale.Interfaces { 161 | interfaces = append(interfaces, pointerString(intf.Name)) 162 | } 163 | 164 | enums := make(ast.EnumValueList, 0, len(typeVale.EnumValues)) 165 | for _, enum := range typeVale.EnumValues { 166 | enumValue := &ast.EnumValueDefinition{ 167 | Description: pointerString(enum.Description), 168 | Name: enum.Name, 169 | Position: p.sharedPosition, 170 | } 171 | enums = append(enums, enumValue) 172 | } 173 | 174 | return &ast.Definition{ 175 | Kind: ast.Object, 176 | Description: pointerString(typeVale.Description), 177 | Name: pointerString(typeVale.Name), 178 | Interfaces: interfaces, 179 | Fields: fieldList, 180 | EnumValues: enums, 181 | Position: p.sharedPosition, 182 | BuiltIn: true, 183 | } 184 | } 185 | 186 | func (p parser) parseInterfaceTypeDefinition(typeVale *FullType) *ast.Definition { 187 | fieldList := p.parseObjectFields(typeVale) 188 | interfaces := make([]string, 0, len(typeVale.Interfaces)) 189 | for _, intf := range typeVale.Interfaces { 190 | interfaces = append(interfaces, pointerString(intf.Name)) 191 | } 192 | 193 | return &ast.Definition{ 194 | Kind: ast.Interface, 195 | Description: pointerString(typeVale.Description), 196 | Name: pointerString(typeVale.Name), 197 | Interfaces: interfaces, 198 | Fields: fieldList, 199 | Position: p.sharedPosition, 200 | BuiltIn: true, 201 | } 202 | } 203 | 204 | func (p parser) parseInputObjectTypeDefinition(typeVale *FullType) *ast.Definition { 205 | fieldList := p.parseInputObjectFields(typeVale) 206 | interfaces := make([]string, 0, len(typeVale.Interfaces)) 207 | for _, intf := range typeVale.Interfaces { 208 | interfaces = append(interfaces, pointerString(intf.Name)) 209 | } 210 | 211 | return &ast.Definition{ 212 | Kind: ast.InputObject, 213 | Description: pointerString(typeVale.Description), 214 | Name: pointerString(typeVale.Name), 215 | Interfaces: interfaces, 216 | Fields: fieldList, 217 | Position: p.sharedPosition, 218 | BuiltIn: true, 219 | } 220 | } 221 | 222 | func (p parser) parseUnionTypeDefinition(typeVale *FullType) *ast.Definition { 223 | unions := make([]string, 0, len(typeVale.PossibleTypes)) 224 | for _, unionValue := range typeVale.PossibleTypes { 225 | unions = append(unions, *unionValue.Name) 226 | } 227 | 228 | return &ast.Definition{ 229 | Kind: ast.Union, 230 | Description: pointerString(typeVale.Description), 231 | Name: pointerString(typeVale.Name), 232 | Types: unions, 233 | Position: p.sharedPosition, 234 | BuiltIn: true, 235 | } 236 | } 237 | 238 | func (p parser) parseEnumTypeDefinition(typeVale *FullType) *ast.Definition { 239 | enums := make(ast.EnumValueList, 0, len(typeVale.EnumValues)) 240 | for _, enum := range typeVale.EnumValues { 241 | enumValue := &ast.EnumValueDefinition{ 242 | Description: pointerString(enum.Description), 243 | Name: enum.Name, 244 | Position: p.sharedPosition, 245 | } 246 | enums = append(enums, enumValue) 247 | } 248 | 249 | return &ast.Definition{ 250 | Kind: ast.Enum, 251 | Description: pointerString(typeVale.Description), 252 | Name: pointerString(typeVale.Name), 253 | EnumValues: enums, 254 | Position: p.sharedPosition, 255 | BuiltIn: true, 256 | } 257 | } 258 | 259 | func (p parser) parseScalarTypeExtension(typeVale *FullType) *ast.Definition { 260 | return &ast.Definition{ 261 | Kind: ast.Scalar, 262 | Description: pointerString(typeVale.Description), 263 | Name: pointerString(typeVale.Name), 264 | Position: p.sharedPosition, 265 | BuiltIn: true, 266 | } 267 | } 268 | 269 | func (p parser) parseTypeSystemDefinition(typeVale *FullType) *ast.Definition { 270 | switch typeVale.Kind { 271 | case TypeKindScalar: 272 | return p.parseScalarTypeExtension(typeVale) 273 | case TypeKindInterface: 274 | return p.parseInterfaceTypeDefinition(typeVale) 275 | case TypeKindEnum: 276 | return p.parseEnumTypeDefinition(typeVale) 277 | case TypeKindUnion: 278 | return p.parseUnionTypeDefinition(typeVale) 279 | case TypeKindObject: 280 | return p.parseObjectTypeDefinition(typeVale) 281 | case TypeKindInputObject: 282 | return p.parseInputObjectTypeDefinition(typeVale) 283 | case TypeKindList, TypeKindNonNull: 284 | panic(fmt.Sprintf("not match Kind: %s", typeVale.Kind)) 285 | } 286 | 287 | panic(fmt.Sprintf("not match Kind: %s", typeVale.Kind)) 288 | } 289 | 290 | func (p parser) buildInputValue(input *InputValue) *ast.ArgumentDefinition { 291 | typ := p.getType(&input.Type) 292 | 293 | var defaultValue *ast.Value 294 | if input.DefaultValue != nil { 295 | defaultValue = &ast.Value{ 296 | Raw: pointerString(input.DefaultValue), 297 | Kind: ast.Variable, 298 | Position: p.sharedPosition, 299 | } 300 | } 301 | 302 | return &ast.ArgumentDefinition{ 303 | Description: pointerString(input.Description), 304 | Name: input.Name, 305 | DefaultValue: defaultValue, 306 | Type: typ, 307 | Position: p.sharedPosition, 308 | } 309 | } 310 | 311 | func (p parser) getType(typeRef *TypeRef) *ast.Type { 312 | if typeRef.Kind == TypeKindList { 313 | itemRef := typeRef.OfType 314 | if itemRef == nil { 315 | panic("Decorated type deeper than introspection query.") 316 | } 317 | 318 | return ast.ListType(p.getType(itemRef), p.sharedPosition) 319 | } 320 | 321 | if typeRef.Kind == TypeKindNonNull { 322 | nullableRef := typeRef.OfType 323 | if nullableRef == nil { 324 | panic("Decorated type deeper than introspection query.") 325 | } 326 | nullableType := p.getType(nullableRef) 327 | nullableType.NonNull = true 328 | 329 | return nullableType 330 | } 331 | 332 | return ast.NamedType(pointerString(typeRef.Name), p.sharedPosition) 333 | } 334 | 335 | func pointerString(s *string) string { 336 | if s == nil { 337 | return "" 338 | } 339 | 340 | return *s 341 | } 342 | -------------------------------------------------------------------------------- /introspection/parser_test.go: -------------------------------------------------------------------------------- 1 | package introspection 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestParseIntrospectionQuery_Parse(t *testing.T) { 12 | t.Parallel() 13 | 14 | tests := []struct { 15 | name string 16 | filename string 17 | expectedErr error 18 | }{ 19 | {"no mutation in schema", "testdata/introspection_result_no_mutation.json", nil}, 20 | } 21 | 22 | for _, testCase := range tests { 23 | test := testCase 24 | t.Run(test.name, func(t *testing.T) { 25 | t.Parallel() 26 | query := readQueryResult(t, test.filename) 27 | 28 | if test.expectedErr == nil { 29 | require.NotPanics(t, func() { 30 | ast := ParseIntrospectionQuery("test", query) 31 | require.NotNil(t, ast) 32 | }) 33 | } else { 34 | require.PanicsWithValue(t, test.expectedErr, func() { 35 | ast := ParseIntrospectionQuery("test", query) 36 | require.Nil(t, ast) 37 | }) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func readQueryResult(t *testing.T, filename string) Query { 44 | t.Helper() 45 | 46 | data, err := ioutil.ReadFile(filename) 47 | require.NoError(t, err) 48 | 49 | query := Query{} 50 | err = json.Unmarshal(data, &query) 51 | require.NoError(t, err) 52 | 53 | return query 54 | } 55 | -------------------------------------------------------------------------------- /introspection/query.go: -------------------------------------------------------------------------------- 1 | package introspection 2 | 3 | const Introspection = `query Query { 4 | __schema { 5 | queryType { name } 6 | mutationType { name } 7 | subscriptionType { name } 8 | types { 9 | ...FullType 10 | } 11 | directives { 12 | name 13 | description 14 | locations 15 | args { 16 | ...InputValue 17 | } 18 | } 19 | } 20 | } 21 | 22 | fragment FullType on __Type { 23 | kind 24 | name 25 | description 26 | fields(includeDeprecated: true) { 27 | name 28 | description 29 | args { 30 | ...InputValue 31 | } 32 | type { 33 | ...TypeRef 34 | } 35 | isDeprecated 36 | deprecationReason 37 | } 38 | inputFields { 39 | ...InputValue 40 | } 41 | interfaces { 42 | ...TypeRef 43 | } 44 | enumValues(includeDeprecated: true) { 45 | name 46 | description 47 | isDeprecated 48 | deprecationReason 49 | } 50 | possibleTypes { 51 | ...TypeRef 52 | } 53 | } 54 | 55 | fragment InputValue on __InputValue { 56 | name 57 | description 58 | type { ...TypeRef } 59 | defaultValue 60 | } 61 | 62 | fragment TypeRef on __Type { 63 | kind 64 | name 65 | ofType { 66 | kind 67 | name 68 | ofType { 69 | kind 70 | name 71 | ofType { 72 | kind 73 | name 74 | ofType { 75 | kind 76 | name 77 | ofType { 78 | kind 79 | name 80 | ofType { 81 | kind 82 | name 83 | ofType { 84 | kind 85 | name 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | }` 94 | -------------------------------------------------------------------------------- /introspection/testdata/introspection_result_no_mutation.json: -------------------------------------------------------------------------------- 1 | { 2 | "__schema": { 3 | "queryType": { 4 | "name": "Query" 5 | }, 6 | "mutationType": null, 7 | "subscriptionType": null, 8 | "types": [ 9 | { 10 | "kind": "OBJECT", 11 | "name": "Query" 12 | } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /introspection/type.go: -------------------------------------------------------------------------------- 1 | package introspection 2 | 3 | type TypeKind string 4 | 5 | const ( 6 | TypeKindScalar TypeKind = "SCALAR" 7 | TypeKindObject TypeKind = "OBJECT" 8 | TypeKindInterface TypeKind = "INTERFACE" 9 | TypeKindUnion TypeKind = "UNION" 10 | TypeKindEnum TypeKind = "ENUM" 11 | TypeKindInputObject TypeKind = "INPUT_OBJECT" 12 | TypeKindList TypeKind = "LIST" 13 | TypeKindNonNull TypeKind = "NON_NULL" 14 | ) 15 | 16 | type FullTypes []*FullType 17 | 18 | func (fs FullTypes) NameMap() map[string]*FullType { 19 | typeMap := make(map[string]*FullType) 20 | for _, typ := range fs { 21 | typeMap[*typ.Name] = typ 22 | } 23 | 24 | return typeMap 25 | } 26 | 27 | type FullType struct { 28 | Kind TypeKind 29 | Name *string 30 | Description *string 31 | Fields []*FieldValue 32 | InputFields []*InputValue 33 | Interfaces []*TypeRef 34 | EnumValues []*struct { 35 | Name string 36 | Description *string 37 | IsDeprecated bool 38 | DeprecationReason *string 39 | } 40 | PossibleTypes []*TypeRef 41 | } 42 | 43 | type FieldValue struct { 44 | Name string 45 | Description *string 46 | Args []*InputValue 47 | Type TypeRef 48 | IsDeprecated bool 49 | DeprecationReason *string 50 | } 51 | 52 | type InputValue struct { 53 | Name string 54 | Description *string 55 | Type TypeRef 56 | DefaultValue *string 57 | } 58 | 59 | type TypeRef struct { 60 | Kind TypeKind 61 | Name *string 62 | OfType *TypeRef 63 | } 64 | 65 | type Query struct { 66 | Schema struct { 67 | QueryType struct{ Name *string } 68 | MutationType *struct{ Name *string } 69 | SubscriptionType *struct{ Name *string } 70 | Types FullTypes 71 | Directives []*DirectiveType 72 | } `json:"__schema"` 73 | } 74 | 75 | type DirectiveType struct { 76 | Name string 77 | Description *string 78 | Locations []string 79 | Args []*InputValue 80 | } 81 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/99designs/gqlgen/api" 7 | "github.com/infiotinc/gqlgenc/clientgen" 8 | "github.com/infiotinc/gqlgenc/config" 9 | "github.com/infiotinc/gqlgenc/generator" 10 | "os" 11 | ) 12 | 13 | func main() { 14 | ctx := context.Background() 15 | cfg, err := config.LoadConfigFromDefaultLocations() 16 | if err != nil { 17 | fmt.Fprintf(os.Stderr, "cfg: %+v", err.Error()) 18 | os.Exit(2) 19 | } 20 | 21 | clientGen := api.AddPlugin(clientgen.New(cfg)) 22 | 23 | if err := generator.Generate(ctx, cfg, clientGen); err != nil { 24 | fmt.Fprintf(os.Stderr, "generate: %+v", err.Error()) 25 | os.Exit(4) 26 | } 27 | } 28 | --------------------------------------------------------------------------------