Skip to content

Commit c7bb77f

Browse files
committed
Fix
1 parent 55f0c3c commit c7bb77f

24 files changed

+500
-706
lines changed

.gitignore

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,3 @@
1-
/.gqlgenc.yml
2-
/models_gen.go
3-
/**/.graphqlconfig
4-
/schema.graphql
5-
/client.go
6-
/query/
7-
/.idea/
8-
9-
coverage.out
10-
.vscode/
11-
12-
# Actual files output by the end-to-end tests
131
generator/testdata/**/actual
2+
3+
gqlgenc

.golangci.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,6 @@ issues:
7474
text: "`Extensions` is unused" # used in line 48
7575
linters:
7676
- structcheck
77-
- path: config/config.go
78-
text: "`client` is unused" # used in config/config.go
79-
linters:
80-
- structcheck
8177
- path: graphqljson/graphql.go
8278
text: "append to slice `frontier` with non-zero initialized length" # used in config/config.go
8379
linters:

README.md

Lines changed: 32 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,60 @@
44

55
This is Go library for building GraphQL client with [gqlgen](https://github.com/99designs/gqlgen)
66

7-
## Motivation
7+
### Query First, not Code First
88

9-
Now, if you build GraphQL api client for Go, have choice:
10-
11-
- [github.com/shurcooL/graphql](https://github.com/shurcooL/graphql)
12-
- [github.com/machinebox/graphql](https://github.com/machinebox/graphql)
13-
14-
These libraries are very simple and easy to handle.
159
However, as I work with [gqlgen](https://github.com/99designs/gqlgen) and [graphql-code-generator](https://graphql-code-generator.com/) every day, I find out the beauty of automatic generation.
1610
So I want to automatically generate types.
1711

18-
## Installation
12+
### based on gqlgen
13+
14+
- [Khan/genqlient](https://github.com/Khan/genqlient) is built from scratch as its own system. However, since gqlgenc is based on gqlgen, knowledge learned from either can be directly applied.
15+
16+
## Usage
1917

2018
```shell script
21-
go get -u github.com/Yamashou/gqlgenc
19+
go get -tool github.com/Yamashou/gqlgenc
2220
```
2321

2422
## How to use
2523

2624
### Client Codes Only
2725

28-
gqlgenc base is gqlgen with [plugins](https://gqlgen.com/reference/plugins/). So the setting is yaml in each format.
29-
gqlgenc can be configured using a `.gqlgenc.yml` file
30-
31-
Load a schema from a remote server:
32-
3326
```yaml
34-
model:
35-
package: generated
36-
filename: ./models_gen.go # https://github.com/99designs/gqlgen/tree/master/plugin/modelgen
27+
# schema for query
28+
schema:
29+
- ""
30+
endpoint:
31+
url: https://api.annict.com/graphql # Where do you want to send your request?
32+
headers: # If you need header for getting introspection query, set it
33+
Authorization: "Bearer ${ANNICT_KEY}" # support environment variables
34+
# generate config
35+
generate:
36+
usedModelsOnly: false
37+
# client to generate
3738
client:
3839
package: generated
3940
filename: ./client.go # Where should any generated client go?
41+
# query to generate
42+
query:
43+
source: "./query/*.graphql" # Where are all the query files located?
44+
package: generated
45+
filename: ./client.go # Where should any generated client go?
46+
# query to generate
47+
model:
48+
package: generated
49+
filename: ./models_gen.go # https://github.com/99designs/gqlgen/tree/master/plugin/modelgen
4050
models:
4151
Int:
4252
model: github.com/99designs/gqlgen/graphql.Int64
4353
Date:
4454
model: github.com/99designs/gqlgen/graphql.Time
4555
federation: # Add this if your schema includes Apollo Federation related directives
4656
version: 2
47-
endpoint:
48-
url: https://api.annict.com/graphql # Where do you want to send your request?
49-
headers: # If you need header for getting introspection query, set it
50-
Authorization: "Bearer ${ANNICT_KEY}" # support environment variables
51-
query:
52-
- "./query/*.graphql" # Where are all the query files located?
53-
generate:
54-
clientInterfaceName: "GithubGraphQLClient" # Determine the name of the generated client interface
55-
structFieldsAlwaysPointers: true # Always use pointers for struct fields (default: true) [same as gqlgen](https://github.com/99designs/gqlgen/blob/e1ef86e795e738654c98553b325a248c02c8c2f8/docs/content/config.md?plain=1#L73)
57+
# input model config
58+
nullable_input_omittable: true
59+
enable_model_json_omitempty_tag: false
60+
enable_model_json_omitzero: true
5661
```
5762
5863
Load a schema from a local file:
@@ -138,21 +143,5 @@ func main() {
138143
}
139144
```
140145

141-
## Documents
142-
143-
- [How to configure gqlgen using gqlgen.yml](https://gqlgen.com/config/)
144-
- [How to write plugins for gqlgen](https://gqlgen.com/reference/plugins/)
145-
146-
147-
## Comments
148-
149-
### Japanese Comments
150-
These codes have Japanese comments. Replace with English.
151-
152-
### Subscription
153-
154-
This client does not support subscription. If you need a subscription, please create an issue or pull request.
155-
156-
### Pre-conditions
146+
## VS
157147

158-
[clientgen](https://github.com/Yamashou/gqlgenc/v3/tree/master/clientgen) is created based on [modelgen](https://github.com/99designs/gqlgen/tree/master/plugin/modelgen). So if you don't have a modelgen, it may be a mysterious move.

client/client.go

Lines changed: 9 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
package client
22

33
import (
4-
"bytes"
5-
"compress/gzip"
64
"context"
7-
"encoding/json"
8-
"errors"
95
"fmt"
10-
"io"
116
"net/http"
12-
13-
"github.com/Yamashou/gqlgenc/v3/graphqljson"
14-
"github.com/vektah/gqlparser/v2/gqlerror"
157
)
168

179
type Client struct {
@@ -49,181 +41,24 @@ func (c *Client) Post(ctx context.Context, operationName, query string, variable
4941
option(client)
5042
}
5143

52-
req, err := newRequest(ctx, client.endpoint, operationName, query, variables)
53-
if err != nil {
54-
return fmt.Errorf("failed to create post request: %w", err)
55-
}
56-
57-
resp, err := client.client.Do(req)
58-
if err != nil {
59-
return fmt.Errorf("request failed: %w", err)
60-
}
61-
defer resp.Body.Close()
62-
63-
return client.parseResponse(resp, out)
64-
}
65-
66-
// PostMultipart send multipart form with files https://gqlgen.com/reference/file-upload/ https://github.com/jaydenseric/graphql-multipart-request-spec
67-
func (c *Client) PostMultipart(ctx context.Context, operationName, query string, variables map[string]any, out any, options ...Option) error {
68-
client := &Client{
69-
client: c.client,
70-
endpoint: c.endpoint,
71-
}
72-
for _, option := range options {
73-
option(client)
74-
}
75-
76-
req, err := multipartRequest(ctx, client.endpoint, operationName, query, variables)
44+
// PostMultipart send multipart form with files https://gqlgen.com/reference/file-upload/ https://github.com/jaydenseric/graphql-multipart-request-spec
45+
req, err := NewMultipartRequest(ctx, client.endpoint, operationName, query, variables)
7746
if err != nil {
7847
return fmt.Errorf("failed to create post multipart request: %w", err)
7948
}
8049

81-
resp, err := client.client.Do(req)
82-
if err != nil {
83-
return fmt.Errorf("request failed: %w", err)
84-
}
85-
defer resp.Body.Close()
86-
87-
return client.parseResponse(resp, out)
88-
}
89-
90-
/////////////////////////////////////////////////////////////////////
91-
// Request
92-
93-
// Request represents an outgoing GraphQL request
94-
type Request struct {
95-
Query string `json:"query"`
96-
Variables map[string]any `json:"variables,omitempty"`
97-
OperationName string `json:"operationName,omitempty"`
98-
}
99-
100-
func newRequest(ctx context.Context, endpoint, operationName, query string, variables map[string]any) (*http.Request, error) {
101-
graphqlRequest := &Request{
102-
Query: query,
103-
Variables: variables,
104-
OperationName: operationName,
105-
}
106-
requestBody, err := json.Marshal(graphqlRequest)
107-
if err != nil {
108-
return nil, fmt.Errorf("encode: %w", err)
109-
}
110-
111-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(requestBody))
112-
if err != nil {
113-
return nil, fmt.Errorf("create request struct failed: %w", err)
114-
}
115-
116-
req.Header = http.Header{
117-
"Content-Type": []string{"application/graphql-response+json;charset=utf-8", "application/json; charset=utf-8"},
118-
"Accept": []string{"application/graphql-response+json;charset=utf-8", "application/json; charset=utf-8"},
119-
}
120-
121-
return req, nil
122-
}
123-
124-
/////////////////////////////////////////////////////////////////////
125-
// Response
126-
127-
// httpError is the error when a gqlErrors cannot be parsed
128-
type httpError struct {
129-
Code int `json:"code"`
130-
Message string `json:"message"`
131-
}
132-
133-
// gqlErrors is the struct of a standard graphql error response
134-
type gqlErrors struct {
135-
Errors gqlerror.List `json:"errors"`
136-
}
137-
138-
func (e *gqlErrors) Error() string {
139-
return e.Errors.Error()
140-
}
141-
142-
// errorResponse represent an handled error
143-
type errorResponse struct {
144-
// http status code is not OK
145-
NetworkError *httpError `json:"networkErrors"`
146-
// http status code is OK but the server returned at least one graphql error
147-
GqlErrors *gqlerror.List `json:"graphqlErrors"`
148-
}
149-
150-
// HasErrors returns true when at least one error is declared
151-
func (er *errorResponse) HasErrors() bool {
152-
return er.NetworkError != nil || er.GqlErrors != nil
153-
}
154-
155-
func (er *errorResponse) Error() string {
156-
content, err := json.Marshal(er)
157-
if err != nil {
158-
return err.Error()
159-
}
160-
161-
return string(content)
162-
}
163-
164-
func (c *Client) parseResponse(resp *http.Response, out any) error {
165-
if resp.Header.Get("Content-Encoding") == "gzip" {
166-
respBody, err := gzip.NewReader(resp.Body)
50+
if req == nil {
51+
req, err = NewRequest(ctx, client.endpoint, operationName, query, variables)
16752
if err != nil {
168-
return fmt.Errorf("failed to decode gzip: %w", err)
53+
return fmt.Errorf("failed to create post request: %w", err)
16954
}
170-
resp.Body = respBody
17155
}
17256

173-
body, err := io.ReadAll(resp.Body)
57+
resp, err := client.client.Do(req)
17458
if err != nil {
175-
return fmt.Errorf("failed to read response body: %w", err)
176-
}
177-
178-
errResponse := &errorResponse{}
179-
isStatusCodeOK := 200 <= resp.StatusCode && resp.StatusCode <= 299
180-
if !isStatusCodeOK {
181-
errResponse.NetworkError = &httpError{
182-
Code: resp.StatusCode,
183-
Message: fmt.Sprintf("Response body %s", string(body)),
184-
}
185-
}
186-
187-
if err := c.unmarshalResponse(body, out); err != nil {
188-
var gqlErrs *gqlErrors
189-
if errors.As(err, &gqlErrs) {
190-
// success to parse graphql error response
191-
errResponse.GqlErrors = &gqlErrs.Errors
192-
} else if isStatusCodeOK {
193-
// status code is OK but the GraphQL response can't be parsed, it's an error.
194-
return fmt.Errorf("http status is OK but %w", err)
195-
}
196-
}
197-
198-
if errResponse.HasErrors() {
199-
return errResponse
200-
}
201-
202-
return nil
203-
}
204-
205-
// response is a GraphQL layer response from a handler.
206-
type response struct {
207-
Data json.RawMessage `json:"data"`
208-
Errors json.RawMessage `json:"errors"`
209-
}
210-
211-
func (c *Client) unmarshalResponse(respBody []byte, out any) error {
212-
resp := response{}
213-
if err := json.Unmarshal(respBody, &resp); err != nil {
214-
return fmt.Errorf("failed to decode response %q: %w", respBody, err)
215-
}
216-
if err := graphqljson.UnmarshalData(resp.Data, out); err != nil {
217-
return fmt.Errorf("failed to decode response data %q: %w", resp.Data, err)
218-
}
219-
220-
if len(resp.Errors) > 0 {
221-
gqlErrs := &gqlErrors{}
222-
if err := json.Unmarshal(respBody, gqlErrs); err != nil {
223-
return fmt.Errorf("faild to decode response error %q: %w", respBody, err)
224-
}
225-
return gqlErrs
59+
return fmt.Errorf("request failed: %w", err)
22660
}
61+
defer resp.Body.Close()
22762

228-
return nil
63+
return ParseResponse(resp, out)
22964
}

0 commit comments

Comments
 (0)