@@ -38,15 +38,15 @@ import (
38
38
"io"
39
39
"mime/multipart"
40
40
"net/http"
41
- "net/textproto"
42
41
43
42
"github.com/pkg/errors"
44
43
)
45
44
46
45
// Client is a client for interacting with a GraphQL API.
47
46
type Client struct {
48
- endpoint string
49
- httpClient * http.Client
47
+ endpoint string
48
+ httpClient * http.Client
49
+ useMultipartForm bool
50
50
51
51
// Log is called with various debug information.
52
52
// To log to standard out, use:
@@ -84,6 +84,66 @@ func (c *Client) Run(ctx context.Context, req *Request, resp interface{}) error
84
84
return ctx .Err ()
85
85
default :
86
86
}
87
+ if len (req .files ) > 0 && ! c .useMultipartForm {
88
+ return errors .New ("cannot send files with PostFields option" )
89
+ }
90
+ if c .useMultipartForm {
91
+ return c .runWithPostFields (ctx , req , resp )
92
+ }
93
+ return c .runWithJSON (ctx , req , resp )
94
+ }
95
+
96
+ func (c * Client ) runWithJSON (ctx context.Context , req * Request , resp interface {}) error {
97
+ var requestBody bytes.Buffer
98
+ requestBodyObj := struct {
99
+ Query string `json:"query"`
100
+ Variables map [string ]interface {} `json:"variables"`
101
+ }{
102
+ Query : req .q ,
103
+ Variables : req .vars ,
104
+ }
105
+ if err := json .NewEncoder (& requestBody ).Encode (requestBodyObj ); err != nil {
106
+ return errors .Wrap (err , "encode body" )
107
+ }
108
+ c .logf (">> variables: %v" , req .vars )
109
+ c .logf (">> query: %s" , req .q )
110
+ gr := & graphResponse {
111
+ Data : resp ,
112
+ }
113
+ r , err := http .NewRequest (http .MethodPost , c .endpoint , & requestBody )
114
+ if err != nil {
115
+ return err
116
+ }
117
+ r .Header .Set ("Content-Type" , "application/json; charset=utf-8" )
118
+ r .Header .Set ("Accept" , "application/json; charset=utf-8" )
119
+ for key , values := range req .Header {
120
+ for _ , value := range values {
121
+ r .Header .Add (key , value )
122
+ }
123
+ }
124
+ c .logf (">> headers: %v" , r .Header )
125
+ r = r .WithContext (ctx )
126
+ res , err := c .httpClient .Do (r )
127
+ if err != nil {
128
+ return err
129
+ }
130
+ defer res .Body .Close ()
131
+ var buf bytes.Buffer
132
+ if _ , err := io .Copy (& buf , res .Body ); err != nil {
133
+ return errors .Wrap (err , "reading body" )
134
+ }
135
+ c .logf ("<< %s" , buf .String ())
136
+ if err := json .NewDecoder (& buf ).Decode (& gr ); err != nil {
137
+ return errors .Wrap (err , "decoding response" )
138
+ }
139
+ if len (gr .Errors ) > 0 {
140
+ // return first error
141
+ return gr .Errors [0 ]
142
+ }
143
+ return nil
144
+ }
145
+
146
+ func (c * Client ) runWithPostFields (ctx context.Context , req * Request , resp interface {}) error {
87
147
var requestBody bytes.Buffer
88
148
writer := multipart .NewWriter (& requestBody )
89
149
if err := writer .WriteField ("query" , req .q ); err != nil {
@@ -122,7 +182,7 @@ func (c *Client) Run(ctx context.Context, req *Request, resp interface{}) error
122
182
return err
123
183
}
124
184
r .Header .Set ("Content-Type" , writer .FormDataContentType ())
125
- r .Header .Set ("Accept" , "application/json" )
185
+ r .Header .Set ("Accept" , "application/json; charset=utf-8 " )
126
186
for key , values := range req .Header {
127
187
for _ , value := range values {
128
188
r .Header .Add (key , value )
@@ -154,9 +214,17 @@ func (c *Client) Run(ctx context.Context, req *Request, resp interface{}) error
154
214
// making requests.
155
215
// NewClient(endpoint, WithHTTPClient(specificHTTPClient))
156
216
func WithHTTPClient (httpclient * http.Client ) ClientOption {
157
- return ClientOption ( func (client * Client ) {
217
+ return func (client * Client ) {
158
218
client .httpClient = httpclient
159
- })
219
+ }
220
+ }
221
+
222
+ // UseMultipartForm uses multipart/form-data and activates support for
223
+ // files.
224
+ func UseMultipartForm () ClientOption {
225
+ return func (client * Client ) {
226
+ client .useMultipartForm = true
227
+ }
160
228
}
161
229
162
230
// ClientOption are functions that are passed into NewClient to
@@ -182,26 +250,9 @@ type Request struct {
182
250
vars map [string ]interface {}
183
251
files []file
184
252
185
- // Header mirrors the Header of a http.Request. It contains
186
- // the request header fields either received
187
- // by the server or to be sent by the client.
188
- //
189
- // If a server received a request with header lines,
190
- //
191
- // Host: example.com
192
- // accept-encoding: gzip, deflate
193
- // Accept-Language: en-us
194
- // fOO: Bar
195
- // foo: two
196
- //
197
- // then
198
- //
199
- // Header = map[string][]string{
200
- // "Accept-Encoding": {"gzip, deflate"},
201
- // "Accept-Language": {"en-us"},
202
- // "Foo": {"Bar", "two"},
203
- // }
204
- Header Header
253
+ // Header represent any request headers that will be set
254
+ // when the request is made.
255
+ Header http.Header
205
256
}
206
257
207
258
// NewRequest makes a new Request with the specified string.
@@ -222,6 +273,8 @@ func (req *Request) Var(key string, value interface{}) {
222
273
}
223
274
224
275
// File sets a file to upload.
276
+ // Files are only supported with a Client that was created with
277
+ // the UseMultipartForm option.
225
278
func (req * Request ) File (fieldname , filename string , r io.Reader ) {
226
279
req .files = append (req .files , file {
227
280
Field : fieldname ,
@@ -230,37 +283,6 @@ func (req *Request) File(fieldname, filename string, r io.Reader) {
230
283
})
231
284
}
232
285
233
- // A Header represents the key-value pairs in an HTTP header.
234
- type Header map [string ][]string
235
-
236
- // Add adds the key, value pair to the header.
237
- // It appends to any existing values associated with key.
238
- func (h Header ) Add (key , value string ) {
239
- textproto .MIMEHeader (h ).Add (key , value )
240
- }
241
-
242
- // Set sets the header entries associated with key to
243
- // the single element value. It replaces any existing
244
- // values associated with key.
245
- func (h Header ) Set (key , value string ) {
246
- textproto .MIMEHeader (h ).Set (key , value )
247
- }
248
-
249
- // Get gets the first value associated with the given key.
250
- // It is case insensitive; textproto.CanonicalMIMEHeaderKey is used
251
- // to canonicalize the provided key.
252
- // If there are no values associated with the key, Get returns "".
253
- // To access multiple values of a key, or to use non-canonical keys,
254
- // access the map directly.
255
- func (h Header ) Get (key string ) string {
256
- return textproto .MIMEHeader (h ).Get (key )
257
- }
258
-
259
- // Del deletes the values associated with key.
260
- func (h Header ) Del (key string ) {
261
- textproto .MIMEHeader (h ).Del (key )
262
- }
263
-
264
286
// file represents a file to upload.
265
287
type file struct {
266
288
Field string
0 commit comments