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
16 type Client struct {
17 url string
18 httpClient *http.Client
19 }
20
21
22
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
34
35
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
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
46
47
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
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
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
96 }
97 err = json.NewDecoder(resp.Body).Decode(&out)
98 if err != nil {
99
100 return err
101 }
102 if out.Data != nil {
103 err := jsonutil.UnmarshalGraphQL(*out.Data, v)
104 if err != nil {
105
106 return err
107 }
108 }
109 if len(out.Errors) > 0 {
110 return out.Errors
111 }
112 return nil
113 }
114
115
116
117
118
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
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