@@ -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.
4746type 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))
156216func 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.
225278func (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.
265287type file struct {
266288 Field string
0 commit comments