github.com/opiuman/genqlient@v1.0.0/graphql/client.go (about)

     1  package graphql
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"strings"
    13  
    14  	"github.com/vektah/gqlparser/v2/gqlerror"
    15  )
    16  
    17  // RequestOption is a function that can be passed to MakeRequest to modify the http request behavior.
    18  type RequestOption func(req *http.Request)
    19  
    20  // Client is the interface that the generated code calls into to actually make
    21  // requests.
    22  type Client interface {
    23  	// MakeRequest must make a request to the client's GraphQL API.
    24  	//
    25  	// ctx is the context that should be used to make this request.  If context
    26  	// is disabled in the genqlient settings, this will be set to
    27  	// context.Background().
    28  	//
    29  	// req contains the data to be sent to the GraphQL server.  Typically GraphQL
    30  	// APIs will expect it to simply be marshalled as JSON, but MakeRequest may
    31  	// customize this.
    32  	//
    33  	// resp is the Response object into which the server's response will be
    34  	// unmarshalled. Typically GraphQL APIs will return JSON which can be
    35  	// unmarshalled directly into resp, but MakeRequest can customize it.
    36  	// If the response contains an error, this must also be returned by
    37  	// MakeRequest.  The field resp.Data will be prepopulated with a pointer
    38  	// to an empty struct of the correct generated type (e.g. MyQueryResponse).
    39  	MakeRequest(
    40  		ctx context.Context,
    41  		req *Request,
    42  		resp *Response,
    43  		opts ...RequestOption,
    44  	) error
    45  }
    46  
    47  type client struct {
    48  	httpClient Doer
    49  	endpoint   string
    50  	method     string
    51  }
    52  
    53  // NewClient returns a Client which makes requests to the given endpoint,
    54  // suitable for most users.
    55  //
    56  // The client makes POST requests to the given GraphQL endpoint using standard
    57  // GraphQL HTTP-over-JSON transport.  It will use the given http client, or
    58  // http.DefaultClient if a nil client is passed.
    59  //
    60  // The typical method of adding authentication headers is to wrap the client's
    61  // Transport to add those headers.  See example/caller.go for an example.
    62  func NewClient(endpoint string, httpClient Doer) Client {
    63  	return newClient(endpoint, httpClient, http.MethodPost)
    64  }
    65  
    66  // NewClientUsingGet returns a Client which makes requests to the given endpoint,
    67  // suitable for most users.
    68  //
    69  // The client makes GET requests to the given GraphQL endpoint using a GET query,
    70  // with the query, operation name and variables encoded as URL parameters.
    71  // It will use the given http client, or http.DefaultClient if a nil client is passed.
    72  //
    73  // The client does not support mutations, and will return an error if passed a request
    74  // that attempts one.
    75  //
    76  // The typical method of adding authentication headers is to wrap the client's
    77  // Transport to add those headers.  See example/caller.go for an example.
    78  func NewClientUsingGet(endpoint string, httpClient Doer) Client {
    79  	return newClient(endpoint, httpClient, http.MethodGet)
    80  }
    81  
    82  func newClient(endpoint string, httpClient Doer, method string) Client {
    83  	if httpClient == nil || httpClient == (*http.Client)(nil) {
    84  		httpClient = http.DefaultClient
    85  	}
    86  	return &client{httpClient, endpoint, method}
    87  }
    88  
    89  // Doer encapsulates the methods from *http.Client needed by Client.
    90  // The methods should have behavior to match that of *http.Client
    91  // (or mocks for the same).
    92  type Doer interface {
    93  	Do(*http.Request) (*http.Response, error)
    94  }
    95  
    96  // Request contains all the values required to build queries executed by
    97  // the graphql.Client.
    98  //
    99  // Typically, GraphQL APIs will accept a JSON payload of the form
   100  //	{"query": "query myQuery { ... }", "variables": {...}}`
   101  // and Request marshals to this format.  However, MakeRequest may
   102  // marshal the data in some other way desired by the backend.
   103  type Request struct {
   104  	// The literal string representing the GraphQL query, e.g.
   105  	// `query myQuery { myField }`.
   106  	Query string `json:"query"`
   107  	// A JSON-marshalable value containing the variables to be sent
   108  	// along with the query, or nil if there are none.
   109  	Variables interface{} `json:"variables,omitempty"`
   110  	// The GraphQL operation name. The server typically doesn't
   111  	// require this unless there are multiple queries in the
   112  	// document, but genqlient sets it unconditionally anyway.
   113  	OpName string `json:"operationName"`
   114  }
   115  
   116  // Response that contains data returned by the GraphQL API.
   117  //
   118  // Typically, GraphQL APIs will return a JSON payload of the form
   119  //	{"data": {...}, "errors": {...}}
   120  // It may additionally contain a key named "extensions", that
   121  // might hold GraphQL protocol extensions. Extensions and Errors
   122  // are optional, depending on the values returned by the server.
   123  type Response struct {
   124  	Data       interface{}            `json:"data"`
   125  	Extensions map[string]interface{} `json:"extensions,omitempty"`
   126  	Errors     gqlerror.List          `json:"errors,omitempty"`
   127  }
   128  
   129  func (c *client) MakeRequest(ctx context.Context, req *Request, resp *Response, opts ...RequestOption) error {
   130  	var httpReq *http.Request
   131  	var err error
   132  	if c.method == http.MethodGet {
   133  		httpReq, err = c.createGetRequest(req)
   134  	} else {
   135  		httpReq, err = c.createPostRequest(req)
   136  	}
   137  
   138  	if err != nil {
   139  		return err
   140  	}
   141  	httpReq.Header.Set("Content-Type", "application/json")
   142  	for _, opt := range opts {
   143  		opt(httpReq)
   144  	}
   145  
   146  	if ctx != nil {
   147  		httpReq = httpReq.WithContext(ctx)
   148  	}
   149  
   150  	httpResp, err := c.httpClient.Do(httpReq)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	defer httpResp.Body.Close()
   155  
   156  	if httpResp.StatusCode != http.StatusOK {
   157  		var respBody []byte
   158  		respBody, err = io.ReadAll(httpResp.Body)
   159  		if err != nil {
   160  			respBody = []byte(fmt.Sprintf("<unreadable: %v>", err))
   161  		}
   162  		return fmt.Errorf("returned error %v: %s", httpResp.Status, respBody)
   163  	}
   164  
   165  	err = json.NewDecoder(httpResp.Body).Decode(resp)
   166  	if err != nil {
   167  		return err
   168  	}
   169  	if len(resp.Errors) > 0 {
   170  		return resp.Errors
   171  	}
   172  	return nil
   173  }
   174  
   175  func (c *client) createPostRequest(req *Request) (*http.Request, error) {
   176  	body, err := json.Marshal(req)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	httpReq, err := http.NewRequest(
   182  		c.method,
   183  		c.endpoint,
   184  		bytes.NewReader(body))
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  
   189  	return httpReq, nil
   190  }
   191  
   192  func (c *client) createGetRequest(req *Request) (*http.Request, error) {
   193  	parsedURL, err := url.Parse(c.endpoint)
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	queryParams := parsedURL.Query()
   199  	queryUpdated := false
   200  
   201  	if req.Query != "" {
   202  		if strings.HasPrefix(strings.TrimSpace(req.Query), "mutation") {
   203  			return nil, errors.New("client does not support mutations")
   204  		}
   205  		queryParams.Set("query", req.Query)
   206  		queryUpdated = true
   207  	}
   208  
   209  	if req.OpName != "" {
   210  		queryParams.Set("operationName", req.OpName)
   211  		queryUpdated = true
   212  	}
   213  
   214  	if req.Variables != nil {
   215  		variables, variablesErr := json.Marshal(req.Variables)
   216  		if variablesErr != nil {
   217  			return nil, variablesErr
   218  		}
   219  		queryParams.Set("variables", string(variables))
   220  		queryUpdated = true
   221  	}
   222  
   223  	if queryUpdated {
   224  		parsedURL.RawQuery = queryParams.Encode()
   225  	}
   226  
   227  	httpReq, err := http.NewRequest(
   228  		c.method,
   229  		parsedURL.String(),
   230  		http.NoBody)
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  
   235  	return httpReq, nil
   236  }