wg-quicker/vendor/github.com/dghubble/sling/sling.go

424 lines
12 KiB
Go

package sling
import (
"encoding/base64"
"io"
"io/ioutil"
"net/http"
"net/url"
goquery "github.com/google/go-querystring/query"
)
const (
contentType = "Content-Type"
jsonContentType = "application/json"
formContentType = "application/x-www-form-urlencoded"
)
// Doer executes http requests. It is implemented by *http.Client. You can
// wrap *http.Client with layers of Doers to form a stack of client-side
// middleware.
type Doer interface {
Do(req *http.Request) (*http.Response, error)
}
// Sling is an HTTP Request builder and sender.
type Sling struct {
// http Client for doing requests
httpClient Doer
// HTTP method (GET, POST, etc.)
method string
// raw url string for requests
rawURL string
// stores key-values pairs to add to request's Headers
header http.Header
// url tagged query structs
queryStructs []interface{}
// body provider
bodyProvider BodyProvider
// response decoder
responseDecoder ResponseDecoder
}
// New returns a new Sling with an http DefaultClient.
func New() *Sling {
return &Sling{
httpClient: http.DefaultClient,
method: "GET",
header: make(http.Header),
queryStructs: make([]interface{}, 0),
responseDecoder: jsonDecoder{},
}
}
// New returns a copy of a Sling for creating a new Sling with properties
// from a parent Sling. For example,
//
// parentSling := sling.New().Client(client).Base("https://api.io/")
// fooSling := parentSling.New().Get("foo/")
// barSling := parentSling.New().Get("bar/")
//
// fooSling and barSling will both use the same client, but send requests to
// https://api.io/foo/ and https://api.io/bar/ respectively.
//
// Note that query and body values are copied so if pointer values are used,
// mutating the original value will mutate the value within the child Sling.
func (s *Sling) New() *Sling {
// copy Headers pairs into new Header map
headerCopy := make(http.Header)
for k, v := range s.header {
headerCopy[k] = v
}
return &Sling{
httpClient: s.httpClient,
method: s.method,
rawURL: s.rawURL,
header: headerCopy,
queryStructs: append([]interface{}{}, s.queryStructs...),
bodyProvider: s.bodyProvider,
responseDecoder: s.responseDecoder,
}
}
// Http Client
// Client sets the http Client used to do requests. If a nil client is given,
// the http.DefaultClient will be used.
func (s *Sling) Client(httpClient *http.Client) *Sling {
if httpClient == nil {
return s.Doer(http.DefaultClient)
}
return s.Doer(httpClient)
}
// Doer sets the custom Doer implementation used to do requests.
// If a nil client is given, the http.DefaultClient will be used.
func (s *Sling) Doer(doer Doer) *Sling {
if doer == nil {
s.httpClient = http.DefaultClient
} else {
s.httpClient = doer
}
return s
}
// Method
// Head sets the Sling method to HEAD and sets the given pathURL.
func (s *Sling) Head(pathURL string) *Sling {
s.method = "HEAD"
return s.Path(pathURL)
}
// Get sets the Sling method to GET and sets the given pathURL.
func (s *Sling) Get(pathURL string) *Sling {
s.method = "GET"
return s.Path(pathURL)
}
// Post sets the Sling method to POST and sets the given pathURL.
func (s *Sling) Post(pathURL string) *Sling {
s.method = "POST"
return s.Path(pathURL)
}
// Put sets the Sling method to PUT and sets the given pathURL.
func (s *Sling) Put(pathURL string) *Sling {
s.method = "PUT"
return s.Path(pathURL)
}
// Patch sets the Sling method to PATCH and sets the given pathURL.
func (s *Sling) Patch(pathURL string) *Sling {
s.method = "PATCH"
return s.Path(pathURL)
}
// Delete sets the Sling method to DELETE and sets the given pathURL.
func (s *Sling) Delete(pathURL string) *Sling {
s.method = "DELETE"
return s.Path(pathURL)
}
// Options sets the Sling method to OPTIONS and sets the given pathURL.
func (s *Sling) Options(pathURL string) *Sling {
s.method = "OPTIONS"
return s.Path(pathURL)
}
// Trace sets the Sling method to TRACE and sets the given pathURL.
func (s *Sling) Trace(pathURL string) *Sling {
s.method = "TRACE"
return s.Path(pathURL)
}
// Connect sets the Sling method to CONNECT and sets the given pathURL.
func (s *Sling) Connect(pathURL string) *Sling {
s.method = "CONNECT"
return s.Path(pathURL)
}
// Header
// Add adds the key, value pair in Headers, appending values for existing keys
// to the key's values. Header keys are canonicalized.
func (s *Sling) Add(key, value string) *Sling {
s.header.Add(key, value)
return s
}
// Set sets the key, value pair in Headers, replacing existing values
// associated with key. Header keys are canonicalized.
func (s *Sling) Set(key, value string) *Sling {
s.header.Set(key, value)
return s
}
// SetBasicAuth sets the Authorization header to use HTTP Basic Authentication
// with the provided username and password. With HTTP Basic Authentication
// the provided username and password are not encrypted.
func (s *Sling) SetBasicAuth(username, password string) *Sling {
return s.Set("Authorization", "Basic "+basicAuth(username, password))
}
// basicAuth returns the base64 encoded username:password for basic auth copied
// from net/http.
func basicAuth(username, password string) string {
auth := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(auth))
}
// Url
// Base sets the rawURL. If you intend to extend the url with Path,
// baseUrl should be specified with a trailing slash.
func (s *Sling) Base(rawURL string) *Sling {
s.rawURL = rawURL
return s
}
// Path extends the rawURL with the given path by resolving the reference to
// an absolute URL. If parsing errors occur, the rawURL is left unmodified.
func (s *Sling) Path(path string) *Sling {
baseURL, baseErr := url.Parse(s.rawURL)
pathURL, pathErr := url.Parse(path)
if baseErr == nil && pathErr == nil {
s.rawURL = baseURL.ResolveReference(pathURL).String()
return s
}
return s
}
// QueryStruct appends the queryStruct to the Sling's queryStructs. The value
// pointed to by each queryStruct will be encoded as url query parameters on
// new requests (see Request()).
// The queryStruct argument should be a pointer to a url tagged struct. See
// https://godoc.org/github.com/google/go-querystring/query for details.
func (s *Sling) QueryStruct(queryStruct interface{}) *Sling {
if queryStruct != nil {
s.queryStructs = append(s.queryStructs, queryStruct)
}
return s
}
// Body
// Body sets the Sling's body. The body value will be set as the Body on new
// requests (see Request()).
// If the provided body is also an io.Closer, the request Body will be closed
// by http.Client methods.
func (s *Sling) Body(body io.Reader) *Sling {
if body == nil {
return s
}
return s.BodyProvider(bodyProvider{body: body})
}
// BodyProvider sets the Sling's body provider.
func (s *Sling) BodyProvider(body BodyProvider) *Sling {
if body == nil {
return s
}
s.bodyProvider = body
ct := body.ContentType()
if ct != "" {
s.Set(contentType, ct)
}
return s
}
// BodyJSON sets the Sling's bodyJSON. The value pointed to by the bodyJSON
// will be JSON encoded as the Body on new requests (see Request()).
// The bodyJSON argument should be a pointer to a JSON tagged struct. See
// https://golang.org/pkg/encoding/json/#MarshalIndent for details.
func (s *Sling) BodyJSON(bodyJSON interface{}) *Sling {
if bodyJSON == nil {
return s
}
return s.BodyProvider(jsonBodyProvider{payload: bodyJSON})
}
// BodyForm sets the Sling's bodyForm. The value pointed to by the bodyForm
// will be url encoded as the Body on new requests (see Request()).
// The bodyForm argument should be a pointer to a url tagged struct. See
// https://godoc.org/github.com/google/go-querystring/query for details.
func (s *Sling) BodyForm(bodyForm interface{}) *Sling {
if bodyForm == nil {
return s
}
return s.BodyProvider(formBodyProvider{payload: bodyForm})
}
// Requests
// Request returns a new http.Request created with the Sling properties.
// Returns any errors parsing the rawURL, encoding query structs, encoding
// the body, or creating the http.Request.
func (s *Sling) Request() (*http.Request, error) {
reqURL, err := url.Parse(s.rawURL)
if err != nil {
return nil, err
}
err = addQueryStructs(reqURL, s.queryStructs)
if err != nil {
return nil, err
}
var body io.Reader
if s.bodyProvider != nil {
body, err = s.bodyProvider.Body()
if err != nil {
return nil, err
}
}
req, err := http.NewRequest(s.method, reqURL.String(), body)
if err != nil {
return nil, err
}
addHeaders(req, s.header)
return req, err
}
// addQueryStructs parses url tagged query structs using go-querystring to
// encode them to url.Values and format them onto the url.RawQuery. Any
// query parsing or encoding errors are returned.
func addQueryStructs(reqURL *url.URL, queryStructs []interface{}) error {
urlValues, err := url.ParseQuery(reqURL.RawQuery)
if err != nil {
return err
}
// encodes query structs into a url.Values map and merges maps
for _, queryStruct := range queryStructs {
queryValues, err := goquery.Values(queryStruct)
if err != nil {
return err
}
for key, values := range queryValues {
for _, value := range values {
urlValues.Add(key, value)
}
}
}
// url.Values format to a sorted "url encoded" string, e.g. "key=val&foo=bar"
reqURL.RawQuery = urlValues.Encode()
return nil
}
// addHeaders adds the key, value pairs from the given http.Header to the
// request. Values for existing keys are appended to the keys values.
func addHeaders(req *http.Request, header http.Header) {
for key, values := range header {
for _, value := range values {
req.Header.Add(key, value)
}
}
}
// Sending
// ResponseDecoder sets the Sling's response decoder.
func (s *Sling) ResponseDecoder(decoder ResponseDecoder) *Sling {
if decoder == nil {
return s
}
s.responseDecoder = decoder
return s
}
// ReceiveSuccess creates a new HTTP request and returns the response. Success
// responses (2XX) are JSON decoded into the value pointed to by successV.
// Any error creating the request, sending it, or decoding a 2XX response
// is returned.
func (s *Sling) ReceiveSuccess(successV interface{}) (*http.Response, error) {
return s.Receive(successV, nil)
}
// Receive creates a new HTTP request and returns the response. Success
// responses (2XX) are JSON decoded into the value pointed to by successV and
// other responses are JSON decoded into the value pointed to by failureV.
// If the status code of response is 204(no content) or the Content-Lenght is 0,
// decoding is skipped. Any error creating the request, sending it, or decoding
// the response is returned.
// Receive is shorthand for calling Request and Do.
func (s *Sling) Receive(successV, failureV interface{}) (*http.Response, error) {
req, err := s.Request()
if err != nil {
return nil, err
}
return s.Do(req, successV, failureV)
}
// Do sends an HTTP request and returns the response. Success responses (2XX)
// are JSON decoded into the value pointed to by successV and other responses
// are JSON decoded into the value pointed to by failureV.
// If the status code of response is 204(no content) or the Content-Length is 0,
// decoding is skipped. Any error sending the request or decoding the response
// is returned.
func (s *Sling) Do(req *http.Request, successV, failureV interface{}) (*http.Response, error) {
resp, err := s.httpClient.Do(req)
if err != nil {
return resp, err
}
// when err is nil, resp contains a non-nil resp.Body which must be closed
defer resp.Body.Close()
// The default HTTP client's Transport may not
// reuse HTTP/1.x "keep-alive" TCP connections if the Body is
// not read to completion and closed.
// See: https://golang.org/pkg/net/http/#Response
defer io.Copy(ioutil.Discard, resp.Body)
// Don't try to decode on 204s or Content-Length is 0
if resp.StatusCode == http.StatusNoContent || resp.ContentLength == 0 {
return resp, nil
}
// Decode from json
if successV != nil || failureV != nil {
err = decodeResponse(resp, s.responseDecoder, successV, failureV)
}
return resp, err
}
// decodeResponse decodes response Body into the value pointed to by successV
// if the response is a success (2XX) or into the value pointed to by failureV
// otherwise. If the successV or failureV argument to decode into is nil,
// decoding is skipped.
// Caller is responsible for closing the resp.Body.
func decodeResponse(resp *http.Response, decoder ResponseDecoder, successV, failureV interface{}) error {
if code := resp.StatusCode; 200 <= code && code <= 299 {
if successV != nil {
return decoder.Decode(resp, successV)
}
} else {
if failureV != nil {
return decoder.Decode(resp, failureV)
}
}
return nil
}