client.go

  1// client is used internally for testing. See readme for alternatives
  2package client
  3
  4import (
  5	"bytes"
  6	"encoding/json"
  7	"fmt"
  8	"io/ioutil"
  9	"net/http"
 10
 11	"github.com/mitchellh/mapstructure"
 12)
 13
 14// Client for graphql requests
 15type Client struct {
 16	url    string
 17	client *http.Client
 18}
 19
 20// New creates a graphql client
 21func New(url string, client ...*http.Client) *Client {
 22	p := &Client{
 23		url: url,
 24	}
 25
 26	if len(client) > 0 {
 27		p.client = client[0]
 28	} else {
 29		p.client = http.DefaultClient
 30	}
 31	return p
 32}
 33
 34type Request struct {
 35	Query         string                 `json:"query"`
 36	Variables     map[string]interface{} `json:"variables,omitempty"`
 37	OperationName string                 `json:"operationName,omitempty"`
 38}
 39
 40type Option func(r *Request)
 41
 42func Var(name string, value interface{}) Option {
 43	return func(r *Request) {
 44		if r.Variables == nil {
 45			r.Variables = map[string]interface{}{}
 46		}
 47
 48		r.Variables[name] = value
 49	}
 50}
 51
 52func Operation(name string) Option {
 53	return func(r *Request) {
 54		r.OperationName = name
 55	}
 56}
 57
 58func (p *Client) MustPost(query string, response interface{}, options ...Option) {
 59	if err := p.Post(query, response, options...); err != nil {
 60		panic(err)
 61	}
 62}
 63
 64func (p *Client) mkRequest(query string, options ...Option) Request {
 65	r := Request{
 66		Query: query,
 67	}
 68
 69	for _, option := range options {
 70		option(&r)
 71	}
 72
 73	return r
 74}
 75
 76func (p *Client) Post(query string, response interface{}, options ...Option) (resperr error) {
 77	r := p.mkRequest(query, options...)
 78	requestBody, err := json.Marshal(r)
 79	if err != nil {
 80		return fmt.Errorf("encode: %s", err.Error())
 81	}
 82
 83	rawResponse, err := p.client.Post(p.url, "application/json", bytes.NewBuffer(requestBody))
 84	if err != nil {
 85		return fmt.Errorf("post: %s", err.Error())
 86	}
 87	defer func() {
 88		_ = rawResponse.Body.Close()
 89	}()
 90
 91	if rawResponse.StatusCode >= http.StatusBadRequest {
 92		responseBody, _ := ioutil.ReadAll(rawResponse.Body)
 93		return fmt.Errorf("http %d: %s", rawResponse.StatusCode, responseBody)
 94	}
 95
 96	responseBody, err := ioutil.ReadAll(rawResponse.Body)
 97	if err != nil {
 98		return fmt.Errorf("read: %s", err.Error())
 99	}
100
101	// decode it into map string first, let mapstructure do the final decode
102	// because it can be much stricter about unknown fields.
103	respDataRaw := struct {
104		Data   interface{}
105		Errors json.RawMessage
106	}{}
107	err = json.Unmarshal(responseBody, &respDataRaw)
108	if err != nil {
109		return fmt.Errorf("decode: %s", err.Error())
110	}
111
112	// we want to unpack even if there is an error, so we can see partial responses
113	unpackErr := unpack(respDataRaw.Data, response)
114
115	if respDataRaw.Errors != nil {
116		return RawJsonError{respDataRaw.Errors}
117	}
118	return unpackErr
119}
120
121type RawJsonError struct {
122	json.RawMessage
123}
124
125func (r RawJsonError) Error() string {
126	return string(r.RawMessage)
127}
128
129func unpack(data interface{}, into interface{}) error {
130	d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
131		Result:      into,
132		TagName:     "json",
133		ErrorUnused: true,
134		ZeroFields:  true,
135	})
136	if err != nil {
137		return fmt.Errorf("mapstructure: %s", err.Error())
138	}
139
140	return d.Decode(data)
141}