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