213 lines
6.6 KiB
Go
213 lines
6.6 KiB
Go
|
package fastly
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"os"
|
||
|
"runtime"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/ajg/form"
|
||
|
"github.com/hashicorp/go-cleanhttp"
|
||
|
"github.com/mitchellh/mapstructure"
|
||
|
)
|
||
|
|
||
|
// APIKeyEnvVar is the name of the environment variable where the Fastly API
|
||
|
// key should be read from.
|
||
|
const APIKeyEnvVar = "FASTLY_API_KEY"
|
||
|
|
||
|
// APIKeyHeader is the name of the header that contains the Fastly API key.
|
||
|
const APIKeyHeader = "Fastly-Key"
|
||
|
|
||
|
// DefaultEndpoint is the default endpoint for Fastly. Since Fastly does not
|
||
|
// support an on-premise solution, this is likely to always be the default.
|
||
|
const DefaultEndpoint = "https://api.fastly.com"
|
||
|
|
||
|
// ProjectURL is the url for this library.
|
||
|
var ProjectURL = "github.com/sethvargo/go-fastly"
|
||
|
|
||
|
// ProjectVersion is the version of this library.
|
||
|
var ProjectVersion = "0.1"
|
||
|
|
||
|
// UserAgent is the user agent for this particular client.
|
||
|
var UserAgent = fmt.Sprintf("FastlyGo/%s (+%s; %s)",
|
||
|
ProjectVersion, ProjectURL, runtime.Version())
|
||
|
|
||
|
// Client is the main entrypoint to the Fastly golang API library.
|
||
|
type Client struct {
|
||
|
// Address is the address of Fastly's API endpoint.
|
||
|
Address string
|
||
|
|
||
|
// HTTPClient is the HTTP client to use. If one is not provided, a default
|
||
|
// client will be used.
|
||
|
HTTPClient *http.Client
|
||
|
|
||
|
// apiKey is the Fastly API key to authenticate requests.
|
||
|
apiKey string
|
||
|
|
||
|
// url is the parsed URL from Address
|
||
|
url *url.URL
|
||
|
}
|
||
|
|
||
|
// DefaultClient instantiates a new Fastly API client. This function requires
|
||
|
// the environment variable `FASTLY_API_KEY` is set and contains a valid API key
|
||
|
// to authenticate with Fastly.
|
||
|
func DefaultClient() *Client {
|
||
|
client, err := NewClient(os.Getenv(APIKeyEnvVar))
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
return client
|
||
|
}
|
||
|
|
||
|
// NewClient creates a new API client with the given key. Because Fastly allows
|
||
|
// some requests without an API key, this function will not error if the API
|
||
|
// token is not supplied. Attempts to make a request that requires an API key
|
||
|
// will return a 403 response.
|
||
|
func NewClient(key string) (*Client, error) {
|
||
|
client := &Client{apiKey: key}
|
||
|
return client.init()
|
||
|
}
|
||
|
|
||
|
func (c *Client) init() (*Client, error) {
|
||
|
if len(c.Address) == 0 {
|
||
|
c.Address = DefaultEndpoint
|
||
|
}
|
||
|
|
||
|
u, err := url.Parse(c.Address)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
c.url = u
|
||
|
|
||
|
if c.HTTPClient == nil {
|
||
|
c.HTTPClient = cleanhttp.DefaultClient()
|
||
|
}
|
||
|
|
||
|
return c, nil
|
||
|
}
|
||
|
|
||
|
// Get issues an HTTP GET request.
|
||
|
func (c *Client) Get(p string, ro *RequestOptions) (*http.Response, error) {
|
||
|
return c.Request("GET", p, ro)
|
||
|
}
|
||
|
|
||
|
// Head issues an HTTP HEAD request.
|
||
|
func (c *Client) Head(p string, ro *RequestOptions) (*http.Response, error) {
|
||
|
return c.Request("HEAD", p, ro)
|
||
|
}
|
||
|
|
||
|
// Post issues an HTTP POST request.
|
||
|
func (c *Client) Post(p string, ro *RequestOptions) (*http.Response, error) {
|
||
|
return c.Request("POST", p, ro)
|
||
|
}
|
||
|
|
||
|
// PostForm issues an HTTP POST request with the given interface form-encoded.
|
||
|
func (c *Client) PostForm(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
|
||
|
return c.RequestForm("POST", p, i, ro)
|
||
|
}
|
||
|
|
||
|
// Put issues an HTTP PUT request.
|
||
|
func (c *Client) Put(p string, ro *RequestOptions) (*http.Response, error) {
|
||
|
return c.Request("PUT", p, ro)
|
||
|
}
|
||
|
|
||
|
// PutForm issues an HTTP PUT request with the given interface form-encoded.
|
||
|
func (c *Client) PutForm(p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
|
||
|
return c.RequestForm("PUT", p, i, ro)
|
||
|
}
|
||
|
|
||
|
// Delete issues an HTTP DELETE request.
|
||
|
func (c *Client) Delete(p string, ro *RequestOptions) (*http.Response, error) {
|
||
|
return c.Request("DELETE", p, ro)
|
||
|
}
|
||
|
|
||
|
// Request makes an HTTP request against the HTTPClient using the given verb,
|
||
|
// Path, and request options.
|
||
|
func (c *Client) Request(verb, p string, ro *RequestOptions) (*http.Response, error) {
|
||
|
req, err := c.RawRequest(verb, p, ro)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
resp, err := checkResp(c.HTTPClient.Do(req))
|
||
|
if err != nil {
|
||
|
return resp, err
|
||
|
}
|
||
|
|
||
|
return resp, nil
|
||
|
}
|
||
|
|
||
|
// RequestForm makes an HTTP request with the given interface being encoded as
|
||
|
// form data.
|
||
|
func (c *Client) RequestForm(verb, p string, i interface{}, ro *RequestOptions) (*http.Response, error) {
|
||
|
values, err := form.EncodeToValues(i)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if ro == nil {
|
||
|
ro = new(RequestOptions)
|
||
|
}
|
||
|
|
||
|
if ro.Headers == nil {
|
||
|
ro.Headers = make(map[string]string)
|
||
|
}
|
||
|
ro.Headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||
|
|
||
|
// There is a super-jank implementation in the form library where fields with
|
||
|
// a "dot" are replaced with "/.". That is then URL encoded and Fastly just
|
||
|
// dies. We fix that here.
|
||
|
body := strings.Replace(values.Encode(), "%5C.", ".", -1)
|
||
|
|
||
|
ro.Body = strings.NewReader(body)
|
||
|
ro.BodyLength = int64(len(body))
|
||
|
|
||
|
return c.Request(verb, p, ro)
|
||
|
}
|
||
|
|
||
|
// checkResp wraps an HTTP request from the default client and verifies that the
|
||
|
// request was successful. A non-200 request returns an error formatted to
|
||
|
// included any validation problems or otherwise.
|
||
|
func checkResp(resp *http.Response, err error) (*http.Response, error) {
|
||
|
// If the err is already there, there was an error higher up the chain, so
|
||
|
// just return that.
|
||
|
if err != nil {
|
||
|
return resp, err
|
||
|
}
|
||
|
|
||
|
switch resp.StatusCode {
|
||
|
case 200, 201, 202, 204, 205, 206:
|
||
|
return resp, nil
|
||
|
default:
|
||
|
return resp, NewHTTPError(resp)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// decodeJSON is used to decode an HTTP response body into an interface as JSON.
|
||
|
func decodeJSON(out interface{}, body io.ReadCloser) error {
|
||
|
defer body.Close()
|
||
|
|
||
|
var parsed interface{}
|
||
|
dec := json.NewDecoder(body)
|
||
|
if err := dec.Decode(&parsed); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||
|
mapToHTTPHeaderHookFunc(),
|
||
|
stringToTimeHookFunc(),
|
||
|
),
|
||
|
WeaklyTypedInput: true,
|
||
|
Result: out,
|
||
|
})
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return decoder.Decode(parsed)
|
||
|
}
|