-
Notifications
You must be signed in to change notification settings - Fork 3.5k
/
client.go
214 lines (185 loc) · 5.79 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package httpc
import (
"errors"
"io"
"net/http"
"net/url"
"path"
errors2 "github.com/influxdata/influxdb/v2/kit/platform/errors"
)
type (
// WriteCloserFn is a write closer wrapper than indicates the type of writer by
// returning the header and header value associated with the writer closer.
// i.e. GZIP writer returns header Content-Encoding with value gzip alongside
// the writer.
WriteCloserFn func(closer io.WriteCloser) (string, string, io.WriteCloser)
// doer provides an abstraction around the actual http client behavior. The doer
// can be faked out in tests or another http client provided in its place.
doer interface {
Do(*http.Request) (*http.Response, error)
}
)
// Client is a basic http client that can make cReqs with out having to juggle
// the token and so forth. It provides sane defaults for checking response
// statuses, sets auth token when provided, and sets the content type to
// application/json for each request. The token, response checker, and
// content type can be overridden on the Req as well.
type Client struct {
addr url.URL
doer doer
defaultHeaders http.Header
writerFns []WriteCloserFn
authFn func(*http.Request) error
respFn func(*http.Response) error
statusFn func(*http.Response) error
}
// New creates a new httpc client.
func New(opts ...ClientOptFn) (*Client, error) {
opt := clientOpt{
authFn: func(*http.Request) error { return nil },
}
for _, o := range opts {
if err := o(&opt); err != nil {
return nil, err
}
}
if opt.addr == "" {
return nil, errors.New("must provide a non empty host address")
}
u, err := url.Parse(opt.addr)
if err != nil {
return nil, err
}
if opt.doer == nil {
opt.doer = defaultHTTPClient(u.Scheme, opt.insecureSkipVerify)
}
return &Client{
addr: *u,
doer: opt.doer,
defaultHeaders: opt.headers,
authFn: opt.authFn,
statusFn: opt.statusFn,
writerFns: opt.writerFns,
}, nil
}
// Delete generates a DELETE request.
func (c *Client) Delete(urlPath ...string) *Req {
return c.Req(http.MethodDelete, nil, urlPath...)
}
// Get generates a GET request.
func (c *Client) Get(urlPath ...string) *Req {
return c.Req(http.MethodGet, nil, urlPath...)
}
// Patch generates a PATCH request.
func (c *Client) Patch(bFn BodyFn, urlPath ...string) *Req {
return c.Req(http.MethodPatch, bFn, urlPath...)
}
// PatchJSON generates a PATCH request. This is to be used with value or pointer to value type.
// Providing a stream/reader will result in disappointment.
func (c *Client) PatchJSON(v interface{}, urlPath ...string) *Req {
return c.Patch(BodyJSON(v), urlPath...)
}
// Post generates a POST request.
func (c *Client) Post(bFn BodyFn, urlPath ...string) *Req {
return c.Req(http.MethodPost, bFn, urlPath...)
}
// PostJSON generates a POST request and json encodes the body. This is to be
// used with value or pointer to value type. Providing a stream/reader will result
// in disappointment.
func (c *Client) PostJSON(v interface{}, urlPath ...string) *Req {
return c.Post(BodyJSON(v), urlPath...)
}
// Put generates a PUT request.
func (c *Client) Put(bFn BodyFn, urlPath ...string) *Req {
return c.Req(http.MethodPut, bFn, urlPath...)
}
// PutJSON generates a PUT request. This is to be used with value or pointer to value type.
// Providing a stream/reader will result in disappointment.
func (c *Client) PutJSON(v interface{}, urlPath ...string) *Req {
return c.Put(BodyJSON(v), urlPath...)
}
// Req constructs a request.
func (c *Client) Req(method string, bFn BodyFn, urlPath ...string) *Req {
bodyF := BodyEmpty
if bFn != nil {
bodyF = bFn
}
headers := make(http.Header, len(c.defaultHeaders))
for header, vals := range c.defaultHeaders {
for _, v := range vals {
headers.Add(header, v)
}
}
var buf nopBufCloser
var w io.WriteCloser = &buf
for _, writerFn := range c.writerFns {
header, headerVal, ww := writerFn(w)
w = ww
headers.Add(header, headerVal)
}
header, headerVal, err := bodyF(w)
if err != nil {
// TODO(@jsteenb2): add a inspection for an OK() or Valid() method, then enforce
// that across all consumers? Same for all bodyFns for that matter.
return &Req{
err: &errors2.Error{
Code: errors2.EInvalid,
Err: err,
},
}
}
if header != "" {
headers.Set(header, headerVal)
}
// w.Close here is necessary since we have to close any gzip writer
// or other writer that requires closing.
if err := w.Close(); err != nil {
return &Req{err: err}
}
var body io.Reader
if buf.Len() > 0 {
body = &buf
}
req, err := http.NewRequest(method, c.buildURL(urlPath...), body)
if err != nil {
return &Req{err: err}
}
cr := &Req{
client: c.doer,
req: req,
authFn: c.authFn,
respFn: c.respFn,
statusFn: c.statusFn,
}
return cr.Headers(headers)
}
// Clone creates a new *Client type from an existing client. This may be
// useful if you want to have a shared base client, then take a specific
// client from that base and tack on some extra goodies like specific headers
// and whatever else that suits you.
// Note: a new net.http.Client type will not be created. It will share the existing
// http.Client from the parent httpc.Client. Same connection pool, different specifics.
func (c *Client) Clone(opts ...ClientOptFn) (*Client, error) {
existingOpts := []ClientOptFn{
WithAuth(c.authFn),
withDoer(c.doer),
WithRespFn(c.respFn),
WithStatusFn(c.statusFn),
}
for h, vals := range c.defaultHeaders {
for _, v := range vals {
existingOpts = append(existingOpts, WithHeader(h, v))
}
}
for _, fn := range c.writerFns {
existingOpts = append(existingOpts, WithWriterFn(fn))
}
return New(append(existingOpts, opts...)...)
}
func (c *Client) buildURL(urlPath ...string) string {
u := c.addr
if len(urlPath) > 0 {
u.Path = path.Join(u.Path, path.Join(urlPath...))
}
return u.String()
}