...

Source file src/github.com/cli/shurcooL-graphql/graphql.go

Documentation: github.com/cli/shurcooL-graphql

     1  package graphql
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"strings"
    11  
    12  	"github.com/cli/shurcooL-graphql/internal/jsonutil"
    13  )
    14  
    15  // Client is a GraphQL client.
    16  type Client struct {
    17  	url        string       // GraphQL server URL.
    18  	httpClient *http.Client // Non-nil.
    19  }
    20  
    21  // NewClient creates a GraphQL client targeting the specified GraphQL server URL.
    22  // If httpClient is nil, then http.DefaultClient is used.
    23  func NewClient(url string, httpClient *http.Client) *Client {
    24  	if httpClient == nil {
    25  		httpClient = http.DefaultClient
    26  	}
    27  	return &Client{
    28  		url:        url,
    29  		httpClient: httpClient,
    30  	}
    31  }
    32  
    33  // Query executes a single GraphQL query request,
    34  // with a query derived from q, populating the response into it.
    35  // Argument q should be a pointer to struct that corresponds to the GraphQL schema.
    36  func (c *Client) Query(ctx context.Context, q any, variables map[string]any) error {
    37  	return c.do(ctx, queryOperation, q, variables, "")
    38  }
    39  
    40  // QueryNamed is the same as Query but allows a name to be specified for the query.
    41  func (c *Client) QueryNamed(ctx context.Context, queryName string, q any, variables map[string]any) error {
    42  	return c.do(ctx, queryOperation, q, variables, queryName)
    43  }
    44  
    45  // Mutate executes a single GraphQL mutation request,
    46  // with a mutation derived from m, populating the response into it.
    47  // Argument m should be a pointer to struct that corresponds to the GraphQL schema.
    48  func (c *Client) Mutate(ctx context.Context, m any, variables map[string]any) error {
    49  	return c.do(ctx, mutationOperation, m, variables, "")
    50  }
    51  
    52  // MutateNamed is the same as Mutate but allows a name to be specified for the mutation.
    53  func (c *Client) MutateNamed(ctx context.Context, queryName string, m any, variables map[string]any) error {
    54  	return c.do(ctx, mutationOperation, m, variables, queryName)
    55  }
    56  
    57  // do executes a single GraphQL operation.
    58  func (c *Client) do(ctx context.Context, op operationType, v any, variables map[string]any, queryName string) error {
    59  	var query string
    60  	switch op {
    61  	case queryOperation:
    62  		query = constructQuery(v, variables, queryName)
    63  	case mutationOperation:
    64  		query = constructMutation(v, variables, queryName)
    65  	}
    66  	in := struct {
    67  		Query     string         `json:"query"`
    68  		Variables map[string]any `json:"variables,omitempty"`
    69  	}{
    70  		Query:     query,
    71  		Variables: variables,
    72  	}
    73  	var buf bytes.Buffer
    74  	err := json.NewEncoder(&buf).Encode(in)
    75  	if err != nil {
    76  		return err
    77  	}
    78  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, &buf)
    79  	if err != nil {
    80  		return err
    81  	}
    82  	req.Header.Set("Content-Type", "application/json")
    83  	resp, err := c.httpClient.Do(req)
    84  	if err != nil {
    85  		return err
    86  	}
    87  	defer resp.Body.Close()
    88  	if resp.StatusCode != http.StatusOK {
    89  		body, _ := io.ReadAll(resp.Body)
    90  		return fmt.Errorf("non-200 OK status code: %v body: %q", resp.Status, body)
    91  	}
    92  	var out struct {
    93  		Data   *json.RawMessage
    94  		Errors Errors
    95  		//Extensions any // Unused.
    96  	}
    97  	err = json.NewDecoder(resp.Body).Decode(&out)
    98  	if err != nil {
    99  		// TODO: Consider including response body in returned error, if deemed helpful.
   100  		return err
   101  	}
   102  	if out.Data != nil {
   103  		err := jsonutil.UnmarshalGraphQL(*out.Data, v)
   104  		if err != nil {
   105  			// TODO: Consider including response body in returned error, if deemed helpful.
   106  			return err
   107  		}
   108  	}
   109  	if len(out.Errors) > 0 {
   110  		return out.Errors
   111  	}
   112  	return nil
   113  }
   114  
   115  // Errors represents the "errors" array in a response from a GraphQL server.
   116  // If returned via error interface, the slice is expected to contain at least 1 element.
   117  //
   118  // Specification: https://spec.graphql.org/October2021/#sec-Errors.
   119  type Errors []struct {
   120  	Message   string
   121  	Locations []struct {
   122  		Line   int
   123  		Column int
   124  	}
   125  	Path       []any
   126  	Extensions map[string]any
   127  	Type       string
   128  }
   129  
   130  // Error implements error interface.
   131  func (e Errors) Error() string {
   132  	b := strings.Builder{}
   133  	l := len(e)
   134  	for i, err := range e {
   135  		b.WriteString(fmt.Sprintf("Message: %s, Locations: %+v", err.Message, err.Locations))
   136  		if i != l-1 {
   137  			b.WriteString("\n")
   138  		}
   139  	}
   140  	return b.String()
   141  }
   142  
   143  type operationType uint8
   144  
   145  const (
   146  	queryOperation operationType = iota
   147  	mutationOperation
   148  )
   149  

View as plain text