5995653583
## Summary: In this commit I refactor the argument-generation logic to move most of the code out of the template and into the type-generator. This logic predates #51, and I didn't think to update it there, but I think it benefits from similar treatment, for similar reasons. Specifically, the main change is to treat variables as another struct type we can generate, rather than handling them inline as a `map[string]interface{}`. Users still pass them the same way, but instead of putting them into a `map[string]interface{}` and JSONifying that, we generate a struct and put them there. This turns out to simplify things quite a lot, because we already have a lot of code to generate types. Notably, the omitempty code goes from a dozen lines to basically two, and fixes a bug (#43) in the process, because now that we have a struct, `json.Marshal` will do our work for us! (And, once we have syntax for it (#14), we'll be able to handle field-level omitempty basically for free.) More importantly, it will simplify custom marshalers (#38, forthcoming) significantly, since we do all that logic at the containing-struct level, but will need to apply it to arguments. It does require two breaking changes: 1. For folks implementing the `graphql.Client` API (rather than just calling `NewClient`): we now pass them variables as an `interface{}` rather than a `map[string]interface{}`. For most callers, including Khan/webapp, this is basically a one-line change to the signature of their `MakeRequest`, and it should be a lot more future-proof. 2. genqlient's handling of the `omitempty` option has changed to match that of `encoding/json`, in particular it now never considers structs "empty". The difference was never intentional (I just didn't realize that behavior of `encoding/json`); arguably our behavior was more useful but I think that's outweighed by the value of consistency with `encoding/json` as well as the simpler and more correct implementation (fixing #43 is actually quite nontrivial otherwise). Once we have custom unmarshaler support (#38), users will be able to map a zero value to JSON null if they wish, which is mostly if not entirely equivalent for GraphQL's purposes. Issue: https://github.com/Khan/genqlient/issues/38 Issue: https://github.com/Khan/genqlient/issues/43 ## Test plan: make check Author: benjaminjkraft Reviewers: StevenACoffman, dnerdy, aberkan, jvoll, mahtabsabet, MiguelCastillo Required Reviewers: Approved By: StevenACoffman, dnerdy Checks: ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14), ✅ Lint, ✅ Lint, ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14) Pull Request URL: https://github.com/Khan/genqlient/pull/103
133 lines
3.7 KiB
Go
133 lines
3.7 KiB
Go
package graphql
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
|
|
"github.com/vektah/gqlparser/v2/gqlerror"
|
|
)
|
|
|
|
// Client is the interface that the generated code calls into to actually make
|
|
// requests.
|
|
//
|
|
// Unstable: This interface is likely to change before v1.0, see #19. Creating
|
|
// a client with NewClient will remain the same.
|
|
type Client interface {
|
|
// MakeRequest must make a request to the client's GraphQL API.
|
|
//
|
|
// ctx is the context that should be used to make this request. If context
|
|
// is disabled in the genqlient settings, this will be set to
|
|
// context.Background().
|
|
//
|
|
// query is the literal string representing the GraphQL query, e.g.
|
|
// `query myQuery { myField }`. variables contains a JSON-marshalable
|
|
// value containing the variables to be sent along with the query,
|
|
// or may be nil if there are none. Typically, GraphQL APIs will
|
|
// accept a JSON payload of the form
|
|
// {"query": "query myQuery { ... }", "variables": {...}}`
|
|
// but MakeRequest may use some other transport, handle extensions, or set
|
|
// other parameters, if it wishes.
|
|
//
|
|
// retval is a pointer to the struct representing the query result, e.g.
|
|
// new(myQueryResponse). Typically, GraphQL APIs will return a JSON
|
|
// payload of the form
|
|
// {"data": {...}, "errors": {...}}
|
|
// and retval is designed so that `data` will json-unmarshal into `retval`.
|
|
// (Errors are returned.) But again, MakeRequest may customize this.
|
|
MakeRequest(
|
|
ctx context.Context,
|
|
opName string,
|
|
query string,
|
|
input, retval interface{},
|
|
) error
|
|
}
|
|
|
|
type client struct {
|
|
httpClient *http.Client
|
|
endpoint string
|
|
method string
|
|
}
|
|
|
|
// NewClient returns a Client which makes requests to the given endpoint,
|
|
// suitable for most users.
|
|
//
|
|
// The client makes POST requests to the given GraphQL endpoint using standard
|
|
// GraphQL HTTP-over-JSON transport. It will use the given http client, or
|
|
// http.DefaultClient if a nil client is passed.
|
|
//
|
|
// The typical method of adding authentication headers is to wrap the client's
|
|
// Transport to add those headers. See example/caller.go for an example.
|
|
func NewClient(endpoint string, httpClient *http.Client) Client {
|
|
if httpClient == nil {
|
|
httpClient = http.DefaultClient
|
|
}
|
|
return &client{httpClient, endpoint, http.MethodPost}
|
|
}
|
|
|
|
type payload struct {
|
|
Query string `json:"query"`
|
|
Variables interface{} `json:"variables,omitempty"`
|
|
// OpName is only required if there are multiple queries in the document,
|
|
// but we set it unconditionally, because that's easier.
|
|
OpName string `json:"operationName"`
|
|
}
|
|
|
|
type response struct {
|
|
Data interface{} `json:"data"`
|
|
Errors gqlerror.List `json:"errors"`
|
|
}
|
|
|
|
func (c *client) MakeRequest(ctx context.Context, opName string, query string, retval interface{}, variables interface{}) error {
|
|
body, err := json.Marshal(payload{
|
|
Query: query,
|
|
Variables: variables,
|
|
OpName: opName,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := http.NewRequest(
|
|
c.method,
|
|
c.endpoint,
|
|
bytes.NewReader(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
if ctx != nil {
|
|
req = req.WithContext(ctx)
|
|
}
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
var respBody []byte
|
|
respBody, err = ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
respBody = []byte(fmt.Sprintf("<unreadable: %v>", err))
|
|
}
|
|
return fmt.Errorf("returned error %v: %s", resp.Status, respBody)
|
|
}
|
|
|
|
var dataAndErrors response
|
|
dataAndErrors.Data = retval
|
|
err = json.NewDecoder(resp.Body).Decode(&dataAndErrors)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(dataAndErrors.Errors) > 0 {
|
|
return dataAndErrors.Errors
|
|
}
|
|
return nil
|
|
}
|