influxdb/pkg/httpc/client.go

216 lines
6.0 KiB
Go

package httpc
import (
"errors"
"io"
"net/http"
"net/url"
"path"
"github.com/influxdata/influxdb"
)
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)
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) {},
}
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, rest ...string) *Req {
return c.Req(http.MethodDelete, nil, urlPath, rest...)
}
// Get generates a GET request.
func (c *Client) Get(urlPath string, rest ...string) *Req {
return c.Req(http.MethodGet, nil, urlPath, rest...)
}
// Patch generates a PATCH request.
func (c *Client) Patch(bFn BodyFn, urlPath string, rest ...string) *Req {
return c.Req(http.MethodPatch, bFn, urlPath, rest...)
}
// 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, rest ...string) *Req {
return c.Patch(BodyJSON(v), urlPath, rest...)
}
// Post generates a POST request.
func (c *Client) Post(bFn BodyFn, urlPath string, rest ...string) *Req {
return c.Req(http.MethodPost, bFn, urlPath, rest...)
}
// 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, rest ...string) *Req {
return c.Post(BodyJSON(v), urlPath, rest...)
}
// Put generates a PUT request.
func (c *Client) Put(bFn BodyFn, urlPath string, rest ...string) *Req {
return c.Req(http.MethodPut, bFn, urlPath, rest...)
}
// 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, rest ...string) *Req {
return c.Put(BodyJSON(v), urlPath, rest...)
}
// Req constructs a request.
func (c *Client) Req(method string, bFn BodyFn, urlPath string, rest ...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: &influxdb.Error{
Code: influxdb.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, rest...), 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, rest ...string) string {
u := c.addr
u.Path = path.Join(u.Path, urlPath)
if len(rest) > 0 {
u.Path = path.Join(u.Path, path.Join(rest...))
}
return u.String()
}