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  }