Skip to content

Commit 05b17f3

Browse files
authored
Merge pull request #15 from machinebox/json-by-default
JSON by default
2 parents 22d430e + 1c8e9e5 commit 05b17f3

File tree

4 files changed

+202
-64
lines changed

4 files changed

+202
-64
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ if err := client.Run(ctx, req, &respData); err != nil {
5151
}
5252
```
5353

54+
### File support via multipart form data
55+
56+
By default, the package will send a JSON body. To enable the sending of files, you can opt to
57+
use multipart form data instead using the `UseMultipartForm` option when you create your `Client`:
58+
59+
```
60+
client := graphql.NewClient("https://machinebox.io/graphql", graphql.UseMultipartForm())
61+
```
62+
5463
For more information, [read the godoc package documentation](http://godoc.org/github.com/machinebox/graphql) or the [blog post](https://blog.machinebox.io/a-graphql-client-library-for-go-5bffd0455878).
5564

5665
## Thanks

graphql.go

Lines changed: 79 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,15 @@ import (
3838
"io"
3939
"mime/multipart"
4040
"net/http"
41-
"net/textproto"
4241

4342
"github.com/pkg/errors"
4443
)
4544

4645
// Client is a client for interacting with a GraphQL API.
4746
type Client struct {
48-
endpoint string
49-
httpClient *http.Client
47+
endpoint string
48+
httpClient *http.Client
49+
useMultipartForm bool
5050

5151
// Log is called with various debug information.
5252
// To log to standard out, use:
@@ -84,6 +84,66 @@ func (c *Client) Run(ctx context.Context, req *Request, resp interface{}) error
8484
return ctx.Err()
8585
default:
8686
}
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 {
87147
var requestBody bytes.Buffer
88148
writer := multipart.NewWriter(&requestBody)
89149
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
122182
return err
123183
}
124184
r.Header.Set("Content-Type", writer.FormDataContentType())
125-
r.Header.Set("Accept", "application/json")
185+
r.Header.Set("Accept", "application/json; charset=utf-8")
126186
for key, values := range req.Header {
127187
for _, value := range values {
128188
r.Header.Add(key, value)
@@ -154,9 +214,17 @@ func (c *Client) Run(ctx context.Context, req *Request, resp interface{}) error
154214
// making requests.
155215
// NewClient(endpoint, WithHTTPClient(specificHTTPClient))
156216
func WithHTTPClient(httpclient *http.Client) ClientOption {
157-
return ClientOption(func(client *Client) {
217+
return func(client *Client) {
158218
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+
}
160228
}
161229

162230
// ClientOption are functions that are passed into NewClient to
@@ -182,26 +250,9 @@ type Request struct {
182250
vars map[string]interface{}
183251
files []file
184252

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
205256
}
206257

207258
// NewRequest makes a new Request with the specified string.
@@ -222,6 +273,8 @@ func (req *Request) Var(key string, value interface{}) {
222273
}
223274

224275
// File sets a file to upload.
276+
// Files are only supported with a Client that was created with
277+
// the UseMultipartForm option.
225278
func (req *Request) File(fieldname, filename string, r io.Reader) {
226279
req.files = append(req.files, file{
227280
Field: fieldname,
@@ -230,37 +283,6 @@ func (req *Request) File(fieldname, filename string, r io.Reader) {
230283
})
231284
}
232285

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-
264286
// file represents a file to upload.
265287
type file struct {
266288
Field string

graphql_json_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package graphql
2+
3+
import (
4+
"context"
5+
"io"
6+
"io/ioutil"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
"time"
11+
12+
"github.com/matryer/is"
13+
)
14+
15+
func TestDoJSON(t *testing.T) {
16+
is := is.New(t)
17+
var calls int
18+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19+
calls++
20+
is.Equal(r.Method, http.MethodPost)
21+
b, err := ioutil.ReadAll(r.Body)
22+
is.NoErr(err)
23+
is.Equal(string(b), `{"query":"query {}","variables":null}`+"\n")
24+
io.WriteString(w, `{
25+
"data": {
26+
"something": "yes"
27+
}
28+
}`)
29+
}))
30+
defer srv.Close()
31+
32+
ctx := context.Background()
33+
client := NewClient(srv.URL)
34+
35+
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
36+
defer cancel()
37+
var responseData map[string]interface{}
38+
err := client.Run(ctx, &Request{q: "query {}"}, &responseData)
39+
is.NoErr(err)
40+
is.Equal(calls, 1) // calls
41+
is.Equal(responseData["something"], "yes")
42+
}
43+
44+
func TestQueryJSON(t *testing.T) {
45+
is := is.New(t)
46+
47+
var calls int
48+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49+
calls++
50+
b, err := ioutil.ReadAll(r.Body)
51+
is.NoErr(err)
52+
is.Equal(string(b), `{"query":"query {}","variables":{"username":"matryer"}}`+"\n")
53+
_, err = io.WriteString(w, `{"data":{"value":"some data"}}`)
54+
is.NoErr(err)
55+
}))
56+
defer srv.Close()
57+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
58+
defer cancel()
59+
60+
client := NewClient(srv.URL)
61+
62+
req := NewRequest("query {}")
63+
req.Var("username", "matryer")
64+
65+
// check variables
66+
is.True(req != nil)
67+
is.Equal(req.vars["username"], "matryer")
68+
69+
var resp struct {
70+
Value string
71+
}
72+
err := client.Run(ctx, req, &resp)
73+
is.NoErr(err)
74+
is.Equal(calls, 1)
75+
76+
is.Equal(resp.Value, "some data")
77+
}
78+
79+
func TestHeader(t *testing.T) {
80+
is := is.New(t)
81+
82+
var calls int
83+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
84+
calls++
85+
is.Equal(r.Header.Get("X-Custom-Header"), "123")
86+
87+
_, err := io.WriteString(w, `{"data":{"value":"some data"}}`)
88+
is.NoErr(err)
89+
}))
90+
defer srv.Close()
91+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
92+
defer cancel()
93+
94+
client := NewClient(srv.URL)
95+
96+
req := NewRequest("query {}")
97+
req.Header.Set("X-Custom-Header", "123")
98+
99+
var resp struct {
100+
Value string
101+
}
102+
err := client.Run(ctx, req, &resp)
103+
is.NoErr(err)
104+
is.Equal(calls, 1)
105+
106+
is.Equal(resp.Value, "some data")
107+
}

graphql_test.go renamed to graphql_multipart_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@ func TestWithClient(t *testing.T) {
2727
}
2828

2929
ctx := context.Background()
30-
client := NewClient("", WithHTTPClient(testClient))
30+
client := NewClient("", WithHTTPClient(testClient), UseMultipartForm())
3131

3232
req := NewRequest(``)
3333
client.Run(ctx, req, nil)
3434

3535
is.Equal(calls, 1) // calls
3636
}
3737

38-
func TestDo(t *testing.T) {
38+
func TestDoUseMultipartForm(t *testing.T) {
3939
is := is.New(t)
4040
var calls int
4141
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -52,7 +52,7 @@ func TestDo(t *testing.T) {
5252
defer srv.Close()
5353

5454
ctx := context.Background()
55-
client := NewClient(srv.URL)
55+
client := NewClient(srv.URL, UseMultipartForm())
5656

5757
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
5858
defer cancel()
@@ -80,7 +80,7 @@ func TestDoErr(t *testing.T) {
8080
defer srv.Close()
8181

8282
ctx := context.Background()
83-
client := NewClient(srv.URL)
83+
client := NewClient(srv.URL, UseMultipartForm())
8484

8585
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
8686
defer cancel()
@@ -107,7 +107,7 @@ func TestDoNoResponse(t *testing.T) {
107107
defer srv.Close()
108108

109109
ctx := context.Background()
110-
client := NewClient(srv.URL)
110+
client := NewClient(srv.URL, UseMultipartForm())
111111

112112
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
113113
defer cancel()
@@ -132,7 +132,7 @@ func TestQuery(t *testing.T) {
132132
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
133133
defer cancel()
134134

135-
client := NewClient(srv.URL)
135+
client := NewClient(srv.URL, UseMultipartForm())
136136

137137
req := NewRequest("query {}")
138138
req.Var("username", "matryer")
@@ -173,7 +173,7 @@ func TestFile(t *testing.T) {
173173
defer srv.Close()
174174
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
175175
defer cancel()
176-
client := NewClient(srv.URL)
176+
client := NewClient(srv.URL, UseMultipartForm())
177177
f := strings.NewReader(`This is a file`)
178178
req := NewRequest("query {}")
179179
req.File("file", "filename.txt", f)

0 commit comments

Comments
 (0)