// client is used internally for testing. See readme for alternatives package client import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" "github.com/mitchellh/mapstructure" ) // Client for graphql requests type Client struct { url string client *http.Client } // New creates a graphql client func New(url string, client ...*http.Client) *Client { p := &Client{ url: url, } if len(client) > 0 { p.client = client[0] } else { p.client = http.DefaultClient } return p } type Request struct { Query string `json:"query"` Variables map[string]interface{} `json:"variables,omitempty"` OperationName string `json:"operationName,omitempty"` } type Option func(r *Request) func Var(name string, value interface{}) Option { return func(r *Request) { if r.Variables == nil { r.Variables = map[string]interface{}{} } r.Variables[name] = value } } func Operation(name string) Option { return func(r *Request) { r.OperationName = name } } func (p *Client) MustPost(query string, response interface{}, options ...Option) { if err := p.Post(query, response, options...); err != nil { panic(err) } } func (p *Client) mkRequest(query string, options ...Option) Request { r := Request{ Query: query, } for _, option := range options { option(&r) } return r } func (p *Client) Post(query string, response interface{}, options ...Option) (resperr error) { r := p.mkRequest(query, options...) requestBody, err := json.Marshal(r) if err != nil { return fmt.Errorf("encode: %s", err.Error()) } rawResponse, err := p.client.Post(p.url, "application/json", bytes.NewBuffer(requestBody)) if err != nil { return fmt.Errorf("post: %s", err.Error()) } defer func() { _ = rawResponse.Body.Close() }() if rawResponse.StatusCode >= http.StatusBadRequest { responseBody, _ := ioutil.ReadAll(rawResponse.Body) return fmt.Errorf("http %d: %s", rawResponse.StatusCode, responseBody) } responseBody, err := ioutil.ReadAll(rawResponse.Body) if err != nil { return fmt.Errorf("read: %s", err.Error()) } // decode it into map string first, let mapstructure do the final decode // because it can be much stricter about unknown fields. respDataRaw := struct { Data interface{} Errors json.RawMessage }{} err = json.Unmarshal(responseBody, &respDataRaw) if err != nil { return fmt.Errorf("decode: %s", err.Error()) } // we want to unpack even if there is an error, so we can see partial responses unpackErr := unpack(respDataRaw.Data, response) if respDataRaw.Errors != nil { return RawJsonError{respDataRaw.Errors} } return unpackErr } type RawJsonError struct { json.RawMessage } func (r RawJsonError) Error() string { return string(r.RawMessage) } func unpack(data interface{}, into interface{}) error { d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ Result: into, TagName: "json", ErrorUnused: true, ZeroFields: true, }) if err != nil { return fmt.Errorf("mapstructure: %s", err.Error()) } return d.Decode(data) }