graphql.go

  1package graphql
  2
  3import (
  4	"bytes"
  5	"context"
  6	"encoding/json"
  7	"fmt"
  8	"net/http"
  9
 10	"github.com/shurcooL/go/ctxhttp"
 11	"github.com/shurcooL/graphql/internal/jsonutil"
 12)
 13
 14// Client is a GraphQL client.
 15type Client struct {
 16	url        string // GraphQL server URL.
 17	httpClient *http.Client
 18}
 19
 20// NewClient creates a GraphQL client targeting the specified GraphQL server URL.
 21// If httpClient is nil, then http.DefaultClient is used.
 22func NewClient(url string, httpClient *http.Client) *Client {
 23	if httpClient == nil {
 24		httpClient = http.DefaultClient
 25	}
 26	return &Client{
 27		url:        url,
 28		httpClient: httpClient,
 29	}
 30}
 31
 32// Query executes a single GraphQL query request,
 33// with a query derived from q, populating the response into it.
 34// q should be a pointer to struct that corresponds to the GraphQL schema.
 35func (c *Client) Query(ctx context.Context, q interface{}, variables map[string]interface{}) error {
 36	return c.do(ctx, queryOperation, q, variables)
 37}
 38
 39// Mutate executes a single GraphQL mutation request,
 40// with a mutation derived from m, populating the response into it.
 41// m should be a pointer to struct that corresponds to the GraphQL schema.
 42func (c *Client) Mutate(ctx context.Context, m interface{}, variables map[string]interface{}) error {
 43	return c.do(ctx, mutationOperation, m, variables)
 44}
 45
 46// do executes a single GraphQL operation.
 47func (c *Client) do(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}) error {
 48	var query string
 49	switch op {
 50	case queryOperation:
 51		query = constructQuery(v, variables)
 52	case mutationOperation:
 53		query = constructMutation(v, variables)
 54	}
 55	in := struct {
 56		Query     string                 `json:"query"`
 57		Variables map[string]interface{} `json:"variables,omitempty"`
 58	}{
 59		Query:     query,
 60		Variables: variables,
 61	}
 62	var buf bytes.Buffer
 63	err := json.NewEncoder(&buf).Encode(in)
 64	if err != nil {
 65		return err
 66	}
 67	resp, err := ctxhttp.Post(ctx, c.httpClient, c.url, "application/json", &buf)
 68	if err != nil {
 69		return err
 70	}
 71	defer resp.Body.Close()
 72	if resp.StatusCode != http.StatusOK {
 73		return fmt.Errorf("unexpected status: %v", resp.Status)
 74	}
 75	var out struct {
 76		Data   *json.RawMessage
 77		Errors errors
 78		//Extensions interface{} // Unused.
 79	}
 80	err = json.NewDecoder(resp.Body).Decode(&out)
 81	if err != nil {
 82		return err
 83	}
 84	if out.Data != nil {
 85		err := jsonutil.UnmarshalGraphQL(*out.Data, v)
 86		if err != nil {
 87			return err
 88		}
 89	}
 90	if len(out.Errors) > 0 {
 91		return out.Errors
 92	}
 93	return nil
 94}
 95
 96// errors represents the "errors" array in a response from a GraphQL server.
 97// If returned via error interface, the slice is expected to contain at least 1 element.
 98//
 99// Specification: https://facebook.github.io/graphql/#sec-Errors.
100type errors []struct {
101	Message   string
102	Locations []struct {
103		Line   int
104		Column int
105	}
106}
107
108// Error implements error interface.
109func (e errors) Error() string {
110	return e[0].Message
111}
112
113type operationType uint8
114
115const (
116	queryOperation operationType = iota
117	mutationOperation
118	//subscriptionOperation // Unused.
119)