github.com/nathanstitt/genqlient@v0.3.1-0.20211028004951-a2bda3c41ab8/graphql/client.go (about) 1 package graphql 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io/ioutil" 9 "net/http" 10 11 "github.com/vektah/gqlparser/v2/gqlerror" 12 ) 13 14 // Client is the interface that the generated code calls into to actually make 15 // requests. 16 // 17 // Unstable: This interface is likely to change before v1.0, see #19. Creating 18 // a client with NewClient will remain the same. 19 type Client interface { 20 // MakeRequest must make a request to the client's GraphQL API. 21 // 22 // ctx is the context that should be used to make this request. If context 23 // is disabled in the genqlient settings, this will be set to 24 // context.Background(). 25 // 26 // query is the literal string representing the GraphQL query, e.g. 27 // `query myQuery { myField }`. variables contains a JSON-marshalable 28 // value containing the variables to be sent along with the query, 29 // or may be nil if there are none. Typically, GraphQL APIs will 30 // accept a JSON payload of the form 31 // {"query": "query myQuery { ... }", "variables": {...}}` 32 // but MakeRequest may use some other transport, handle extensions, or set 33 // other parameters, if it wishes. 34 // 35 // retval is a pointer to the struct representing the query result, e.g. 36 // new(myQueryResponse). Typically, GraphQL APIs will return a JSON 37 // payload of the form 38 // {"data": {...}, "errors": {...}} 39 // and retval is designed so that `data` will json-unmarshal into `retval`. 40 // (Errors are returned.) But again, MakeRequest may customize this. 41 MakeRequest( 42 ctx context.Context, 43 opName string, 44 query string, 45 input, retval interface{}, 46 ) error 47 } 48 49 type client struct { 50 httpClient Doer 51 endpoint string 52 method string 53 } 54 55 // NewClient returns a Client which makes requests to the given endpoint, 56 // suitable for most users. 57 // 58 // The client makes POST requests to the given GraphQL endpoint using standard 59 // GraphQL HTTP-over-JSON transport. It will use the given http client, or 60 // http.DefaultClient if a nil client is passed. 61 // 62 // The typical method of adding authentication headers is to wrap the client's 63 // Transport to add those headers. See example/caller.go for an example. 64 func NewClient(endpoint string, httpClient Doer) Client { 65 if httpClient == nil || httpClient == (*http.Client)(nil) { 66 httpClient = http.DefaultClient 67 } 68 return &client{httpClient, endpoint, http.MethodPost} 69 } 70 71 // Doer encapsulates the methods from *http.Client needed by Client. 72 // The methods should have behavior to match that of *http.Client 73 // (or mocks for the same). 74 type Doer interface { 75 Do(*http.Request) (*http.Response, error) 76 } 77 78 type payload struct { 79 Query string `json:"query"` 80 Variables interface{} `json:"variables,omitempty"` 81 // OpName is only required if there are multiple queries in the document, 82 // but we set it unconditionally, because that's easier. 83 OpName string `json:"operationName"` 84 } 85 86 type response struct { 87 Data interface{} `json:"data"` 88 Errors gqlerror.List `json:"errors"` 89 } 90 91 func (c *client) MakeRequest(ctx context.Context, opName string, query string, retval interface{}, variables interface{}) error { 92 body, err := json.Marshal(payload{ 93 Query: query, 94 Variables: variables, 95 OpName: opName, 96 }) 97 if err != nil { 98 return err 99 } 100 101 req, err := http.NewRequest( 102 c.method, 103 c.endpoint, 104 bytes.NewReader(body)) 105 if err != nil { 106 return err 107 } 108 req.Header.Set("Content-Type", "application/json") 109 110 if ctx != nil { 111 req = req.WithContext(ctx) 112 } 113 resp, err := c.httpClient.Do(req) 114 if err != nil { 115 return err 116 } 117 defer resp.Body.Close() 118 119 if resp.StatusCode != http.StatusOK { 120 var respBody []byte 121 respBody, err = ioutil.ReadAll(resp.Body) 122 if err != nil { 123 respBody = []byte(fmt.Sprintf("<unreadable: %v>", err)) 124 } 125 return fmt.Errorf("returned error %v: %s", resp.Status, respBody) 126 } 127 128 var dataAndErrors response 129 dataAndErrors.Data = retval 130 err = json.NewDecoder(resp.Body).Decode(&dataAndErrors) 131 if err != nil { 132 return err 133 } 134 135 if len(dataAndErrors.Errors) > 0 { 136 return dataAndErrors.Errors 137 } 138 return nil 139 }