1
2
3 package client
4
5 import (
6 "bytes"
7 "encoding/json"
8 "fmt"
9 "io"
10 "net/http"
11 "net/http/httptest"
12 "regexp"
13
14 "github.com/mitchellh/mapstructure"
15 )
16
17 type (
18
19 Client struct {
20 h http.Handler
21 dc *mapstructure.DecoderConfig
22 opts []Option
23 }
24
25
26
27
28 Option func(bd *Request)
29
30
31 Request struct {
32 Query string `json:"query"`
33 Variables map[string]interface{} `json:"variables,omitempty"`
34 OperationName string `json:"operationName,omitempty"`
35 Extensions map[string]interface{} `json:"extensions,omitempty"`
36 HTTP *http.Request `json:"-"`
37 }
38
39
40 Response struct {
41 Data interface{}
42 Errors json.RawMessage
43 Extensions map[string]interface{}
44 }
45 )
46
47
48
49 func New(h http.Handler, opts ...Option) *Client {
50 p := &Client{
51 h: h,
52 opts: opts,
53 }
54
55 return p
56 }
57
58
59 func (p *Client) MustPost(query string, response interface{}, options ...Option) {
60 if err := p.Post(query, response, options...); err != nil {
61 panic(err)
62 }
63 }
64
65
66
67 func (p *Client) Post(query string, response interface{}, options ...Option) error {
68 respDataRaw, err := p.RawPost(query, options...)
69 if err != nil {
70 return err
71 }
72
73
74 unpackErr := unpack(respDataRaw.Data, response, p.dc)
75
76 if respDataRaw.Errors != nil {
77 return RawJsonError{respDataRaw.Errors}
78 }
79 return unpackErr
80 }
81
82
83
84
85 func (p *Client) RawPost(query string, options ...Option) (*Response, error) {
86 r, err := p.newRequest(query, options...)
87 if err != nil {
88 return nil, fmt.Errorf("build: %w", err)
89 }
90
91 w := httptest.NewRecorder()
92 p.h.ServeHTTP(w, r)
93
94 if w.Code >= http.StatusBadRequest {
95 return nil, fmt.Errorf("http %d: %s", w.Code, w.Body.String())
96 }
97
98
99
100 respDataRaw := &Response{}
101 err = json.Unmarshal(w.Body.Bytes(), &respDataRaw)
102 if err != nil {
103 return nil, fmt.Errorf("decode: %w", err)
104 }
105
106 return respDataRaw, nil
107 }
108
109 func (p *Client) newRequest(query string, options ...Option) (*http.Request, error) {
110 bd := &Request{
111 Query: query,
112 HTTP: httptest.NewRequest(http.MethodPost, "/", nil),
113 }
114 bd.HTTP.Header.Set("Content-Type", "application/json")
115
116
117 for _, option := range p.opts {
118 option(bd)
119 }
120
121 for _, option := range options {
122 option(bd)
123 }
124
125 contentType := bd.HTTP.Header.Get("Content-Type")
126 switch {
127 case regexp.MustCompile(`multipart/form-data; ?boundary=.*`).MatchString(contentType):
128 break
129 case "application/json" == contentType:
130 requestBody, err := json.Marshal(bd)
131 if err != nil {
132 return nil, fmt.Errorf("encode: %w", err)
133 }
134 bd.HTTP.Body = io.NopCloser(bytes.NewBuffer(requestBody))
135 default:
136 panic("unsupported encoding " + bd.HTTP.Header.Get("Content-Type"))
137 }
138
139 return bd.HTTP, nil
140 }
141
142
143 func (p *Client) SetCustomDecodeConfig(dc *mapstructure.DecoderConfig) {
144 p.dc = dc
145 }
146
147 func unpack(data interface{}, into interface{}, customDc *mapstructure.DecoderConfig) error {
148 dc := &mapstructure.DecoderConfig{
149 TagName: "json",
150 ErrorUnused: true,
151 ZeroFields: true,
152 }
153 if customDc != nil {
154 dc = customDc
155 }
156 dc.Result = into
157
158 d, err := mapstructure.NewDecoder(dc)
159 if err != nil {
160 return fmt.Errorf("mapstructure: %w", err)
161 }
162
163 return d.Decode(data)
164 }
165
View as plain text