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}